Chuyển tới nội dung chính

Pools

Contract address

Pools do not have their own contract. They share the one launchpad address with microsales and Melt Pools — one address per chain, one ABI, all three launch styles.

NetworkChain IDLaunchpad
BNB Chain (mainnet)560xD9CcE715619fA9082d21375B6B37A3a0d99138F4
BNB Chain (testnet)970x0AA21d47b600B04527c09BbD9A30aF32cb64f6cF
Robinhood Chain46630x7E0752B42f9db92F12E221E10fFfa10bfE8668C6

An individual pool is identified by its fundManager address, which you pass as an argument to the calls below. That is a per-sale handle, not a contract you call.

A pool is a sale for a token you already have. You deploy your ERC20, approve the launchpad to pull what the sale needs, and the launchpad runs the raise and lists the token. This is the path to use when you own the token contract and want control over price, caps, whitelist, and vesting. If you want the launchpad to mint the token for you instead, see microsale.md.

Pools come in three shapes. They share most of the machinery but differ in how price is set.

Contents​

The pieces​

  • Your token. You bring the ERC20. The launchpad never mints it. At create time it pulls the presale allocation plus the liquidity allocation from your wallet into the pool's fund manager, so you approve first.
  • The pool. One sale, identified by its fundManager address, the handle you pass to almost every call.
  • The fund manager. A per-pool address that escrows your tokens and the raised funds until finalize. You never call it directly.
  • The owner. By default the owner you set in the create params (it can differ from the wallet that sends the create transaction). The owner finalizes, cancels, edits, and sets the whitelist.

A pool is always Active (0), Finalized (1), or Cancelled (2).

Pool types​

TypePriceRaise measured inFinalizes when
Presalefixed rate you setcurrency, up to hardCapcap hit, or soft cap met at end time
Fair Launchemergent: total raised divided by a fixed token supplycurrency, above softCapsoft cap met at end time
Subscriptionfixed rate, but oversubscribed and allocated afterwardtokens committed vs softCapTokenssoft cap met and allocation approved
  • Presale is the classic fixed-price sale. Contributors buy tokens at rate until the hard cap fills. Everyone who gets in pays the same price.
  • Fair Launch does not fix a price. You commit a fixed totalSellingTokens, contributors put in as much as the rules allow, and the final price is whatever the total raise implies. More raised means a higher price per token.
  • Subscription lets people commit more than the sale can fill, then allocates pro-rata (or by whatever formula the platform runs off-chain), publishes the result as a Merkle root, and refunds the excess when contributors claim. Use it when you expect heavy oversubscription and want a fair split.

Two overflow variants (OverflowPresale, OverflowFairLaunch) exist in the claim and withdraw surface, but there is no create path for them today, so you will only meet them if you interact with a pool someone created through another route. The three types above are the ones you can launch.

Payment currencies​

Presale, fair-launch, and subscription pools raise in the chain's native coin or a supported ERC20 from the launchpad's currency allow-list — a separate list from the Melt Pool / microsale quote tokens. Pass the chosen token as currency in the create params (address(0) for native), and read the live set with getAllowedCurrencies(). See Supported currencies for the full per-chain list.

Before you create: approve your token​

Every create call pulls tokens from the sender. Compute how many with the matching helper, then approve the launchpad for that amount.

// Presale example; there are calculateFairLaunchTotalTokens and calculateSubscriptionTotalTokens too
const needed = await launchpad.calculatePresaleTotalTokens(params);

const token = new ethers.Contract(params.token, ERC20_ABI, signer);
await token.approve(LAUNCHPAD, needed);

The total covers the presale allocation plus the liquidity tokens (unless the pool is manual listing). Fee-on-transfer and rebasing tokens are rejected: the launchpad checks that the fund manager received exactly needed, and reverts Transfer not enough token to pool otherwise. If the platform charges a pool creation fee, send it as the exact msg.value on the create call.

Creating a presale​

createPresale(CreatePresaleParams params) is payable and permissionless.

FieldTypeNotes
owneraddresswho owns the pool afterward; can differ from the sender
routeraddressa supported DEX router, or address(0) for a manual-listing pool
tokenaddressyour ERC20
currencyaddressraise currency; address(0) for native, else a supported ERC20
startTime, endTimeuint256sale window; set both or leave both at 0
publicSaleStartTimeuint256when the whitelist window ends and the public phase opens
rateuint256presale price: tokens per one whole currency unit
listingRateuint256DEX listing price; 0 for manual listing
minContribution, maxContributionuint256per-wallet bounds in the raise currency
softCap, hardCapuint256funding targets; softCap * 4 >= hardCap and softCap < hardCap
lockDurationuint256LP lock seconds; 0 or at least the chain minimum
listingPercentuint256percent of the raise used for liquidity, 20 to 100
refundTypeenumRefund (0) returns unsold tokens, Burn (1) burns them
poolDetailsstringfree-form metadata
affiliateBpsuint256affiliate reward in BPS; 0 to disable, capped by the platform
tgePercent, cyclePercent, cycleuint256vesting; see Claiming and vesting
isManualListingbooltrue skips auto-listing; then router, lock, and listing fields must be 0
const params = {
owner: myAddress,
router: ROUTER,
token: TOKEN,
currency: ethers.ZeroAddress, // native raise
startTime: start, endTime: end,
publicSaleStartTime: start, // no whitelist window
rate: ethers.parseEther("1000"), // 1000 tokens per 1 BNB
listingRate: ethers.parseEther("800"),
minContribution: ethers.parseEther("0.1"),
maxContribution: ethers.parseEther("5"),
softCap: ethers.parseEther("50"),
hardCap: ethers.parseEther("100"),
lockDuration: 0n, // burn LP
listingPercent: 60n, // 60% of the raise into liquidity
refundType: 0, // refund unsold tokens
poolDetails: "",
affiliateBps: 0n,
tgePercent: 0n, cyclePercent: 0n, cycle: 0n, // no vesting
isManualListing: false,
};

const fee = (await launchpad.getMasterConfig()).creationFee;
const tx = await launchpadRW.createPresale(params, { value: fee });
const rc = await tx.wait();
const created = rc.logs.find((l) => l.eventName === "PoolCreated");
const fundManager = created.args.fundManager;

PoolCreated(index, fundManager, owner, token, timestamp) gives you the fundManager handle. The owner in the event is the sender; the pool's stored owner is params.owner.

Creating a fair launch​

createFairLaunch(CreateFairLaunchParams params). Same idea, but you fix the token supply and let the price float. There is no rate, hardCap, or minContribution.

FieldTypeNotes
owner, router, token, currencyaddressas in presale
startTime, endTimeuint256duration must be within the platform's fair-launch maximum
publicSaleStartTimeuint256whitelist window end
maxContributionuint256per-wallet cap in the raise currency
softCapuint256minimum raise for the sale to succeed
totalSellingTokensuint256the fixed supply on offer; must be above zero
lockDuration, listingPercentuint256as in presale (0 for manual listing)
poolDetailsstringmetadata
affiliateBpsuint256affiliate BPS, capped
tgePercent, cyclePercent, cycleuint256vesting
isManualListingboolas in presale

The refund handling is fixed to Refund. The final per-token price is the total raised divided by totalSellingTokens, so contributors do not know their exact allocation until the sale ends.

Creating a subscription​

createSubscription(CreateSubscriptionParams params). Built for oversubscription: contributors commit, then the platform allocates and publishes the result. There is no manual-listing option, and LP is always locked.

FieldTypeNotes
owner, router, token, currencyaddressrouter is required (no manual listing)
startTime, endTime, publicSaleStartTimeuint256as usual
rateuint256price used to derive the implied cap
listingRateuint256DEX listing price
maxContributionuint256per-wallet cap in the raise currency
softCapTokensuint256soft cap in tokens; above zero and below totalSellingTokens
totalSellingTokensuint256total tokens on offer
hardCapTokensPerUseruint256per-wallet token cap; above zero
lockDurationuint256required, at least the chain minimum
listingPercentuint25620 to 100
refundTypeenumRefund or Burn
poolDetailsstringmetadata

After the sale ends, the pool needs an allocation before it can finalize or contributors can claim. That step is covered in Subscription allocation.

Whitelisting​

Whitelisting is optional and Merkle-based. If publicSaleStartTime equals the sale start, there is no whitelist window and anyone can contribute from the start. To gate the early window, the owner sets a Merkle root and (optionally) time-boxed tiers.

// owner only
await launchpadRW.setWhitelistRoot(fundManager, merkleRoot);

// optional tiers, and/or set the public phase start in one call
await launchpadRW.setPublicSaleStartTimeAndWhitelistTiers(fundManager, publicStart, tiers);

What a contributor passes during the whitelist window: the same (index, tier, allocation) values used to build their Merkle leaf, plus the proof. The leaf is the standard double-hashed encoding:

leaf = keccak256(bytes.concat(keccak256(abi.encode(index, user, tier, allocation))))

allocation, when above zero, becomes that wallet's contribution cap for the whitelist window. Once block.timestamp >= publicSaleStartTime, the whitelist is not checked and you pass zeros and an empty proof. Tiers let you open the window in stages: a tier with a non-zero duration is active only within its slice of time; a single tier with duration == 0 just opens at its start.

Read the tier config with getWhitelistTiers(fundManager).

Editing pool details​

Besides the whitelist setters above, the owner (or a platform operator) can update a handful of other fields after creation:

FunctionUpdates
updatePoolDetails(fundManager, poolDetails)free-form metadata; the owner can only call this while the pool is Active, an operator can call it any time
setAffiliateBps(fundManager, affiliateBps)affiliate reward BPS; can only be raised, capped by the platform's maxAffiliateBps
setRequireContributorValidation(fundManager, requireContributorValidation)toggles whether contributions need the platform's signature gate
setClaimTime(fundManager, claimTime)when claiming opens
setTime(fundManager, startTime, endTime)the sale window; only before the current startTime has passed
setEndTime(fundManager, endTime)shortens endTime; it cannot be extended
setMaxContribution(fundManager, maxContribution)per-wallet cap; must stay above minContribution

Each of these emits PoolUpdated(index, fundManager, timestamp).

Contributing​

Which function to call depends on the pool's currency. Read pool.currency: address(0) is native, anything else is an ERC20.

Native pools:

function contribute(
address fundManager,
uint256 index, // your whitelist leaf index (0 in the public phase)
uint256 tier, // whitelist tier (0 in the public phase)
uint256 allocation, // whitelisted allocation (0 in the public phase)
bytes32[] proof, // Merkle proof (empty in the public phase)
address referrer, // affiliate referrer, or the zero address
bytes signature // platform authorization, or empty (see below)
) external payable; // contribution amount = msg.value

ERC20 pools use contributeCustomCurrency(...) with the same arguments plus an explicit amount, and no msg.value. Approve the launchpad for amount first.

// native, public phase, no referrer, no signature gate
await launchpadRW.contribute(
fundManager, 0, 0, 0, [], ethers.ZeroAddress, "0x",
{ value: ethers.parseEther("1") }
);

// erc20 pool
const quote = new ethers.Contract(pool.currency, ERC20_ABI, signer);
await quote.approve(LAUNCHPAD, amount);
await launchpadRW.contributeCustomCurrency(
fundManager, 0, 0, 0, amount, [], ethers.ZeroAddress, "0x"
);

Two things to watch:

  • Signature gate. If the pool was created with requireContributorValidation, each contribution needs a platform-signed authorization in signature (an EIP-191 signature by the platform verification wallet over keccak256(abi.encodePacked(fundManager, user, amount))). Your app requests it from the platform before contributing. If the pool does not require it, pass 0x.
  • Referrer. Pass a referrer address to credit an affiliate on Presale and Fair Launch pools (when the pool set affiliateBps). Pass the zero address to skip it.

Common reverts: Amount cant be 0, Pool is either completed or cancelled, Contribution outside of start and end time, and the per-type caps (Buying amount exceeds hard cap, Contribute more than allowed, Min contribution not reached).

Every contribution emits Contributed(index, fundManager, user, amount, timestamp).

Finalizing​

The owner finalizes when the finish conditions are met. Finalizing takes the platform fee, adds liquidity, and locks or burns the LP. Each type has its own call and its own condition.

TypeCallFinalizes when
PresalefinalizePresale(fundManager)hard cap hit, or the gap left is below minContribution, or soft cap met and the sale ended
Fair LaunchfinalizeFairLaunch(fundManager)soft cap met and the sale ended
SubscriptionfinalizeSubscription(fundManager)allocation approved and totalVolumePurchased >= softCapTokens
await launchpadRW.finalizePresale(fundManager); // owner

All three emit Finalized(index, fundManager, timestamp). Fair Launch computes the final rate at this point from the raise and the fixed supply, so read the pool back afterward to get it.

Claiming and vesting​

After finalize, contributors claim. The right call depends on the pool type, because Subscription and overflow pools settle refunds with a Merkle proof.

// Presale and Fair Launch
await launchpadRW.claim(fundManager);

// Subscription: claim your allocation and get the excess refunded, with your proof
await launchpadRW.claimAndRefundWithProof(fundManager, index, purchased, proof);

Calling the wrong one reverts with a message telling you which to use.

Vesting​

If the pool set tgePercent, cyclePercent, and cycle, tokens unlock over time instead of all at once. At claim start a wallet can take tgePercent of its purchase, then another cyclePercent every cycle seconds, up to the full amount. Read how much is claimable right now with:

const claimable = await launchpad.claimableVestingTokens(fundManager, user);

claim sends whatever is currently unlocked, so a vesting contributor calls it repeatedly as more unlocks. With no vesting params set, the whole allocation is claimable at once.

Every claim emits Claimed(index, fundManager, user, claimed, totalPurchased, timestamp).

The owner (or an operator) can claim on behalf of a batch of contributors instead of waiting for each to call claim themselves, for any finalized non-Subscription pool:

await launchpadRW.distributeTokens(fundManager, startIndex, endIndex); // owner or operator

Refunds and exits​

Failed or cancelled pool​

withdraw(fundManager) returns a contribution in full when the pool failed (ended below soft cap) or was cancelled.

await launchpadRW.withdraw(fundManager);

For a live Presale or Fair Launch, this is allowed only after endTime when the soft cap was missed. For a Subscription, only after cancellation. There is no fee.

Early exit while live​

emergencyWithdraw(fundManager) leaves a live Presale or Fair Launch for a 10% penalty, blocked in the last 15 minutes before endTime. Subscription pools do not support it.

await launchpadRW.emergencyWithdraw(fundManager);

Cancelling​

The owner cancels an Active pool, which opens full withdrawals for contributors. Afterward the owner reclaims the escrowed tokens with withdrawCancelledTokens(fundManager).

await launchpadRW.cancel(fundManager);
await launchpadRW.withdrawCancelledTokens(fundManager); // returns the pool's tokens to the owner

Cancel emits Cancelled(index, fundManager, timestamp); each refund emits WithdrawContribution(index, fundManager, user, amount, timestamp).

The owner (or an operator) can also refund a batch of contributors directly instead of waiting for each to call withdraw themselves, for a Cancelled Presale, Fair Launch, or Subscription pool (not the Overflow variants):

await launchpadRW.distributeRefund(fundManager, startIndex, endIndex); // owner or operator

Subscription allocation​

A Subscription pool needs one extra step between the sale ending and contributors claiming: the allocation. Because oversubscription means not everyone gets their full commitment, the split is computed off-chain by the platform (admin) and published on-chain as a Merkle root.

The lifecycle, and where each actor fits:

  1. The sale ends. Commitments are locked in.
  2. Admin computes the allocation and calls submitAllocation(fundManager, allocationRoot, totalVolumePurchased). The calc stage moves to needConfirm.
  3. Admin calls approveAllocation(fundManager). The stage moves to calculated.
  4. Anyone can now finalizeSubscription(fundManager) (the owner usually does).
  5. Contributors call claimAndRefundWithProof(fundManager, index, purchased, proof) to take their allocation and get the rest refunded, or refundExcessiveWithProof(...) to only take the refund.

You do not run steps 2 and 3; the platform does. What your app needs is the per-user purchased amount and proof, which the platform publishes alongside the root. Track the stage yourself with:

const stage = await launchpad.getSubscriptionCalculationStage(fundManager);
// 0 notCalculated, 1 needConfirm, 2 calculated

The subscription leaf encoding is keccak256(bytes.concat(keccak256(abi.encode(index, user, purchased)))). Watch AllocationSubmitted and AllocationApproved to know when claiming opens.

Reading pool state​

All reads are open to anyone.

FunctionReturns
getPoolByIndex(index)the Pool at a creation index
getPoolByFundManager(fundManager)the Pool for a handle
getPoolsByIndexes(indexes[])batch by index
getPoolsByFundManagers(fundManagers[])batch by handle
getPoolCount()total pools ever created
getContributionInfo(fundManager, user)one contributor's ledger
getContributorCountByFundManager(fundManager)contributor count
getContributorCountByPoolIndex(index)contributor count, by creation index
getContributionInfosByFundManager(fundManager, start, end)a page of contributors
getWhitelistTiers(fundManager)the whitelist tiers
excessiveCommitment(fundManager, user, purchased)Subscription only: how much of a wallet's commitment is refundable at a given allocation; 0 for other pool types

One caveat worth coding around: getPoolByFundManager does not verify the address belongs to a real pool. An unknown address maps to pool index 0 and returns that pool. Confirm the result by checking returnedPool.fundManager === fundManager before you trust it.

The full Pool struct is in events-and-data.md.

Error reference​

Create

MessageCause
Invalid fee amountmsg.value is not exactly the pool creation fee
Invalid router / Cannot set router for manual listing poolrouter does not match the listing mode
Invalid currencycurrency is not native and not a supported ERC20
Invalid contribution settingsminContribution > maxContribution
Invalid cap settingspresale caps break softCap * 4 >= hardCap and softCap < hardCap
Invalid time settings / Both time has to be unsetbad start/end pair
Invalid listing percentlistingPercent outside 20 to 100
Invalid total selling tokensfair launch with totalSellingTokens == 0
Not enough balance / Not enough allowanceyou did not fund or approve the token total
Transfer not enough token to poolfee-on-transfer or rebasing token

Contribute

MessageCause
Invalid currencyusing the native call on an ERC20 pool, or vice versa
Amount cant be 0zero contribution
Pool is either completed or cancelledpool not active
Contribution outside of start and end timebefore start or after end
Buying amount exceeds hard cap / Contribute more than allowed / Min contribution not reachedper-type caps
Invalid signaturepool requires a platform authorization and it was missing or wrong
WhitelistManager: Invalid proofwrong Merkle proof during the whitelist window

Finalize / claim / withdraw

MessageCause
Unauthorizednot the pool owner
It is not time to finishfinalize conditions not yet met
Pool is not finalizedclaim before finalize
Invalid proofwrong proof for an overflow or subscription claim/refund
Soft cap reachedfull withdraw attempted on a pool that met soft cap
Pool is still in progressfull withdraw before the sale ended
It is too late to do emergency withdraw contributioninside the last 15 minutes