← Back to Blog

Bonding Curve Security: Price Manipulation, Sandwich Attacks, and Token Launch Exploits

2026-04-18 bonding curve token launch price manipulation sandwich reserve defi solidity security

Bonding curves are mathematically elegant. Given a supply S and a price function P(S), every trade is fully deterministic — no order book, no counterparty discovery, no bid-ask spread. Protocols from Pump.fun to early Bancor to countless governance token launches rely on this property.

That determinism is also the attack surface.

When every price is predictable from on-chain state, attackers can compute exactly what a pending transaction will do to the curve before it confirms. They can front-run launches at the floor price, sandwich ordinary trades with precision, exploit parameter updates, and drain reserves through uncapped owner functions. None of these attacks require zero-days — only the ability to read the mempool and bid higher gas.

This post covers six vulnerability classes in bonding curve implementations, with vulnerable Solidity, attack mechanics, and the fixes that close each one.


1. Front-Running During Token Launch

The vulnerability

A linear bonding curve starts at price P(0) — a small initial value that gives early buyers a low entry price. The launch transaction is visible in the mempool before it confirms.

// VULNERABLE: launch is a public mempool event with no front-run protection
contract BondingCurve {
    uint256 public totalSupply;
    uint256 public constant SLOPE = 1e12; // price = slope * supply
    mapping(address => uint256) public balances;

    function launch() external {
        // Curve goes live here — price is at its minimum
        totalSupply = 0;
    }

    function buy() external payable {
        uint256 price = SLOPE * totalSupply;
        uint256 tokensBought = msg.value / price;
        balances[msg.sender] += tokensBought;
        totalSupply += tokensBought;
    }
}

The attacker submits buy() with higher gas before launch() confirms:

  1. Attacker buys at totalSupply = 0 — the floor price P(0)
  2. Legitimate buyers execute at higher supply and higher price
  3. Attacker sells immediately at the elevated price

On a 10,000 ETH launch, the attacker captures 20–30% of first-block price appreciation at the expense of legitimate buyers.

The fix

Use a commit-reveal scheme or restrict initial purchases to a whitelist for a fixed window.

// FIXED: commit-reveal launch — attacker cannot predict the activation block
contract BondingCurveSecure {
    bytes32 public commitHash;
    bool public launched;
    uint256 public launchBlock;
    uint256 public constant WHITELIST_BLOCKS = 50; // ~10 min

    mapping(address => bool) public whitelist;

    function commitLaunch(bytes32 _hash) external onlyOwner {
        commitHash = _hash;
    }

    function revealLaunch(bytes32 secret) external onlyOwner {
        require(keccak256(abi.encodePacked(secret)) == commitHash, "bad reveal");
        launched = true;
        launchBlock = block.number;
    }

    function buy(uint256 minTokensOut) external payable {
        require(launched, "not launched");
        if (block.number < launchBlock + WHITELIST_BLOCKS) {
            require(whitelist[msg.sender], "whitelist only");
        }
        // ... rest of buy logic with slippage check
    }
}

2. No Slippage Protection on Curve Trades

The vulnerability

Without a minTokensOut parameter, a bonding curve buy() offers no protection against MEV bots inserting trades between submission and confirmation.

// VULNERABLE: no minTokensOut, no deadline — sandwich-able
contract BondingCurve {
    uint256 public reserve;
    uint256 public supply;

    function buy() external payable {
        uint256 tokensOut = calculateTokens(msg.value, reserve, supply);
        supply += tokensOut;
        reserve += msg.value;
        _mint(msg.sender, tokensOut);
    }

    function sell(uint256 tokenAmount) external {
        uint256 ethOut = calculateEth(tokenAmount, reserve, supply);
        supply -= tokenAmount;
        reserve -= ethOut;
        _burn(msg.sender, tokenAmount);
        payable(msg.sender).transfer(ethOut);
    }
}

A sandwich attack in three steps:

  1. Front-run: Bot buys tokens, pushing price up ~15%
  2. Victim: User's buy() executes at the inflated price, receiving 15% fewer tokens
  3. Back-run: Bot sells at the elevated price, keeping the spread

The bot earns the spread; the victim gets far fewer tokens than quoted. Without a deadline, a stale transaction can sit in the mempool for hours and execute at a wildly different price.

The fix

Add minTokensOut and deadline to every buy and sell function — the same pattern Uniswap V2 established.

// FIXED: slippage and deadline protection on all trade functions
contract BondingCurveSecure {
    uint256 public reserve;
    uint256 public supply;

    function buy(
        uint256 minTokensOut,
        uint256 deadline
    ) external payable {
        require(block.timestamp <= deadline, "expired");
        uint256 tokensOut = calculateTokens(msg.value, reserve, supply);
        require(tokensOut >= minTokensOut, "slippage exceeded");
        supply += tokensOut;
        reserve += msg.value;
        _mint(msg.sender, tokensOut);
    }

    function sell(
        uint256 tokenAmount,
        uint256 minEthOut,
        uint256 deadline
    ) external {
        require(block.timestamp <= deadline, "expired");
        uint256 ethOut = calculateEth(tokenAmount, reserve, supply);
        require(ethOut >= minEthOut, "slippage exceeded");
        supply -= tokenAmount;
        reserve -= ethOut;
        _burn(msg.sender, tokenAmount);
        payable(msg.sender).transfer(ethOut);
    }
}

Front-ends should default minTokensOut to 99% of the quoted amount and deadline to now plus five minutes.


3. Reserve Drain via Rounding Manipulation

The vulnerability

Solidity integer division always truncates. A buy computes tokensOut = ethIn * supply / reserve and a sell computes ethOut = tokenIn * reserve / supply — both round down. The per-trade error is a fraction of a wei, but an attacker contract can execute millions of round trips in a single block.

// VULNERABLE: rounding loss accumulates in reserve, drainable at scale
contract BondingCurve {
    uint256 public reserve;
    uint256 public supply;

    function buy() external payable {
        // Integer division truncates — tiny loss on every call
        uint256 tokensOut = (msg.value * supply) / reserve;
        supply += tokensOut;
        reserve += msg.value;
        _mint(msg.sender, tokensOut);
    }

    function sell(uint256 tokenAmount) external {
        // Symmetric truncation — each round trip leaks reserve
        uint256 ethOut = (tokenAmount * reserve) / supply;
        supply -= tokenAmount;
        reserve -= ethOut;
        _burn(msg.sender, tokenAmount);
        payable(msg.sender).transfer(ethOut);
    }
}

Each 1-wei buy followed by an immediate sell extracts a tiny positive difference from the reserve. At 1 million iterations — achievable in a single block from a contract — a 100 ETH reserve can leak several ETH.

The fix

Enforce a minimum trade size and verify the invariant: a full round trip must never return more than the input.

// FIXED: minimum trade size + round-trip invariant check
contract BondingCurveSecure {
    uint256 public reserve;
    uint256 public supply;
    uint256 public constant MIN_TRADE_ETH = 0.001 ether; // dust guard

    function buy(uint256 minTokensOut, uint256 deadline) external payable {
        require(msg.value >= MIN_TRADE_ETH, "below minimum trade size");
        require(block.timestamp <= deadline, "expired");
        uint256 tokensOut = (msg.value * supply) / reserve;
        require(tokensOut >= minTokensOut, "slippage exceeded");
        require(tokensOut > 0, "zero tokens out");
        supply += tokensOut;
        reserve += msg.value;
        _mint(msg.sender, tokensOut);
    }

    function sell(
        uint256 tokenAmount,
        uint256 minEthOut,
        uint256 deadline
    ) external {
        require(tokenAmount > 0, "zero amount");
        require(block.timestamp <= deadline, "expired");
        // Sell rounds DOWN — caller receives <= fair value, reserve protected
        uint256 ethOut = (tokenAmount * reserve) / supply;
        require(ethOut >= minEthOut, "slippage exceeded");
        supply -= tokenAmount;
        reserve -= ethOut;
        _burn(msg.sender, tokenAmount);
        payable(msg.sender).transfer(ethOut);
    }
}

Add a fuzz test verifying sell(buy(x)) <= x across a range of inputs.


4. Curve Parameter Front-Running (Admin Update)

The vulnerability

Many bonding curves let an admin adjust the slope, intercept, or reserve ratio. That parameter update is visible in the mempool, letting an attacker front-run with a favorable trade and back-run once the parameters change.

// VULNERABLE: instant parameter update — front-runnable
contract BondingCurve {
    uint256 public slope;
    uint256 public intercept;
    address public owner;

    constructor(uint256 _slope, uint256 _intercept) {
        slope = _slope;
        intercept = _intercept;
        owner = msg.sender;
    }

    // Owner calls this to rebalance — visible in mempool before confirmation
    function setParameters(uint256 newSlope, uint256 newIntercept)
        external
        onlyOwner
    {
        slope = newSlope;
        intercept = newIntercept;
    }

    function price(uint256 supply) public view returns (uint256) {
        return slope * supply + intercept;
    }
}

Suppose the owner increases slope to correct undervaluation. An attacker sees the pending setParameters() call and executes:

  1. Front-run: Buy at the old lower price
  2. Parameter update: Slope increases, curve shifts up
  3. Back-run: Sell at the new higher price

The attacker extracts the price delta the admin intended to correct — the larger the change, the larger the profit.

The fix

Apply a timelock to all parameter changes. A 24-hour delay makes the attack window costly to exploit.

// FIXED: timelocked parameter update — no instant front-run opportunity
contract BondingCurveSecure {
    uint256 public slope;
    uint256 public intercept;
    address public owner;

    uint256 public constant TIMELOCK_DELAY = 24 hours;

    struct PendingUpdate {
        uint256 newSlope;
        uint256 newIntercept;
        uint256 eta;
    }

    PendingUpdate public pendingUpdate;

    event ParameterUpdateQueued(uint256 newSlope, uint256 newIntercept, uint256 eta);
    event ParameterUpdateExecuted(uint256 newSlope, uint256 newIntercept);

    function queueParameterUpdate(uint256 newSlope, uint256 newIntercept)
        external
        onlyOwner
    {
        uint256 eta = block.timestamp + TIMELOCK_DELAY;
        pendingUpdate = PendingUpdate(newSlope, newIntercept, eta);
        emit ParameterUpdateQueued(newSlope, newIntercept, eta);
    }

    function executeParameterUpdate() external onlyOwner {
        require(block.timestamp >= pendingUpdate.eta, "timelock active");
        slope = pendingUpdate.slope;
        intercept = pendingUpdate.intercept;
        delete pendingUpdate;
        emit ParameterUpdateExecuted(slope, intercept);
    }
}

5. Virtual Reserve Manipulation

The vulnerability

Bancor-style curves seed a virtual reserve — protocol-owned ETH that sets the floor price before real liquidity enters. If it is too small relative to total token supply, a single large buy pushes the price arbitrarily high, inflating cost for every subsequent buyer.

// VULNERABLE: virtual reserve too small — one buyer distorts price for everyone
contract BancorCurve {
    uint256 public realReserve;
    uint256 public virtualReserve = 1 ether; // only 1 ETH virtual backing
    uint256 public supply = 1_000_000 * 1e18; // 1M tokens total supply

    function buy() external payable {
        uint256 totalReserve = realReserve + virtualReserve;
        // Bancor formula — simplified
        uint256 tokensOut = supply * (
            (1e18 + (msg.value * 1e18) / totalReserve) - 1e18
        ) / 1e18;
        realReserve += msg.value;
        supply -= tokensOut;
        _mint(msg.sender, tokensOut);
    }
}

With a 1 ETH virtual reserve and 1 million tokens, a whale buying 10 ETH pushes the price roughly 10x. Every subsequent buyer pays 10x the intended launch price, while the whale sells to newcomers at a 10x premium.

The fix

Size the virtual reserve relative to the expected raise target and cap per-transaction purchases.

// FIXED: virtual reserve sized to support expected liquidity; per-tx cap
contract BancorCurveSecure {
    uint256 public realReserve;
    // 10% of expected 100 ETH raise target
    uint256 public virtualReserve = 10 ether;
    uint256 public supply = 1_000_000 * 1e18;
    // 1% of virtual reserve per tx — limits price impact
    uint256 public constant MAX_BUY_ETH = 0.1 ether;

    function buy(uint256 minTokensOut, uint256 deadline) external payable {
        require(msg.value <= MAX_BUY_ETH, "exceeds per-tx cap");
        require(block.timestamp <= deadline, "expired");
        uint256 totalReserve = realReserve + virtualReserve;
        uint256 tokensOut = _bancorFormula(msg.value, totalReserve, supply);
        require(tokensOut >= minTokensOut, "slippage exceeded");
        realReserve += msg.value;
        supply -= tokensOut;
        _mint(msg.sender, tokensOut);
    }
}

6. Rug Pull via Reserve Extraction Function

The vulnerability

Many bonding curve contracts include an admin function to withdraw fees or recover ETH. With no withdrawal cap and no timelock, the owner can drain the entire reserve in a single transaction, leaving token holders with worthless assets.

// VULNERABLE: owner can drain 100% of reserve in one call
contract BondingCurve {
    uint256 public reserve;
    address public owner;

    modifier onlyOwner() {
        require(msg.sender == owner, "not owner");
        _;
    }

    function extractReserve(uint256 amount) external onlyOwner {
        require(amount <= reserve, "insufficient reserve");
        reserve -= amount;
        payable(owner).transfer(amount); // uncapped, no timelock
    }
}

The attack: owner calls extractReserve(reserve). All backing ETH is gone; token holders' sells revert. The only recourse is legal, not technical.

The fix

Apply two controls: a per-epoch withdrawal cap and governance ownership instead of a single EOA.

// FIXED: capped, timelocked, governance-controlled reserve extraction
contract BondingCurveSecure {
    uint256 public reserve;
    address public governance; // multisig or DAO

    uint256 public constant EPOCH_DURATION = 7 days;
    uint256 public constant MAX_EPOCH_WITHDRAWAL_BPS = 500; // 5% per epoch

    uint256 public epochStart;
    uint256 public epochWithdrawn;

    event ReserveExtracted(address indexed to, uint256 amount);

    modifier onlyGovernance() {
        require(msg.sender == governance, "not governance");
        _;
    }

    function extractReserve(uint256 amount, address to) external onlyGovernance {
        if (block.timestamp >= epochStart + EPOCH_DURATION) {
            epochStart = block.timestamp;
            epochWithdrawn = 0; // reset epoch
        }

        uint256 maxThisEpoch = (reserve * MAX_EPOCH_WITHDRAWAL_BPS) / 10_000;
        require(
            epochWithdrawn + amount <= maxThisEpoch,
            "epoch withdrawal cap exceeded"
        );

        reserve -= amount;
        epochWithdrawn += amount;
        payable(to).transfer(amount);
        emit ReserveExtracted(to, amount);
    }
}

Replacing onlyOwner with a multisig or DAO requires multiple signers and prevents unilateral extraction.


What ContractScan Detects

ContractScan detects all six vulnerability classes using static analysis, data flow tracing, and semantic pattern matching.

Vulnerability Detection Method Severity
Front-running during launch Detects public launch functions with no commit-reveal or access control on initial buy calls High
No slippage protection Flags buy/sell functions missing minTokensOut and deadline parameters High
Reserve drain via rounding Identifies curves where sell returns can exceed buy inputs on repeated micro-trades Medium
Curve parameter front-running Detects onlyOwner parameter setters applied without timelock or delay mechanism High
Virtual reserve manipulation Flags Bancor-style curves where virtual reserve is disproportionately small vs token supply Medium
Rug pull reserve extraction Detects unrestricted transfer/send calls from reserve balance gated only by onlyOwner Critical

Scan your bonding curve contract at contractscan.io before deployment.


Important Notes

This post is for informational and educational purposes only. It does not constitute financial, legal, or investment advice. The security analysis provided is based on available data and automated tools, which may not capture all potential vulnerabilities. Always conduct a professional audit before deploying smart contracts.

Scan your contract for this vulnerability
Free QuickScan — Unlimited quick scans. No signup required.. No signup required.
Scan a Contract →