// SPDX-License-Identifier: MIT pragma solidity 0.8.26; import "./Varve.sol"; import "./Mocks.sol"; /** * The property suite, executed on Robinhood Chain's own EVM. * * `eth_call` with no `to` runs creation code and returns whatever the * constructor returns, so each property is a real execution against real chain * state, costs nothing, needs no key and leaves nothing behind. * * A PROPERTY IS AN EXTERNAL CALL IN try/catch, and it has to be. Called * internally, a property that reverts takes the whole suite down and the * runner reports "eth_call reverted" — which is a different finding from * "P14 failed". The constructor cannot self-call (there is no code at its * address yet), so it deploys a group and calls THAT. * * THE GROUPS ARE A DEPLOYABILITY CONSTRAINT, NOT AN ORGANISING PRINCIPLE. * Written as one contract this suite compiles to more than twice EIP-170's * 24,576-byte limit, so the inner CREATE fails and every property reports * "revert: no reason". tools/pack-props.mjs measures each candidate group and * splits on the real number rather than a guess — the three properties that * deploy a mock carry that mock's creation code with them, so a hand split * guesses wrong and then guesses wrong again. */ contract VarveTest { constructor(uint256 idx) { bool pass; string memory why; Group g; if (idx <= 5) g = Group(address(new PropsA())); else if (idx <= 13) g = Group(address(new PropsB())); else if (idx <= 17) g = Group(address(new PropsC())); else if (idx <= 25) g = Group(address(new PropsD())); else g = Group(address(new PropsE())); 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 */ uint8 constant OFF = 3; uint256 constant UNIT = 10 ** DEC; function _fresh(uint256 fund) internal returns (MockToken t, Varve v) { t = new MockToken(DEC); v = new Varve(address(t), "Varve USDG", "vUSDG", OFF, address(0), 0, 0); t.mint(address(this), fund); t.approve(address(v), type(uint256).max); } /* A vault with NO steward but a real cap and gap, so that when `report` refuses, the steward check is the only thing that could have refused it. The first version of P19 used `_fresh`, whose cap is 0 ppm — so the call died on TooLarge and the property passed against a contract with the zero-steward check REMOVED. A guard is not tested by observing that something else also says no. */ function _stewardless(uint256 fund) internal returns (MockToken t, Varve v) { t = new MockToken(DEC); v = new Varve(address(t), "Varve USDG", "vUSDG", OFF, address(0), 100_000, 0); t.mint(address(this), fund); t.approve(address(v), type(uint256).max); } function _stewarded(uint256 fund, uint32 capPpm, uint32 gap) internal returns (MockToken t, Varve v) { t = new MockToken(DEC); v = new Varve(address(t), "Varve USDG", "vUSDG", OFF, address(this), capPpm, gap); t.mint(address(this), fund); t.approve(address(v), type(uint256).max); } function _s(uint256 n) internal pure returns (string memory) { if (n == 0) return "0"; uint256 j = n; uint256 len; while (j != 0) { len++; j /= 10; } bytes memory b = new bytes(len); while (n != 0) { b[--len] = bytes1(uint8(48 + n % 10)); n /= 10; } return string(b); } } 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 (false, "no such property in this group"); } /** P1 — every mutation writes exactly one layer, and a no-op writes none. */ function p1() internal returns (bool, string memory) { (MockToken t, Varve v) = _fresh(1000 * UNIT); uint64 a = v.laminae(); /* 1 after genesis */ v.deposit(100 * UNIT, address(this)); uint64 b = v.laminae(); v.settle(); /* nothing arrived */ uint64 c = v.laminae(); t.transfer(address(v), 5 * UNIT); v.settle(); /* income */ uint64 d = v.laminae(); v.redeem(v.balanceOf(address(this)) / 2, address(this), address(this)); uint64 e = v.laminae(); if (a != 1) return (false, "genesis layer missing"); if (b != 2) return (false, "deposit wrote no layer"); if (c != 2) return (false, "an empty settle wrote a layer"); if (d != 3) return (false, "income wrote no layer"); if (e != 4) return (false, "redeem wrote no layer"); return (true, "4 layers for 4 mutations, none for the no-op"); } /** P2 — a layer's numbers are the vault's state immediately after it. */ function p2() internal returns (bool, string memory) { (, Varve v) = _fresh(1000 * UNIT); v.deposit(137 * UNIT, address(this)); bytes32 c1 = v.chronology(); /* Refold the layer from the state the vault reports NOW. If the event carried anything else, this cannot reproduce the accumulator. */ bytes32 c0 = v.fold(bytes32(0), 0, v.GENESIS(), 0, 0, uint64(block.number)); bytes32 want = v.fold(c0, 1, v.DEPOSIT(), uint128(v.totalAssets()), uint128(v.totalSupply()), uint64(block.number)); if (c1 != want) return (false, "the layer does not match the state it claims to describe"); return (true, "refolded from totalAssets/totalSupply and it matches"); } /** P3 — the chronology is the whole series in order, over five layers. */ function p3() internal returns (bool, string memory) { (MockToken t, Varve v) = _fresh(1000 * UNIT); bytes32 acc = v.fold(bytes32(0), 0, v.GENESIS(), 0, 0, uint64(block.number)); v.deposit(100 * UNIT, address(this)); acc = v.fold(acc, 1, v.DEPOSIT(), uint128(v.totalAssets()), uint128(v.totalSupply()), uint64(block.number)); t.transfer(address(v), 7 * UNIT); v.settle(); acc = v.fold(acc, 2, v.ACCRUE(), uint128(v.totalAssets()), uint128(v.totalSupply()), uint64(block.number)); v.deposit(50 * UNIT, address(this)); acc = v.fold(acc, 3, v.DEPOSIT(), uint128(v.totalAssets()), uint128(v.totalSupply()), uint64(block.number)); v.redeem(1000, address(this), address(this)); acc = v.fold(acc, 4, v.WITHDRAW(), uint128(v.totalAssets()), uint128(v.totalSupply()), uint64(block.number)); if (acc != v.chronology()) return (false, "five layers refolded do not reproduce the chronology"); return (true, "5 layers, refolded in order, reproduce chronology() exactly"); } /** P4 — a NEGATIVE control. Drop one layer and the accumulator differs. Without this, P3 proves only that some hash equals some hash. */ function p4() internal returns (bool, string memory) { (MockToken t, Varve v) = _fresh(1000 * UNIT); bytes32 acc = v.fold(bytes32(0), 0, v.GENESIS(), 0, 0, uint64(block.number)); v.deposit(100 * UNIT, address(this)); uint128 a1 = uint128(v.totalAssets()); uint128 s1 = uint128(v.totalSupply()); t.transfer(address(v), 7 * UNIT); v.settle(); uint128 a2 = uint128(v.totalAssets()); uint128 s2 = uint128(v.totalSupply()); /* Skip the deposit layer — the shape of a reader who missed a log. */ bytes32 missing = v.fold(acc, 1, v.ACCRUE(), a2, s2, uint64(block.number)); bytes32 full = v.fold(v.fold(acc, 1, v.DEPOSIT(), a1, s1, uint64(block.number)), 2, v.ACCRUE(), a2, s2, uint64(block.number)); if (full != v.chronology()) return (false, "the full fold does not match, so the control proves nothing"); if (missing == v.chronology()) return (false, "a missing layer produced the same chronology"); return (true, "a dropped layer changes the accumulator; the full one still matches"); } /** P5 — a NEGATIVE control. Two layers swapped differ from the same two in order. Order is part of what is committed to. */ function p5() internal returns (bool, string memory) { (MockToken t, Varve v) = _fresh(1000 * UNIT); bytes32 acc = v.fold(bytes32(0), 0, v.GENESIS(), 0, 0, uint64(block.number)); v.deposit(100 * UNIT, address(this)); uint128 a1 = uint128(v.totalAssets()); uint128 s1 = uint128(v.totalSupply()); t.transfer(address(v), 7 * UNIT); v.settle(); uint128 a2 = uint128(v.totalAssets()); uint128 s2 = uint128(v.totalSupply()); bytes32 inOrder = v.fold(v.fold(acc, 1, v.DEPOSIT(), a1, s1, uint64(block.number)), 2, v.ACCRUE(), a2, s2, uint64(block.number)); bytes32 swapped = v.fold(v.fold(acc, 1, v.ACCRUE(), a2, s2, uint64(block.number)), 2, v.DEPOSIT(), a1, s1, uint64(block.number)); if (inOrder != v.chronology()) return (false, "in-order fold does not match"); if (swapped == inOrder) return (false, "reordering two layers changed nothing"); return (true, "order is committed to, not just membership"); } } contract PropsB is Base { function run(uint256 i) external returns (bool, string memory) { if (i == 6) return p6(); if (i == 7) return p7(); if (i == 8) return p8(); if (i == 9) return p9(); if (i == 10) return p10(); if (i == 11) return p11(); if (i == 12) return p12(); if (i == 13) return p13(); return (false, "no such property in this group"); } /** P6 — income has a layer and a kind of its own. */ function p6() internal returns (bool, string memory) { (MockToken t, Varve v) = _fresh(1000 * UNIT); v.deposit(100 * UNIT, address(this)); uint256 p0 = v.pricePerShare(); t.transfer(address(v), 10 * UNIT); uint256 pMid = v.pricePerShare(); if (pMid != p0) return (false, "a bare transfer moved the share price"); uint256 income = v.settle(); uint256 p1_ = v.pricePerShare(); if (income != 10 * UNIT) return (false, "settle booked the wrong amount"); if (p1_ <= p0) return (false, "income did not raise the share price"); return (true, string.concat("price ", _s(p0), " -> ", _s(p1_), " on ", _s(income), " booked")); } /** P7 — a loss has a layer and a kind of its own. This is the event the standard is missing most: nothing in ERC-4626 fires when a vault is marked down. */ function p7() internal returns (bool, string memory) { (, Varve v) = _stewarded(1000 * UNIT, 100_000, 0); v.deposit(100 * UNIT, address(this)); uint256 p0 = v.pricePerShare(); uint64 n0 = v.laminae(); v.report(-int256(5 * UNIT)); uint256 p1_ = v.pricePerShare(); if (v.laminae() != n0 + 1) return (false, "a markdown wrote no layer"); if (p1_ >= p0) return (false, "a markdown did not lower the price"); if (v.totalAssets() != 95 * UNIT) return (false, "markdown arithmetic wrong"); return (true, string.concat("marked down 5, price ", _s(p0), " -> ", _s(p1_))); } /** P8 — the whole price series is derivable from the layers alone. Reconstruct the price from (assets, supply) with no state read. */ function p8() internal returns (bool, string memory) { (MockToken t, Varve v) = _fresh(10_000 * UNIT); v.deposit(100 * UNIT, address(this)); t.transfer(address(v), 3 * UNIT); v.settle(); v.deposit(250 * UNIT, address(this)); t.transfer(address(v), 11 * UNIT); v.settle(); uint128 a = uint128(v.totalAssets()); uint128 s = uint128(v.totalSupply()); /* The reader's arithmetic: one whole share, from the layer only. */ uint256 fromLayer = (uint256(10 ** v.decimals()) * (uint256(a) + 1)) / (uint256(s) + 10 ** v.offset()); if (fromLayer != v.pricePerShare()) return (false, "the layer does not reproduce pricePerShare()"); return (true, string.concat("price from the layer alone: ", _s(fromLayer))); } /** P9 — deposit then immediately redeem never returns more than went in. */ function p9() internal returns (bool, string memory) { (, Varve v) = _fresh(10_000 * UNIT); uint256 shares = v.deposit(1234567, address(this)); uint256 back = v.redeem(shares, address(this), address(this)); if (back > 1234567) return (false, "a round trip made money"); return (true, string.concat("in 1234567, out ", _s(back))); } /** P10 — preview functions agree with the calls they preview, all four. */ function p10() internal returns (bool, string memory) { (MockToken t, Varve v) = _fresh(100_000 * UNIT); v.deposit(500 * UNIT, address(this)); t.transfer(address(v), 13 * UNIT); v.settle(); /* an awkward price */ uint256 pd = v.previewDeposit(777_777); if (v.deposit(777_777, address(this)) != pd) return (false, "previewDeposit disagrees with deposit"); uint256 pm = v.previewMint(999_999); if (v.mint(999_999, address(this)) != pm) return (false, "previewMint disagrees with mint"); uint256 pw = v.previewWithdraw(321_123); if (v.withdraw(321_123, address(this), address(this)) != pw) return (false, "previewWithdraw disagrees with withdraw"); uint256 pr = v.previewRedeem(444_444); if (v.redeem(444_444, address(this), address(this)) != pr) return (false, "previewRedeem disagrees with redeem"); return (true, "all four previews match their calls at a non-round price"); } /** P11 — conversions round in the vault's favour, both directions. */ function p11() internal returns (bool, string memory) { (MockToken t, Varve v) = _fresh(100_000 * UNIT); v.deposit(1000 * UNIT, address(this)); t.transfer(address(v), 7); v.settle(); uint256 s = v.previewDeposit(1_000_003); uint256 backToAssets = v.convertToAssets(s); if (backToAssets > 1_000_003) return (false, "depositing then converting back gained assets"); uint256 need = v.previewWithdraw(1_000_003); if (v.convertToAssets(need) < 1_000_003 && need <= s) return (false, "previewWithdraw under-charges"); return (true, "deposit rounds down, withdraw rounds up"); } /** P12 — totalAssets is tracked, so a donation is not a price move. */ function p12() internal returns (bool, string memory) { (MockToken t, Varve v) = _fresh(10_000 * UNIT); v.deposit(100 * UNIT, address(this)); uint256 before = v.totalAssets(); t.transfer(address(v), 500 * UNIT); if (v.totalAssets() != before) return (false, "a donation moved totalAssets"); if (t.balanceOf(address(v)) == before) return (false, "the donation never arrived, so this proves nothing"); return (true, "600 held, 100 booked, until somebody calls settle()"); } /** P13 — the inflation attack fails, AND it fails with the virtual offset switched off. Two locks, one door; the accounting is the one that closes it. */ function p13() internal returns (bool, string memory) { MockToken t = new MockToken(DEC); Varve v = new Varve(address(t), "V", "V", 0, address(0), 0, 0); /* offset 0 */ Inflator inf = new Inflator(); t.mint(address(inf), 100_000 * UNIT); (uint256 vs, uint256 vBack, ) = inf.attack(address(v), address(t), 1000 * UNIT); if (vs == 0) return (false, "the victim never deposited, so nothing was tested"); uint256 kept = (vBack * 10_000) / (1000 * UNIT); if (kept < 9_999) return (false, string.concat("victim recovered only ", _s(kept), " bps")); return (true, string.concat("offset 0, victim recovered ", _s(kept), " bps of 10000")); } } contract PropsC is Base { function run(uint256 i) external returns (bool, string memory) { if (i == 14) return p14(); if (i == 15) return p15(); if (i == 16) return p16(); if (i == 17) return p17(); return (false, "no such property in this group"); } /** P14 — a fee-on-transfer asset credits what ARRIVED, not what was asked for. Anything else and the other holders pay the fee. */ function p14() internal returns (bool, string memory) { FeeToken t = new FeeToken(DEC, 100); /* 1% */ Varve v = new Varve(address(t), "V", "V", OFF, address(0), 0, 0); t.mint(address(this), 10_000 * UNIT); t.approve(address(v), type(uint256).max); v.deposit(1000 * UNIT, address(this)); if (v.totalAssets() != 990 * UNIT) return (false, string.concat("booked ", _s(v.totalAssets()), ", 990000000 arrived")); return (true, "asked 1000, arrived 990, booked 990"); } /** P15 — the reentrancy guard, AND the attack must actually fire. An exploit that dies on an allowance is not a passing test. */ function p15() internal returns (bool, string memory) { HookToken t = new HookToken(DEC); Varve v = new Varve(address(t), "V", "V", OFF, address(0), 0, 0); t.mint(address(this), 10_000 * UNIT); t.mint(address(t), 10_000 * UNIT); t.approve(address(v), type(uint256).max); t.arm(address(v), 100 * UNIT); if (!t.armed()) return (false, "the hook was never armed"); v.deposit(1000 * UNIT, address(this)); if (!t.fired()) return (false, "the hook never re-entered, so the guard was not tested"); if (!t.innerReverted()) return (false, "the reentrant deposit SUCCEEDED"); return (true, "the hook fired and the reentrant deposit reverted"); } /** P16 — share decimals are asset decimals plus the offset, so the price view has resolution. Quoting per share UNIT returns the integer 1 forever and two correct properties then fail on a correct contract. */ function p16() internal returns (bool, string memory) { (, Varve v) = _fresh(1000 * UNIT); if (v.decimals() != DEC + OFF) return (false, "share decimals are not asset decimals + offset"); v.deposit(100 * UNIT, address(this)); if (v.pricePerShare() != UNIT) return (false, string.concat("a fresh vault prices a share at ", _s(v.pricePerShare()))); return (true, string.concat("share decimals ", _s(v.decimals()), ", one share = ", _s(v.pricePerShare()))); } /** P17 — maxWithdraw is withdrawable in full. */ function p17() internal returns (bool, string memory) { (MockToken t, Varve v) = _fresh(10_000 * UNIT); v.deposit(1000 * UNIT, address(this)); t.transfer(address(v), 17 * UNIT); v.settle(); uint256 m = v.maxWithdraw(address(this)); v.withdraw(m, address(this), address(this)); return (true, string.concat("withdrew maxWithdraw of ", _s(m), " in one call")); } } contract PropsD is Base { function run(uint256 i) external returns (bool, string memory) { if (i == 18) return p18(); if (i == 19) return p19(); if (i == 20) return p20(); if (i == 21) return p21(); if (i == 22) return p22(); if (i == 23) return p23(); if (i == 24) return p24(); if (i == 25) return p25(); return (false, "no such property in this group"); } /** P18 — a third party spends allowance, and an infinite one is not decremented. Real USDG does decrement max, so never assert otherwise about a token you did not write. */ function p18() internal returns (bool, string memory) { (, Varve v) = _fresh(10_000 * UNIT); v.deposit(1000 * UNIT, address(this)); Stranger s = new Stranger(); if (s.callRedeem(address(v), 1000, address(this))) return (false, "a stranger redeemed with no allowance"); v.approve(address(s), 5000); if (!s.callRedeem(address(v), 1000, address(this))) return (false, "an approved stranger was refused"); if (v.allowance(address(this), address(s)) != 4000) return (false, "allowance was not spent"); v.approve(address(s), type(uint256).max); s.callRedeem(address(v), 1000, address(this)); if (v.allowance(address(this), address(s)) != type(uint256).max) return (false, "infinite allowance was decremented"); return (true, "finite allowance spent, infinite one left alone"); } /** P19 — with no steward, nobody can mark the vault at all. CLASSIFY THE REFUSAL. The vault is built with a real cap and a zero gap so the steward check is the only thing that can say no, and the control proves the same call SUCCEEDS on an otherwise identical vault that has a steward. Without the control, "it reverted" is evidence of nothing — the first version of this property used a vault whose cap was 0 ppm, so the call died on TooLarge and the property passed against a contract with the zero-steward check removed entirely. */ function p19() internal returns (bool, string memory) { (, Varve v) = _stewardless(10_000 * UNIT); v.deposit(1000 * UNIT, address(this)); Stranger s = new Stranger(); if (s.callReport(address(v), int256(1 * UNIT))) return (false, "a stranger marked a stewardless vault"); (bool mine, ) = address(v).call(abi.encodeWithSignature("report(int256)", int256(1 * UNIT))); if (mine) return (false, "the deployer marked a stewardless vault"); (, Varve w) = _stewarded(10_000 * UNIT, 100_000, 0); w.deposit(1000 * UNIT, address(this)); (bool ok, ) = address(w).call(abi.encodeWithSignature("report(int256)", int256(1 * UNIT))); if (!ok) return (false, "the control was refused too, so the refusal was not about the steward"); return (true, "refused with no steward, accepted with one, everything else identical"); } /** P20 — the report cap binds, and an ARMED stranger is refused. */ function p20() internal returns (bool, string memory) { (, Varve v) = _stewarded(10_000 * UNIT, 10_000, 0); /* 1% cap */ v.deposit(1000 * UNIT, address(this)); Stranger s = new Stranger(); if (s.callReport(address(v), int256(1 * UNIT))) return (false, "a stranger reported"); (bool over, ) = address(v).call(abi.encodeWithSignature("report(int256)", int256(11 * UNIT))); if (over) return (false, "a report above the cap was accepted"); v.report(int256(9 * UNIT)); if (v.totalAssets() != 1009 * UNIT) return (false, "an in-bounds report did not apply"); return (true, "1% cap binds at 10, 9 accepted, 11 refused, stranger refused"); } /** P21 — the cadence bound binds, with the same control discipline. EVERY OPERATION IN AN eth_call SHARES ONE BLOCK, so "the second report is too soon" cannot be staged by doing two of them: with a gap of 5 the FIRST one is already too soon, because the constructor sets lastReport to the deployment block. That is the bound working, and the control is an identical vault with gap 0. */ function p21() internal returns (bool, string memory) { (, Varve v) = _stewarded(10_000 * UNIT, 100_000, 5); v.deposit(1000 * UNIT, address(this)); (bool tooSoon, ) = address(v).call(abi.encodeWithSignature("report(int256)", int256(1 * UNIT))); if (tooSoon) return (false, "a report landed inside the 5-block gap"); (, Varve w) = _stewarded(10_000 * UNIT, 100_000, 0); w.deposit(1000 * UNIT, address(this)); (bool ok, ) = address(w).call(abi.encodeWithSignature("report(int256)", int256(1 * UNIT))); if (!ok) return (false, "the gap-0 control was refused too, so the refusal was not about cadence"); if (w.totalAssets() != 1001 * UNIT) return (false, "the control report did not apply"); return (true, "refused at gap 5, accepted at gap 0, same block and same amount"); } /** P22 — a deposit that would mint nothing is refused rather than writing an empty layer and handing the assets to everybody else. TWO GUARDS, TWO CASES, and they are not the same case. `assets_ == 0` catches a zero deposit; `shares == 0` catches a NON-zero deposit at a price coarse enough to round it away, which is the one that costs the depositor real money. The sabotage pair shows which guard is load-bearing: removing the first changes nothing, because the second also refuses. */ function p22() internal returns (bool, string memory) { (MockToken t, Varve v) = _fresh(100_000 * UNIT); (bool zero, ) = address(v).call(abi.encodeWithSignature("deposit(uint256,address)", uint256(0), address(this))); if (zero) return (false, "a zero deposit was accepted"); if (v.laminae() != 1) return (false, "a refused deposit still wrote a layer"); /* Seed 1 unit, then donate enough that one more unit buys no shares. */ v.deposit(1, address(this)); t.transfer(address(v), 10_000 * UNIT); v.settle(); if (v.previewDeposit(1) != 0) return (false, "the price is not coarse enough to test the shares guard"); uint64 n = v.laminae(); (bool dust, ) = address(v).call(abi.encodeWithSignature("deposit(uint256,address)", uint256(1), address(this))); if (dust) return (false, "a deposit that mints zero shares was accepted"); if (v.laminae() != n) return (false, "the refused dust deposit wrote a layer"); return (true, "zero refused, and a non-zero deposit that would mint nothing refused too"); } /** P23 — nobody can reduce the vault's holdings without burning shares. */ function p23() internal returns (bool, string memory) { (MockToken t, Varve v) = _fresh(10_000 * UNIT); v.deposit(1000 * UNIT, address(this)); uint256 held = t.balanceOf(address(v)); uint256 supply = v.totalSupply(); Stranger s = new Stranger(); s.callReport(address(v), -int256(1 * UNIT)); s.callRedeem(address(v), 1, address(this)); if (t.balanceOf(address(v)) != held) return (false, "a stranger moved assets out"); if (v.totalSupply() != supply) return (false, "a stranger changed the supply"); return (true, "a stranger changed neither the holdings nor the supply"); } /** P24 — the share price cannot be moved by anything except a layer. */ function p24() internal returns (bool, string memory) { (MockToken t, Varve v) = _fresh(10_000 * UNIT); v.deposit(1000 * UNIT, address(this)); uint256 p = v.pricePerShare(); bytes32 c = v.chronology(); v.approve(address(this), 5); v.transfer(address(0xBEEF), 100); t.transfer(address(v), 1); v.convertToAssets(1e18); v.previewRedeem(1e18); if (v.pricePerShare() != p) return (false, "the price moved with no layer written"); if (v.chronology() != c) return (false, "the chronology moved without a mutation"); return (true, "approve, transfer, donate and five views all left both alone"); } /** P25 — a share transfer moves the claim and not the price. */ function p25() internal returns (bool, string memory) { (, Varve v) = _fresh(10_000 * UNIT); v.deposit(1000 * UNIT, address(this)); uint256 mine = v.balanceOf(address(this)); v.transfer(address(0xBEEF), mine / 4); if (v.balanceOf(address(0xBEEF)) != mine / 4) return (false, "the transfer did not arrive"); if (v.maxRedeem(address(0xBEEF)) != mine / 4) return (false, "the recipient cannot redeem what they hold"); return (true, "the share is an ordinary ERC-20 and carries the claim with it"); } } contract PropsE is Base { function run(uint256 i) external returns (bool, string memory) { if (i == 26) return p26(); if (i == 27) return p27(); if (i == 28) return p28(); if (i == 29) return p29(); return (false, "no such property in this group"); } /** P26 — withdrawing everything empties the vault, leaving no stranded shares and at most the virtual offset's dust. */ function p26() internal returns (bool, string memory) { (MockToken t, Varve v) = _fresh(10_000 * UNIT); v.deposit(1000 * UNIT, address(this)); t.transfer(address(v), 31 * UNIT); v.settle(); v.redeem(v.balanceOf(address(this)), address(this), address(this)); if (v.totalSupply() != 0) return (false, "shares left over"); if (v.totalAssets() > 1) return (false, string.concat("assets left over: ", _s(v.totalAssets()))); return (true, string.concat("supply 0, assets ", _s(v.totalAssets()), " after the last holder left")); } /** P27 — the genesis layer records an EMPTY vault, so a reader has the series start rather than inferring it from the first deposit. */ function p27() internal returns (bool, string memory) { (, Varve v) = _fresh(1000 * UNIT); bytes32 g = v.fold(bytes32(0), 0, v.GENESIS(), 0, 0, uint64(block.number)); if (v.chronology() != g) return (false, "the genesis layer is not (0, 0)"); return (true, "the series starts at assets 0, supply 0"); } /** P28 — the four kinds are distinguishable, which is what lets a reader tell income from a deposit, and an empty settle is not one of them. */ function p28() internal returns (bool, string memory) { (MockToken t, Varve v) = _fresh(10_000 * UNIT); v.deposit(100 * UNIT, address(this)); if (v.settle() != 0) return (false, "an empty settle claimed income"); t.transfer(address(v), 4 * UNIT); if (v.settle() != 4 * UNIT) return (false, "settle booked the wrong income"); if (v.ACCRUE() == v.DEPOSIT()) return (false, "ACCRUE and DEPOSIT are the same kind"); if (v.MARKDOWN() == v.WITHDRAW()) return (false, "MARKDOWN and WITHDRAW are the same kind"); return (true, "four kinds, distinguishable, and an empty settle is not one"); } /** P29 — a markdown cannot be undone by the next settle. THE FUZZER FOUND THIS AND NO PROPERTY COULD SEE IT. A markdown reduces `_assets` while the tokens stay in the contract, so the surplus it leaves is indistinguishable from unbooked income — and `settle()` is permissionless. It took a markdown FOLLOWED BY a settle, in that order, and every property here builds one state and asserts one thing about it. A randomised run of forty operations did it twice in four seeds. This is the regression test; the fuzzer is why it exists. */ function p29() internal returns (bool, string memory) { (MockToken t, Varve v) = _stewarded(10_000 * UNIT, 200_000, 0); v.deposit(1000 * UNIT, address(this)); uint256 p0 = v.pricePerShare(); v.report(-int256(100 * UNIT)); uint256 p1_ = v.pricePerShare(); if (p1_ >= p0) return (false, "the markdown did not lower the price"); /* Anybody may call this, and it must find nothing to book. */ if (v.settle() != 0) return (false, "settle re-booked the written-off surplus"); if (v.pricePerShare() != p1_) return (false, "the price recovered without new income"); /* But real income still books normally on top of it. */ t.transfer(address(v), 7 * UNIT); if (v.settle() != 7 * UNIT) return (false, "genuine income no longer books after a markdown"); if (v.pricePerShare() <= p1_) return (false, "genuine income did not raise the price"); return (true, "markdown stands, settle books 0, real income still books 7"); } }