blockulator

Library

The deployed FP127 object, the inline Solidity library, the encoding, and every op.

FP127 is one Yul source that generates two forms. Both are in the repository and both are proven equal on the full int256 domain, including which inputs revert and with what data.

Encoding

A value is one int256 holding value × 2^128, two's complement. Encode with int256(value * 2**128), decode by dividing by 2^128. ONE is 2^128. Range about ±1.7 × 10^38, 128 fractional bits, about 38 decimal digits. Every parameter and result of every op is that one type; there is no 18-decimal flavour of each op. fromFixed18 and toFixed18 are the bridge to WAD, and they are the only place a WAD value should meet FP127.

Errors are Overflow(), DivisionByZero() and OutOfRange(). Arithmetic is checked: nothing wraps.

Deployed form

network
sepolia (chain id 11155111)
address
0xe43F720861074497db2e974E5c4554E4A3b341B9
deployer
0x4e59b44847b379578588920cA78FbF26c0B4956C (the CREATE2 proxy)
salt
"FP127 v3" = 0x4650313237207633000000000000000000000000000000000000000000000000
init code hash
0x67362c4ef1771ae6109a3d8a8291ab42dcde9619a2f1deb849292c81823a65e8
runtime
7593 bytes, keccak 0xab019bcc0e8ce61720b018c96fae06f7b05cc7caaf7c30550c095f82922db496
compiler
Yul, solc 0.8.24+commit.e11b9ed9, optimizer 200 runs, shanghai
deployed
block 11861162, source commit 7b74fbc
verified
Sourcify, Blockscout. Etherscan has no Yul path.
record
FP127.md, fp127.json

Any contract can staticcall it, and every contract that does runs identical bytecode. There is no storage, no owner and no upgrade path; a change to the source is a new init code hash, a new salt and a new address, recorded in the repository.

A read from the command line, sqrt(2):

cast call 0xe43F720861074497db2e974E5c4554E4A3b341B9 "sqrt(int256)(int256)" \
  680564733841876926926749214863536422912 \
  --rpc-url https://ethereum-sepolia-rpc.publicnode.com
# 481231938336009023090067544955250113854
python3 -c "print(481231938336009023090067544955250113854 / 2**128)"
# 1.4142135623730951

From Solidity, through the interface:

import {IFP127} from "fp127/IFP127.sol";

contract StaticcallExample {
    IFP127 public constant FP127 = IFP127(0xe43F720861074497db2e974E5c4554E4A3b341B9);
    int256 internal constant ONE = int256(1) << 128;

    function accrue(int256 principalWad, int256 rateWad, uint256 steps) external view returns (int256 balanceWad) {
        int256 b = FP127.fromFixed18(principalWad);
        int256 growth = FP127.add(ONE, FP127.fromFixed18(rateWad));
        for (uint256 i = 0; i < steps; i++) {
            b = FP127.mul(b, growth);
        }
        return FP127.toFixed18(b);
    }
}

Inline form

FP127Lib.sol holds the same Yul bodies in memory-safe assembly blocks, one internal pure function per op, so there is no external call and no shared address. It is the cheapest per op and the form to use when the math belongs inside your contract.

forge install securedataresearch/blockulator
echo "fp127/=lib/blockulator/contracts/src/fp127/" >> remappings.txt
import {FP127Lib} from "fp127/FP127Lib.sol";

contract CompoundExample {
    function accrue(int256 principalWad, int256 rateWad, uint256 steps) external pure returns (int256 balanceWad) {
        int256 b = FP127Lib.fromFixed18(principalWad);
        int256 growth = FP127Lib.add(FP127Lib.ONE, FP127Lib.fromFixed18(rateWad));
        for (uint256 i = 0; i < steps; i++) {
            b = FP127Lib.mul(b, growth);
        }
        return FP127Lib.toFixed18(b);
    }
}

Both examples are compiled and tested in the repository under test/examples/, against the compound scenario of the ladder. Through the deployed object each op costs about 700 gas more than inline, the price of the staticcall; the compare page has both columns for every op.

The ops

Generated from the Yul source: the signature and selector, what the op computes, the domain, and what it reverts on. Precision and gas are joined from the committed reports.

opwhatdomainrevertsbits
min / mean
gas
object
gas
inline
fromFixed18(int256)
0x9aea0a13
Convert an 18-decimal fixed-point value to 127.128. |x| < 2^127 OutOfRange() if |x| >= 2^127 exact 1223 778
toFixed18(int256)
0xa2745c74
Convert a 127.128 value to 18-decimal fixed-point. any x never exact 1269 790
add(int256,int256)
0xa5f3c23b
a + b. Reverts with Overflow on signed overflow. any a, b Overflow() on signed overflow exact 1303 281
sub(int256,int256)
0xadefc37b
a - b. Reverts with Overflow on signed overflow. any a, b Overflow() on signed overflow exact 1340 319
mul(int256,int256)
0xbbe93d91
floor(a * b / 2^128), the 127.128 product. any a, b Overflow() when a·b / 2^128 does not fit in int256 exact 1569 472
div(int256,int256)
0x43509138
a / b in 127.128, truncated toward zero. b != 0 DivisionByZero() when b == 0; Overflow() when the quotient does not fit exact 1401 939
exp2(int256)
0xdbbb06d2
2^x. x < 127; returns 0 below -128 Overflow() for x >= 127 146 / 211 5116 5173
exp(int256)
0xe46751e3
e^x, computed as 2^(x * log2(e)). x <= 88; returns 0 below -88 Overflow() for x > 88 146 / 221 5261 5353
exp10(int256)
0x4c0dafa2
10^x, computed as 2^(x * log2(10)). x·log2(10) < 127 Overflow() above that 146 / 211 4858 5689
log2(int256)
0xe0d68737
log2(x) for x > 0. x > 0 OutOfRange() for x <= 0 255 / 255 1970 832
ln(int256)
0x8e6f2353
ln(x) = log2(x) * ln(2) for x > 0, scaled at 2^-192 before one rounding. Reverts with OutOfRange otherwise. x > 0 OutOfRange() for x <= 0 255 / 255 1696 999
log10(int256)
0xb47ca3c7
log10(x) = log2(x) / log2(10) for x > 0, scaled at 2^-192 before one rounding. Reverts with OutOfRange otherwise. x > 0 OutOfRange() for x <= 0 255 / 255 1943 1042
log2Up(int256)
0xa1985bb6
ceil(log2(x)) for x > 0. Exact for powers of two. x > 0 OutOfRange() for x <= 0 exact 1648 755
sqrt(int256)
0x665df460
sqrt(x) for x >= 0. x >= 0 OutOfRange() for x < 0 exact 3753 3564
pow(int256,int256)
0x92b0c5b2
x^y = 2^(y * log2(x)) for x >= 0. x >= 0 OutOfRange() for x < 0; Overflow() when the result does not fit 122 / 226 5803 6408
cbrt(int256)
0xd4b6188c
Cube root, defined for all x: cbrt(-x) = -cbrt(x). any x Overflow() only at -2^255 123 / 152 6163 6792
gavg(int256,int256)
0xa13ed853
Geometric mean sqrt(a * b). Reverts with OutOfRange if a * b < 0. a·b >= 0 OutOfRange() if a·b < 0; Overflow() if a·b does not fit exact 4161 3846
hypot(int256,int256)
0xc479210c
sqrt(a^2 + b^2), checked. any a, b Overflow() if a^2 + b^2 does not fit exact 4684 4175
inv(int256)
0x3a02c983
1 / x. Reverts with DivisionByZero for x == 0. x != 0 DivisionByZero() for x == 0; Overflow() when 1/x does not fit exact 1295 1078
abs(int256)
0x1b5ac4b5
|x|. Reverts with Overflow for x == -2^255, which has no positive. any x Overflow() at -2^255 exact 706 612
neg(int256)
0x54a53dc1
-x. Reverts with Overflow for x == -2^255. any x Overflow() at -2^255 exact 869 604
sign(int256)
0xae610b48
-1, 0 or 1 in 127.128. any x never exact 1275 576
min(int256,int256)
0x29aa9cbe
Signed minimum. any a, b never exact 925 493
max(int256,int256)
0x81fe5786
Signed maximum. any a, b never exact 1197 515
clamp(int256,int256,int256)
0x7b8d0f0c
min(hi, max(lo, x)). If lo > hi the result is hi, as in the Huff. any x, lo, hi never exact ——
avg(int256,int256)
0xebf274be
floor((a + b) / 2) without intermediate overflow. any a, b never exact 1632 501
zeroFloorSub(int256,int256)
0xff78cf2d
max(0, a - b), with the subtraction checked. any a, b Overflow() if a - b does not fit exact 1751 683
dist(int256,int256)
0x2110b689
|a - b|, checked. any a, b Overflow() if a - b does not fit exact 976 736
lerp(int256,int256,int256)
0x84e13b60
a + t * (b - a), checked. t is not clamped to [0, 1]. any a, b, t Overflow() on any intermediate that does not fit exact ——
floor(int256)
0xda3ff648
Largest integer <= x. Clearing the fraction bits of a two's complement word is a floor for both signs. any x never exact 1401 583
ceil(int256)
0x42fba7ae
Smallest integer >= x, as -floor(-x). Reverts with Overflow when that integer is 2^127, which does not fit. any x Overflow() when the result is 2^127 exact 858 658
frac(int256)
0xae2785ac
x - floor(x), always in [0, 1). any x never exact 1266 601
round(int256)
0x58b4370b
Nearest integer, halves rounded up: floor(x + 1/2). Checked. any x Overflow() when the result is 2^127 exact 980 761
gcd(int256,int256)
0x63b03896
gcd of the integer parts |floor(a)|, |floor(b)|, as a 127.128 integer. any a, b never exact 1209 753
factorial(int256)
0x5ad78e8a
floor(n)! for 0 <= n < 34. 33! is the largest that fits. 0 <= n < 34 OutOfRange() outside that exact 1581 1290
pi(int256)
0x929edbfb
pi by Ramanujan's 1914 series, floor(terms) terms: 1/pi = (2 sqrt2 / 9801) sum_k (4k)! (1103 + 26390 k) / ((k!)^4 396^(4k)). Each term adds about eight digits: 7, 15, 23, 31, then 36 at five terms, the format's floor; more terms change nothing. 0 <= terms < 8, an integer in 127.128 OutOfRange() outside that 122 / 139 5364 5987
lambertW0(int256)
0x6de23d34
Principal branch of the Lambert W function, W(x) e^W(x) = x, for x >= -1/e. x >= -1/e OutOfRange() below -1/e 65 / 249 36086 45497
lambertWm1(int256)
0xae94abc8
Secondary real branch of the Lambert W function, W(x) e^W(x) = x with W(x) <= -1, for -1/e <= x < 0. -1/e <= x < 0 OutOfRange() outside that 67 / 243 29159 35588

Bits are relative correct bits against mpmath at 200 decimal places over the committed vectors; "exact" is bit-identical. Gas is the median over each op's input ladder: "object" includes the staticcall to the deployed FP127, "inline" is FP127Lib. Sources: ops.json (generated from the Yul source), precision.json, gas.json. lerp and clamp are not on the gas ladder.

For agents

The same address and ABI are published for machines at /.well-known/agent.json and /llms.txt. The ABI is /data/abi.json. Reads are eth_call: no gas, no wallet, no transaction.