Built on Robinhood Chain · block 58,933,985

Ullage

The number in the call is a request. The number that arrives is a fact. EIP-20 does not promise they are equal — and almost every contract holding your tokens has written down the first one.

166 tokens weighed on chain 23 properties run on Robinhood Chain nothing credited from an argument

Three sentences EIP-20 does not contain

What transfer actually promises,
and what everyone reads into it.

That the recipient gets the amount

The standard describes what happens to the SENDER. It never says the recipient's balance rises by the same number, and a token that takes a cut on the way through is not violating anything — it is being read wrongly by everything that integrates with it.

That a failure reverts

SHOULD throw, not MUST — and the reference ABI returns a bool precisely so that a token can decline by returning false instead. Declared without a return type, Solidity discards that value and never warns.

That the call returns anything

Some of the largest token contracts in circulation return nothing at all, because they were written before the ABI settled. A strict bool decode reverts on a transfer that in fact succeeded, and the units are then stuck in a contract that thinks the call failed.

What this chain actually does

Everything here delivers. That is the reason nobody checks.

166 of 166live tokens delivered exactly the number in the call, down every path we asked
4would not move their holder's balance at all
3of those reverted with no reason at all — an empty buffer, and nothing to report

2684 contracts emitted an ERC-20 Transfer in 15,000 blocks. The 170 most active were weighed, at one pinned block, down 4 paths each — three holders to a fresh address, and the second holder to the first, which is a sell. Every row is published.

A result of zero is not a reason to relax. It is the reason the assumption is everywhere: it has been true every time anybody looked, so nothing was ever built to notice when it stops.

An assumption that has never been tested is not the same as one that has been. The four tokens below are on this chain today, and three of them refuse without saying why — which is the shape the first real failure will arrive in.

And the moment a token from another chain is bridged here, or one is deployed with a tax on it, the difference between the two numbers stops being zero and every contract that wrote down the request is holding a figure that was never true.

tokenwhat it said when it refused
ND4 nothing an empty revert buffer
PONS nothing an empty revert buffer
QUOTRON a custom error 0xc16ce029, and four bytes is all there is
SHRUB nothing an empty revert buffer

A refusal that says nothing is its own defect: the caller cannot tell a blacklist from a paused contract from a bug.

The contract

Every number it writes down
is a difference it measured itself.

Ullage is an escrow. You deposit, it credits you, you withdraw. The whole of it is that it never takes a quantity from an argument.

A deposit credits what arrived

balanceOf(this) after, minus before. Never amount. A fee-on-transfer token that is sent 100 and delivers 97 is credited with 97, and a token that reports success and moves nothing is refused rather than credited with zero.

A withdrawal debits what left

The contract's own balance before, minus after. A token that moves less than it was asked to leaves the difference credited where it belongs, and the payee's shortfall is returned to the caller rather than hidden or reverted on.

Which gives one invariant, and it is checked after every operation in the suite

booked[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 outside a transfer can still break it — a rebase, a blacklist — and that is the one case a receiving account cannot prevent. So it is reported: deficit() names the number and payouts follow the balance.

The zoo

Ten ways a transfer is true and useless,
each one a contract that was executed.

Not descriptions. Every row is a real contract in contracts/Mocks.sol, deployed and run against Robinhood Chain's own EVM inside the property suite, and the property named beside it is the one that has to notice.

the failurethe contractwhat it doescaught by
a fee on the way through FeeToken Moves 100 and delivers 97. Nothing reverts, nothing returns false, and every set of books that wrote down 100 is wrong from that moment on. P2, P9, P21
success, and nothing moved LyingToken Returns true, does not revert, and has code. It passes every check short of weighing the balance. P3
no return value at all SilentToken The pre-ABI shape, and the reason SafeERC20 exists. A caller that decodes a bool reverts on a transfer that worked. P4
false instead of a revert FalseToken Declines through a value that Solidity discards without a warning when the interface is declared without a return type. P5
no code at the address 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. P6
half a word back ShortReturnToken Eight bytes. abi.decode reverts on it; a hand-rolled mload reads whatever is next in memory, which is usually non-zero and therefore "true". P7
a cap that truncates TruncateToken Asked for 50, moves 20, out of both accounts, and says nothing about it. P23
a fee out of the sender SurchargeToken The payee is made whole and the sender is charged more than it authorised. The one shortfall this contract refuses rather than reports. P10
a callback mid-transfer HookToken Lands between the two balance readings, which is exactly where measuring a difference is weakest. P11
units removed from outside RebaseToken A downward rebase or a blacklist sweep. The one failure a receiving account genuinely cannot prevent — so it is named rather than smoothed. P12, P22

Executed, not asserted

Every claim on this page was run on the chain.

23 of 23properties, through eth_call with no to — no testnet, no fork, no funded account, no key
16 of 16sabotages: the contract compiled again with one defect at a time, each caught by a specific property
1declared survivor, published with its reason — hiding it would hide where the floor is

Every property and every sabotage, with its result →

Weigh one yourself.

Paste any token and any holder. The page runs the sweep's own measurement in your browser, against live state, with no wallet and no transaction — and refuses to report a result if its control does not hold.

Open the app