// SPDX-License-Identifier: MIT pragma solidity 0.8.26; /** * The token zoo. * * Every one of these is a shape that exists on a live chain today. None of them * is a strawman: each is the smallest contract that reproduces one specific way * `transfer` can be true and useless at the same time. * * They exist so the properties can be run against the failure rather than * against a description of it. A test that asserts "we handle fee-on-transfer" * by reading the source is not a test. */ /** A plain, conforming ERC-20. Returns `true`, reverts on failure, moves exactly what it was asked to move. The control: every property that holds here and nowhere else is measuring the zoo, not the contract. */ contract GoodToken { string public name = "Good"; string public symbol = "GOOD"; uint8 public immutable decimals; uint256 public totalSupply; mapping(address => uint256) public balanceOf; mapping(address => mapping(address => uint256)) public allowance; constructor(uint8 d) { decimals = d; } function mint(address to, uint256 v) public virtual { balanceOf[to] += v; totalSupply += v; } function approve(address s, uint256 v) external returns (bool) { allowance[msg.sender][s] = v; return true; } function transfer(address to, uint256 v) external virtual returns (bool) { _move(msg.sender, to, v); return true; } function transferFrom(address f, address to, uint256 v) external virtual returns (bool) { _spend(f, v); _move(f, to, v); return true; } function _spend(address f, uint256 v) internal { uint256 a = allowance[f][msg.sender]; if (a != type(uint256).max) { require(a >= v, "allowance"); allowance[f][msg.sender] = a - v; } } function _move(address f, address t, uint256 v) internal virtual { require(balanceOf[f] >= v, "balance"); unchecked { balanceOf[f] -= v; } balanceOf[t] += v; } } /** * FEE ON TRANSFER. Burns `bps` of every movement, so the recipient's balance * rises by less than the number in the call. This is the shape that breaks an * integration silently: nothing reverts, nothing returns false, and the books * are wrong from the first deposit onward. */ contract FeeToken is GoodToken { uint16 public immutable bps; constructor(uint8 d, uint16 b) GoodToken(d) { bps = b; } function _move(address f, address t, uint256 v) internal override { require(balanceOf[f] >= v, "balance"); uint256 cut = (v * bps) / 10_000; unchecked { balanceOf[f] -= v; } balanceOf[t] += v - cut; totalSupply -= cut; } } /** * RETURNS NOTHING. The pre-ABI shape, and the reason `SafeERC20` exists: a * strict caller that decodes a `bool` reverts on a transfer that in fact * succeeded, so the correct handling is to accept an empty buffer — but only * once you know something ran, which is what `NoCode` below is about. */ contract SilentToken is GoodToken { constructor(uint8 d) GoodToken(d) {} function transfer(address to, uint256 v) external override returns (bool) { _move(msg.sender, to, v); assembly { return(0, 0) } } function transferFrom(address f, address to, uint256 v) external override returns (bool) { _spend(f, v); _move(f, to, v); assembly { return(0, 0) } } } /** * RETURNS FALSE INSTEAD OF REVERTING. Declines, and reports it through a value * that `token.transfer(to, v);` in Solidity discards without a warning when the * interface is declared without a return type. */ contract FalseToken is GoodToken { constructor(uint8 d) GoodToken(d) {} function transfer(address, uint256) external pure override returns (bool) { return false; } function transferFrom(address, address, uint256) external pure override returns (bool) { return false; } } /** * REPORTS SUCCESS AND MOVES NOTHING. The most dangerous member of the zoo, * because every check short of weighing the balance passes: it returns `true`, * it does not revert, and it has code. */ contract LyingToken is GoodToken { constructor(uint8 d) GoodToken(d) {} function transfer(address, uint256) external pure override returns (bool) { return true; } function transferFrom(address, address, uint256) external pure override returns (bool) { return true; } } /** * RETURNS A HALF-WORD. Not silence and not a bool — 8 bytes. `abi.decode` of a * short buffer reverts, and a hand-rolled `mload` of it reads whatever is next * in memory, which is usually non-zero and therefore "true". */ contract ShortReturnToken is GoodToken { constructor(uint8 d) GoodToken(d) {} function transfer(address to, uint256 v) external override returns (bool) { _move(msg.sender, to, v); assembly { mstore(0, 1) return(0, 8) } } function transferFrom(address f, address to, uint256 v) external override returns (bool) { _spend(f, v); _move(f, to, v); assembly { mstore(0, 1) return(0, 8) } } } /** * BALANCES MOVE WITHOUT A TRANSFER. A downward rebase, a blacklist sweep, a * token whose owner can burn from an address. This is the one way a correctly * written receiving account can still end up owing more than it holds, which * is why the deficit is a reported number rather than a prevented one. */ contract RebaseToken is GoodToken { constructor(uint8 d) GoodToken(d) {} function slash(address who, uint256 v) external { balanceOf[who] -= v; totalSupply -= v; } } /** * TAKES ITS FEE OUT OF THE SENDER'S REMAINING BALANCE. The recipient gets the * full amount and the holder is charged more than it authorised — the mirror * image of FeeToken, and the case where a receiving account must refuse rather * than report. */ contract SurchargeToken is GoodToken { uint256 public immutable extra; constructor(uint8 d, uint256 e) GoodToken(d) { extra = e; } function _move(address f, address t, uint256 v) internal override { require(balanceOf[f] >= v + extra, "balance"); unchecked { balanceOf[f] -= v + extra; } balanceOf[t] += v; totalSupply -= extra; } } /** * CALLS BACK. ERC-777 and every token with a transfer hook. The callback lands * between the two balance readings, which is exactly where a receiving account * that measures a difference is at its most vulnerable. */ contract HookToken is GoodToken { address public hookTarget; bytes public hookData; bool private firing; /* The token SWALLOWS the callback's revert, which is what a real hooked token does and what makes this shape dangerous: the outer transfer succeeds whether or not the re-entrant call was refused. These two flags exist so a property can tell the difference between "the guard held" and "the callback never ran", which otherwise look identical from outside. */ bool public hookFired; bool public hookReverted; constructor(uint8 d) GoodToken(d) {} function arm(address t, bytes calldata d) external { hookTarget = t; hookData = d; } function _move(address f, address t, uint256 v) internal override { super._move(f, t, v); if (hookTarget != address(0) && !firing) { firing = true; (bool ok, ) = hookTarget.call(hookData); firing = false; hookFired = true; hookReverted = !ok; } } } /** * MOVES LESS THAN IT WAS ASKED TO MOVE, out of BOTH accounts. A per-transfer * cap that truncates instead of reverting — the shape a "max transaction" * token ships with. The sender is debited the truncated figure too, so the * contract's own balance falls by less than the request, and a receiving * account that debits the request strands the difference in nobody's name. */ contract TruncateToken is GoodToken { uint256 public immutable maxTx; constructor(uint8 d, uint256 m) GoodToken(d) { maxTx = m; } function _move(address f, address t, uint256 v) internal override { uint256 m = v > maxTx ? maxTx : v; require(balanceOf[f] >= m, "balance"); unchecked { balanceOf[f] -= m; } balanceOf[t] += m; } } /** Reverts with a reason string, so the pass-through can be checked. */ contract ReasonToken is GoodToken { constructor(uint8 d) GoodToken(d) {} function transfer(address, uint256) external pure override returns (bool) { revert("frozen"); } function transferFrom(address, address, uint256) external pure override returns (bool) { revert("frozen"); } }