the exponential curve
contents
reading this pagethe machine in one paragraphconstantsthe kernel, derivedthe closed form, provedthe ladder, computedfixed point arithmeticthe step, line by linecollect and pourthe burn and the poolthe adapterstorageevents and the tapereading the state from a nodereproducing every figureinvariantsfailure modeswhat the machine cannot dodeploymentsourceglossary

the exponential curve, in full

the companion to the article. everything the article summarizes, stated exactly.

reading this page

this page is written for someone who intends to check the machine rather than believe it. it repeats nothing from the article except the definitions, and it states every quantity with the exactness the contract itself uses. where a number appears here it is either a constant compiled into the contract, a value derived from those constants by arithmetic anyone can repeat, or a live read from the chain rendered in a slot that is blank until the read succeeds. nothing on this page is illustrative.

three things are worth holding in mind throughout. the contract counts blocks and only blocks; seconds appear on this site only where the site has measured them. every arithmetic operation in the contract is on unsigned integers, and every fraction is an eighteen decimal fixed point integer, so the results are exact to the last digit and reproducible in any language. and the contract has no owner, so the word "we" does not appear anywhere in it or here.

the machine in one paragraph

a contract holds a reserve of the chain's native coin. the reserve is fed by the creator fees that pons pays for one token, which are 4% of every swap of that token, pulled in by anyone calling collect, and by anyone sending coin directly, recorded as pour. every 128 blocks is an epoch. anyone may call step, which closes every whole epoch since the last step, keeps the reserve times one minus λ to the power of the number of epochs closed, spends the difference buying the token on its pons market, and transfers every token bought to the dead address. λ is one minus two to the power of minus one over 2048, so the reserve halves every 2048 epochs, which is 262144 blocks, when nothing new arrives. each step, collect and pour writes an event. that is all.

constants

the contract has the following fixed values. the first three are set once at deployment and cannot change afterward. the rest are compiled into the bytecode.

namevaluemeaning
TOKENimmutable, set at deploymentthe token whose creator fees feed the reserve and whose supply the burns reduce
MARKETimmutable, set at deploymentthe pons market contract for the token, the only destination the reserve can be sent to
FEESimmutable, set at deploymentthe pons contract that holds accrued creator fees until claimed
DEPLOY_BLOCKimmutable, the block of deploymentthe first block from which receipts exist; the site reads events from here
DEAD0x000000000000000000000000000000000000dEaDwhere bought tokens are sent; no key exists for it
EPOCH128blocks per epoch
ONE_MINUS_LAMBDA999661606496243683(1 − λ) as an eighteen decimal fixed point integer, equal to 2^(−1/2048) truncated
WAD1000000000000000000the fixed point unit, 10^18
SWAP FEE4%, set on pons at launchthe share of every swap of the token that pons pays to the contract; not stored in the contract, not readable from it

λ itself is never stored. it is one WAD minus ONE_MINUS_LAMBDA, which is 338393503756316 in fixed point, or 0.000338393503756316 as a decimal. the half life is EPOCH times 2048 equals 262144 blocks exactly, because (1 λ)^2048 is 2^(1) by construction. the contract does not know this number; it is a consequence.

the 4% is not a constant of the contract. it is a term of the token on pons, fixed when the token was launched, and the contract never reads it; the contract only ever sees what collect brings in. it is stated here because it is the whole of the inflow, and because it lets a reader with the token's swap history compute what the reserve should have received to the wei and compare it with the Collect events.

the kernel, derived

write R for the reserve immediately after a step, and let swaps of total volume V occur during the following epoch, so that an amount f equal to 0.04 V arrives. at the next step the reserve is R plus f, the contract keeps (R + f)(1 λ) and spends (R + f)λ. now suppose amounts f_1, f_2, ... f_n arrived during epochs 1 through n, one step was taken at the end of each, and the reserve started empty. by induction the reserve after the n th step is the sum over j from 1 to n of f_j times (1 λ) to the power n j + 1, and the amount spent at the n th step is λ times the sum over j of f_j times (1 λ) to the power n j. this is a discrete convolution of the inflow sequence with the kernel λ(1 λ)^a, where a is the age in epochs. the kernel sums to one over all ages, which says that every unit of inflow is eventually spent in full, and the kernel halves every 2048 epochs, which says how.

the continuous form, for readers who prefer it: with inflow rate φ(τ) and decay rate μ = ln(1 λ) per epoch, the spend rate at time t is μ times the integral from minus infinity to t of φ(τ) e^(μ(t τ)) dτ. at the contract's λ, μ is ln 2 over 2048, which is 0.000338450 per epoch. the discrete and continuous forms differ only in that the contract acts at epoch boundaries, and the difference vanishes at the resolution of one epoch.

the closed form, proved

claim: calling step once after k whole epochs leaves exactly the reserve that k consecutive calls would have left, and spends exactly what they would have spent in total, provided nothing arrived in between. proof: one call keeps R(1 λ). a second keeps R(1 λ)(1 λ) = R(1 λ)^2. after k calls the reserve is R(1 λ)^k. the total spent is R minus that, which is R(1 (1 λ)^k). the single call computes (1 λ)^k directly and keeps R times it, spending the rest. the two are the same expression.

if something did arrive in between, the two histories differ, and the single call is the one the contract performs: an inflow during idle epochs is treated as having arrived at the start of the idle stretch, because the reserve it joins is decayed as a whole. this slightly favors spending over keeping for coin that arrived late in an idle stretch, by a factor of at most (1 λ)^k on that coin. the contract does not track arrival epochs per inflow, because doing so would require unbounded storage, and the bias can be removed entirely by anyone calling step every epoch, which is free to do.

the block bookkeeping follows from the same rule. lastStepBlock advances by k times EPOCH, never to the current block, so the fractional epoch in progress at the moment of the call is carried forward and closed by the next call. the sum of all k over all steps equals epochsDone, and lastStepBlock always equals DEPLOY_BLOCK plus epochsDone times EPOCH. this is the first invariant.

the ladder, computed

(1 λ)^k is computed by binary exponentiation over a table of seventeen constants, rung 0 through rung 16, where rung i equals (1 λ) to the power 2^i as a WAD integer, truncated toward zero. because 2048 is 2^11, every rung from 11 up is an exact power of one half: rung 11 is one half, rung 12 is one quarter, rung 13 is one sixteenth, and so on, and rung 17 would be 2^(64), which is below one WAD unit, so it and every later rung is zero. the table:

rungexponent 2^ivalue as WAD integervalue as decimal
019996616064962436830.999661606496243683
129993233275026507520.999323327502650752
249986471128909701730.998647112890970173
389972960560854701260.997296056085470126
4169945994234836331750.994599423483633175
5329892280131939754840.989228013193975484
6649785720620877001340.978572062087700134
71289576032806985736460.957603280698573646
82569170040432046712310.917004043204671231
95128408964152537145430.840896415253714543
1010247071067811865475240.707106781186547524
1120485000000000000000000.5
1240962500000000000000000.25
138192625000000000000000.0625
141638439062500000000000.00390625
1532768152587890625000.0000152587890625
16655362328306430.000000000232830643

to compute (1 λ)^k: if k is 131072 or more, the result is zero. otherwise start with r equal to one WAD, and for each bit i of k that is set, replace r with r times rung i divided by WAD, using integer division. at most seventeen multiplications occur. each multiplication truncates by less than one WAD unit, so the result is below the true value by less than seventeen units in the eighteenth decimal place, and never above it. the contract therefore keeps very slightly less and spends very slightly more than the real number would, by an amount below seventeen parts in 10^18 of the reserve, which is below seventeen wei per step on a reserve of one coin.

the rung values are reproducible from the single expression 2^(2^i / 2048) evaluated to eighteen decimals and truncated. a reader who wants to verify them can do so in any arbitrary precision calculator; the site does not verify them because the site trusts the deployed bytecode, and the bytecode is on the explorer.

fixed point arithmetic

all products are computed as a times b divided by WAD, in that order, in 256 bit unsigned integers. the largest intermediate is reserve times rung 0, and reserve is a balance of the native coin in wei, so overflow requires a reserve above roughly 1.15 times 10^59 wei, which exceeds the supply of any chain. all divisions are integer divisions and truncate toward zero. no negative number ever appears: keep is reserve times r over WAD, which is at most reserve, and spend is reserve minus keep, which is at least zero. the contract refuses to step when spend is zero, which can only happen when the reserve is zero: keep is truncated toward zero, so spend is at least one wei whenever the reserve is at least one wei.

the step, line by line

the body of step does the following, in this order, with no reentrancy guard because no external call happens before the state is fully updated and no function of the contract can be usefully reentered.

k       = (block.number − lastStepBlock) / EPOCH        integer division
require k > 0                                            at least one whole epoch has closed
r       = survive(k)                                     (1 − λ)^k as WAD, by the ladder
keep    = reserve · r / WAD
spend   = reserve − keep
require spend > 0
lastStepBlock += k · EPOCH                               not to block.number
epochsDone    += k
reserve        = keep                                    state is final before any call leaves
before  = TOKEN.balanceOf(this)
MARKET.buy{value: spend}(TOKEN, 0)                       the only outbound transfer of coin in the contract
got     = TOKEN.balanceOf(this) − before
TOKEN.transfer(DEAD, got)                                must return true
totalSpent  += spend
totalBurned += got
stepCount   += 1
emit Step(stepCount, block.number, k, spend, got, keep)

the order matters in one place. the reserve is written down before the market is called, so a market that reentered the contract would find the reserve already reduced and a k of zero, and step would revert on the first require. collect and pour can be reentered harmlessly, since they only add.

collect and pour

collect records the contract's balance, calls the fee vault's claim for the token, and adds the increase in balance to the reserve. it uses the balance difference rather than a return value so that it is correct regardless of what the vault returns. it emits Collect with the amount and the reserve after, even when the amount is zero, so that a call that found nothing is still on the record. anyone may call it, and there is no reason to call it more than once per epoch, but nothing prevents it.

pour is the receive function and an explicit payable function of the same name. it adds msg.value to the reserve and emits Pour with the sender, the amount and the reserve after. it exists so that coin sent to the contract by any path the author anticipated is counted. coin forced into the contract by a path the author did not anticipate, such as a self destruct, raises the balance without raising the reserve; it is stranded, never spent, and never counted. the second invariant is therefore that the contract's balance is greater than or equal to the reserve, with equality in every history that contains no forced transfer.

the burn and the pool

the market is a constant product pool between the native coin and the token, with reserves x of coin and y of token and a fee γ taken from the input. a buy of s coin adds (1 γ)s to x and removes from y the amount that keeps x times y constant, which is y x y / (x + (1 γ)s). the contract passes zero as the minimum output and accepts whatever the pool gives. it then measures what it received and sends all of it to the dead address, which is an address whose private key would have to hash to a chosen pattern and does not exist. the token contract has no burn function the curve relies on; a transfer to the dead address is the burn, and the dead address balance is readable by anyone as proof.

the site does not compute B from x, y and γ, because it does not need to: B is in the receipt. the equation is stated so that a reader can check a receipt against the pool state at that block, using an archive node, and confirm that the pool did what a constant product pool does. a discrepancy would mean the market is not what this document says it is, and that would be worth knowing.

because the step is a swap on the same market, it pays the 4% fee too. of each spend s, 0.04 s goes back to pons as fee in the contract's own name and 0.96 s is the input the pool prices. the equation for B is therefore evaluated with s in place of the pool's own fee term when the pool's fee and the token's fee are the same thing, and with both when they are not; which of the two holds is a fact about the pons market that the explorer shows and this page does not assume. either way the 0.04 s comes back on the next collect, so over the life of the curve the only coin that truly leaves the reserve is the part the pool keeps as tokens.

the adapter

the contract makes exactly two external calls that are not to the token. both are stated here as the contract makes them, so that they can be compared against the pons contracts on the explorer.

interface IPonsFees   { function claim(address token) external; }
interface IPonsMarket { function buy(address token, uint256 minOut) external payable returns (uint256); }

these two signatures are the only place where the machine depends on the shape of another program. before deployment they must be matched against the pons fee vault and market that the token actually uses, and if the real selectors or argument lists differ, the adapter and only the adapter changes. nothing in the kernel, the ladder, the storage or the events depends on them. after deployment they cannot change, and if pons ever stopped answering them, collect and step would revert and the reserve would sit where it is until the end of the chain.

storage

the contract's storage is four slots. the first packs three 64 bit unsigned integers: lastStepBlock in bytes 0 through 7, stepCount in bytes 8 through 15, epochsDone in bytes 16 through 23, with bytes 24 through 31 unused and always zero. the second is reserve, the third totalSpent, the fourth totalBurned, each a full 256 bit unsigned integer. nothing else is ever written. a reader with an rpc node can read all four with eth_getStorageAt at indices 0 through 3 and decode them without the abi, and the site's readout table can be checked against those raw slots directly.

the immutables are not in storage. they are in the bytecode, and the explorer shows them in the verified source. the constants EPOCH, ONE_MINUS_LAMBDA, WAD and DEAD are likewise in the bytecode.

events and the tape

three events, with these exact signatures:

event Step(uint64 indexed index, uint64 blockNumber, uint64 epochs, uint256 spend, uint256 burned, uint256 reserveAfter)
event Collect(uint256 amount, uint256 reserveAfter)
event Pour(address indexed from, uint256 amount, uint256 reserveAfter)

the tape on the article page is the list of Step events from DEPLOY_BLOCK to the latest block, newest first. the site fetches them with eth_getLogs filtered by the contract address and the Step topic, and thereafter fetches only new blocks. because index is indexed, the n th step can be fetched alone by topic. the reserveAfter of step n plus every Collect and Pour amount between step n and step n + 1 must equal the reserve that step n + 1 started from, which is reserveAfter of n + 1 plus spend of n + 1. this is the third invariant, and it lets the whole history be replayed from the events alone without reading storage at all.

reading the state from a node

the site reads the following, and nothing else. every call is a read; the site never sends a transaction.

calltargetwhat it feeds
eth_blockNumber and eth_getBlockByNumber(latest)the chaincurrent block, its timestamp
eth_getBlockByNumber(latest − 256)the chainobserved block interval, measured over 256 blocks
reserve()the curvereserve
stepCount(), lastStepBlock(), epochsDone()the curvesteps, last step block, epochs closed
totalSpent(), totalBurned()the curvetotals
EPOCH(), ONE_MINUS_LAMBDA()the curveepoch, one minus λ, and the half life check
pending()the curveepochs owed, spend if stepped now
totalSupply(), decimals()the tokensupply and formatting
balanceOf(0x…dEaD)the tokenat the dead address
eth_getLogs(Step)the curvethe tape

the observed block interval is the only figure on the site that is a measurement rather than a read. it is the difference between the timestamps of the latest block and the block 256 earlier, divided by 256, and it is refreshed every 64 polls. the half life in seconds is that interval times 262144 and is shown only when the contract's constants have been read and match the ones in this document; otherwise the slot is blank, so that a contract deployed with other constants can never be described by a wrong number.

reproducing every figure

each figure on the article page is a deterministic drawing from the constants. fig. 1 draws 64 bars whose heights are 150 times 2 to the power of minus i over 16, which is the kernel sampled every 128 epochs. fig. 2 draws bars of height 6 times (1 λ)^j for j from 0 to 7, and one bar of height 6 times the sum of those, which is 47.94 pixels, and the equality of the two totals is the telescoping identity. fig. 3 draws the seventeen rungs of the ladder as bar lengths of 500 pixels times the rung value. fig. 4 draws x y = 1 for x from 0.55 to 2 and marks the points x = 1 and x = 1.12, which is a step whose (1 γ)s is twelve percent of the pool's coin reserve, chosen so that the geometry is visible; it is not a claim about any real step. fig. 5 draws the four storage slots at one cell per byte. all five can be regenerated from the algorithms in the article's source with no chain data.

invariants

the following hold in every block after deployment, and any reader can check them from public state.

one: lastStepBlock equals DEPLOY_BLOCK plus epochsDone times 128. two: the contract's balance is at least reserve, and equal to it in any history without forced transfers. three: for every step n, its reserveAfter plus all Collect and Pour amounts before step n + 1 equals step n + 1's reserveAfter plus its spend. four: totalSpent equals the sum of spend over all Step events, and totalBurned equals the sum of burned over all Step events. five: the token balance of the dead address is at least totalBurned. six: for every step, spend equals reserveBefore minus reserveBefore times survive(epochs) over WAD, where reserveBefore is reconstructed by invariant three. seven: the contract's token balance is zero after every step. eight: the sum of all Collect amounts is at most 4% of the token's total swap volume in the native coin since deployment, with equality when every fee has been collected and no fee was paid to any address other than the contract.

failure modes

no one calls step: the reserve is not spent, no receipt is written, the epochs owed figure on the site grows, and the next call closes them all in one multiplication. no one calls collect: the fees accrue at pons in the contract's name and are not in the reserve; the site shows the reserve, not the accrual, because the site reads the contract and not pons. pons reverts on claim: collect reverts, nothing changes. pons reverts on buy: step reverts, nothing changes, including lastStepBlock. the token's transfer to the dead address returns false: step reverts, and the tokens bought are returned to the pool by the revert, so nothing is stranded. the rpc node returns stale or false data: the site shows it, and the config file names the node so that a reader can change it.

the sandwich: a step is a public buy with no minimum output, so a searcher can front run it. the searcher's profit is bounded by the price impact of the step, which is bounded by the size of the step, which is λ of the reserve after one epoch and 1 (1 λ)^k after k. keeping k small is the whole mitigation, and keeping k small costs any holder one transaction per epoch. the contract does not check price because a price check needs a reference, and every reference is either the pool itself, which is what is being manipulated, or an oracle, which is a second mechanism with its own operator.

the reserve overflowing: impossible on any real chain, as shown under fixed point arithmetic. the step counter overflowing: 2^64 steps at one per 128 blocks is longer than the chain will exist.

what the machine cannot do

it cannot pay anyone. it cannot send the native coin anywhere but the market, and it cannot send tokens anywhere but the dead address. it cannot change λ, the epoch, the token, the market or the fee vault. it cannot be paused, upgraded, or destroyed. it cannot refuse a step from any caller, and it cannot favor one. it cannot know the time. it cannot know the price. it cannot read this page.

deployment

the contract is deployed with three constructor arguments: the token, its pons market, and the pons fee vault. the deploying account has no role after the constructor returns. the constructor records the deployment block and sets lastStepBlock to it, so the first epoch begins at deployment. after deployment, the token's creator fee recipient must be set to the contract address through whatever mechanism pons provides for that, and from then on collect will find fees. the source must be verified on the explorer so that the immutables and constants can be read from it. the site's config file is then filled with the contract address, the token address, the market address, the deployment block, the rpc url and the explorer base, and the slots fill.

the epoch, ONE_MINUS_LAMBDA and the ladder are compiled constants and are not constructor arguments on purpose. a curve with a different half life is a different contract with a different address, and it would need its own page, because the half life row on this one would render blank for it.

source

the complete source of the contract, as deployed. compare it to the verified source on the explorer; the two must be identical.

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

interface IERC20 {
    function balanceOf(address a) external view returns (uint256);
    function transfer(address to, uint256 amount) external returns (bool);
}

interface IPonsFees {
    function claim(address token) external;
}

interface IPonsMarket {
    function buy(address token, uint256 minOut) external payable returns (uint256);
}

contract ExponentialCurve {
    address public immutable TOKEN;
    address public immutable MARKET;
    address public immutable FEES;
    uint64  public immutable DEPLOY_BLOCK;

    address public constant DEAD = 0x000000000000000000000000000000000000dEaD;
    uint64  public constant EPOCH = 128;
    uint256 public constant ONE_MINUS_LAMBDA = 999661606496243683;
    uint256 public constant WAD = 1e18;

    uint64  public lastStepBlock;
    uint64  public stepCount;
    uint64  public epochsDone;
    uint256 public reserve;
    uint256 public totalSpent;
    uint256 public totalBurned;

    event Step(uint64 indexed index, uint64 blockNumber, uint64 epochs, uint256 spend, uint256 burned, uint256 reserveAfter);
    event Collect(uint256 amount, uint256 reserveAfter);
    event Pour(address indexed from, uint256 amount, uint256 reserveAfter);

    constructor(address token, address market, address fees) {
        TOKEN = token;
        MARKET = market;
        FEES = fees;
        DEPLOY_BLOCK = uint64(block.number);
        lastStepBlock = uint64(block.number);
    }

    receive() external payable {
        _pour();
    }

    function pour() external payable {
        _pour();
    }

    function _pour() internal {
        reserve += msg.value;
        emit Pour(msg.sender, msg.value, reserve);
    }

    function collect() external {
        uint256 before = address(this).balance;
        IPonsFees(FEES).claim(TOKEN);
        uint256 got = address(this).balance - before;
        reserve += got;
        emit Collect(got, reserve);
    }

    function rung(uint256 i) public pure returns (uint256) {
        if (i == 0)  return 999661606496243683;
        if (i == 1)  return 999323327502650752;
        if (i == 2)  return 998647112890970173;
        if (i == 3)  return 997296056085470126;
        if (i == 4)  return 994599423483633175;
        if (i == 5)  return 989228013193975484;
        if (i == 6)  return 978572062087700134;
        if (i == 7)  return 957603280698573646;
        if (i == 8)  return 917004043204671231;
        if (i == 9)  return 840896415253714543;
        if (i == 10) return 707106781186547524;
        if (i == 11) return 500000000000000000;
        if (i == 12) return 250000000000000000;
        if (i == 13) return 62500000000000000;
        if (i == 14) return 3906250000000000;
        if (i == 15) return 15258789062500;
        if (i == 16) return 232830643;
        return 0;
    }

    function survive(uint256 k) public pure returns (uint256 r) {
        if (k >= 131072) return 0;
        r = WAD;
        uint256 i = 0;
        while (k > 0) {
            if (k & 1 == 1) r = r * rung(i) / WAD;
            k >>= 1;
            i += 1;
        }
    }

    function pending() public view returns (uint256 epochs, uint256 spend) {
        epochs = (block.number - lastStepBlock) / EPOCH;
        spend = reserve - reserve * survive(epochs) / WAD;
    }

    function step() external {
        uint256 k = (block.number - lastStepBlock) / EPOCH;
        require(k > 0, "epoch not closed");
        uint256 keep = reserve * survive(k) / WAD;
        uint256 spend = reserve - keep;
        require(spend > 0, "nothing to spend");
        lastStepBlock += uint64(k * EPOCH);
        epochsDone += uint64(k);
        reserve = keep;
        uint256 before = IERC20(TOKEN).balanceOf(address(this));
        IPonsMarket(MARKET).buy{value: spend}(TOKEN, 0);
        uint256 got = IERC20(TOKEN).balanceOf(address(this)) - before;
        require(IERC20(TOKEN).transfer(DEAD, got), "burn failed");
        totalSpent += spend;
        totalBurned += got;
        stepCount += 1;
        emit Step(stepCount, uint64(block.number), uint64(k), spend, got, keep);
    }
}

glossary

reserve: the native coin the contract holds and counts. epoch: 128 blocks. step: the function that closes epochs and spends. λ: the fraction spent per epoch, 1 2^(1/2048). half life: 2048 epochs, 262144 blocks. kernel: the weight λ(1 λ)^a given to inflow of age a epochs. ladder: the seventeen precomputed powers used to raise (1 λ) to any k. rung: one entry of the ladder. survive: the function returning (1 λ)^k. WAD: 10^18, the fixed point unit. dead address: 0x000000000000000000000000000000000000dEaD. receipt: an emitted event. tape: the list of Step receipts. pons: the robinhood chain launchpad, which pays creator fees and runs the token's market. market: the pons constant product pool for the token. collect: the function that pulls creator fees. pour: the function and receive path that accepts direct transfers. swap fee: 4% of every swap of the token, paid by pons to the contract; the only designed inflow.