// SPDX-License-Identifier: MIT pragma solidity 0.8.26; import "./Ullage.sol"; import "./Mocks.sol"; /** * Ullage's property suite, executed on Robinhood Chain's own EVM. * * `eth_call` with no `to` runs creation code against real chain state and * returns whatever the constructor returns. No testnet, no fork, no funded * account, no key, nothing installed. * * A PROPERTY IS AN EXTERNAL CALL IN try/catch. Called internally, one that * reverts takes the whole suite down with it and the runner reports "eth_call * reverted" — which is a different finding from "P11 failed", and the * difference is the whole value of the run. The constructor cannot call itself * (there is no code at its address yet), so it deploys a group and calls that. * * THE GROUPS ARE AN EIP-170 CONSTRAINT, NOT AN ORGANISING PRINCIPLE. Written as * one contract this exceeds 24,576 bytes, the inner CREATE returns zero, and * every property reports "revert: no reason" — which reads like a broken * contract rather than a group that will not fit. * * WHERE A REVERT IS THE EXPECTED RESULT, THE SELECTOR IS CHECKED. Observing * that "something refused" is not a test of a specific guard: with the guard * removed, the call very often still reverts, one line further down, for an * unrelated reason. Every negative property here names the error it wants. */ contract UllageTest { constructor(uint256 idx) { bool pass; string memory why; Group g; if (idx <= 6) g = Group(address(new PropsA())); else if (idx <= 9) g = Group(address(new PropsB())); else if (idx <= 12) g = Group(address(new PropsBB())); else if (idx <= 18) g = Group(address(new PropsC())); else g = Group(address(new PropsD())); try g.run(idx) returns (bool ok, string memory reason) { pass = ok; why = reason; } catch Error(string memory reason) { pass = false; why = string.concat("revert: ", reason); } catch { pass = false; why = "revert: no reason"; } bytes memory out = abi.encode(pass, why); assembly { return(add(out, 32), mload(out)) } } } interface Group { function run(uint256) external returns (bool, string memory); } abstract contract Base { uint8 constant DEC = 6; /* USDG's decimals, so the numbers are the real shape */ uint256 constant UNIT = 10 ** DEC; uint256 constant HUNDRED = 100 * UNIT; address constant OTHER = address(0xB0B); /* ---- assertions, as values rather than reverts ------------------- */ function _eq(uint256 a, uint256 b, string memory what) internal pure returns (bool, string memory) { if (a == b) return (true, ""); return (false, string.concat(what, ": ", _u(a), " != ", _u(b))); } function _u(uint256 v) internal pure returns (string memory) { if (v == 0) return "0"; uint256 n = v; uint256 len; while (n != 0) { len++; n /= 10; } bytes memory b = new bytes(len); while (v != 0) { b[--len] = bytes1(uint8(48 + (v % 10))); v /= 10; } return string(b); } function _sel(bytes memory e) internal pure returns (bytes4 s) { if (e.length < 4) return bytes4(0); assembly { s := mload(add(e, 32)) } } function _hex4(bytes4 s) internal pure returns (string memory) { bytes memory h = "0123456789abcdef"; bytes memory o = new bytes(10); o[0] = "0"; o[1] = "x"; for (uint256 i = 0; i < 4; i++) { o[2 + i * 2] = h[uint8(s[i]) >> 4]; o[3 + i * 2] = h[uint8(s[i]) & 0xf]; } return string(o); } /* THE INVARIANT. Checked after operations in a dozen properties, because a receiving account that has written down more than it is holding is the single failure this contract exists to make impossible. */ function _solvent(Ullage u, address t) internal view returns (bool, string memory) { uint256 b = u.booked(t); uint256 h = u.held(t); if (b <= h) return (true, ""); return (false, string.concat("booked ", _u(b), " exceeds held ", _u(h))); } function _good(uint256 fund) internal returns (GoodToken t, Ullage u) { t = new GoodToken(DEC); u = new Ullage(); t.mint(address(this), fund); t.approve(address(u), type(uint256).max); } } /* ==================================================================== * * A — what a deposit writes down * ==================================================================== */ contract PropsA is Base { function run(uint256 i) external returns (bool, string memory) { if (i == 1) return p1(); if (i == 2) return p2(); if (i == 3) return p3(); if (i == 4) return p4(); if (i == 5) return p5(); return p6(); } /** P1 — on a conforming token the measured arrival is the amount, and the books balance. The control for the whole suite: if this fails, the zoo is broken, not the contract. */ function p1() internal returns (bool, string memory) { (GoodToken t, Ullage u) = _good(HUNDRED); uint256 arrived = u.deposit(address(t), HUNDRED); (bool ok, string memory w) = _eq(arrived, HUNDRED, "arrived"); if (!ok) return (ok, w); (ok, w) = _eq(u.credit(address(t), address(this)), HUNDRED, "credit"); if (!ok) return (ok, w); (ok, w) = _eq(u.booked(address(t)), HUNDRED, "booked"); if (!ok) return (ok, w); (ok, w) = _solvent(u, address(t)); if (!ok) return (ok, w); return (true, "deposited 100.000000 and booked exactly 100.000000"); } /** P2 — THE PROPERTY THE CONTRACT IS FOR. A 3% fee-on-transfer token is sent 100 and delivers 97. The credit is 97. Every integration that writes down 100 here is holding a number that is not true. */ function p2() internal returns (bool, string memory) { FeeToken t = new FeeToken(DEC, 300); Ullage u = new Ullage(); t.mint(address(this), HUNDRED); t.approve(address(u), type(uint256).max); uint256 arrived = u.deposit(address(t), HUNDRED); (bool ok, string memory w) = _eq(arrived, 97 * UNIT, "arrived"); if (!ok) return (ok, w); (ok, w) = _eq(u.credit(address(t), address(this)), 97 * UNIT, "credit"); if (!ok) return (ok, w); (ok, w) = _eq(u.booked(address(t)), 97 * UNIT, "booked"); if (!ok) return (ok, w); (ok, w) = _eq(u.held(address(t)), 97 * UNIT, "held"); if (!ok) return (ok, w); (ok, w) = _solvent(u, address(t)); if (!ok) return (ok, w); return (true, "asked 100.000000, 97.000000 arrived, 97.000000 booked"); } /** P3 — a token that returns true and moves nothing is refused. It passes every check that does not involve weighing the balance. */ function p3() internal returns (bool, string memory) { LyingToken t = new LyingToken(DEC); Ullage u = new Ullage(); t.mint(address(this), HUNDRED); t.approve(address(u), type(uint256).max); try u.deposit(address(t), HUNDRED) returns (uint256 got) { return (false, string.concat("credited ", _u(got), " for a transfer that never happened")); } catch (bytes memory e) { if (_sel(e) != Ullage.NothingArrived.selector) { return (false, string.concat("wrong error ", _hex4(_sel(e)))); } return (true, "a token that reports success and moves nothing is refused"); } } /** P4 — a token that returns NO DATA works. Rejecting it is the other half of the same bug: a strict bool decode reverts on a transfer that succeeded, and the units are then stuck in a contract that thinks the call failed. */ function p4() internal returns (bool, string memory) { SilentToken t = new SilentToken(DEC); Ullage u = new Ullage(); t.mint(address(this), HUNDRED); t.approve(address(u), type(uint256).max); uint256 arrived = u.deposit(address(t), HUNDRED); (bool ok, string memory w) = _eq(arrived, HUNDRED, "arrived"); if (!ok) return (ok, w); (ok, w) = _solvent(u, address(t)); if (!ok) return (ok, w); return (true, "an empty return buffer is accepted, and the arrival is still weighed"); } /** P5 — a token that declines by returning `false` is refused, with the error that names it. */ function p5() internal returns (bool, string memory) { FalseToken t = new FalseToken(DEC); Ullage u = new Ullage(); t.mint(address(this), HUNDRED); t.approve(address(u), type(uint256).max); try u.deposit(address(t), HUNDRED) { return (false, "a transfer that returned false was accepted"); } catch (bytes memory e) { if (_sel(e) != Ullage.TransferReturnedFalse.selector) { return (false, string.concat("wrong error ", _hex4(_sel(e)))); } return (true, "`false` is a refusal, and it is read"); } } /** P6 — AN ADDRESS WITH NO CODE ACCEPTS EVERY CALL. It returns success and an empty buffer, which is byte-for-byte what a correct USDT-shaped transfer returns. Only the code check separates them. */ function p6() internal returns (bool, string memory) { Ullage u = new Ullage(); address notAToken = address(0xDEAD); try u.deposit(notAToken, HUNDRED) { return (false, "a call to an address with no code was taken for a transfer"); } catch (bytes memory e) { if (_sel(e) != Ullage.NotAToken.selector) { return (false, string.concat("wrong error ", _hex4(_sel(e)))); } return (true, "an address with no code is not a token, whatever it returns"); } } } /* ==================================================================== * * B — what a withdrawal writes down * ==================================================================== */ contract PropsB is Base { function run(uint256 i) external returns (bool, string memory) { if (i == 7) return p7(); if (i == 8) return p8(); return p9(); } /** P7 — a return value that is neither silence nor a word is refused rather than decoded into whatever it happens to look like. */ function p7() internal returns (bool, string memory) { ShortReturnToken t = new ShortReturnToken(DEC); Ullage u = new Ullage(); t.mint(address(this), HUNDRED); t.approve(address(u), type(uint256).max); try u.deposit(address(t), HUNDRED) { return (false, "an 8-byte return value was decoded as a bool"); } catch (bytes memory e) { if (_sel(e) != Ullage.MalformedReturn.selector) { return (false, string.concat("wrong error ", _hex4(_sel(e)))); } return (true, "a half-word return is malformed, not `true`"); } } /** P8 — on a conforming token both measurements agree with the request, and the credit falls by exactly what left. */ function p8() internal returns (bool, string memory) { (GoodToken t, Ullage u) = _good(HUNDRED); u.deposit(address(t), HUNDRED); (uint256 left, uint256 arrived) = u.withdraw(address(t), 40 * UNIT); (bool ok, string memory w) = _eq(left, 40 * UNIT, "left"); if (!ok) return (ok, w); (ok, w) = _eq(arrived, 40 * UNIT, "arrived"); if (!ok) return (ok, w); (ok, w) = _eq(u.credit(address(t), address(this)), 60 * UNIT, "credit"); if (!ok) return (ok, w); (ok, w) = _eq(u.booked(address(t)), 60 * UNIT, "booked"); if (!ok) return (ok, w); (ok, w) = _solvent(u, address(t)); if (!ok) return (ok, w); return (true, "40.000000 left, 40.000000 arrived, 60.000000 still booked"); } /** P9 — THE TWO NUMBERS COME APART, AND BOTH ARE RETURNED. On a fee token the contract's balance falls by the full amount and the payee receives less. The debit is what left; the shortfall is reported, not hidden, and not reverted — reverting would make the token permanently unwithdrawable, which is a worse answer than the truth. */ function p9() internal returns (bool, string memory) { FeeToken t = new FeeToken(DEC, 300); Ullage u = new Ullage(); t.mint(address(this), HUNDRED); t.approve(address(u), type(uint256).max); uint256 credited = u.deposit(address(t), HUNDRED); /* 97 */ (uint256 left, uint256 arrived) = u.withdrawTo(address(t), 50 * UNIT, OTHER); (bool ok, string memory w) = _eq(left, 50 * UNIT, "left"); if (!ok) return (ok, w); (ok, w) = _eq(arrived, 485 * UNIT / 10, "arrived"); /* 48.5 */ if (!ok) return (ok, w); (ok, w) = _eq(t.balanceOf(OTHER), 485 * UNIT / 10, "payee balance"); if (!ok) return (ok, w); (ok, w) = _eq(u.credit(address(t), address(this)), credited - 50 * UNIT, "credit"); if (!ok) return (ok, w); (ok, w) = _solvent(u, address(t)); if (!ok) return (ok, w); return (true, "50.000000 left the contract and 48.500000 arrived; both are returned"); } } /* ==================================================================== * * BB — the shortfalls that are refused * ==================================================================== */ contract PropsBB is Base { function run(uint256 i) external returns (bool, string memory) { if (i == 10) return p10(); if (i == 11) return p11(); return p12(); } /** P10 — the one shortfall that DOES revert. A token that takes its cut out of the sender leaves the contract lighter than the holder authorised, and no receiving account may allow that. */ function p10() internal returns (bool, string memory) { SurchargeToken t = new SurchargeToken(DEC, 5 * UNIT); Ullage u = new Ullage(); t.mint(address(this), HUNDRED + 5 * UNIT); t.approve(address(u), type(uint256).max); u.deposit(address(t), HUNDRED); try u.withdrawTo(address(t), 10 * UNIT, OTHER) { return (false, "the contract lost more than it was told to send"); } catch (bytes memory e) { if (_sel(e) != Ullage.Overdraw.selector) { return (false, string.concat("wrong error ", _hex4(_sel(e)))); } return (true, "a contract may deliver less than asked; it may never lose more"); } } /** P11 — REENTRANCY, WHICH IS WHERE MEASURING A DIFFERENCE IS WEAKEST. The callback lands between the two readings, so without the latch the outer deposit's second reading includes the inner deposit's tokens and the same units are credited twice — 70 booked against 60 held. The token SWALLOWS the callback's revert, which is what a hooked token really does, so the outer deposit succeeds either way and the outcome has to be read off the books rather than off a thrown error. `hookFired` is the check that stops this being vacuous. A guard that is never reached and a guard that held look identical from out here, and this family has already shipped a property that tested nothing for three runs because the thing it was guarding against never happened. */ function p11() internal returns (bool, string memory) { HookToken t = new HookToken(DEC); Ullage u = new Ullage(); t.mint(address(this), HUNDRED); t.approve(address(u), type(uint256).max); /* THE RE-ENTRANT CALLER HAS TO BE FUNDED, and the first version of this property was not. Armed to call `deposit` directly, the token itself becomes msg.sender — an address with no balance and no allowance — so the inner call reverted on the allowance whether the latch was there or not, and removing the latch changed nothing. The property passed and tested the token's empty wallet. */ Reenterer r = new Reenterer(); t.mint(address(r), 10 * UNIT); r.setup(GoodToken(address(t)), u); t.arm(address(r), abi.encodeCall(Reenterer.go, (address(t), 10 * UNIT))); u.deposit(address(t), 50 * UNIT); if (!t.hookFired()) return (false, "the callback never ran, so this property tested nothing"); if (!t.hookReverted()) return (false, "the re-entrant deposit was allowed to run"); (bool ok, string memory w) = _eq(u.credit(address(t), address(this)), 50 * UNIT, "credit"); if (!ok) return (ok, w); (ok, w) = _eq(u.booked(address(t)), 50 * UNIT, "booked"); if (!ok) return (ok, w); (ok, w) = _solvent(u, address(t)); if (!ok) return (ok, w); return (true, "the callback ran, was refused, and 50.000000 was credited once"); } /** P12 — THE ONE FAILURE A RECEIVING ACCOUNT CANNOT PREVENT. Units removed from outside a transfer put the books above the balance. It is named rather than smoothed, and the payout cap follows the balance. */ function p12() internal returns (bool, string memory) { RebaseToken t = new RebaseToken(DEC); Ullage u = new Ullage(); t.mint(address(this), HUNDRED); t.approve(address(u), type(uint256).max); u.deposit(address(t), HUNDRED); t.slash(address(u), 30 * UNIT); (bool ok, string memory w) = _eq(u.deficit(address(t)), 30 * UNIT, "deficit"); if (!ok) return (ok, w); (ok, w) = _eq(u.credit(address(t), address(this)), HUNDRED, "credit is untouched"); if (!ok) return (ok, w); (ok, w) = _eq(u.available(address(t), address(this)), 70 * UNIT, "available"); if (!ok) return (ok, w); (ok, w) = _eq(u.surplus(address(t)), 0, "surplus"); if (!ok) return (ok, w); return (true, "30.000000 taken from outside: the deficit is named and the quote follows the balance"); } } /* ==================================================================== * * C — the boundaries * ==================================================================== */ contract PropsC is Base { function run(uint256 i) external returns (bool, string memory) { if (i == 13) return p13(); if (i == 14) return p14(); if (i == 15) return p15(); if (i == 16) return p16(); if (i == 17) return p17(); return p18(); } /** P13 — the cap is TIGHT, in both directions: exactly `available` goes through, and one unit more is refused with the cap it reports. Checking only that too much reverts passes against a contract that pays nothing at all. */ function p13() internal returns (bool, string memory) { RebaseToken t = new RebaseToken(DEC); Ullage u = new Ullage(); t.mint(address(this), HUNDRED); t.approve(address(u), type(uint256).max); u.deposit(address(t), HUNDRED); t.slash(address(u), 30 * UNIT); uint256 cap = u.available(address(t), address(this)); try u.withdrawTo(address(t), cap + 1, OTHER) { return (false, "one unit above the cap was paid"); } catch (bytes memory e) { if (_sel(e) != Ullage.InsufficientCredit.selector) { return (false, string.concat("wrong error ", _hex4(_sel(e)))); } } (uint256 left, uint256 arrived) = u.withdrawTo(address(t), cap, OTHER); (bool ok, string memory w) = _eq(left, cap, "left"); if (!ok) return (ok, w); (ok, w) = _eq(arrived, cap, "arrived"); if (!ok) return (ok, w); (ok, w) = _eq(u.held(address(t)), 0, "held after"); if (!ok) return (ok, w); return (true, string.concat("exactly ", _u(cap), " is payable and ", _u(cap + 1), " is not")); } /** P14 — a plain transfer in names no owner, so it sits unbooked until somebody claims it, and it can be claimed exactly once. */ function p14() internal returns (bool, string memory) { (GoodToken t, Ullage u) = _good(HUNDRED); t.transfer(address(u), 25 * UNIT); (bool ok, string memory w) = _eq(u.surplus(address(t)), 25 * UNIT, "surplus"); if (!ok) return (ok, w); (ok, w) = _eq(u.booked(address(t)), 0, "booked before"); if (!ok) return (ok, w); uint256 claimed = u.book(address(t)); (ok, w) = _eq(claimed, 25 * UNIT, "claimed"); if (!ok) return (ok, w); (ok, w) = _eq(u.surplus(address(t)), 0, "surplus after"); if (!ok) return (ok, w); (ok, w) = _solvent(u, address(t)); if (!ok) return (ok, w); try u.book(address(t)) { return (false, "the same surplus was booked twice"); } catch (bytes memory e) { if (_sel(e) != Ullage.NothingToBook.selector) { return (false, string.concat("wrong error ", _hex4(_sel(e)))); } } return (true, "an unattributed 25.000000 is claimable once, and only once"); } /** P15 — and booking cannot reach a deposit. A guard that is only tested with an empty contract is not tested: here somebody else's 60 is already credited when the claim is made. */ function p15() internal returns (bool, string memory) { (GoodToken t, Ullage u) = _good(HUNDRED); u.deposit(address(t), 60 * UNIT); t.transfer(address(u), 25 * UNIT); uint256 claimed = u.book(address(t)); (bool ok, string memory w) = _eq(claimed, 25 * UNIT, "claimed"); if (!ok) return (ok, w); (ok, w) = _eq(u.booked(address(t)), 85 * UNIT, "booked"); if (!ok) return (ok, w); (ok, w) = _solvent(u, address(t)); if (!ok) return (ok, w); return (true, "booking takes the 25.000000 nobody was credited with, and not the 60.000000 that was"); } /** P16 — the contract refuses to be its own payee. Crediting an address whose balance is the measurement makes the next reading meaningless. */ function p16() internal returns (bool, string memory) { (GoodToken t, Ullage u) = _good(HUNDRED); u.deposit(address(t), HUNDRED); try u.depositFor(address(t), UNIT, address(u)) { return (false, "the contract credited itself"); } catch (bytes memory e) { if (_sel(e) != Ullage.BadRecipient.selector) return (false, string.concat("deposit: ", _hex4(_sel(e)))); } try u.withdrawTo(address(t), UNIT, address(u)) { return (false, "the contract paid itself"); } catch (bytes memory e) { if (_sel(e) != Ullage.BadRecipient.selector) return (false, string.concat("withdraw: ", _hex4(_sel(e)))); } try u.withdrawTo(address(t), UNIT, address(0)) { return (false, "the contract paid the zero address"); } catch (bytes memory e) { if (_sel(e) != Ullage.BadRecipient.selector) return (false, string.concat("zero: ", _hex4(_sel(e)))); } return (true, "the contract is not a valid payee, and neither is address zero"); } /** P17 — one holder's exit does not touch another's books. The flattering version of this checks a balance; this one makes the second holder actually withdraw and requires the full amount. */ function p17() internal returns (bool, string memory) { (GoodToken t, Ullage u) = _good(HUNDRED); Holder h = new Holder(); t.transfer(address(h), 40 * UNIT); h.setup(t, u); u.deposit(address(t), 60 * UNIT); h.put(address(t), 40 * UNIT); (bool ok, string memory w) = _eq(u.booked(address(t)), HUNDRED, "booked"); if (!ok) return (ok, w); u.withdrawTo(address(t), 60 * UNIT, OTHER); (ok, w) = _eq(u.credit(address(t), address(h)), 40 * UNIT, "other credit"); if (!ok) return (ok, w); uint256 got = h.take(address(t), 40 * UNIT); (ok, w) = _eq(got, 40 * UNIT, "second holder received"); if (!ok) return (ok, w); (ok, w) = _eq(u.booked(address(t)), 0, "booked at the end"); if (!ok) return (ok, w); (ok, w) = _solvent(u, address(t)); if (!ok) return (ok, w); return (true, "the first exit leaves the second holder whole, and the second is paid in full"); } /** P18 — the token's own revert reason survives. A wrapper that replaces it with its own makes every caller debug the wrapper. */ function p18() internal returns (bool, string memory) { ReasonToken t = new ReasonToken(DEC); Ullage u = new Ullage(); t.mint(address(this), HUNDRED); t.approve(address(u), type(uint256).max); try u.deposit(address(t), HUNDRED) { return (false, "a reverting token was accepted"); } catch Error(string memory reason) { if (keccak256(bytes(reason)) != keccak256(bytes("frozen"))) { return (false, string.concat("reason was rewritten to: ", reason)); } return (true, "the token said `frozen`, and that is what the caller is told"); } catch { return (false, "the token's reason string was discarded"); } } } /* ==================================================================== * * D — the long run * ==================================================================== */ contract PropsD is Base { function run(uint256 i) external returns (bool, string memory) { if (i == 19) return p19(); if (i == 20) return p20(); if (i == 21) return p21(); if (i == 22) return p22(); return p23(); } /** P19 — THE INVARIANT UNDER A MIXED SEQUENCE, checked after every step rather than at the end. A final-state check passes against a contract that is briefly insolvent in the middle, which is exactly when a callback would read it. */ function p19() internal returns (bool, string memory) { FeeToken t = new FeeToken(DEC, 250); Ullage u = new Ullage(); t.mint(address(this), 1000 * UNIT); t.approve(address(u), type(uint256).max); uint256 steps; for (uint256 k = 1; k <= 6; k++) { u.deposit(address(t), k * 7 * UNIT); (bool ok, string memory w) = _solvent(u, address(t)); if (!ok) return (false, string.concat("after deposit ", _u(k), ": ", w)); steps++; uint256 cap = u.available(address(t), address(this)); if (cap > 3 * UNIT) { u.withdrawTo(address(t), 3 * UNIT, OTHER); (ok, w) = _solvent(u, address(t)); if (!ok) return (false, string.concat("after withdrawal ", _u(k), ": ", w)); steps++; } } return (true, string.concat("booked never exceeded held across ", _u(steps), " operations on a fee token")); } /** P20 — a withdrawal of nothing is refused. A zero that is allowed to proceed emits a settlement event for a transfer that did not happen. */ function p20() internal returns (bool, string memory) { (GoodToken t, Ullage u) = _good(HUNDRED); u.deposit(address(t), HUNDRED); try u.withdraw(address(t), 0) { return (false, "a withdrawal of zero was settled"); } catch (bytes memory e) { if (_sel(e) != Ullage.NothingArrived.selector) { return (false, string.concat("wrong error ", _hex4(_sel(e)))); } return (true, "zero is not a settlement"); } } /** P21 — a fee token can be fully emptied. The credit and the books both reach zero, which is the check that the debit is the measured departure rather than the request: debiting the request strands the difference. */ function p21() internal returns (bool, string memory) { FeeToken t = new FeeToken(DEC, 300); Ullage u = new Ullage(); t.mint(address(this), HUNDRED); t.approve(address(u), type(uint256).max); uint256 credited = u.deposit(address(t), HUNDRED); u.withdrawTo(address(t), credited, OTHER); (bool ok, string memory w) = _eq(u.credit(address(t), address(this)), 0, "credit"); if (!ok) return (ok, w); (ok, w) = _eq(u.booked(address(t)), 0, "booked"); if (!ok) return (ok, w); (ok, w) = _eq(u.held(address(t)), 0, "held"); if (!ok) return (ok, w); return (true, string.concat("the whole ", _u(credited), " came back out and nothing was stranded")); } /** P22 — a deficit does not become somebody else's loss by accident. After a slash the first holder can take the whole remaining balance, and the second holder is then quoted zero rather than a number the contract cannot honour. */ function p22() internal returns (bool, string memory) { RebaseToken t = new RebaseToken(DEC); Ullage u = new Ullage(); t.mint(address(this), HUNDRED); t.approve(address(u), type(uint256).max); Holder h = new Holder(); t.transfer(address(h), 40 * UNIT); h.setup(GoodToken(address(t)), u); u.deposit(address(t), 60 * UNIT); h.put(address(t), 40 * UNIT); t.slash(address(u), 70 * UNIT); (bool ok, string memory w) = _eq(u.deficit(address(t)), 70 * UNIT, "deficit"); if (!ok) return (ok, w); uint256 cap = u.available(address(t), address(this)); (ok, w) = _eq(cap, 30 * UNIT, "first holder's cap"); if (!ok) return (ok, w); u.withdrawTo(address(t), cap, OTHER); (ok, w) = _eq(u.available(address(t), address(h)), 0, "second holder's cap"); if (!ok) return (ok, w); (ok, w) = _eq(u.credit(address(t), address(h)), 40 * UNIT, "second holder's credit"); if (!ok) return (ok, w); return (true, "the balance runs out first-come, and the quote says so instead of promising 40.000000"); } /** P23 — THE DEBIT IS THE DEPARTURE. A token with a per-transfer cap moves 20 when it is asked for 50, out of both accounts, and says nothing. The credit must fall by the 20 that left. A contract that debits the 50 it asked for has taken 30 from its own holder and given them to nobody — the same error as crediting a request, pointing the other way. */ function p23() internal returns (bool, string memory) { TruncateToken t = new TruncateToken(DEC, 20 * UNIT); Ullage u = new Ullage(); t.mint(address(this), HUNDRED); t.approve(address(u), type(uint256).max); u.deposit(address(t), 20 * UNIT); u.deposit(address(t), 20 * UNIT); u.deposit(address(t), 20 * UNIT); (bool ok, string memory w) = _eq(u.credit(address(t), address(this)), 60 * UNIT, "credit before"); if (!ok) return (ok, w); (uint256 left, uint256 arrived) = u.withdrawTo(address(t), 50 * UNIT, OTHER); (ok, w) = _eq(left, 20 * UNIT, "left"); if (!ok) return (ok, w); (ok, w) = _eq(arrived, 20 * UNIT, "arrived"); if (!ok) return (ok, w); (ok, w) = _eq(u.credit(address(t), address(this)), 40 * UNIT, "credit after"); if (!ok) return (ok, w); (ok, w) = _eq(u.booked(address(t)), 40 * UNIT, "booked"); if (!ok) return (ok, w); (ok, w) = _solvent(u, address(t)); if (!ok) return (ok, w); return (true, "asked for 50.000000, 20.000000 left, and exactly 20.000000 was debited"); } } /** A funded third party that re-enters from inside a token's transfer hook. It holds its own units and its own allowance, so the inner deposit is one that would genuinely succeed — which is the only version of this that tests the latch rather than an empty wallet. */ contract Reenterer { GoodToken t; Ullage u; function setup(GoodToken t_, Ullage u_) external { t = t_; u = u_; t.approve(address(u), type(uint256).max); } function go(address tok, uint256 v) external { u.deposit(tok, v); } } /** A second party, so "another holder" is a real address with its own allowance rather than the test contract wearing a different hat. */ contract Holder { GoodToken t; Ullage u; function setup(GoodToken t_, Ullage u_) external { t = t_; u = u_; t.approve(address(u), type(uint256).max); } function put(address tok, uint256 v) external returns (uint256) { return u.deposit(tok, v); } function take(address tok, uint256 v) external returns (uint256 arrived) { (, arrived) = u.withdrawTo(tok, v, address(this)); } }