Bonding Curves & Dynamic Token Pricing
Solidity Tokenomics DeFi
How the smart contract becomes the market maker
We're going to look at how bonding curves replace the old model of fixed-price token sales. Instead of every launch ending in a gas war, the contract itself sets the price using a math formula and adjusts it as people buy and sell. We'll cover four curve shapes, see them live in Desmos, then look at the Solidity.
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?
Think about a fixed-price mint: everyone sends a transaction at the same time, gas spikes, bots win. The price was arbitrary to begin with. With a bonding curve the contract replaces all of that with a single math function.
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.
The key insight: price is not set by people — it's set by supply. The contract is the counterparty for every trade. You always know what you'll pay because the formula is public and deterministic. Let's look at the four most common shapes.
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
Each shape fits a different goal. Linear is the simplest — great starting point. Exponential rewards the earliest buyers hard. Log flattens out so latecomers aren't punished. Sigmoid has a built-in ceiling. We'll go through each one with a live graph and Solidity code.
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 = ∫S S+Δ 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.
This is the most important math concept in this whole workshop. When someone mints 10 tokens, each one is priced at a different supply level. The total cost is the area under the price curve from current supply to current supply plus 10. For linear curves we can compute that area with a simple formula — that's what makes them gas-efficient. For exponential and log, we'll use a library or approximate with a loop for clarity.
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: ∫10 15 (0.2s+1)ds = [0.1s²+s]10 15 = (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.
Let's make this concrete. With m=0.2 and b=1, if current supply is 10 and someone buys 5 tokens: token 11 costs P(10)=3, token 12 costs P(11)=3.2, and so on. Summing those 5 prices gives 17. The continuous integral gives 17.5 — slightly different because the integral counts fractional areas. The Solidity formula uses the discrete version with the +1 correction. The point: one formula replaces a loop. For expo/log, the integral is harder but the principle is identical.
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.
This is the Desmos graph for the linear curve. Drag m to steepen the slope — that means each new token costs more. Drag b to raise the floor price. When someone asks "how much for 20 tokens?" it's the area under this line from current supply to supply + 20.
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);
}
}
Walk through costToMint line by line: s1 is where supply starts, s2 is where it ends, and the formula is just the discrete integral of the line. No loop — O(1) gas regardless of amount. The sell function mirrors it: same formula, but supply goes down. Point out the refund of excess ETH in mint.
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 = U nsigned D ecimal, 60 integer bits, 18 fractional digits
Real number 2.5 → stored as 2_500_000_000_000_000_000 (2.5 × 1018 ).
Before we code exponential or log curves we have to solve a fundamental problem: Solidity has no floating point. PRBMath fixes this with a convention: every number is an integer, but we agree that 1e18 means 1.0. So 0.5 is stored as 5 followed by 17 zeros. The library provides exp, ln, mul, div that all respect this convention. The type is called UD60x18 — Unsigned Decimal, 60 bits for the integer part, 18 digits for the fraction. Once you understand this encoding, the next three contracts are just plugging formulas in.
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.
Here's the concrete workflow. Step one: wrap your raw numbers with ud(). 0.5e18 means 0.5 in fixed-point. Step two: call math functions — mul, exp, ln, div — they all take and return UD60x18. Step three: unwrap() to get a regular uint256 for comparisons or storage. Walk through the numbers: k times s equals 0.8, e to the 0.8 is about 2.23, times a gives about 1.11. That's the price of the 10th token on our exponential curve with a=0.5, k=0.08. The unwrapped value in wei is that big integer. This three-step pattern — wrap, math, unwrap — is used in every remaining contract.
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.
This is the "FOMO curve". Early is cheap, late is brutal. Slide k from 0.05 to 0.2 and narrate: "if you got in at token 10 you paid almost nothing, at token 60 it's 100x." Great for NFT drops where you explicitly want to reward early supporters. Dangerous without caps.
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.
Walk through the PRBMath usage: ud() wraps a raw number, mul/exp do the math in fixed-point, unwrap() gives us back a uint256 in wei. The loop sums price at each integer supply level — clear to read, but O(n). In production you'd use the closed-form integral a/k times the difference of two exponentials. The per-tx cap of 50 is a safety rail.
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.
This curve is the inverse personality of exponential. It jumps quickly at low supply then levels off. Good for utility tokens where you want to bootstrap early demand with rising prices but don't want to punish latecomers with insane costs. c = 1 is the safe default to avoid ln(0).
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);
}
}
Same PRBMath pattern: wrap, compute, unwrap. The key detail is adding c before taking ln — that's what prevents ln(0). The integral of k·ln(s+c) is k·[(s+c)·ln(s+c) − (s+c)] — you can drop that in for O(1) gas in production.
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.
The sigmoid has three clear zones: a cheap floor zone, a transition zone where price ramps fast, and a ceiling zone where it flattens near L. Move S0 to control when the transition happens. Raise k to make the transition sharper. This is perfect when you want a soft cap — price can never exceed L.
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);
}
}
The tricky part here is that UD60x18 is unsigned — you can't compute a negative exponent directly. So we split: when s >= S0, the exponent is positive and we use L*e^x/(1+e^x). When s < S0, we flip and use L/(1+e^x). Both give the same sigmoid, just computed safely. Walk through one branch with real numbers if students look confused.
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.
None of the contracts we showed are production-ready as-is. Before deploying: replace loops with integrals, add ReentrancyGuard from OpenZeppelin, add a buy/sell spread (e.g., sell returns 95% of the curve price), add caps, and add a pause mechanism.
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.
The biggest real-world risk is MEV. A sandwich attack buys before you and sells after, pocketing the price difference. A spread (e.g., sell price = 95% of buy price) makes this unprofitable. Precision: always fuzz test with small and huge supplies. Reserve drain: never let sell return 100% — that's a bank run waiting to happen.
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.
Quick recap for decision-making: Linear if you want fairness. Exponential if you want to explicitly reward early movers. Log if you want early boost but stable long-term. Sigmoid if you need a price ceiling. You can also combine: start sigmoid then switch to linear after a cap is hit.
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).
This is the most versatile family. With one slider you can go from square root (gentle, sub-linear) through linear to quadratic (aggressive, super-linear). pump.fun uses a steep polynomial where early tokens are nearly free and late ones explode. Friend.tech is literally x squared over 16000. Bancor's connector weight maps directly to the exponent: CW=1 gives constant price, CW=0.5 gives linear, smaller CW gives steeper curves.
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.
The red curve is inverse/hyperbolic — notice how it shoots up near the max supply. That's a natural hard cap: you never need to check supply explicitly, the price itself makes minting impossible. Drag S_max to shift where the wall hits. The orange curve is the Friend.tech model — a clean quadratic. Bancor's formula P = R/(S*CW) is another way to express power curves: R is the reserve balance, S is supply, CW is a constant between 0 and 1 that controls convexity.
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.
Piecewise is the simplest approach: just define flat prices for each supply bracket. Tier 1 is 0.01 ETH for tokens 0-100, tier 2 is 0.05 ETH for 100-500, etc. The downside is the cliff at each boundary — everyone rushes to buy right before a tier change. Hybrid curves fix this by using smooth functions within each segment. Real projects mix and match: start with a gentle linear phase, transition to a sigmoid for the mid-range, then flatten at a cap. The key takeaway: any monotonic function that maps supply to price can be a bonding curve. Choose or combine based on your tokenomics goals.
Resources
PRBMath is the go-to for fixed-point math. The Speed Run Ethereum guide walks through a full implementation. OpenZeppelin for the security primitives you'll want to add.
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?
Prompt students: pick a scenario and defend a curve choice. Ask: what fee percentage would make sandwiching unprofitable? Walk through a sell-all scenario — does the contract have enough reserve?