---
title: "Trade on the bonding curve"
description: "Quote, buy, and sell a launch token while it trades on its bonding curve."
---

> Documentation Index
> Fetch the complete documentation index at: https://developer.clank.trade/llms.txt
> Use this file to discover all available pages before exploring further.

# Trade on the bonding curve

Every Clank launch token starts on a bonding curve. While the curve is in its
`Trading` state, buyers exchange the configured pair token for launch tokens
and sellers exchange launch tokens back for that pair token. The pair token is
either native ETH or an approved ERC-20.

This guide calls the contracts directly with Viem. It assumes your app already
has a `publicClient`, a connected `walletClient`, and the launch-token address.
See the [quickstart](/quickstart) for a smaller native-ETH buy example.
The examples below are sections of the same `bonding-curve.ts` module.

- **Quote without a wallet** — Read the curve and show expected outputs, fees, and refunds before asking
the user to connect or sign.
- **Execute after confirmation** — Apply slippage, handle approval, simulate, and submit only after the user
accepts the displayed quote.

- **Pons V2 compatible** — Existing Pons V2 integrations can keep their core execution and migration
flow while adopting Clank V2's richer reads.

<div class="mt-4 grid gap-3 sm:grid-cols-2">
<div class="rounded-md bg-card/70 p-3 ring ring-border/70">
<p class="m-0 font-semibold text-foreground">Reusable flow</p>
<p class="mt-1 mb-0">
`buy`, `sell`, `getReserves`, `graduate`, `createGraduatedPool`, and
their core events retain the canonical interfaces.
</p>
</div>
<div class="rounded-md bg-card/70 p-3 ring ring-border/70">
<p class="m-0 font-semibold text-foreground">Richer reads</p>
<p class="mt-1 mb-0">
`quoteBuyFor`, `quoteSell`, and `state`, used below, are Clank V2
additions.
</p>
</div>
<div class="rounded-md bg-card/70 p-3 ring ring-border/70 sm:col-span-2">
<p class="m-0 font-semibold text-foreground">Improved curve math</p>
<p class="mt-1 mb-0">
Maintains the 1 billion token supply through migration and a consistent
market cap across graduation.
</p>
</div>
</div>

## Resolve the curve and check its state

On Robinhood Chain, resolve the curve through the canonical factory instead of
accepting an untrusted curve address. Then read `state()` from that curve. Only
state `0` (`Trading`) accepts buys and sells.

```ts title="bonding-curve.ts"
import {
  erc20Abi,
  parseAbi,
  zeroAddress,
  type Address,
  type PublicClient,
  type WalletClient,
} from "viem";

const CLANK_FACTORY = "0x798daAa0707C1e538bB5Acf0867Ac0e1A84cccF2" as const;

const factoryAbi = parseAbi([
  "function getLaunchedToken(address launchToken) view returns ((address launchToken,address curve,address deployer,address creatorFeeRecipient,address pairToken,uint256 graduationThreshold,uint24 poolFee,int24 tickSpacing,uint16 creatorTaxBps,bool buybackEnabled,uint8 phase,uint256 sweptPairToken,uint256 sweptLaunchTokens,uint256 sweptAt,bool exists) launched)",
]);

const curveAbi = parseAbi([
  "function state() view returns (uint8)",
  "function quoteBuyFor(address recipient,uint256 grossPairTokenIn) view returns (uint256 grossPairTokenUsed,uint256 netPairTokenIn,uint256 fee,uint256 launchTokensOut,uint256 pairTokenRefund)",
  "function quoteSell(uint256 launchTokensIn) view returns (uint256 grossPairTokenOut,uint256 netPairTokenOut,uint256 fee)",
  "function buy(uint256 pairTokenIn,uint256 minLaunchTokensOut,address recipient) payable returns (uint256 launchTokensOut)",
  "function sell(uint256 launchTokensIn,uint256 minPairTokenOut,address recipient) returns (uint256 pairTokenOut)",
]);

const getTradingCurve = async (
  publicClient: PublicClient,
  launchToken: Address
) => {
  const launch = await publicClient.readContract({
address: CLANK_FACTORY,
abi: factoryAbi,
functionName: "getLaunchedToken",
args: [launchToken],
  });

  if (!launch.exists) throw new Error("Not a Clank launch token");

  const state = await publicClient.readContract({
address: launch.curve,
abi: curveAbi,
functionName: "state",
  });

  if (state !== 0) throw new Error("The bonding curve is no longer trading");

  return {
curve: launch.curve,
pairToken: launch.pairToken,
  } as const;
};
```

The complete curve lifecycle is:

| State         | Value | Trading behavior                                             |
| ------------- | ----: | ------------------------------------------------------------ |
| `Trading`     |     0 | Buys and sells are enabled.                                  |
| `Ready`       |     1 | The sale allocation is exhausted; curve trading is closed.   |
| `Swept`       |     2 | Reserves have moved to the factory for graduation.           |
| `PoolCreated` |     3 | Trade through the graduated Uniswap V4 pool.                 |
| `Rescued`     |     4 | Graduation recovery completed; curve trading remains closed. |

> **Read the curve state**
>
> Do not use the factory's `phase` field for this check. It is a
> graduation-compatibility field and does not distinguish `Trading` from
> `Ready`.

## Quote a buy

Keep quoting separate from execution so your app can display the expected
output, fees, and refund before asking for a wallet signature. `quoteBuyFor`
accepts the total amount of pair tokens the buyer is offering, including fees.
Quote for the actual launch-token recipient: during the launch window, the
total fee can depend on whether that recipient is exempt from the decaying
anti-snipe tax.

```ts title="bonding-curve.ts"
const quoteBondingCurveBuy = async ({
  publicClient,
  launchToken,
  recipient,
  requestedPairTokenIn,
}: {
  publicClient: PublicClient;
  launchToken: Address;
  recipient: Address;
  requestedPairTokenIn: bigint;
}) => {
  const { curve, pairToken } = await getTradingCurve(publicClient, launchToken);
  const [pairTokenUsed, netPairTokenIn, fee, launchTokensOut, pairTokenRefund] =
await publicClient.readContract({
  address: curve,
  abi: curveAbi,
  functionName: "quoteBuyFor",
  args: [recipient, requestedPairTokenIn],
});

  return {
curve,
pairToken,
recipient,
requestedPairTokenIn,
pairTokenUsed,
netPairTokenIn,
fee,
launchTokensOut,
pairTokenRefund,
  } as const;
};

type BondingCurveBuyQuote = Awaited<ReturnType<typeof quoteBondingCurveBuy>>;
```

Call `quoteBondingCurveBuy` whenever the input or recipient changes and render
fields such as `launchTokensOut`, `fee`, and `pairTokenRefund`.

## Buy

Pass the displayed quote unchanged to `buyOnBondingCurve` only after the user
confirms it. The function handles a required pair-token approval before
submitting the buy.

```ts title="bonding-curve.ts"
const BPS = 10_000n;

const minimumOutput = (amount: bigint, slippageBps: bigint) =>
  (amount * (BPS - slippageBps)) / BPS;

const buyOnBondingCurve = async ({
  publicClient,
  walletClient,
  quote,
  slippageBps = 100n,
}: {
  publicClient: PublicClient;
  walletClient: WalletClient;
  quote: BondingCurveBuyQuote;
  slippageBps?: bigint;
}) => {
  const [trader] = await walletClient.getAddresses();
  if (!trader) throw new Error("Connect a wallet");
  if (slippageBps < 0n || slippageBps >= BPS) {
throw new Error("Invalid slippage");
  }

  const minimumLaunchTokensOut = minimumOutput(
quote.launchTokensOut,
slippageBps
  );

  // A terminal buy can use only part of the offered input and refund the rest.
  // Scale the contract argument so its proportional partial-fill check still
  // enforces minimumLaunchTokensOut for the amount that is actually used.
  const minimumForOfferedAmount =
quote.pairTokenUsed === quote.requestedPairTokenIn
  ? minimumLaunchTokensOut
  : (minimumLaunchTokensOut * quote.requestedPairTokenIn +
      quote.pairTokenUsed -
      1n) /
    quote.pairTokenUsed;

  if (quote.pairToken !== zeroAddress) {
const allowance = await publicClient.readContract({
  address: quote.pairToken,
  abi: erc20Abi,
  functionName: "allowance",
  args: [trader, quote.curve],
});

if (allowance < quote.requestedPairTokenIn) {
  const { request } = await publicClient.simulateContract({
    account: trader,
    address: quote.pairToken,
    abi: erc20Abi,
    functionName: "approve",
    args: [quote.curve, quote.requestedPairTokenIn],
  });
  const hash = await walletClient.writeContract(request);
  await publicClient.waitForTransactionReceipt({ hash });
}
  }

  const { request } = await publicClient.simulateContract({
account: trader,
address: quote.curve,
abi: curveAbi,
functionName: "buy",
args: [
  quote.requestedPairTokenIn,
  minimumForOfferedAmount,
  quote.recipient,
],
value: quote.pairToken === zeroAddress ? quote.requestedPairTokenIn : 0n,
  });
  const hash = await walletClient.writeContract(request);
  const receipt = await publicClient.waitForTransactionReceipt({ hash });
  if (receipt.status !== "success") throw new Error("Buy reverted");

  return { hash, receipt } as const;
};
```

> **Pair-token handling**
>
> For a native-ETH curve, `msg.value` must exactly equal `requestedPairTokenIn`.
> For an ERC-20 curve, approve that pair token for the curve and send no native
> value. A terminal buy may consume less than the offered amount; the curve
> returns the unused `pairTokenRefund` in the same pair token.

The final buy can also move the curve out of `Trading`. Re-resolve and re-quote
after any failed simulation instead of submitting a stale transaction.

## Quote a sell

`quoteSell` returns the gross pair-token amount removed from the curve, the net
pair-token amount paid to the recipient, and the fee. Return those amounts
without mixing display data with execution-time slippage.

```ts title="bonding-curve.ts"
const quoteBondingCurveSell = async ({
  publicClient,
  launchToken,
  launchTokensIn,
}: {
  publicClient: PublicClient;
  launchToken: Address;
  launchTokensIn: bigint;
}) => {
  const { curve, pairToken } = await getTradingCurve(publicClient, launchToken);
  const [grossPairTokenOut, netPairTokenOut, fee] =
await publicClient.readContract({
  address: curve,
  abi: curveAbi,
  functionName: "quoteSell",
  args: [launchTokensIn],
});

  return {
curve,
pairToken,
launchToken,
launchTokensIn,
grossPairTokenOut,
netPairTokenOut,
fee,
  } as const;
};

type BondingCurveSellQuote = Awaited<ReturnType<typeof quoteBondingCurveSell>>;
```

Call `quoteBondingCurveSell` whenever the launch-token input changes. Display
`netPairTokenOut`, `fee`, and any other quote fields your interface needs.

## Sell

A sell needs an allowance for the launch token. Pass the confirmed sell quote
to `sellOnBondingCurve`; the function approves the curve when necessary before
submitting the sell.

```ts title="bonding-curve.ts"
const sellOnBondingCurve = async ({
  publicClient,
  walletClient,
  quote,
  slippageBps = 100n,
}: {
  publicClient: PublicClient;
  walletClient: WalletClient;
  quote: BondingCurveSellQuote;
  slippageBps?: bigint;
}) => {
  const [trader] = await walletClient.getAddresses();
  if (!trader) throw new Error("Connect a wallet");
  if (slippageBps < 0n || slippageBps >= BPS) {
throw new Error("Invalid slippage");
  }

  const minimumPairTokenOut = minimumOutput(quote.netPairTokenOut, slippageBps);

  const allowance = await publicClient.readContract({
address: quote.launchToken,
abi: erc20Abi,
functionName: "allowance",
args: [trader, quote.curve],
  });

  if (allowance < quote.launchTokensIn) {
const { request } = await publicClient.simulateContract({
  account: trader,
  address: quote.launchToken,
  abi: erc20Abi,
  functionName: "approve",
  args: [quote.curve, quote.launchTokensIn],
});
const hash = await walletClient.writeContract(request);
await publicClient.waitForTransactionReceipt({ hash });
  }

  const { request } = await publicClient.simulateContract({
account: trader,
address: quote.curve,
abi: curveAbi,
functionName: "sell",
args: [quote.launchTokensIn, minimumPairTokenOut, trader],
  });
  const hash = await walletClient.writeContract(request);
  const receipt = await publicClient.waitForTransactionReceipt({ hash });
  if (receipt.status !== "success") throw new Error("Sell reverted");

  return { hash, receipt } as const;
};
```

The curve pays native ETH when `quote.pairToken` is the zero address; otherwise
it pays that ERC-20. No pair-token approval is needed for a sell.

## Slippage, fees, and transaction safety

> **Quotes are snapshots**
>
> Always simulate immediately before requesting a wallet signature. Another
> trade can move the curve or complete it first; request a fresh quote after a
> failed simulation.

- Slippage is expressed in basis points: `100n` is 1%. Apply it to
  `launchTokensOut` for a buy and `netPairTokenOut` for a sell.
- Buy quotes include the bonding fee and any recipient-specific anti-snipe tax.
  Use `quoteBuyFor`, not `quoteBuy`, when the caller and recipient can differ.
- Wait for an approval receipt before submitting the trade that depends on it.
  A submitted transaction hash alone does not mean the allowance is available.
- Treat all amounts as raw integer units. Use `parseEther` only for native ETH
  or an 18-decimal asset; use `parseUnits` with the ERC-20's actual decimals.
- Set a finite slippage limit. Passing zero as the minimum output removes price
  protection.

Once `state()` returns `3`, route new trades through the launch token's Uniswap
V4 pool. See [Trade after graduation](/guides/trade-after-graduation).

Source: https://developer.clank.trade/guides/trade-on-the-bonding-curve/index.mdx
