Read it

The source

Embedded from disk at build time, and checked against the file byte for byte — these are the exact characters the compiler turned into the runtime the property suite ran.

Runtime 0x22f01b7157c20d7da22eed66d4cfd8648d582251b6fe3daa8558c346a7beb0af, 3,074 bytes. solc 0.8.26, optimizer on, 200 runs. 23 properties and 16 sabotages were executed against it on Robinhood Chain's own EVM.

The other two files are served as well and are worth reading next to these: Mocks.sol — the token zoo, eleven contracts each reproducing one way a transfer is true and useless — and UllageTest.sol, the property suite itself.

contracts/Ullage.sol

The escrow

Every number it writes down is a difference between two balances it read itself. 3,074 bytes of runtime, no owner, no upgrade path, no pause, no fee.

// SPDX-License-Identifier: MITpragma solidity 0.8.26; /** * Ullage — an escrow that books the amount that ARRIVED. * * WHY THIS EXISTS. Every ERC-20 integration is written against one sentence in * EIP-20: * *     "transfers `_value` amount of tokens to address `_to` ... The function *      SHOULD throw if the message caller's account balance does not have *      enough tokens to spend." * * Note what it does not say. It does not say the recipient's balance rises by * `_value`. It does not say the function reverts when it fails — `SHOULD`, not * MUST, and the reference ABI returns a `bool` precisely so that it can return * `false` instead. And it does not say the call returns anything at all: the * largest token contracts in circulation return nothing, because they were * written before the ABI settled, and a `bool` decode of empty returndata * reverts on a transfer that in fact succeeded. * * So `token.transfer(to, amount)` compiles into a sentence with three separate * ways of being false, and an integration that writes down `amount` afterwards * has recorded a request, not an outcome. * *   - a fee-on-transfer token moves 100 and delivers 97 *   - a token that returns `false` reports failure through a value the caller *     usually discards, so the caller books a transfer that never happened *   - a token that returns nothing makes a strict caller revert on success *   - an address with NO CODE accepts every call and returns nothing, so a *     "transfer" to a mistyped or self-destructed token succeeds forever * * ULLAGE is the shipping term for the gap: the difference between what a * vessel was said to contain and what a gauge finds in it. The bill of lading * is a claim; the outturn is a measurement; the ullage is the discrepancy, and * it is the number the receiving party is entitled to insist on. * * THE WHOLE CONTRACT IS THAT ONE IDEA. Nothing here is credited from an * argument. Every number this contract writes down is a difference between two * balances it read itself, on either side of the call that was supposed to move * them: * *     deposit  credits `balanceOf(this)` after minus before  — never `amount` *     withdraw debits  `balanceOf(this)` before minus after  — never `amount` *              and reports what the payee's balance actually did * * Which gives the invariant the whole thing is for, and which property 1 checks * after every single operation in the suite: * *     booked[token] <= IERC20(token).balanceOf(address(this)) * * The contract can never have promised more of a token than it is holding, * because it never wrote down a number it did not weigh. A token that claws * units back from the outside — a downward rebase, a blacklist, a fee taken on * a balance rather than a transfer — can still break it, and that is the one * case a receiving account genuinely cannot prevent. So it is REPORTED rather * than smoothed: `deficit()` names the number, and payouts are capped at what * is actually held. * * WHAT IS DELIBERATELY NOT HERE. No owner, no upgrade path, no pause, no fee, * and no pro-rata arbitration of a shortfall. Ullage is a gauge, not a court. * Its job is to make the two numbers separately visible and to refuse to write * either of them down from an argument; deciding who bears a shortfall that a * token created is somebody else's contract. */ interface IERC20 {    function balanceOf(address account) external view returns (uint256);    function transfer(address to, uint256 amount) external returns (bool);    function transferFrom(address from, address to, uint256 amount) external returns (bool);} contract Ullage {    /* ---------------------------------------------------------------- state */     /** Units of `token` this contract owes `owner`. Only ever moved by a        measured difference of two balances. */    mapping(address token => mapping(address owner => uint256)) public credit;     /** The sum of `credit[token][*]`. Kept as a running total rather than        recomputed, because the invariant that matters is a comparison against        a live balance and both sides of it must be cheap. */    mapping(address token => uint256) public booked;     /* --------------------------------------------------------------- events */     /** `asked` and `arrived` are emitted SEPARATELY and always both. A log that        records only the amount that was requested is the same mistake as an        integration that records only the amount that was requested. */    event Deposited(address indexed token, address indexed from, address indexed to, uint256 asked, uint256 arrived);    event Withdrawn(address indexed token, address indexed owner, address indexed to, uint256 asked, uint256 left, uint256 arrived);    event Booked(address indexed token, address indexed to, uint256 amount);     /* --------------------------------------------------------------- errors */     error NothingArrived();    error Overdraw(uint256 asked, uint256 left);    error InsufficientCredit(uint256 have, uint256 want);    error NotAToken(address token);    error TransferReturnedFalse(address token);    error MalformedReturn(address token, uint256 length);    error BadRecipient();    error Reentered();    error NothingToBook();     /* A single-slot reentrancy latch. Every state-changing function here reads       a balance, calls a token, and reads the same balance again — which is       exactly the shape a callback token exists to exploit. Without the latch a       token whose `transfer` re-enters `deposit` can make the second reading of       `balanceOf(this)` include its own inner deposit and be credited twice.       Property 12 does that on purpose. */    uint256 private _entered = 1;     modifier lock() {        if (_entered != 1) revert Reentered();        _entered = 2;        _;        _entered = 1;    }     /* ------------------------------------------------------------- views */     /**     * What the contract is actually holding.     *     * The code check is HERE, and not only at the call site, because it has to     * run before the first balance is read. `balanceOf` on an address with no     * code returns an empty buffer, and solc's own decode of that reverts with     * no data at all — so without this line the most important error in the     * contract arrives as a bare failure with nothing in it, and the caller is     * told only that something went wrong. Property 6 checks the selector, so     * it fails on a nameless revert exactly as it would on the wrong one.     */    function held(address token) public view returns (uint256) {        if (token.code.length == 0) revert NotAToken(token);        return IERC20(token).balanceOf(address(this));    }     /** Units present that nobody has been credited with — a plain `transfer`        into this address rather than a `deposit`. */    function surplus(address token) public view returns (uint256) {        uint256 h = held(token);        uint256 b = booked[token];        unchecked { return h > b ? h - b : 0; }    }     /** THE ULLAGE. Units this contract has written down and is no longer        holding. By construction it can only be created from OUTSIDE — a        downward rebase, a blacklist, a token that moves balances without a        transfer — because nothing in here ever books a number it did not        weigh. It is reported rather than absorbed. */    function deficit(address token) public view returns (uint256) {        uint256 h = held(token);        uint256 b = booked[token];        unchecked { return b > h ? b - h : 0; }    }     /** The most `owner` can ask `withdraw` to send. Capped at what is actually        in the contract, because a quote above the balance is the thing this        whole project is about. */    function available(address token, address owner) public view returns (uint256) {        uint256 c = credit[token][owner];        uint256 h = held(token);        return c < h ? c : h;    }     /* ------------------------------------------------------------ deposits */     function deposit(address token, uint256 amount) external returns (uint256) {        return depositFor(token, amount, msg.sender);    }     /**     * Pull `amount` from the caller and credit `to` with WHAT ARRIVED.     *     * The two readings straddle the transfer, so a fee, a rebase inside the     * call, or a token that moves a different number than it was asked for are     * all handled by the same three lines — none of them is a special case,     * because `amount` is never used as a quantity, only as an instruction.     */    function depositFor(address token, uint256 amount, address to) public lock returns (uint256 arrived) {        if (to == address(0) || to == address(this)) revert BadRecipient();         uint256 before = held(token);        _call(token, abi.encodeCall(IERC20.transferFrom, (msg.sender, address(this), amount)));        uint256 after_ = held(token);         /* `<=` and not `<`: a token that reports success and moves nothing at           all is the single most common shape of the bug this contract exists           for, and crediting zero would let it accumulate silently. */        if (after_ <= before) revert NothingArrived();        unchecked { arrived = after_ - before; }         credit[token][to] += arrived;        booked[token] += arrived;        emit Deposited(token, msg.sender, to, amount, arrived);    }     /**     * Credit the caller with any unbooked surplus.     *     * Somebody who sends tokens here with a plain `transfer` has told this     * contract a quantity and nothing else — not who it is for. The contract     * will not guess, so the units sit unbooked until someone claims them, and     * claiming is deliberately open. That is a race, and it is the honest     * shape of the situation: the alternative is to invent an owner.     */    function book(address token) external lock returns (uint256 amount) {        amount = surplus(token);        if (amount == 0) revert NothingToBook();        credit[token][msg.sender] += amount;        booked[token] += amount;        emit Booked(token, msg.sender, amount);    }     /* ----------------------------------------------------------- withdrawals */     function withdraw(address token, uint256 amount) external returns (uint256, uint256) {        return withdrawTo(token, amount, msg.sender);    }     /**     * Send `amount` to `to`, and return BOTH measured numbers: how much left     * this contract, and how much reached the payee.     *     * The debit is `left`, not `amount`. If a token takes its fee out of the     * sender the contract's own balance falls by more than it authorised, and     * that is the one case here that reverts — a receiving account may deliver     * less than was asked for, because the token did that, but it may never     * lose more than the holder instructed it to send.     *     * A shortfall at the PAYEE's end does not revert. It is returned, and     * emitted, and it is the number this contract is named after. Reverting     * would make every fee-on-transfer token permanently unwithdrawable, which     * is a worse outcome than telling the truth about it.     */    function withdrawTo(address token, uint256 amount, address to) public lock returns (uint256 left, uint256 arrived) {        if (to == address(0) || to == address(this)) revert BadRecipient();        if (amount == 0) revert NothingArrived();         uint256 c = credit[token][msg.sender];        uint256 cap = available(token, msg.sender);        if (amount > cap) revert InsufficientCredit(cap, amount);         uint256 beforeSelf = held(token);        uint256 beforeTo = IERC20(token).balanceOf(to);         _call(token, abi.encodeCall(IERC20.transfer, (to, amount)));         uint256 afterSelf = held(token);        uint256 afterTo = IERC20(token).balanceOf(to);         unchecked {            left = beforeSelf > afterSelf ? beforeSelf - afterSelf : 0;            arrived = afterTo > beforeTo ? afterTo - beforeTo : 0;        }         /* The contract must never be lighter than the holder instructed. */        if (left > amount) revert Overdraw(amount, left);         /* Debit the measurement, not the instruction. A token that moved less           than asked leaves the difference credited, where it belongs. */        credit[token][msg.sender] = c - left;        booked[token] -= left;         emit Withdrawn(token, msg.sender, to, amount, left, arrived);    }     /* ------------------------------------------------------------ the call */     /**     * One ERC-20 call, with all four failure shapes separated.     *     * The order matters. `token.code.length` is checked BEFORE the returndata     * length, because an address with no code accepts every call, consumes no     * gas worth speaking of, and returns success with an empty buffer — which     * is indistinguishable from a correct USDT-shaped transfer if you look at     * the returndata first. Every "I sent tokens to the wrong address and the     * transaction succeeded" story is that check being absent.     */    function _call(address token, bytes memory data) private {        if (token.code.length == 0) revert NotAToken(token);         (bool ok, bytes memory ret) = token.call(data);         if (!ok) {            /* Hand the token's own revert reason back rather than replacing it               with ours. A wrapper that swallows the reason turns "transfer               amount exceeds balance" into "call failed", and the caller then               debugs the wrapper. */            if (ret.length > 0) {                assembly ("memory-safe") { revert(add(ret, 32), mload(ret)) }            }            revert TransferReturnedFalse(token);        }         /* Returned nothing: the pre-ABI shape. Legal here, because the code           check above has already established that something ran. */        if (ret.length == 0) return;         /* Returned something that is not a word. Not a bool, not silence —           refuse rather than decode it into whatever it happens to look like. */        if (ret.length < 32) revert MalformedReturn(token, ret.length);         if (!abi.decode(ret, (bool))) revert TransferReturnedFalse(token);    }}

Download the raw file →

contracts/Probe.sol

The measurement

Installed at a real holder's address with an eth_call state override, so msg.sender inside the token is the account that genuinely owns the units. These are the bytes the sweep used for every row of the table, and the bytes the app runs in your browser.

// SPDX-License-Identifier: MITpragma solidity 0.8.26; /** * Probe — the measurement, run from inside a real holder's address. * * HOW IT GETS THERE. `eth_call` takes a third parameter, a state override, and * one of the things it can override is an account's CODE. So this runtime is * installed at the address of an account that genuinely owns the token, for the * duration of one call that changes nothing, and `msg.sender` inside the * token's `transfer` is therefore the address that actually holds the units. * * That is the whole reason this works without a funded key, an approval, or a * fork. A probe deployed the ordinary way — through `eth_call` with no `to`, * which is how the property suite runs — lands at a fresh address that owns * nothing, and every token in the census would refuse it for lack of balance. * The finding would be "no token on this chain will let you move anything", * which is a statement about the probe. * * WHAT IT RETURNS, and why all five numbers rather than a verdict: * *   shape     how the call ENDED — true, silence, false, a half-word, a revert *   sent      how far the holder's own balance FELL *   recv      how far the recipient's balance ROSE *   retLen    the length of the return buffer, kept because 0 and 32 are the *             difference between a pre-ABI token and a modern one *   retWord   the first word of it, so `false` is a fact and not a deduction * * `sent` and `recv` are read separately and are not assumed to be equal — they * are the two numbers the entire sweep is about. A fee-on-transfer token makes * them differ; a truncating token makes both differ from the request; a token * that reports success and moves nothing makes both zero. * * NOTHING HERE REVERTS ON A FINDING. A probe that throws when the token * misbehaves destroys the measurement it was sent to take: the caller learns * only that something went wrong, and every distinct failure arrives looking * the same. */contract Probe {    uint8 constant OK_TRUE = 0;      /* returned a word, non-zero */    uint8 constant OK_EMPTY = 1;     /* returned nothing at all — the pre-ABI shape */    uint8 constant OK_FALSE = 2;     /* returned a word, zero — a refusal, reported as a value */    uint8 constant OK_SHORT = 3;     /* returned something that is not a word */    uint8 constant REVERTED = 4;    uint8 constant NO_CODE = 5;      /* not a token: every call to it "succeeds" */     function run(address token, address to, uint256 amount)        external        returns (uint8 shape, uint256 sent, uint256 recv, uint256 retLen, bytes32 retWord, bytes memory ret)    {        /* An address with no code accepts every call and returns an empty           buffer, which is byte-for-byte what a correct pre-ABI token returns.           Checked first, because after the call the two are indistinguishable. */        if (token.code.length == 0) return (NO_CODE, 0, 0, 0, bytes32(0), "");         uint256 s0 = _bal(token, address(this));        uint256 r0 = _bal(token, to);         bool ok;        (ok, ret) = token.call(            abi.encodeWithSelector(bytes4(0xa9059cbb), to, amount)   /* transfer(address,uint256) */        );         retLen = ret.length;        if (ret.length >= 32) {            assembly ("memory-safe") { retWord := mload(add(ret, 32)) }        }         /* The balances are read again even after a revert. A reverted call           cannot have moved anything, but reading it is what makes that a           measurement rather than an assumption. */        uint256 s1 = _bal(token, address(this));        uint256 r1 = _bal(token, to);        unchecked {            sent = s0 > s1 ? s0 - s1 : 0;            recv = r1 > r0 ? r1 - r0 : 0;        }         /* THE REFUSAL IS RETURNED WHOLE, and this is not a nicety. Seven tokens           in the first run of this sweep refused the probe, and "seven refused"           is not a finding — it is a prompt to ask whether the probe broke them.           Overriding a holder's code makes that address a CONTRACT, and a token           with an anti-bot check on `msg.sender.code.length` will refuse it for           a reason that has nothing to do with the token being wrong. Only the           token's own words separate the two, so they are carried back. */        if (!ok) return (REVERTED, sent, recv, retLen, retWord, ret);        if (ret.length == 0) shape = OK_EMPTY;        else if (ret.length < 32) shape = OK_SHORT;        else shape = retWord == bytes32(0) ? OK_FALSE : OK_TRUE;    }     /** The holder's balance, as this token reports it. A token that cannot        answer `balanceOf` at all reads as zero here, and the caller sees a        request that moved nothing — which is the correct account of it. */    function _bal(address token, address who) private view returns (uint256 v) {        (bool ok, bytes memory d) = token.staticcall(            abi.encodeWithSelector(bytes4(0x70a08231), who)          /* balanceOf(address) */        );        if (ok && d.length >= 32) {            assembly ("memory-safe") { v := mload(add(d, 32)) }        }    }}

Download the raw file →