Bonding Curves &
Dynamic Token Pricing

Solidity Tokenomics DeFi

How the smart contract becomes the market maker

The Problem with Fixed Prices

  • Gas wars — everyone fighting to be first at the same price
  • No price discovery — price is guessed, not market-driven
  • Unfair distribution — bots and whales front-run normal users
  • No built-in liquidity — need to bootstrap a DEX pool separately
What if the contract itself could set a fair price and provide liquidity?

What is a Bonding Curve?

A math function inside the contract: Price = f(Supply)

  • Buy → contract mints tokens → supply goes up → price goes up
  • Sell → contract burns tokens → supply goes down → price goes down
  • The contract holds ETH (or stablecoins) as a reserve
  • No order book, no DEX needed — the curve is the market
Mental model: a vending machine where each next item costs a little more than the last.

Four Curve Shapes

1. Linear

P = m·S + b

Steady, predictable — community tokens

2. Exponential

P = a · ekS

Cheap early, rockets later — hype launches

3. Logarithmic

P = k · ln(S + c)

Quick rise then levels off — utility tokens

4. Sigmoid (S-curve)

P = L / (1 + e−k(S−S₀))

Slow start → surge → plateau — memberships

Why Integrals? (The Key Trick)

Buying multiple tokens? Each one is priced at a different supply level.

  • Total cost = area under the curve from S to S + amount
  • Cost = ∫SS+Δ P(s) ds
  • Linear → closed-form (no loop)  |  Exp/Log/Sigmoid → integral or sum
Closed-form = one formula, O(1) gas.
Loop/sum = iterate each token, O(n) gas — fine for teaching, avoid in production.

Integral Walkthrough: Linear Example

Curve: P(s) = 0.2s + 1   Current supply S = 10, buying Δ = 5 tokens

  • Token 11: P(10) = 3.0  |  Token 12: P(11) = 3.2  |  …  |  Token 15: P(14) = 3.8
  • Loop sum: 3.0 + 3.2 + 3.4 + 3.6 + 3.8 = 17.0
  • Integral:1015(0.2s+1)ds = [0.1s²+s]1015 = (22.5+15)−(10+10) = 17.5
Discrete sum ≠ continuous integral (off by half-steps). Solidity adjusts: m*(s1+s2+1)*amount/2 + b*amount.
Same idea applies to every curve — only the integral formula changes.

Linear Curve: P = m·S + b

  • m (slope) = price growth per token  |  b (base) = price at supply 0
  • Drag the sliders to see the line tilt and shift

X = supply, Y = price. Area under a segment = total cost to mint those tokens.

Linear Solidity

Use case: community tokens, access passes — predictable price steps.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract LinearBondingToken {
    uint256 public immutable m;  // slope  (wei per token²)
    uint256 public immutable b;  // base   (wei per token)
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;

    event Bought(address indexed buyer, uint256 amount, uint256 cost);
    event Sold(address indexed seller, uint256 amount, uint256 refund);

    constructor(uint256 _m, uint256 _b) { m = _m; b = _b; }

    /// @notice Spot price at a given supply level
    function price(uint256 s) public view returns (uint256) {
        return m * s + b;
    }

    /// @notice Total cost to mint `amount` tokens (closed-form integral)
    ///  Area under line from s1 to s2:
    ///  ∫(m·s + b)ds = m/2·(s2² − s1²) + b·(s2 − s1)
    ///  With integers: m·(s1 + s2 + 1)·amount / 2 + b·amount
    function costToMint(uint256 amount) public view returns (uint256) {
        uint256 s1 = totalSupply;
        uint256 s2 = s1 + amount;
        return m * (s1 + s2 + 1) * amount / 2 + b * amount;
    }

    function mint(uint256 amount) external payable {
        uint256 cost = costToMint(amount);
        require(msg.value >= cost, "Insufficient payment");
        totalSupply += amount;
        balanceOf[msg.sender] += amount;
        emit Bought(msg.sender, amount, cost);
        if (msg.value > cost) payable(msg.sender).transfer(msg.value - cost);
    }

    function sell(uint256 amount) external {
        require(balanceOf[msg.sender] >= amount, "Not enough tokens");
        uint256 s1 = totalSupply;
        uint256 s2 = s1 - amount;
        uint256 refundAmt = m * (s1 + s2 + 1) * amount / 2 + b * amount;
        balanceOf[msg.sender] -= amount;
        totalSupply -= amount;
        emit Sold(msg.sender, amount, refundAmt);
        payable(msg.sender).transfer(refundAmt);
    }
}

The Problem: No Floats in Solidity

We need ex, ln(x), fractions — but Solidity only has integers.

  • Fixed-point trick: pick a scale factor (1e18) and treat every uint256 as having 18 implicit decimals
  • So 1e18 = 1.0,  5e17 = 0.5,  8e16 = 0.08
  • PRBMath (UD60x18 type): a library that does exp, ln, mul, div on these scaled numbers
  • UD60x18 = Unsigned Decimal, 60 integer bits, 18 fractional digits
Real number 2.5 → stored as 2_500_000_000_000_000_000 (2.5 × 1018).

PRBMath: Wrap → Math → Unwrap

import {UD60x18, ud, unwrap, exp, ln, mul, div} from "prb-math/UD60x18.sol";

// 1. WRAP: convert a raw number into UD60x18
UD60x18 a = ud(0.5e18);    // a = 0.5
UD60x18 k = ud(0.08e18);   // k = 0.08
UD60x18 s = ud(10e18);     // supply = 10

// 2. MATH: use library functions (they stay in UD60x18)
UD60x18 ks   = mul(k, s);         // 0.08 * 10 = 0.8
UD60x18 eKs  = exp(ks);           // e^0.8 ≈ 2.2255
UD60x18 price = mul(a, eKs);      // 0.5 * 2.2255 ≈ 1.1128

// 3. UNWRAP: get raw uint256 back (in wei)
uint256 priceWei = unwrap(price);  // ≈ 1_112_749_685_157_800_000
Key operations:   ud() wraps  |  mul / div for × ÷  |  exp / ln for ex / ln(x)  |  unwrap() extracts raw uint256

This pattern repeats in every expo/log/sigmoid contract below.

Exponential Curve: P = a · ekS

  • a = starting price (at S = 0)  |  k = growth rate
  • Drag k up to see how brutal late prices get

At k = 0.08, token #50 costs ~27× what token #1 cost.

Exponential Solidity

Use case: hype launches, NFT mints — strong early-adopter reward.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {UD60x18, ud, unwrap, exp, mul} from "prb-math/UD60x18.sol";

contract ExpoBondingToken {
    UD60x18 public immutable a;   // base price   (e.g. ud(0.5e18) = 0.5)
    UD60x18 public immutable k;   // growth rate   (e.g. ud(0.08e18) = 0.08)
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;

    event Bought(address indexed buyer, uint256 amount, uint256 cost);

    constructor(UD60x18 _a, UD60x18 _k) { a = _a; k = _k; }

    /// @notice Price at supply s: a * e^(k * s)
    function price(uint256 s) public view returns (uint256) {
        // wrap s into fixed-point, multiply by k, take e^, scale by a
        UD60x18 ks = mul(k, ud(s * 1e18));   // k * s  (fixed-point)
        return unwrap(mul(a, exp(ks)));        // a * e^(k*s) → raw wei
    }

    /// @notice Sum prices for each token (teaching version — O(n) gas)
    function costToMint(uint256 amount) public view returns (uint256) {
        uint256 cost;
        uint256 s = totalSupply;
        for (uint256 i = 0; i < amount; i++) {
            cost += price(s + i);
        }
        return cost;
    }

    function mint(uint256 amount) external payable {
        require(amount <= 50, "Max 50 per tx");
        uint256 cost = costToMint(amount);
        require(msg.value >= cost, "Insufficient payment");
        totalSupply += amount;
        balanceOf[msg.sender] += amount;
        emit Bought(msg.sender, amount, cost);
        if (msg.value > cost) payable(msg.sender).transfer(msg.value - cost);
    }
}
Loop is for teaching. Production: Cost = (a/k)·(ek(S+Δ) − ekS) → O(1) gas.

Logarithmic Curve: P = k · ln(S + c)

  • k = vertical stretch  |  c = shift (avoids ln 0)
  • Rises fast early, then flattens — opposite of exponential

Early growth is steep, late growth is gentle — good for utility tokens.

Logarithmic Solidity

Use case: utility tokens — bootstrap then stabilize price.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {UD60x18, ud, unwrap, ln, mul} from "prb-math/UD60x18.sol";

contract LogBondingToken {
    UD60x18 public immutable k;   // vertical scale  (e.g. ud(2e18) = 2.0)
    UD60x18 public immutable c;   // shift to avoid ln(0) (e.g. ud(1e18) = 1.0)
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;

    event Bought(address indexed buyer, uint256 amount, uint256 cost);

    constructor(UD60x18 _k, UD60x18 _c) { k = _k; c = _c; }

    /// @notice Price at supply s: k * ln(s + c)
    function price(uint256 s) public view returns (uint256) {
        UD60x18 inner = ud(s * 1e18 + unwrap(c));  // (s + c) in fixed-point
        return unwrap(mul(k, ln(inner)));            // k * ln(s + c)
    }

    /// @notice Sum prices (teaching version — use integral in prod)
    function costToMint(uint256 amount) public view returns (uint256) {
        uint256 cost;
        uint256 s = totalSupply;
        for (uint256 i = 0; i < amount; i++) {
            cost += price(s + i);
        }
        return cost;
    }

    function mint(uint256 amount) external payable {
        uint256 cost = costToMint(amount);
        require(msg.value >= cost, "Insufficient payment");
        totalSupply += amount;
        balanceOf[msg.sender] += amount;
        emit Bought(msg.sender, amount, cost);
        if (msg.value > cost) payable(msg.sender).transfer(msg.value - cost);
    }
}

Sigmoid (S-Curve): P = L / (1 + e−k(S−S₀))

  • L = ceiling price  |  k = steepness  |  S₀ = inflection point
  • Three phases: slow start → rapid ramp → plateau near L

Slide S₀ to shift the ramp. Slide k to sharpen or soften it.

Sigmoid Solidity

Use case: memberships, credits — built-in price ceiling.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {UD60x18, ud, unwrap, exp, mul, div} from "prb-math/UD60x18.sol";

contract SigmoidBondingToken {
    UD60x18 public immutable L;    // price ceiling
    UD60x18 public immutable k;    // steepness
    uint256 public immutable S0;   // inflection supply
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;

    event Bought(address indexed buyer, uint256 amount, uint256 cost);

    constructor(UD60x18 _L, UD60x18 _k, uint256 _S0) { L = _L; k = _k; S0 = _S0; }

    /// @notice Sigmoid price: L / (1 + e^(-k * (s - S0)))
    ///  Split into two branches to avoid negative exponents
    ///  (UD60x18 is unsigned — can't represent negatives)
    function price(uint256 s) public view returns (uint256) {
        if (s >= S0) {
            // s >= S0 → exponent is positive → e^(k*(s-S0))
            UD60x18 pos = exp(mul(k, ud((s - S0) * 1e18)));
            // L * pos / (1 + pos)  — approaches L as pos grows
            return unwrap(div(mul(L, pos), ud(1e18 + unwrap(pos))));
        } else {
            // s < S0 → flip sign: 1 / (1 + e^(k*(S0-s)))
            UD60x18 pos = exp(mul(k, ud((S0 - s) * 1e18)));
            // L / (1 + pos)  — approaches 0 as pos grows
            return unwrap(div(L, ud(1e18 + unwrap(pos))));
        }
    }

    function costToMint(uint256 amount) public view returns (uint256) {
        uint256 cost;
        uint256 s = totalSupply;
        for (uint256 i = 0; i < amount; i++) cost += price(s + i);
        return cost;
    }

    function mint(uint256 amount) external payable {
        uint256 cost = costToMint(amount);
        require(msg.value >= cost, "Insufficient payment");
        totalSupply += amount;
        balanceOf[msg.sender] += amount;
        emit Bought(msg.sender, amount, cost);
        if (msg.value > cost) payable(msg.sender).transfer(msg.value - cost);
    }
}

Production Checklist

  • Closed-form integrals — avoid loops, save gas
  • Reentrancy guard — state changes before transfers
  • Buy/sell spread or fee — protects reserve, deters MEV
  • Per-tx & supply caps — prevent whale manipulation
  • Pausable + Events — emergency stop; emit Bought/Sold
Teaching contracts above skip fees, guards, and caps for clarity. Add them before deploying with real money.

Risks & Gotchas

  • Front-running / MEV — bots sandwich your buy. Fix: spread/fees, max slippage.
  • Precision loss — integer math rounds badly on exp/log. Fix: PRBMath + fuzz tests.
  • Reserve drain — sell price = buy price lets large sells empty reserve. Fix: sell at discount.
  • Extreme params — large k on exp can overflow. Test with realistic supply ranges.

Which Curve Should I Use?

Linear

Fair, boring, predictable.
Community tokens, DAOs.

Exponential

FOMO rocket — reward early, punish late.
NFT drops, hype mints.

Logarithmic

Fast start, then chill.
Utility tokens, API credits.

Sigmoid

Built-in ceiling, three phases.
Memberships, bandwidth credits.

Power Curves: P = a · Sn

  • Quadratic (n=2): P = a·S² — Bancor v1, pump.fun. Aggressive, rewards early.
  • Square Root (n=0.5): P = a·√S — gentle sub-linear growth. Bancor CW=0.5.
  • Slide n to morph between them. a scales the price.

Bancor connector weight: n = 1/CW − 1. CW=0.5 → n=1 (linear). CW=0.33 → n=2 (quadratic).

Inverse & Bancor Curves

  • Inverse / Hyperbolic: P = k / (Smax − S) — price → ∞ near cap. Natural hard cap.
  • Friend.tech style: P = S² / 16000 — pure quadratic, real-world example.
  • Bancor Power: P = R / (S · CW) — CW (connector weight) controls curvature. CW=1 → constant, CW<1 → rising.

Drag Smax to shift the price wall. Drag k to scale the inverse curve.

Piecewise & Hybrid Curves

Piecewise / Step

P = Pi for tier i

  • Fixed price per supply bracket
  • Simple but creates cliff dynamics at tier boundaries
  • Used in tiered NFT mints, ICO tranches

Custom / Hybrid

P = fi(S) per segment

  • Combine curves: linear start → sigmoid mid → flat cap
  • Friend.tech: S²/16000 (quadratic segment)
  • Any monotonic f(S) can be a bonding curve
Key insight: pick the shape that matches your economic goal. You're not limited to one formula — stitch segments together.

Resources

Q&A

Discussion prompts:

  • Which curve would you pick for a DAO governance token? Why?
  • How would you add a spread/fee to the linear contract?
  • What happens if someone tries to sell all tokens at once?