blockulator

Each scenario is measured twice: against the exact inputs, which is what a protocol sees, and against the inputs as each format rounds them, which isolates the arithmetic. The chart covers every input; the table below it is the reference input, cell by cell.

BL127's and BL19's deployed contracts checked live on Sepolia: 1,913 results at block 12,017,732, 0 mismatches. The other libraries' results reproduce locally with make ladder. 2026-10-29, runner 0x7683126a…. Re-run: uv run scripts/ladder/livecap.py

How this was measured
truth
For every scenario, input and N the same loop is run in exact real arithmetic with mpmath at 300 decimal digits (scripts/ladder/ladder.py), from the exact rational parameters in scenarios.json. Closed forms are used where the loop has one (compounding, the cumulative product, the geometric mean, the round trip, per-second and continuous accrual, the option walk, the decay and the adaptive rate); the bonding curve, the amortising loan and the Black-Scholes chain are stepped.
rounding
Inputs are floored on the grids and rounded to nearest on the floats: each library receives the parameters floored into its own representation (2^-128, 2^-64, 10^-18 or 10^-27 units) or, for ABDKMathQuad and BL19, the nearest float (scripts/ladder/inputs.json). truthFormat is the exact loop started from those parameters, so digitsFormat measures the library's arithmetic alone, while digits measures what a protocol sees: the format's rounding of the inputs plus the arithmetic. One exception: Morpho accrues from the per-second rate, rateAnnual / 31536000 floored to WAD, while truthFormat is built from the annual rate the other libraries receive, so Morpho's digitsFormat still includes its own input's rounding. Likewise in the adaptive rate, FraxLend and Euler compute from their own inputs (utilisation, step, half-life or speed) while truthFormat is built from rate0 and x, so their digitsFormat includes the rounding of those inputs (Euler's speed floored to WAD per second, FraxLend's half-life to whole seconds).
inputs
The parameters are short decimals (0.1/365, 10000, 0.05) as a protocol's governance would set them. A short decimal can sit exactly on the 10^-18 and 10^-27 grids, so WAD and RAY libraries often receive their inputs exactly while the binary formats round them; this favours WAD and RAY in digits (against exact inputs) and is visible as the gap between digits and digitsFormat. The decay scenario's eps are reciprocals of time constants in seconds (1/1036800 for twelve days), which no grid holds exactly.
powers
Integer powers are each library's own idiom, not one algorithm: BL127 and BL19 use compound (a correctly rounded (1 + r)^n that never forms a rounded 1 + r), Maker uses rpow (binary exponentiation in RAY), the WAD and 64.64 libraries and binary128 use exp(n ln(1 + r)) or their pow. Per-second accrual and compound-pow measure those choices as much as the formats.
digits
digits = floor(-log10 |v - t| / |t|), the number of correct leading significant digits by relative error, an integer; a result equal to the truth is exact (exact = true, digits = 100, the one cap on this field) and is drawn as a marker, never as a number. digits1 is the same quantity floored to one decimal instead of to an integer. It is not a string comparison: a value can be correct to 36 digits while its 37th printed digit happens to agree with the truth, or while a carry makes the printed prefix differ earlier.
precision
No format can be right to more digits than it holds at the result's magnitude: prec is 237 log10 2 = 71.3 for BL19 and 113 log10 2 = 34.0 for binary128 at any magnitude, and -log10(ulp / |t|) on the fixed grids, with ulp 2^-128 (BL127), 2^-64 (64.64), 10^-18 (WAD) or 10^-27 (RAY): 38.5 for BL127 at 1, 42.5 at 10^4. A cell may land closer to the truth than that by luck; digitsCapped = min(digits1, prec), and prec for an exact cell, is what the charts plot and the summaries aggregate, so no median or band exceeds the format's precision.
medians
Each scenario's median, minimum and maximum at each N are taken over the inputs that every library with any completed input at that N completed, so every library's median is over the same inputs. Each library's completed count is reported, and a library that completed fewer than all inputs is shown as k/n. Where no input is common to all of them, a revert counts as 0 digits over all inputs instead (basis revertsAsZero); the dataset records the basis and the inputs per N.
interest
For the compounding and accrual scenarios the digits are also measured on the interest, b - principal against t - principal (interestDigits1, interestCapped, with the format's precision taken against the interest's magnitude). Relative digits of a balance flatter a format when the interest is a small part of it: after one day the interest is about 10^-4 of the balance.
wei
For the deep-tail option, absErrWei is |v - t| in wei of the strike at 18 decimals ($100 = 10^20 wei). Past d1 = -8 the true price falls below one wei and every library's error with it, so relative digits there describe the format, not money.
headline
The Results page shows a single N = 10,000 per scenario; the ladder page shows every N.
agree
agree is the count of leading significant digits of the printed value (42 significant digits) that are literally equal to the printed truth. The demo page colours the rest of the value with it. It is a reading aid, not a measurement: a carry across 0.999… / 1.000… gives 0.
gas
gasTotal is measured inside the adapter around the scenario function (the parameter reads and the loop), after the runner has warmed the adapter and the BL127 object, so the call into the adapter and the ABI decoding are excluded. The BL127 (deployed) form includes one staticcall per op, and so does snekmate, a Vyper library that a Solidity adapter can only call; BL127Lib and the other libraries are internal calls. Aave, Morpho and Maker answer only the accrual scenarios their models describe, FraxLend and Euler only the adaptive rate, and OpenZeppelin and Uniswap FullMath, which have no exp, ln or real power (and FullMath no root), only the scenarios built from mul, div and their roots; each reports Unsupported elsewhere, which the tables and charts leave empty (a library with no model is not a revert). gasPerStep = gasTotal / N. Reverted cells carry no gas.
live
LadderRunner.run(scenario, form, n, p) on Sepolia executes any cell with one eth_call and returns (ok, raw, reason, gasPerStep, gasTotal). scripts/ladder/livecap.py sends every cell's floored parameters p and compares the returned raw word with the committed one bit for bit (or the revert selector for a reverted cell). liveMaxN is the largest N per form the public RPC completed within its gas limit, measured by scripts/ladder/livecap.py.
reproduce
make ladder regenerates ladder.json and ladder.md from the Foundry harness (test/ladder/Ladder.t.sol) and this script; make ladder-check fails CI when the committed dataset differs. Versions of every library and of forge are recorded in the dataset. Generated 2026-10-28; versions: aave aave-v3-origin@8305565ae342 (MathUtils, transcribed), abdk v3.2, balancer balancer-v3-monorepo@2b9ce4b1b8ad (LogExpMath.sol, vendored), bl127 6c2eeaf, euler evk-periphery@af436991e135 (IRMAdaptiveCurve, ExpLib, transcribed), forge forge Version: 1.7.1, fraxlend fraxlend@2bed49d4dcc6 (VariableInterestRate V2, transcribed), huff sepolia:0xfae694D0c2c44181791F838c54Ed64C3151FfE30@11857616, maker dss pot.sol rpow via solady v0.1.26, morpho morpho-blue@8e26ca6a8dbc (MathLib, transcribed), openzeppelin openzeppelin-contracts v5.7.0 (Math, SignedMath, vendored), prb-math v4.2.0, snekmate v0.1.2 (math module, vyper 0.4.3), solady v0.1.26, solmate solmate@89365b8 (no tag: v6 predates SignedWadMath), uniswap v4-core v4.0.0 (FullMath, vendored).

Daily compound interest

Step: b = b * (1 + r). Result: balance after N days.

Per-block or per-day accrual in every lending protocol: one mul per step, N steps.

Over all 10 inputs

Median correct digits over the inputs every library with a result at that N completed, each capped at what its format holds at that magnitude (BL19 71.3, binary128 34.0, the fixed grids by their last place), so no line passes its format's precision. The WAD libraries are the gold lines, labelled once. BL127Lib coincides with BL127 and is not drawn; Aave, Morpho and Maker appear only on the accrual scenarios their models describe. Hover a point for the library, the range over the inputs and N.

Floating pointFixed point
steps NBL19QuadBL12764.64SoladySolmatePRB SDPRB UDsnekBalancerOZUniswapMakerAaveMorphoFraxEuler
171.333.938.819.518.818.818.818.818.818.818.818.827.4————
271.233.638.519.318.518.518.518.518.518.518.518.526.9————
570.733.238.118.818.118.118.118.118.118.118.118.126.6————
1070.432.937.818.517.817.817.817.817.817.817.817.826.3————
2070.132.637.518.217.517.517.517.517.517.517.517.526.0————
5069.732.237.117.817.117.117.117.117.117.117.117.125.6————
10069.431.936.817.516.816.816.816.816.816.816.816.825.3————
36568.831.336.217.016.316.316.316.316.316.316.316.324.8————
1,00068.430.935.816.615.815.815.815.815.815.815.815.824.3————
10,00067.429.934.815.614.814.814.814.814.814.814.814.823.3————

Median correct digits over the inputs every library completed at that N (hover a cell for the range), each capped at what its format holds; "exact" when every one of them is exact. k/n: the library completed k of n inputs and the rest reverted; rev: every input reverted. —: no model for this scenario. BL127Lib equals BL127 and is left out.

The inputs and the ops
inputparameterswhy
reference referenceprincipal = 10000, rate = 0.1/365$10,000 at 10% APR, the headline cell
tiny-lowprincipal = 1, rate = 0.01/365one unit at 1%: the integer part is small, the fraction carries everything
tiny-highprincipal = 1, rate = 0.5/365one unit at 50%
mid-1pctprincipal = 10000, rate = 0.01/365a slow rate: the per-step factor is close to 1
mid-5pctprincipal = 10000, rate = 0.05/365a typical lending rate
mid-50pctprincipal = 10000, rate = 0.5/365a high rate: the balance grows 150x by N = 10,000
largeprincipal = 1000000, rate = 0.1/365a million-unit position
hugeprincipal = 100000000, rate = 0.1/365a hundred-million-unit pool
huge-highprincipal = 100000000, rate = 0.5/365the largest balance the ladder reaches, near 10^14
fractionprincipal = 0.001, rate = 0.1/365a sub-unit balance: leading zeros in WAD

Library calls per step: BL19 mul; Quad mul; BL127 mul; 64.64 mul; Solady sMulWad; Solmate wadMul; PRB SD mul; PRB UD mul; snek mul_div(a, b, 1e18); Balancer mulDown; OZ mulDiv(a, b, 1e18); Uniswap mulDiv(a, b, 1e18); Maker rpow, rmul; Aave unsupported; Morpho unsupported; Frax unsupported; Euler unsupported; BL127Lib mul. Parameters reach each adapter floored into its representation on the grids and rounded to nearest on the floats (ladder-inputs.json).

The reference input, step by step: principal = 10000, rate = 0.1/365

Correct digits against the exact truth at each N, capped at what the format holds. Hover a cell for the value itself, the digits against the format-rounded truth, of the interest, the error in units of the last place and the gas per step.

Floating pointFixed point
steps NBL19QuadBL12764.64SoladySolmatePRB SDPRB UDsnekBalancerOZUniswapMakerAaveMorphoFraxEuler
171.333.938.819.719.519.519.519.519.519.519.519.527.5————
271.333.538.519.419.219.219.219.219.219.219.219.226.9————
570.933.138.119.018.818.818.818.818.818.818.818.826.6————
1070.632.937.818.718.518.518.518.518.518.518.518.526.3————
2070.132.637.518.418.218.218.218.218.218.218.218.226.0————
5069.832.237.118.017.817.817.817.817.817.817.817.825.6————
10069.631.936.817.717.517.517.517.517.517.517.517.525.3————
36568.931.336.217.116.916.916.916.916.916.916.916.924.8————
1,00068.530.935.816.716.516.516.516.516.516.516.516.524.3————
10,00067.529.934.815.715.515.515.515.515.515.515.515.523.3————

Full dataset: ladder.json. Regenerate with make ladder.