COOK Launchpad — Integration Guide for Routers, Aggregators & Trading Terminals
COOK ships two factories: V1, where curves are priced in native BNB, and V2, where curves are priced in a registered ERC20 quote asset (RWA liquidity). They share the same curve maths and the same graduation model, but the money leg differs. Everything below is taken from the deployed contracts, not from a spec.
01Quick facts
02V1 vs V2 — what actually differs
V1 (native BNB) V2 (ERC20 / RWA quote)
──────────────────── ────────────────────────── ──────────────────────────────
Factory 0x48b9DD95…B9785b 0x63d3fD20…2719806
Money leg msg.value (BNB) ERC20 transferFrom(quote)
Approval needed sells only create, buy AND sell
Create createToken{value: fee} createToken(quote, …) + approve
Buy buyFor{value}(…) buy(token, amountIn, …)
Sell sellFrom(…) / sell(…) sell(token, amount, …)
Decimals always 18 quoteDecimals — DO NOT assume 18
Fee config global constants per quote, via quotes(quote)
Graduated pair TOKEN / WBNB TOKEN / QUOTE
Router swap fn …SupportingFeeOnTransfer swapExactTokensForTokens
Tokens (ETH variants) SupportingFeeOnTransferTokensevery V2 amount is denominated in the quote asset's own decimals. Read quoteDecimals from the token view and format with it. Hardcoding 18 will silently misprice any 6- or 8-decimal quote.
03Two phases — route differently
BONDING GRADUATED
──────────────────────────────── ────────────────────────────────
Venue : COOK factory bonding curve Venue : PancakeSwap V2 pair
Buy : V1 buyFor() · V2 buy() Buy : router, fee-on-transfer variant
Sell : V1 sellFrom() · V2 sell() Sell : router, fee-on-transfer variant
Token : TRANSFER-LOCKED Token : freely transferable, 1% swap taxDetect with factory.getToken(token).graduated — the field exists on both versions. Graduation happens automatically inside the buy that fills the curve — same transaction, no separate listing step, no window to snipe the listing itself.
Calling the curve functions after graduation reverts AlreadyGraduated (0xe6a0d45f). Calling the PancakeSwap pair before graduation is impossible — the pair does not exist yet.
04The constraint that breaks naive integrations
COOKToken._transfer rejects any transfer where neither side is the factory, reverting TransfersLocked (0xdb89e3f4). This is deliberate — it is what stops anyone seeding an AMM pool or dumping OTC before the curve completes.
The API is built so you never need custody. Every path has the factory on one side:
V1 BUY : you send BNB → factory transfers straight to the end user
V1 SELL: user approves the FACTORY → factory pulls straight from the user
V2 BUY : user approves the FACTORY for the QUOTE → factory pulls quote, sends token to user
V2 SELL: user approves the FACTORY for the TOKEN → factory pulls token, sends quote backThe quote asset itself is a normal ERC20 with no lock, so only the meme token side is constrained.
After graduation the meme token is a normal (taxed) ERC-20 and you can route it however you like.
05Detecting which factory a token belongs to
Tokens are not cross-registered. Ask V1 first, fall back to V2 — an unknown token reverts UnknownToken (0x8698bf37).
async function resolve(token: Address) {
try {
const v = await read(FACTORY_V1, "getToken", [token]);
return { version: 1, view: v, quote: "BNB", decimals: 18 };
} catch {
const v = await read(FACTORY_V2, "getToken", [token]);
return { version: 2, view: v, quote: v.quote, decimals: v.quoteDecimals };
}
}Cheaper at scale: index TokenCreated from both factories and keep the mapping locally. The V2 event carries an extra indexed quote topic (see §13), which is the simplest way to tell the two apart from logs alone.
The V2 token view is a superset of V1's: it adds quote, quoteName, quoteSymbol, quoteDecimals and reports the raise as quoteRaised instead of a BNB amount.
06Buying
V1 buyFor — permissionless
function buyFor(
address token,
address recipient, // end user; tokens go here directly
uint256 minTokensOut,
uint256 deadline // unix seconds, 0 = no deadline
) external payable returns (uint256 tokensOut);Taking your fee: skim the BNB before forwarding. The factory only ever sees the net amount, so your fee never touches the curve accounting.
function buyWithFee(address token, address user) external payable {
uint256 fee = msg.value * 25 / 10_000; // your 0.25%
payable(feeCollector).transfer(fee);
factory.buyFor{value: msg.value - fee}(token, user, minOut, deadline);
}- Tokens never pass through you.
recipientreceives them directly from the factory. - Overfill refunds go to recipient, not to the caller. If the order is larger than the remaining curve inventory, the factory spends only what it needs and refunds the rest to
recipient. Your contract will not receive stray BNB, so it does not need areceive()for this path.
V2 buy — quote pulled from msg.sender
function buy(
address token,
uint256 amountIn, // in QUOTE base units (quoteDecimals!)
uint256 minTokensOut,
uint256 deadline
) external returns (uint256 tokensOut);V2 has no buyFor: the tokens go to msg.sender, and the quote is pulled from msg.sender. During bonding your contract cannot legally receive the meme token, so a router must not call buy on its own behalf — it would revert TransfersLocked. Have the end user call buy from their own wallet and charge your fee on the quote transfer before the buy, or off-chain.
// end-user flow, two transactions
// 1) exact-amount approval of the QUOTE token to the V2 factory
await quote.approve(FACTORY_V2, amountIn); // never type(uint256).max
// 2) buy
await factoryV2.buy(token, amountIn, minTokensOut, deadline);Check quote.allowance(user, FACTORY_V2) first and only send the approval when it is short — re-approving every trade wastes a transaction. Overfill refunds are returned in the quote asset to the buyer.
07Selling
V1 sellFrom — allowlist required
function sellFrom(
address token,
address owner, // the end user whose tokens are sold
uint256 tokenAmount,
uint256 minBnbOut,
address recipient, // where the BNB lands — you, or the user
uint256 deadline
) external returns (uint256 bnbOut);The user approves the factory, not you. The factory then pulls owner → factory, which the transfer lock permits.
// 1) user (off-chain, their own wallet): token.approve(FACTORY, exactAmount)
// 2) you:
function sellWithFee(address token, address user, uint256 amount) external {
uint256 before = address(this).balance;
factory.sellFrom(token, user, amount, minOut, address(this), deadline);
uint256 gross = address(this).balance - before;
uint256 fee = gross * 25 / 10_000; // your 0.25%
payable(feeCollector).transfer(fee);
payable(user).transfer(gross - fee);
}Your contract needs a receive() external payable for this path.
Getting allowlisted
sellFrom is gated by isAllowedRouter[msg.sender], set by the COOK operator/owner via setAllowedRouter. Unlisted callers revert NotOperator (0x7c214f04).
Contact the COOK team with your router address to be added. Expect scrutiny: a cleared router picks recipient, minBnbOut and the timing, so it can in principle redirect the full proceeds of anyone holding an open approval. We ask integrators to:
- Use exact-amount approvals in your UI — never
type(uint256).maxon the factory. - Set
recipientto the end user where your flow allows it. - Keep the router contract immutable or behind a timelock.
If you would rather not be allowlisted, you can still route sells by having the user call sell() directly and charging your fee off-chain or on the buy side only.
V2 sell — user-signed, no allowlist
function sell(
address token,
uint256 tokenAmount,
uint256 minQuoteOut, // in QUOTE base units
uint256 deadline
) external returns (uint256 quoteOut);There is no sellFrom on V2 and therefore no router allowlist. The seller must be msg.sender, must approve the meme token to the V2 factory for the exact amount, and receives the quote asset directly.
await token.approve(FACTORY_V2, tokenAmount); // exact amount
await factoryV2.sell(token, tokenAmount, minQuoteOut, deadline);Pass the raw base-unit balance straight through — converting through a float first loses precision and produces "insufficient balance" reverts on max sells.
08V2 quote registry
V2 only accepts quote assets the operator has registered. Enumerate them instead of hardcoding addresses:
function totalQuotes() external view returns (uint256);
function quoteList(uint256 index) external view returns (address);
function quotes(address quote) external view returns (
bool enabled,
uint256 creationFee, // in quote base units
uint256 raiseTarget, // graduation threshold, quote base units
uint16 tradeFeeBps, // protocol fee on the curve
uint16 creatorFeeShareBps // creator's cut of that fee
);Skip any quote where enabled == false: creates and buys against it revert. Fees and the graduation target are per quote on V2 — read them rather than reusing the V1 constants.
Pull symbol(), name() and decimals() from the ERC20 itself (or from the token view's mirrored fields) for display.
function createToken(
address quote,
string name_,
string symbol_,
string metadataURI_,
uint256 launchBuy, // quote base units, 0 for no dev buy
uint256 minTokensOut
) external returns (address token);The caller must have approved creationFee + launchBuy of the quote asset to the factory. launchBuy is capped by maxInitialBuyBps (5% of supply) — exceeding it reverts InitialBuyTooLarge.
09Quoting
// V1 — amounts in wei (BNB)
function getBuyQuote(address token, uint256 bnbIn)
external view returns (uint256 tokensOut, uint256 fee, uint256 refund);
function getSellQuote(address token, uint256 tokenAmount)
external view returns (uint256 bnbOut, uint256 fee);
// V2 — amounts in QUOTE base units
function getBuyQuote(address token, uint256 amountIn)
external view returns (uint256 tokensOut, uint256 fee, uint256 refund);
function getSellQuote(address token, uint256 tokenAmount)
external view returns (uint256 quoteOut, uint256 fee);
// both
function currentPrice(address token) external view returns (uint256);
function bondingProgressBps(address token) external view returns (uint256);Same signatures, different unit. These run the exact same math as buy / sell, so quoted == executed absent other traffic in between. fee is the protocol fee in the money asset; getSellQuote's output is already net of it.
Both revert AlreadyGraduated for graduated tokens — check graduated first, or catch the selector and fall through to your PancakeSwap quoting path.
Remember to subtract your own fee from what you display, since neither factory has knowledge of it. For a USD figure on V2, multiply by the quote asset's USD price — the curve is priced in the quote, not in BNB.
Curve mechanics, if you want to quote locally
Identical on both versions — constant product with virtual reserves:
k = x0 · y0
x = x0 + raised (BNB on V1, quote units on V2)
y = y0 − tokensSold
y0 = 1,066,666,666.666… (VIRTUAL_TOKEN_RESERVE, larger than real supply on purpose)
x0 = raiseTarget / 3 (V2: raiseTarget comes from quotes(quote))Sold on the curve: 800,000,000 (80%). Paired into the burned LP at graduation: 200,000,000 (20%) plus the entire raise. y0 = S²/(S−L) is chosen so the curve's closing price equals the pool's opening price — there is no price step at graduation for you to arbitrage or to explain to users.
10After graduation — PancakeSwap V2
Standard pair, with one hard requirement:
You must use the …SupportingFeeOnTransferTokens router variants. The token takes 1% on swaps against the pair, soswapExactETHForTokens/swapExactTokensForETHwill revert. Set slippage tolerance to at least 1.5%.
V1 graduated pair : TOKEN / WBNB
path : [WBNB, TOKEN] / [TOKEN, WBNB]
fn : swapExactETHForTokensSupportingFeeOnTransferTokens
swapExactTokensForETHSupportingFeeOnTransferTokens
V2 graduated pair : TOKEN / QUOTE (no WBNB hop, the quote IS the pair asset)
path : [QUOTE, TOKEN] / [TOKEN, QUOTE]
fn : swapExactTokensForTokensSupportingFeeOnTransferTokens
note : approve the input ERC20 to the PancakeSwap router firstRead the pair address from getToken(token).pair rather than deriving it — V2 pairs are not against WBNB, so a WBNB-based getPair lookup returns the zero address.
Wallet-to-wallet transfers are not taxed. Only transfers where the pair is the counterparty.
11Displaying the tax correctly
The nominal tax is exactly 1%, both directions, both versions. A sell measured with no swap-back pending costs 94 bps — the 1% tax, with rounding.
The variance comes from the swap-back. The token withholds tax in tokens. When the buffer reaches swapThreshold (0.05% of supply = 500,000), the next sell first swaps that buffer out (to BNB on V1, to the quote asset on V2), and only then executes the user's own sell. The buffer's price impact therefore lands inside that one transaction.
12Error selectors
Factory (shared by V1 and V2)
0x025dbdd4 InsufficientFee payment < creationFee()
0x37c6d202 InitialBuyTooLarge launch buy exceeds 5% of supply
0x8199f5f3 SlippageExceeded minTokensOut / minQuoteOut not met
0x70f65caa DeadlinePassed past deadline
0xe6a0d45f AlreadyGraduated token has moved to PancakeSwap
0x8698bf37 UnknownToken not created by THIS factory (check the other one)
0x7c214f04 NotOperator V1: caller is not an allowlisted router
0x1f2a2005 ZeroAmount
0xd92e233d ZeroAddress
Token
0xdb89e3f4 TransfersLocked transfer attempted during bonding
0x13be252b InsufficientAllowance user has not approved the factory
0xf4d678b8 InsufficientBalance
V2 extra failure modes (surface as ERC20 reverts, not custom selectors)
quote not registered / disabled → create + buy revert
quote allowance < amountIn → ERC20: insufficient allowance
meme-token allowance < tokenAmount → sell reverts before touching the curve13Events for indexing
V1 (0x48b9DD95…)
TokenCreated 0x91de26bc430b3a4f1d6cfb11d72f2e5ca75d7622d37b2a88a8998ec28e747a11
Trade 0xf4682780a0e17045675b1fdf80687447d8cc9589bc87f718bcba8cbd53a2e81e
Graduated 0x487dc7f66c623fb0ff13f9024a3ff9675453d069e075eceb12d9f8d7870e2374// V1
event Trade(
address indexed token,
address indexed trader, // your router, when you route
address indexed recipient, // the end user
bool isBuy,
uint256 bnbAmount,
uint256 tokenAmount,
uint256 fee,
uint256 bnbReserve, // curve state AFTER this trade
uint256 tokensSold
);V2 emits its own TokenCreated with a third indexed topic, the quote asset — index both factories separately and use that topic to tag the version:
// V2
event TokenCreated(
address indexed token,
address indexed creator,
address indexed quote, // <- V2 only; the money asset for this curve
string name,
string symbol,
string metadataURI,
uint256 raiseTarget, // quote base units
uint256 virtualQuote,
uint256 timestamp
);V2 trade amounts are in quote base units. Always join against quoteDecimals before formatting or aggregating volume across quotes.
