BondingCurve is the AMM pricing engine for the Bullshot Protocol. Each token created through BullshotFactory gets its own BondingCurve clone. Trading happens on the bonding curve until the ETH reserve crosses the launchThreshold, at which point all liquidity is migrated to PancakeSwap automatically.
Key Features:
amountOutMin / deadlineThe curve uses a constant-product formula with virtual reserves:
k = virtualEthReserve × virtualTokenReserve
Initial state (at deployment):
| Variable | Value |
|---|---|
virtualTokenReserve |
1,000,000,000 ether |
tokenReserve |
800,000,000 ether |
virtualEthReserve |
6 ether |
correlation (k) |
6,000,000,000 ether² |
launchThreshold |
24 ether |
The initial price per token is approximately:
price = virtualEthReserve / virtualTokenReserve = 6 / 1,000,000,000 = 6e-9 BNB/token
| Name | Type | Value | Description |
|---|---|---|---|
FEE_DENOMINATOR |
uint16 |
1000 | Per-mille denominator for fee calculations |
| Name | Type | Visibility | Description |
|---|---|---|---|
totalSupply |
uint256 |
public | Fixed token total supply (1,000,000,000 ether) |
virtualTokenReserve |
uint256 |
public | Current virtual token reserve (decreases on buy, increases on sell) |
tokenReserve |
uint256 |
public | Real tokens remaining for sale on the curve |
virtualEthReserve |
uint256 |
public | Current virtual ETH reserve (increases on buy, decreases on sell) |
ethReserve |
uint256 |
public | Real BNB held by this contract |
correlation |
uint256 |
public | Constant k = virtualEthReserve × virtualTokenReserve |
launchThreshold |
uint256 |
public | ETH amount that triggers DEX launch (~24 BNB) |
buyFeePercent |
uint8 |
public | Buy fee per-mille (charged on top of amountIn) |
sellFeePercent |
uint8 |
public | Sell fee per-mille (deducted from amountOut) |
launchFeePercent |
uint8 |
public | Fee per-mille of the ETH pool taken at DEX launch |
feeRecipient |
address payable |
public | Fee recipient address |
uniswapV2Factory |
IUniswapV2Factory |
public | PancakeSwap factory |
uniswapV2Router |
IUniswapV2Router02 |
public | PancakeSwap router |
token |
BCToken |
public | The BCToken associated with this bonding curve |
pair |
address |
public | PancakeSwap LP pair address (address(0) until first buy or DEX launch) |
factory |
address |
public | The BullshotFactory address |
Returned by getData().
| Field | Type | Description |
|---|---|---|
totalSupply |
uint256 |
Total token supply |
tokenReserve |
uint256 |
Remaining tokens for sale |
virtualTokenReserve |
uint256 |
Current virtual token reserve |
ethReserve |
uint256 |
Real BNB held in the curve |
virtualEthReserve |
uint256 |
Current virtual ETH reserve |
launchThreshold |
uint256 |
BNB level that triggers DEX launch |
buyFeePercent |
uint8 |
Buy fee per-mille |
sellFeePercent |
uint8 |
Sell fee per-mille |
pair |
address |
PancakeSwap pair address |
balance |
uint256 |
Token balance of the queried account |
event Buy(address indexed buyer, uint amountIn, uint amountOut);
Note: This event is emitted on the BondingCurve. The BullshotFactory also proxies this to its global Buy event.
event Sell(address indexed seller, uint amountIn, uint amountOut);
Emitted when the bonding curve triggers DEX migration.
event Launch(uint tokenAmount, uint ethAmount);
Parameters:
tokenAmount: Tokens added to the PancakeSwap LPethAmount: BNB added to the PancakeSwap LP (after launch fee)function buy(
uint256 amountIn,
uint256 amountOutMin,
address to,
uint256 deadline
) external payable returns (uint256 amountOut)
Buy tokens from the bonding curve. msg.value must exactly equal amountIn + buyFee.
Parameters:
amountIn: BNB amount to spend (wei), excluding feeamountOutMin: Minimum token amount to receive (slippage protection)to: Recipient address of the purchased tokensdeadline: Unix timestamp — tx reverts if block.timestamp > deadlinemsg.value requirement:
buyFee = amountIn * buyFeePercent / FEE_DENOMINATOR
msg.value = amountIn + buyFee
Returns: amountOut — tokens received in wei
Pricing formula:
newVirtualEthReserve = virtualEthReserve + amountIn
newVirtualTokenReserve = k / newVirtualEthReserve
amountOut = virtualTokenReserve - newVirtualTokenReserve
Effects:
feeRecipientvirtualEthReserve and virtualTokenReservetoBuy (via BullshotFactory)ethReserve >= launchThreshold, triggers DEX launchDEX Launch Sequence:
token.launch() — unlocks free transfersuniswapV2Router.addLiquidityETH — migrates everything to PancakeSwapaddress(0))Launchfunction sell(
uint256 amountIn,
uint256 amountOutMin,
uint256 deadline
) external returns (uint256 amountOut)
Sell tokens back to the bonding curve.
Prerequisites:
token.approve(bondingCurveAddress, amountIn);
Parameters:
amountIn: Token amount to sell (wei)amountOutMin: Minimum BNB to receive (slippage protection)deadline: Unix timestampReturns: amountOut — BNB received (after sell fee) in wei
Pricing formula:
newVirtualTokenReserve = virtualTokenReserve + amountIn
newVirtualEthReserve = k / newVirtualTokenReserve
amountOut = virtualEthReserve - newVirtualEthReserve
sellFee = amountOut * sellFeePercent / FEE_DENOMINATOR
net BNB to seller = amountOut - sellFee
function calcBuyExactIn(uint256 amountIn) public view returns (
uint256 amountOut,
uint256 amountInMax,
uint256 amountOutMax
)
Simulates a buy: given a BNB amount, returns expected token output.
Parameters:
amountIn: BNB to spend (wei), excluding feeReturns:
amountOut: Expected tokens receivedamountInMax: Max BNB that can be spent (if amountOut would exceed tokenReserve)amountOutMax: Max tokens available (= tokenReserve if curve would be drained)function calcBuyExactOut(uint256 amountOut) public view returns (
uint256 amountIn,
uint256 amountOutMax
)
Simulates a buy: given a desired token amount, returns the BNB cost.
Returns:
amountIn: BNB required to buy amountOut tokensamountOutMax: Capped to tokenReserve if amountOut exceeds itfunction calcSellExactIn(uint256 amountIn) public view returns (
uint256 amountOut,
uint256 amountInMax,
uint256 amountOutMax
)
Simulates a sell: given a token amount, returns expected BNB output (after fee).
function calcSellExactOut(uint256 amountOut) public view returns (
uint256 amountIn,
uint256 amountOutMax
)
Simulates a sell: given a desired BNB amount, returns the token cost required.
function getData(address account) public view returns (Data memory data)
Returns a full state snapshot of the bonding curve plus the token balance of account.
function getPostLaunchPrice() public view returns (uint256)
Returns the current token price in wei from PancakeSwap reserves. Only valid after DEX launch (pair != address(0)).
Formula:
if token0 == tokenAddress:
price = reserve1 * 1e18 / reserve0
else:
price = reserve0 * 1e18 / reserve1
const bondingCurve = new ethers.Contract(bondingCurveAddress, BondingCurveABI, signer);
const amountIn = ethers.utils.parseEther("0.1"); // 0.1 BNB
const buyFeePercent = await bondingCurve.buyFeePercent();
const buyFee = amountIn.mul(buyFeePercent).div(1000);
const msgValue = amountIn.add(buyFee);
// Pre-calculate expected output
const [amountOut] = await bondingCurve.calcBuyExactIn(amountIn);
const slippage = amountOut.mul(95).div(100); // 5% slippage tolerance
const deadline = Math.floor(Date.now() / 1000) + 60; // 1 min
const tx = await bondingCurve.buy(amountIn, slippage, signerAddress, deadline, {
value: msgValue
});
await tx.wait();
const token = new ethers.Contract(tokenAddress, BCTokenABI, signer);
const bondingCurve = new ethers.Contract(bondingCurveAddress, BondingCurveABI, signer);
const amountIn = ethers.utils.parseEther("1000000"); // 1M tokens
// Approve bondingCurve to spend tokens
await token.approve(bondingCurveAddress, amountIn);
// Pre-calculate expected BNB output (after fee)
const [amountOut] = await bondingCurve.calcSellExactIn(amountIn);
const slippage = amountOut.mul(95).div(100);
const deadline = Math.floor(Date.now() / 1000) + 60;
const tx = await bondingCurve.sell(amountIn, slippage, deadline);
await tx.wait();
const data = await bondingCurve.getData(userAddress);
console.log("Token reserve:", ethers.utils.formatEther(data.tokenReserve));
console.log("ETH reserve:", ethers.utils.formatEther(data.ethReserve));
console.log("Launch threshold:", ethers.utils.formatEther(data.launchThreshold));
console.log("User balance:", ethers.utils.formatEther(data.balance));
console.log("Launched:", data.pair !== ethers.constants.AddressZero);
msg.value must equal exactly amountIn + (amountIn * buyFeePercent / 1000). Any mismatch causes "Wrong value" revert.ethReserve >= launchThreshold, the launch sequence runs within the same buy() call and cannot be reversed.address(0) — liquidity is permanently locked.ethReserve = 0 so all sells on the bonding curve will revert. Users must trade on PancakeSwap directly.tokenReserve starts at 800M (80% of supply); the remaining 200M are reserved for the PancakeSwap LP.