Math for Smart Contracts

Curves, Fixed-Point & PRBMath

Pre-requisite Solidity Math

From middle-school math to reading PRBMath source code

What You Need to Know

  • ✓ Basic Solidity: uint256, functions, msg.value
  • ✓ What a smart contract is and how to deploy one
  • ✗ No calculus needed
  • ✗ No prior math library experience needed
By the end: you'll understand curves as functions, why floats don't exist in Solidity, how to use PRBMath, and how it works under the hood.

Part 1: What Is a Function?

A function is a machine: input → rule → output

f(x) = 2x + 3

Input (x)Output f(x)
03
15
513
1023

Same thing in Solidity:

function f(uint256 x) pure returns (uint256) {
    return 2 * x + 3;
}
Every Solidity view/pure function that takes a number and returns a number is a math function.

Graphing a Function

A graph plots every (input, output) pair as a dot. Connect the dots → a line or curve.

X-axis = input  |  Y-axis = output  |  Drag m to tilt, b to shift  |  f(x) = m·x + b → in bonding curves: x = supply, y = price

Slope: How Fast Does It Rise?

slope = Δy / Δx

  • m = 2 → steep, output doubles per unit
  • m = 0.1 → gentle, barely changes
  • m = 0 → flat line (fixed price!)
In DeFi: slope = how much price increases per token minted. Try m = 0 vs m = 1.

Straight Lines vs. Curves

A line has constant slope. A curve has changing slope.

Blue (linear): same rate  |  Orange (expo): accelerates  |  Green (log): slows down  |  Each shape = a different economic personality.

Part 2: The Five Curves You'll See in DeFi

Linear

y = mx + b

Constant growth

Quadratic

y = ax²

Accelerating growth

Square Root

y = √x

Decelerating growth

Exponential

y = ex

Explosive growth

Logarithmic

y = ln(x)

Diminishing growth

Linear: y = mx + b

The idea: output grows at a constant rate.

xy (m=0.5, b=1)Δy
01.0
22.0+1.0
43.0+1.0
64.0+1.0
85.0+1.0
Δy is always the same → straight line.
// Solidity: linear pricing
function price(uint256 supply)
    pure returns (uint256)
{
    // m = 0.5 (as 5e17 in fixed-point)
    // b = 1   (as 1e18 in fixed-point)
    return supply * 5e17 / 1e18 + 1;
    // simplified: supply / 2 + 1
}

DeFi use: community tokens, DAOs. Fair and predictable — every token adds the same price bump.

Quadratic: y = ax²

The idea: the further right, the faster it grows.

xy (a=1)Δy
11
24+3
39+5
525+9
Δy increases each step → curved upward.

DeFi use: Friend.tech (P = S²/16000), pump.fun. Rewards early buyers aggressively.

Square Root: y = √x

The idea: fast early rise, then flattens.

xy = √xΔy
11.0
42.0+1.0
164.0+1.0
10010.0
To double the output you must quadruple the input.

DeFi use: Bancor continuous tokens (CW = 0.5). Rewards early growth but doesn't punish latecomers.

Exponential: y = ex

The idea: output multiplies rather than adds.

xy = exRatio
01.0
12.7×2.7
320.1×2.7
5148.4×2.7
Each step multiplies by 2.7. By x=5, it's 148× the start!

What is e? ≈ 2.71828 — a special constant. Like π, it shows up everywhere in growth/decay.

Sidebar: What Is e?

You know π ≈ 3.14 (circles). Meet e ≈ 2.718 (growth).

  • Imagine $1 in a bank at 100% annual interest
  • Compounded once: $1 × 2 = $2.00
  • Compounded twice: $1 × 1.5² = $2.25
  • Compounded daily: $1 × (1 + 1/365)365 = $2.714...
  • Compounded infinitely: $1 × e = $2.71828...
e = the limit of (1 + 1/n)n as n → ∞.
It's the base rate of continuous growth. That's why ekx means "grow continuously at rate k".

Logarithm: y = ln(x)

The idea: inverse of expo. "What exponent gives me x?"

xy = ln(x)Δy
10.00
102.30+2.30
1004.61+2.31
1→10 gains 2.3. 10→100 gains just 2.3 more. Logarithmic = diminishing returns.

DeFi use: utility tokens. Fast early price growth, then stable — latecomers aren't punished.

All Five Together

  • Linear  |  Quadratic  |  Square Root  |  Exponential  |  Logarithmic
Same input range, wildly different outputs. Choosing a curve = choosing economics.

Part 3: Area Under a Curve

When you buy multiple tokens, each one costs a different price. How do you find the total cost?

Total cost = area under the curve between your start and end supply.

Shaded area = total ETH to buy tokens from supply 2 to 8.

Counting Rectangles (The Loop Way)

Curve: P(s) = 0.5s + 1  |  Buy tokens #3 – #7 (5 tokens)

TokensPrice
322.0
432.5
543.0
653.5
764.0

Total = 2+2.5+3+3.5+4 = 15.0

Solidity: for (i=0; i<5; i++) cost += price(s+i);
Works, but O(n) gas — expensive for large buys.

The Shortcut: One Formula

Linear curve → area = trapezoid: Area = (top + bottom) × width / 2

Bottom = P(2) = 2.0  |  Top = P(6) = 4.0  |  Width = 5

= (2.0 + 4.0) × 5 / 2 = 15.0

One multiplication replaces the entire loop.
O(1) gas instead of O(n). Works for any amount!
Every curve has its own shortcut (the "integral"). Linear: trapezoid. Exponential: (a/k)·(ekS2−ekS1). You don't derive them — just plug in.

Part 4: Solidity Can't Do Decimals

Quick: what's the result of these Solidity operations?

uint256 a = 7 / 2;    // = ?
uint256 b = 1 / 3;    // = ?
uint256 c = 5 / 10;   // = ?
a = 3, b = 0, c = 0 — Solidity throws away the decimal part!
  • 7/2 should be 3.5 → Solidity gives you 3 (truncates toward zero)
  • 1/3 should be 0.333... → Solidity gives you 0
  • No float, no double, no decimal type exists in Solidity

The Trick: Pretend Integers Are Decimals

What if we agree: "1e18 means 1.0"?

Real NumberFixed-Point (×1018)Solidity Literal
1.01,000,000,000,000,000,0001e18
0.5500,000,000,000,000,0005e17
0.0880,000,000,000,000,0008e16
2.52,500,000,000,000,000,00025e17
3.141593,141,590,000,000,000,0003141590000000000000
This is called fixed-point arithmetic. We multiply every number by 1018 before storing it. The 18 zeros are the "decimal places".

Fixed-Point Arithmetic: The Rules

Addition & Subtraction

Just add/subtract normally:

// 1.5 + 0.3 = 1.8
uint256 a = 15e17; // 1.5
uint256 b = 3e17;  // 0.3
uint256 c = a + b; // 18e17 = 1.8 ✓
Addition & subtraction: no adjustment needed.

Multiplication

Divide by 1e18 after multiplying:

// 1.5 × 0.3 = 0.45
uint256 a = 15e17; // 1.5
uint256 b = 3e17;  // 0.3
uint256 c = a * b / 1e18;
// = 45e34 / 1e18 = 45e16 = 0.45 ✓
Multiplication: must divide by 1e18 to fix the double scaling.

Fixed-Point: Division Trap

// WRONG: 3.0 / 2.0 = ???
uint256 a = 3e18;  // 3.0
uint256 b = 2e18;  // 2.0
uint256 wrong = a / b;        // = 1  (lost all decimals!)

// RIGHT: multiply FIRST, then divide
uint256 right = a * 1e18 / b; // = 3e36 / 2e18 = 15e17 = 1.5 ✓
Rule: for fixed-point division, multiply the numerator by 1e18 before dividing.
OperationFormulaWhy
a + ba + bScales match
a − ba - bScales match
a × ba * b / 1e18Undo double-scaling
a ÷ ba * 1e18 / bPre-scale numerator

Danger: Overflow

// uint256 max = 2^256 - 1 ≈ 1.16 × 10^77

// Multiplying two large fixed-point numbers:
uint256 a = 1000e18; // 1000.0
uint256 b = 1000e18; // 1000.0
// a * b = 1e42 → still fits in uint256 ✓

// But three multiplications chained:
// a * b * c before dividing = 1e63 → might overflow!

// Safe approach: divide between multiplications
uint256 ab = a * b / 1e18;  // 1e42 / 1e18 = 1e24 ✓
uint256 result = ab * c / 1e18; // stays manageable
Rule of thumb: always divide by 1e18 between each multiplication, not all at the end.

PRBMath handles this for you internally — that's a big reason to use it.

Part 5: PRBMath — The Library

PRBMath gives you exp, ln, sqrt, pow, mul, div — all in fixed-point.

Without PRBMath

// How do you compute e^(0.08)?
// You can't. Solidity has no exp().
// You'd need a Taylor series:
// e^x ≈ 1 + x + x²/2 + x³/6 + ...
// Each term needs careful fixed-point
// mul and div with overflow checks.
// 20+ lines of tricky math code.

With PRBMath

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

UD60x18 result = exp(ud(0.08e18));
// result ≈ 1.0833e18
// Done. One line.
PRBMath wraps all the overflow-safe, precision-maximizing fixed-point math behind simple function calls.

UD60x18: The Type

Unsigned Decimal, 60 integer bits, 18 fractional digits
  • It's still a uint256 under the hood — just wrapped in a custom type
  • The 18 digits = same precision as ERC-20 tokens (ETH has 18 decimals)
  • 60 integer bits = up to ~1.15 × 1018 before the decimal point
  • Unsigned = no negative numbers (there's SD59x18 for signed)
// UD60x18 is just a wrapper around uint256
type UD60x18 is uint256;

// The "ud" function wraps a raw uint256 into this type
function ud(uint256 x) pure returns (UD60x18) {
    return UD60x18.wrap(x);
}

// The "unwrap" function gets the raw uint256 back
function unwrap(UD60x18 x) pure returns (uint256) {
    return UD60x18.unwrap(x);
}

Think of UD60x18 as a "labeled box" that says "I'm a fixed-point number — use special math on me".

The Three-Step Pattern

Every PRBMath computation follows the same workflow:

1 WRAP raw numbers into UD60x18   →   2 MATH using library functions   →   3 UNWRAP back to uint256
import {UD60x18, ud, unwrap, exp, mul} from "prb-math/UD60x18.sol";

// STEP 1: WRAP — convert raw numbers to fixed-point
UD60x18 base  = ud(0.5e18);   // 0.5 in fixed-point
UD60x18 rate  = ud(0.08e18);  // 0.08 in fixed-point
UD60x18 supply = ud(10e18);   // 10 in fixed-point

// STEP 2: MATH — compute using library functions
UD60x18 exponent = mul(rate, supply);  // 0.08 × 10 = 0.8
UD60x18 growth   = exp(exponent);       // e^0.8 ≈ 2.2255
UD60x18 price    = mul(base, growth);   // 0.5 × 2.2255 ≈ 1.1128

// STEP 3: UNWRAP — get raw uint256 back
uint256 priceWei = unwrap(price);       // ≈ 1_112_749_685_157_800_000
You never do * or / directly on UD60x18. Always use mul(), div(), exp(), ln().

PRBMath Cheat Sheet

OpCodeExample
Wrapud(n)ud(2e18) = 2.0
Unwrapunwrap(x)raw uint256
a × bmul(a,b)2.0 × 3.0 = 6.0
a ÷ bdiv(a,b)6.0 ÷ 2.0 = 3.0
OpCodeExample
exexp(x)exp(1.0) ≈ 2.718
ln(x)ln(x)ln(e) ≈ 1.0
√xsqrt(x)sqrt(4.0) = 2.0
xypow(x,y)pow(2.0, 3.0) = 8.0
Import: import {UD60x18, ud, unwrap, exp, ln, mul, div, sqrt, pow} from "prb-math/UD60x18.sol";

Hands-On: Build a Linear Price Function

Let's build price(s) = 0.2 · s + 1 using PRBMath.

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

contract LinearPrice {
    UD60x18 public immutable slope; // m = 0.2
    UD60x18 public immutable base;  // b = 1.0

    constructor() {
        slope = ud(0.2e18);  // 0.2 in fixed-point
        base  = ud(1e18);    // 1.0 in fixed-point
    }

    function price(uint256 supply) public view returns (uint256) {
        // Step 1: WRAP supply
        UD60x18 s = ud(supply * 1e18);

        // Step 2: MATH — m * s + b
        UD60x18 ms = mul(slope, s);     // 0.2 × supply
        UD60x18 result = ms + base;     // + operator works for UD60x18 addition!

        // Step 3: UNWRAP
        return unwrap(result);
    }
}

// price(0)  → 1.0e18  (= 1.0 ETH)
// price(10) → 3.0e18  (= 3.0 ETH)
// price(20) → 5.0e18  (= 5.0 ETH)
Addition works with + because UD60x18 overloads the operator. Multiplication needs mul().

Hands-On: Build an Exponential Price Function

price(s) = 0.5 · e0.08·s — the "FOMO curve".

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

contract ExpoPrice {
    UD60x18 public immutable a; // base = 0.5
    UD60x18 public immutable k; // rate = 0.08

    constructor() {
        a = ud(0.5e18);   // 0.5
        k = ud(0.08e18);  // 0.08
    }

    function price(uint256 supply) public view returns (uint256) {
        // 1. WRAP
        UD60x18 s = ud(supply * 1e18);

        // 2. MATH
        UD60x18 ks    = mul(k, s);       // 0.08 × supply
        UD60x18 expKs = exp(ks);          // e^(0.08 × supply)
        UD60x18 result = mul(a, expKs);   // 0.5 × e^(0.08 × supply)

        // 3. UNWRAP
        return unwrap(result);
    }
}

// price(0)  → 0.5e18    (= 0.50 ETH  — cheap!)
// price(10) → 1.1127e18 (= 1.11 ETH)
// price(50) → 27.29e18  (= 27.29 ETH — expensive!)
// price(100)→ 1490.47e18(= 1490 ETH — insane!)

Hands-On: Build a Logarithmic Price Function

price(s) = 2 · ln(s + 1) — the "fair growth" curve.

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

contract LogPrice {
    UD60x18 public immutable k; // scale = 2.0
    UD60x18 public immutable c; // shift = 1.0 (avoids ln(0))

    constructor() {
        k = ud(2e18);
        c = ud(1e18);
    }

    function price(uint256 supply) public view returns (uint256) {
        // 1. WRAP
        UD60x18 s = ud(supply * 1e18);

        // 2. MATH
        UD60x18 inner = s + c;            // supply + 1 (avoids ln(0)!)
        UD60x18 logVal = ln(inner);        // ln(supply + 1)
        UD60x18 result = mul(k, logVal);   // 2 × ln(supply + 1)

        // 3. UNWRAP
        return unwrap(result);
    }
}

// price(0)  → 0.00e18  (= 0 ETH — free!)
// price(1)  → 1.39e18  (= 1.39 ETH — quick rise)
// price(10) → 4.79e18  (= 4.79 ETH)
// price(100)→ 9.23e18  (= 9.23 ETH — barely moved!)
// price(1000)→13.82e18 (= 13.82 ETH — still gentle)

Common PRBMath Mistakes

MistakeWhat HappensFix
ud(10) instead of ud(10e18) Wraps 10 as 0.00000000000000001 Always multiply by 1e18 first
a * b on UD60x18 Compiler error (no * operator) Use mul(a, b)
ln(ud(0)) Reverts (ln(0) = -∞) Add shift: ln(s + c)
Forgetting unwrap() Type mismatch: UD60x18 vs uint256 Unwrap before returning/comparing
exp(ud(100e18)) Overflow (e^100 is astronomical) Keep exponents small (< 133e18)
#1 mistake: forgetting the e18 when wrapping. ud(5)ud(5e18)!

Part II: PRBMath Internals

How the Math Actually Works Under the Hood

Now that we know how to USE PRBMath, let's open the hood and read the source code.

What We'll Cover

6 Repo Architecture & Types

7 mul() — Phantom Overflow

8 div() — Scale-First Pattern

9 mulDiv() — The 512-bit Core

10 exp() — Bit Decomposition

11 ln() — Binary Logarithm

12 sqrt() — Newton's Method

13 pow() & Gas Costs

Goal: Read PRBMath source code confidently. Know why each trick is needed, not just what it does.

Part 6: Repo Architecture

PRBMath v4 — github.com/PaulRBerg/prb-math

Two Type Families

UD60x18

Unsigned Decimal

60 integer digits, 18 decimals

Range: 0 → ~1.16 × 1059

type UD60x18 is uint256;
// 1.0 = 1_000_000_000_000_000_000
// stored as a plain uint256

SD59x18

Signed Decimal

59 integer digits, 18 decimals

Range: ~-5.8 × 1058 → +5.8 × 1058

type SD59x18 is int256;
// -1.0 = -1_000_000_000_000_000_000
// uses two's complement
Both are user-defined value types (Solidity 0.8.8+). Zero runtime cost — just a uint256 / int256 underneath.

File Structure (v4)

prb-math/
├── src/
│   ├── Common.sol          ← mulDiv, mulDiv18, sqrt (shared)
│   ├── ud60x18/
│   │   ├── Casting.sol     ← wrap(), unwrap(), ud()
│   │   ├── Constants.sol   ← UNIT, MAX, PI, E
│   │   ├── Conversions.sol ← convert(), from UD to SD
│   │   ├── Errors.sol      ← PRBMath_UD60x18_*
│   │   ├── Helpers.sol     ← add, sub (using + -)
│   │   ├── Math.sol        ← mul, div, exp, exp2, ln,
│   │   │                      log2, log10, sqrt, pow, avg
│   │   └── ValueType.sol   ← type UD60x18 is uint256
│   └── sd59x18/            ← (mirrors ud60x18 structure)
└── test/
We'll focus on Common.sol and ud60x18/Math.sol — that's where all the algorithms live.

The UNIT Constant

/// @dev The unit number, which gives the decimal precision of UD60x18.
uint256 constant uUNIT = 1e18;
UD60x18 constant UNIT = UD60x18.wrap(1e18);

Every function in PRBMath revolves around this single idea:

real_value = stored_uint256 / 1018

Real ValueStored uint256
1.01_000_000_000_000_000_000
3.143_140_000_000_000_000_000
0.0011_000_000_000_000_000
Why 1018? Matches ETH's wei precision. 1 ETH = 1018 wei. No conversion needed when working with token amounts.

Part 7: mul() — Phantom Overflow

The simplest operation reveals the deepest trick.

The Problem with Naive Multiply

// ❌ WRONG — overflows!
function naiveMul(UD60x18 x, UD60x18 y) pure returns (UD60x18) {
    return UD60x18.wrap(
        UD60x18.unwrap(x) * UD60x18.unwrap(y) / 1e18
    );
}

// x = 2.0 → stored as 2e18
// y = 3.0 → stored as 3e18
// x * y = 2e18 * 3e18 = 6e36  ← PHANTOM OVERFLOW!
// 6e36 / 1e18 = 6e18 = 6.0   ← correct answer…
//                              IF it doesn't overflow first
Phantom overflow: the intermediate x * y value exceeds uint256 even though the final result fits. uint256.max ≈ 1.16e77, but two large UD60x18 values multiplied can reach ~1.34e77.

PRBMath's mul() — Actual Source

/// @notice Multiplies two UD60x18 numbers together,
/// returning a new UD60x18 number.
function mul(UD60x18 x, UD60x18 y) pure returns (UD60x18 result) {
    result = wrap(mulDiv18(unwrap(x), unwrap(y)));
}

Just 1 line! The real work is in mulDiv18.

mulDiv18(a, b) computes (a × b) / 1e18 without intermediate overflow.
It's a specialized version of the general mulDiv(a, b, denominator).

Part 8: div() — Scale First

Division has the opposite problem of multiplication.

The Problem with Naive Division

// ❌ WRONG — loses ALL precision!
function naiveDiv(UD60x18 x, UD60x18 y) pure returns (UD60x18) {
    return UD60x18.wrap(
        UD60x18.unwrap(x) / UD60x18.unwrap(y)
    );
}

// x = 1.0 → stored as 1e18
// y = 3.0 → stored as 3e18
// x / y = 1e18 / 3e18 = 0     ← integer division truncates!
// Expected: 0.333...e18
Precision loss: dividing two same-scale numbers cancels out the scale factor, giving you an unscaled integer (often 0).

PRBMath's div() — Scale First

/// @notice Divides two UD60x18 numbers, returning a UD60x18 number.
function div(UD60x18 x, UD60x18 y) pure returns (UD60x18 result) {
    result = wrap(mulDiv(unwrap(x), uUNIT, unwrap(y)));
}

Trick: compute (x × 1e18) / y instead of x / y.

Step by step

1 x = 1e18 (represents 1.0)

2 Multiply by 1e18 → 1e36

3 Divide by 3e18 → 333...e15

4 Result ≈ 0.333e18 ✓

By scaling up before dividing, we preserve 18 decimals of precision. And mulDiv ensures the intermediate x × 1e18 doesn't overflow.

Part 9: mulDiv() — The 512-bit Heart

The most important function in PRBMath.

~80 lines of Yul assembly that make everything else possible.

mulDiv — What It Computes

mulDiv(x, y, d) = ⌊(x × y) / d⌋

…without intermediate overflow, even when x × y exceeds uint256!

Key insight: use a 512-bit intermediate. The EVM is 256-bit, so PRBMath simulates 512-bit math using two uint256 variables: prod0 (low bits) and prod1 (high bits).

Based on Remco Bloemen's algorithm & "Hacker's Delight" by Henry S. Warren.

mulDiv — Step 1: 512-bit Product

// Compute the 512-bit product [prod1 prod0]
uint256 prod0;  // low  256 bits of x * y
uint256 prod1;  // high 256 bits of x * y
assembly {
    let mm := mulmod(x, y, not(0))  // x*y mod 2^256
    prod0 := mul(x, y)              // low bits (wraps)
    prod1 := sub(sub(mm, prod0), lt(mm, prod0))
}

1 mul(x,y) gives low 256 bits (EVM wraps)

2 mulmod(x,y,2²⁵⁶-1) gives x*y mod (2²⁵⁶-1)

3 Subtract to get prod1 (high bits)

Why mulmod? The EVM MULMOD opcode internally uses a 512-bit intermediate — PRBMath exploits this to extract the high bits!

mulDiv — Step 2: Fast Path

// Handle cases where product fits in 256 bits
if (prod1 == 0) {
    return prod0 / denominator;
}
If prod1 == 0, the product fits in 256 bits. Just do a normal division. This is the common case for small-to-medium values — almost free! ~40 gas

The expensive path (below) only runs when values are large enough to actually overflow 256 bits.

mulDiv — Step 3: 512-bit Division

When prod1 > 0, things get interesting:

// Make sure the result is less than 2^256
require(denominator > prod1);

// Subtract 256 bit number from 512 bit number
assembly {
    // Compute remainder using mulmod
    let remainder := mulmod(x, y, denominator)
    // Subtract remainder from 512-bit product
    prod1 := sub(prod1, gt(remainder, prod0))
    prod0 := sub(prod0, remainder)
}
After subtracting the remainder, [prod1:prod0] is now evenly divisible by denominator. The problem reduces to dividing a 512-bit number by a 256-bit divisor.

mulDiv — Step 4: Factor Out Powers of 2

// Factor powers of two out of denominator
// Compute largest power of two divisor of denominator.
uint256 twos = denominator & (~denominator + 1);
assembly {
    // Divide denominator by twos
    denominator := div(denominator, twos)
    // Divide [prod1:prod0] by twos
    prod0 := div(prod0, twos)
    // Flip twos such that it is 2^256 / twos.
    // If twos is zero, then it becomes one.
    twos := add(div(sub(0, twos), twos), 1)
}
// Shift bits from prod1 into prod0
prod0 |= prod1 * twos;
twos = d & (-d) isolates the lowest set bit — a classic Hacker's Delight trick. This factors out all powers of 2 from the denominator, simplifying the division.

mulDiv — Step 5: Modular Inverse

// Compute the modular inverse of denominator mod 2^256
// using Newton-Raphson iteration.
// Start with a seed that is correct for four bits.
uint256 inverse = (3 * denominator) ^ 2;

// Use Newton-Raphson to double precision each step
inverse *= 2 - denominator * inverse; // 8  bits
inverse *= 2 - denominator * inverse; // 16 bits
inverse *= 2 - denominator * inverse; // 32 bits
inverse *= 2 - denominator * inverse; // 64 bits
inverse *= 2 - denominator * inverse; // 128 bits
inverse *= 2 - denominator * inverse; // 256 bits

// Final result
result = prod0 * inverse;
Division by d becomes multiplication by d's modular inverse. 6 multiplications instead of an expensive division. Each iteration doubles the precision.

mulDiv — Complete Picture

1 512-bit multiply

Use MULMOD to get high bits

2 Fast path check

prod1 == 0 → normal div

3 Subtract remainder

Make evenly divisible

4 Factor out 2's

d & (-d) trick

5 Modular inverse

Newton-Raphson × 6 iter

6 Multiply

prod0 × inverse = result

Gas cost: ~140 gas for the full 512-bit path, ~40 gas for the fast path.
For comparison: a single SSTORE is 20,000 gas. mulDiv is cheap.

Part 10: exp() — Computing ex

The hardest function. No Taylor series — too expensive.

exp() Strategy: Change of Base

PRBMath does not compute ex directly. Instead:

ex = 2x × log₂(e)

❌ Taylor series
ex = 1 + x + x²/2! + x³/3! + ...
Needs ~20 terms → 20 mulDivs → ~3000 gas
✓ Base-2 decomposition
Convert to exp2() which uses bit tricks
Much fewer operations → ~1600 gas
function exp(UD60x18 x) pure returns (UD60x18 result) {
    // x_192x64 = x * log2(e), scaled to 192.64 format
    // then call exp2()
    result = wrap(exp2(mulDiv18(unwrap(x), uLOG2_E)));
}

exp2() — The Bit Decomposition Trick

To compute 2y, split y into integer + fraction:

25.75 = 25 × 20.75

Integer part: 25

// Just a bit shift!
result = 1 << integerPart;
// 2^5 = 32 = 1 << 5
// Cost: 3 gas (SHL opcode)

Fractional part: 20.75

This is the hard part. PRBMath uses a product of precomputed factors.

// 0.75 in binary = 0.110000...
// 2^0.75 = 2^0.5 × 2^0.25
//        = √2   × ⁴√2
Key insight: any binary fraction is a sum of negative powers of 2. So 2frac = product of precomputed 21/2ⁿ values for each set bit.

exp2() — Actual Code (Simplified)

// The fractional part in 192.64-bit fixed-point
uint256 x = xValue;  // the fractional bits

// For each bit position, multiply by precomputed 2^(1/2^n)
if (x & 0x8000000000000000 > 0)    // bit 63: 2^(1/2)
    result = (result * 0x16A09E667F3BCC909) >> 64;
if (x & 0x4000000000000000 > 0)    // bit 62: 2^(1/4)
    result = (result * 0x1306FE0A31B7152DF) >> 64;
if (x & 0x2000000000000000 > 0)    // bit 61: 2^(1/8)
    result = (result * 0x1172B83C7D517ADCE) >> 64;
if (x & 0x1000000000000000 > 0)    // bit 60: 2^(1/16)
    result = (result * 0x10B5586CF9890F62A) >> 64;
// ... continues for all 64 bits ...
if (x & 0x2 > 0)                   // bit 1: 2^(1/2^63)
    result = (result * 0x10000000000000001) >> 64;
if (x & 0x1 > 0)                   // bit 0: 2^(1/2^64)
    result = (result * 0x10000000000000001) >> 64;
64 if checks, each with 1 multiply + 1 shift. No loops, no branches — pure branchless math. ~1500 gas total

Part 11: ln() — Binary Logarithm

Same trick in reverse: ln(x) = log₂(x) / log₂(e)

ln() → log2() → Bit Extraction

function ln(UD60x18 x) pure returns (UD60x18 result) {
    // ln(x) = log2(x) / log2(e)
    // But we compute: log2(x) * (1/log2(e)) = log2(x) * ln(2)
    unchecked {
        result = wrap(mulDiv(log2(unwrap(x)), uLOG2_E_INV, uUNIT));
    }
}

Just like exp() delegates to exp2(), ln() delegates to log2().

log2() is where the real algorithm lives. It finds the binary logarithm by extracting bits one at a time — from integer part down to fractional part.

log2() — Integer Part

// n = integer part of log2(x)
// Find the highest set bit using binary search
uint256 n;
if (xUint >= 2**128) { xUint >>= 128; n += 128; }
if (xUint >= 2**64)  { xUint >>= 64;  n += 64;  }
if (xUint >= 2**32)  { xUint >>= 32;  n += 32;  }
if (xUint >= 2**16)  { xUint >>= 16;  n += 16;  }
if (xUint >= 2**8)   { xUint >>= 8;   n += 8;   }
if (xUint >= 2**4)   { xUint >>= 4;   n += 4;   }
if (xUint >= 2**2)   { xUint >>= 2;   n += 2;   }
if (xUint >= 2**1)   { n += 1; }
Binary search for the highest set bit — finds floor(log₂(x)) in exactly 8 comparisons. This is the same algorithm the EVM itself uses internally. ~80 gas

log2() — Fractional Part

// y = x / 2^n  (normalize to range [1, 2))
// Now compute fractional bits by repeated squaring
int256 resultInt;
// ...after integer part calculation...

// Compute 64 fractional bits, one at a time
y = (y * y) >> 127;        // square and check
if (y >= 2 * UNIT) {       // if ≥ 2.0...
    resultInt |= (1 << 63); // ...set bit 63
    y >>= 1;               // normalize back
}
y = (y * y) >> 127;
if (y >= 2 * UNIT) {
    resultInt |= (1 << 62); // set bit 62
    y >>= 1;
}
// ... repeat for all 64 bits ...
Repeated squaring: if y² ≥ 2, then the next log₂ bit is 1 (divide by 2 and continue). Otherwise it's 0. One bit per iteration, 64 iterations for 64-bit precision.

Part 12: sqrt() — Newton's Method

The oldest algorithm in PRBMath — over 3,000 years old.

sqrt() — The Babylonian Method

To find √x, repeatedly improve a guess: g' = (g + x/g) / 2

function sqrt(uint256 x) pure returns (uint256 result) {
    if (x == 0) return 0;

    // Initial guess: closest power of 2 to √x
    // (uses the same binary search as log2)
    result = 1;
    uint256 xAux = x;
    if (xAux >= 2**128) { xAux >>= 128; result <<= 64; }
    if (xAux >= 2**64)  { xAux >>= 64;  result <<= 32; }
    if (xAux >= 2**32)  { xAux >>= 32;  result <<= 16; }
    if (xAux >= 2**16)  { xAux >>= 16;  result <<= 8; }
    if (xAux >= 2**8)   { xAux >>= 8;   result <<= 4; }
    if (xAux >= 2**4)   { xAux >>= 4;   result <<= 2; }
    if (xAux >= 2**2)   { result <<= 1; }

    // Newton-Raphson: 7 iterations (doubles bits each time)
    result = (result + x / result) >> 1; // ~8 bits
    result = (result + x / result) >> 1; // ~16 bits
    result = (result + x / result) >> 1; // ~32 bits
    result = (result + x / result) >> 1; // ~64 bits
    result = (result + x / result) >> 1; // ~128 bits
    result = (result + x / result) >> 1; // ~256 bits
    result = (result + x / result) >> 1; // exact

    // Round down
    if (result > x / result) result = x / result;
}

sqrt() for UD60x18 — Scale Adjustment

/// @notice Calculates the square root of x using the
/// Babylonian method.
function sqrt(UD60x18 x) pure returns (UD60x18 result) {
    uint256 xUint = unwrap(x);
    if (xUint > uMAX_UD60x18 / uUNIT) revert ...;
    // Multiply by UNIT (1e18) for scale adjustment,
    // then take integer sqrt
    result = wrap(prbSqrt(xUint * uUNIT));
}

Why multiply by 1e18?

√(x × 1e18) = √x × √(1e18) = √x × 1e9

But we need √x × 1e18 (UD60x18 format).

So: √(x × 1e18 × 1e18) = √(x × 1e36) = √x × 1e18 ✓

Input x is already scaled by 1e18. Multiply by another 1e18 before sqrt, so the result comes out correctly scaled.

Part 13: pow() & Gas Costs

Putting it all together.

pow() — Just exp(y × ln(x))

/// @notice Raises x to the power of y.
function pow(UD60x18 x, UD60x18 y)
    pure returns (UD60x18 result)
{
    uint256 xUint = unwrap(x);
    uint256 yUint = unwrap(y);

    if (xUint == 0) return y == ZERO ? UNIT : ZERO;
    if (yUint == uUNIT) return x;

    // x^y = exp(y * ln(x))
    // = exp2(y * log2(x))    ← base-2 version
    result = exp2(mul(y, log2(x)));
}
pow() is the most expensive function because it calls both log2 and exp2. But it's still just ~3000 gas — 15% of a single SSTORE.

Gas Cost Summary

FunctionAlgorithmGas (approx)vs. Naive
mul()mulDiv18~100safe from phantom overflow
div()mulDiv (scale first)~15018-digit precision preserved
sqrt()Newton × 7 iterations~400impossible naively
exp()exp2 + bit decomp~1600Taylor: ~3000+
ln()log2 + repeated squaring~1200Taylor: ~4000+
pow()exp2(y × log2(x))~2800loop: O(n) gas
For reference: SSTORE = 20,000 gas, SLOAD = 2,100 gas, LOG = 375+ gas.
All PRBMath operations are cheaper than a single storage read.

Techniques You've Learned

TechniqueUsed In
512-bit via MULMODmulDiv
Modular inversemulDiv
Newton-RaphsonmulDiv, sqrt
d & (-d) bit trickmulDiv
TechniqueUsed In
Binary search (MSB)log2, sqrt
Repeated squaringlog2
Precomputed constantsexp2
Change of baseexp, ln, pow
These 8 techniques appear throughout DeFi math — Uniswap v3's TickMath, Balancer's LogExpMath, Solmate's FixedPointMathLib. Learn them once, read any library.

How Everything Connects


  pow(x, y) ──→ exp2( y × log2(x) )
                  │         │
                  ▼         ▼
  exp(x) ───→ exp2()    log2() ◄──── ln(x)
                │           │
          [bit decomp]  [repeated squaring]
                │           │
                ▼           ▼
            mulDiv()    mulDiv()
              │
        [512-bit product]
        [modular inverse]
        [Newton-Raphson]

  mul(x,y) ──→ mulDiv18(x, y)  ──→ mulDiv(x, y, 1e18)
  div(x,y) ──→ mulDiv(x, 1e18, y)
  sqrt(x)  ──→ Newton-Raphson + mulDiv for scale
        

mulDiv() is the foundation. Every function ultimately relies on it.

Code Reading Exercises

Beginner

  1. Open Common.sol — find the mulDiv function. Count the Yul assembly blocks.
  2. Find the 6 Newton-Raphson lines in mulDiv. What's the seed value?
  3. In Math.sol, trace mul() → how many function calls deep does it go?

Advanced

  1. In exp2(), find the constant for 21/2. Verify it: compute √2 × 2⁶⁴.
  2. Why does log2() use 192.64 format instead of 60x18?
  3. What happens if you call exp(ud(134e18))? Where does it revert and why?
Repo: npm install @prb/math then open node_modules/@prb/math/src/

Further Reading

PRBMath is one of the best-documented Solidity libraries. The source code comments explain every step — read them!

Putting It All Together

What You Now Know

  • ✓ Functions: input → output
  • ✓ Graphs: x-axis = supply, y-axis = price
  • ✓ Five curve shapes and their personalities
  • ✓ Area under curve = total cost
  • ✓ Fixed-point: multiply by 1e18
  • ✓ PRBMath: wrap → math → unwrap
  • ✓ mulDiv: 512-bit safe arithmetic
  • ✓ How exp, ln, sqrt, pow work internally

What's Next

  • Full buy/sell contracts with reserves
  • Closed-form integrals for O(1) gas
  • Sigmoid curves with sign handling
  • Production safety: reentrancy, fees, caps
  • Power curves, Bancor, Friend.tech
You have all the math and internals knowledge you need. The bonding curves workshop is just plugging these pieces together.

Exercise: Can You Read This?

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

contract MysteryPrice {
    function getPrice(uint256 supply) external pure returns (uint256) {
        UD60x18 s = ud(supply * 1e18);
        UD60x18 k = ud(0.05e18);
        UD60x18 a = ud(2e18);
        UD60x18 b = ud(1e18);

        UD60x18 expPart = exp(mul(k, s));
        UD60x18 result  = mul(a, expPart) + b;

        return unwrap(result);
    }
}

Questions:

  • What formula does this implement? (Hint: 2·e0.05s + ???)
  • What is the price at supply = 0?
  • Is the price at supply = 100 higher or lower than 1000 ETH?

Playground: Build Your Own Curve

Drag the sliders to design a curve. What economic personality does it create?

  • Set n = 0.5 for √x  |  n = 1 for linear  |  n = 2 for quadratic
  • a scales the price  |  b sets a floor price

Formula: P = a · xn + b. This single family covers most DeFi curves.

Resources

Q&A

Quick check — can you answer these?

  • Why can't Solidity do 0.5 * 3 natively?
  • What does ud(5e18) represent?
  • Why do we add + c before calling ln()?
  • What's the difference between a linear and exponential curve?
If you can answer all four → you're ready for Workshop 007: Bonding Curves!