SKIP TO CONTENT

Demo build — mock protocol data. No chain, no ABIs yet.

  • SYNCING: ……

RfvInvariant.t.sol

The one that matters most: rfvPerToken() >= 1e18 after every possible operation.

245 lines7.5 KBSolidity
Source of test/RfvInvariant.t.sol, 245 lines of Solidity
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.24;
3 
4import {Test, console2} from "forge-std/Test.sol";
5import {Base} from "./Base.t.sol";
6import {K401} from "../src/K401.sol";
7import {K401Treasury} from "../src/K401Treasury.sol";
8import {K401Distributor} from "../src/K401Distributor.sol";
9import {K401Staking} from "../src/K401Staking.sol";
10import {K401SeatRegistry} from "../src/K401SeatRegistry.sol";
11import {K401BondDepository} from "../src/K401BondDepository.sol";
12import {K401Buyback} from "../src/K401Buyback.sol";
13import {K401Oracle} from "../src/K401Oracle.sol";
14import {MockERC20} from "./mocks/MockERC20.sol";
15import {MockUniV2Pair} from "./mocks/MockUniV2.sol";
16 
17/**
18 * @dev Stateful handler: a fuzzer drives every value-moving path in the protocol
19 * and the invariant checks the hard floor after each one.
20 */
21contract RfvHandler is Test {
22 K401 public k401;
23 K401Treasury public treasury;
24 K401Distributor public distributor;
25 K401Staking public staking;
26 K401SeatRegistry public registry;
27 K401BondDepository public bonds;
28 K401Buyback public buyback;
29 K401Oracle public oracle;
30 MockERC20 public usdg;
31 MockUniV2Pair public pair;
32 
33 address[3] public actors;
34 uint256 public bondMarketId;
35 
36 uint256 public rebases;
37 uint256 public bondsSold;
38 uint256 public buybacks;
39 uint256 public fuses;
40 
41 constructor(
42 K401 k401_,
43 K401Treasury treasury_,
44 K401Distributor distributor_,
45 K401Staking staking_,
46 K401SeatRegistry registry_,
47 K401BondDepository bonds_,
48 K401Buyback buyback_,
49 K401Oracle oracle_,
50 MockERC20 usdg_,
51 MockUniV2Pair pair_,
52 address[3] memory actors_,
53 uint256 bondMarketId_
54 ) {
55 k401 = k401_;
56 treasury = treasury_;
57 distributor = distributor_;
58 staking = staking_;
59 registry = registry_;
60 bonds = bonds_;
61 buyback = buyback_;
62 oracle = oracle_;
63 usdg = usdg_;
64 pair = pair_;
65 actors = actors_;
66 bondMarketId = bondMarketId_;
67 }
68 
69 function _actor(uint256 seed) internal view returns (address) {
70 return actors[seed % actors.length];
71 }
72 
73 function _hop(uint256 secs) internal {
74 vm.warp(vm.getBlockTimestamp() + secs);
75 pair.sync();
76 try oracle.checkpoint() {} catch {}
77 }
78 
79 function warp(uint256 secs) external {
80 _hop(bound(secs, 31 minutes, 3 hours));
81 }
82 
83 function rebase(uint256 secs) external {
84 _hop(bound(secs, 8 hours, 9 hours));
85 try distributor.rebase() {
86 rebases++;
87 } catch {}
88 }
89 
90 function bond(uint256 seed, uint256 amount) external {
91 address a = _actor(seed);
92 amount = bound(amount, 1e6, 20_000e6);
93 usdg.mint(a, amount);
94 vm.startPrank(a);
95 usdg.approve(address(bonds), type(uint256).max);
96 try bonds.bond(bondMarketId, amount) {
97 bondsSold++;
98 } catch {}
99 vm.stopPrank();
100 }
101 
102 function redeem(uint256 seed) external {
103 address a = _actor(seed);
104 vm.prank(a);
105 try bonds.redeemBond(bondMarketId) {} catch {}
106 }
107 
108 function sellToBuyback(uint256 seed, uint256 amount) external {
109 address a = _actor(seed);
110 uint256 bal = k401.balanceOf(a);
111 if (bal == 0) return;
112 amount = bound(amount, 1, bal);
113 vm.startPrank(a);
114 k401.approve(address(buyback), type(uint256).max);
115 try buyback.sell(amount, 0) {
116 buybacks++;
117 } catch {}
118 vm.stopPrank();
119 }
120 
121 function transfer(uint256 seed, uint256 seed2, uint256 amount) external {
122 address from = _actor(seed);
123 address to = _actor(seed2 + 1);
124 uint256 bal = k401.balanceOf(from);
125 if (bal == 0) return;
126 amount = bound(amount, 1, bal);
127 vm.prank(from);
128 try k401.transfer(to, amount) {} catch {}
129 }
130 
131 function clockIn(uint256 seed) external {
132 address a = _actor(seed);
133 uint256[] memory ids = k401.seatsOf(a);
134 if (ids.length == 0) return;
135 uint256[] memory one = new uint256[](1);
136 one[0] = ids[ids.length - 1];
137 vm.prank(a);
138 try staking.clockIn(one) {} catch {}
139 }
140 
141 function clockOut(uint256 seed) external {
142 address a = _actor(seed);
143 uint256[] memory ids = staking.stakedSeatsOf(a);
144 if (ids.length == 0) return;
145 uint256[] memory one = new uint256[](1);
146 one[0] = ids[0];
147 vm.prank(a);
148 try staking.clockOut(one) {} catch {}
149 }
150 
151 function materialize(uint256 seed) external {
152 address a = _actor(seed);
153 vm.prank(a);
154 try staking.materialize() {} catch {}
155 }
156 
157 function fuse(uint256 seed) external {
158 address a = _actor(seed);
159 uint256[] memory ids = k401.seatsOf(a);
160 if (ids.length < 3) return;
161 uint256[] memory three = new uint256[](3);
162 three[0] = ids[0];
163 three[1] = ids[1];
164 three[2] = ids[2];
165 vm.prank(a);
166 try registry.fuse(three) {
167 fuses++;
168 } catch {}
169 }
170}
171 
172contract RfvInvariantTest is Base {
173 RfvHandler internal handler;
174 
175 function setUp() public override {
176 super.setUp();
177 _primeOracle();
178 
179 vm.prank(owner);
180 uint256 mid = bonds.createMarket(address(usdg), false, 500, 100);
181 
182 _giveSeats(alice, 60);
183 _giveSeats(bob, 40);
184 _giveSeats(carol, 25);
185 
186 handler = new RfvHandler(
187 k401,
188 treasury,
189 distributor,
190 staking,
191 registry,
192 bonds,
193 buyback,
194 oracle,
195 usdg,
196 pair,
197 [alice, bob, carol],
198 mid
199 );
200 usdg.mint(address(buyback), 500_000e6);
201 
202 targetContract(address(handler));
203 }
204 
205 /// forge-config: default.invariant.runs = 24
206 /// forge-config: default.invariant.depth = 24
207 function invariant_rfvPerTokenNeverBelowOne() public view {
208 assertGe(treasury.rfvPerToken(), 1e18, "HARD FLOOR BREACHED");
209 }
210 
211 /// forge-config: default.invariant.runs = 24
212 /// forge-config: default.invariant.depth = 24
213 function invariant_reserveCoversSupply() public view {
214 assertGe(treasury.reserveValueWad(), k401.totalSupply(), "reserve no longer covers supply");
215 }
216 
217 /// forge-config: default.invariant.runs = 24
218 /// forge-config: default.invariant.depth = 24
219 function invariant_stakingIndexOnlyGrows() public view {
220 assertGe(staking.index(), 1e18);
221 }
222 
223 /// @dev Proves the fuzzer actually exercised the value-moving paths rather than
224 /// reverting its way to a green invariant. Runs once, after the whole campaign.
225 function afterInvariant() public view {
226 console2.log("rebases ", handler.rebases());
227 console2.log("bondsSold ", handler.bondsSold());
228 console2.log("buybacks ", handler.buybacks());
229 console2.log("fuses ", handler.fuses());
230 assertGt(handler.rebases() + handler.bondsSold() + handler.buybacks() + handler.fuses(), 0);
231 }
232 
233 /// forge-config: default.invariant.runs = 24
234 /// forge-config: default.invariant.depth = 24
235 function invariant_seatCountMatchesHolderBalances() public view {
236 uint256 sum;
237 address[3] memory holders = [alice, bob, carol];
238 for (uint256 i; i < 3; ++i) {
239 assertEq(k401.seatBalanceOf(holders[i]), k401.balanceOf(holders[i]) / 1e18);
240 sum += k401.seatBalanceOf(holders[i]);
241 }
242 assertEq(k401.totalSeats(), sum);
243 }
244}
245 

Click any line number to deep-link to it — the target line highlights on load.