SKIP TO CONTENT

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

  • SYNCING: ……

AuditFixes.t.sol

Regression tests for the audit fixes. One per finding.

164 lines5.7 KBSolidity
Source of test/AuditFixes.t.sol, 164 lines of Solidity
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.24;
3 
4import {Base} from "./Base.t.sol";
5import {K401} from "../src/K401.sol";
6import {K401Treasury} from "../src/K401Treasury.sol";
7import {K401Staking} from "../src/K401Staking.sol";
8import {K401StockDesk} from "../src/K401StockDesk.sol";
9 
10/// @dev Regression tests for the audit fixes. One per finding.
11contract AuditFixesTest is Base {
12 uint8 internal constant NVDA_ID = 0;
13 
14 /*////////////////////// C-2: role lists are sealable //////////////////////*/
15 
16 function test_C2_ownerCannotAddAMinterOnceSealed() public {
17 vm.startPrank(owner);
18 k401.lockMinters();
19 vm.expectRevert(K401.RolesLocked.selector);
20 k401.addMinter(owner);
21 vm.stopPrank();
22 }
23 
24 function test_C2_ownerCannotAddABurnerOnceSealed() public {
25 vm.startPrank(owner);
26 k401.lockBurners();
27 vm.expectRevert(K401.RolesLocked.selector);
28 k401.addBurner(owner);
29 vm.stopPrank();
30 }
31 
32 /// @dev The whole point: an unsealed minter list let the owner mint without limit,
33 /// which makes every "immutable parameter" guarantee vacuous.
34 function test_C2_unlimitedMintIsClosedOff() public {
35 vm.prank(owner);
36 k401.lockMinters();
37 
38 vm.prank(owner);
39 vm.expectRevert(K401.RolesLocked.selector);
40 k401.addMinter(alice);
41 
42 vm.prank(alice);
43 vm.expectRevert(K401.NotAuthorized.selector);
44 k401.mint(alice, 1_000_000e18);
45 }
46 
47 /*////////////////////// H-2: equity feeds must be fresh //////////////////////*/
48 
49 function test_H2_navRefusesAFrozenEquityFeed() public {
50 _giveSeats(alice, 1);
51 usdg.mint(address(treasury), 10_000e6);
52 nvda.mint(address(treasury), 5e18);
53 
54 assertGt(treasury.nav(), 0, "marks fine while the feed is live");
55 
56 // Freeze the feed beyond MAX_FEED_AGE.
57 nvdaFeed.setUpdatedAt(_now() - (treasury.MAX_FEED_AGE() + 1));
58 vm.expectRevert(K401Treasury.StaleFeed.selector);
59 treasury.nav();
60 }
61 
62 function test_H2_stockDeskRefusesAFrozenEquityFeed() public {
63 _primeOracle();
64 _giveSeats(alice, 1);
65 usdg.mint(address(stockDesk), 10_000e6);
66 
67 nvdaFeed.setUpdatedAt(_now() - (stockDesk.MAX_FEED_AGE() + 1));
68 vm.expectRevert(K401StockDesk.StaleFeed.selector);
69 stockDesk.executeBatch(NVDA_ID, 10_000e6, 0);
70 }
71 
72 /// @dev A frozen feed must halt emission rather than emit against a bad NAV.
73 function test_H2_rebaseHaltsOnAFrozenEquityFeed() public {
74 _primeOracle();
75 _giveSeats(alice, 4);
76 uint256[] memory ids = k401.seatsOf(alice);
77 vm.prank(alice);
78 staking.clockIn(_ids(ids[0]));
79 nvda.mint(address(treasury), 5e18);
80 
81 _skip(8 hours + 1);
82 pair.sync();
83 oracle.checkpoint();
84 nvdaFeed.setUpdatedAt(_now() - (treasury.MAX_FEED_AGE() + 1));
85 
86 vm.expectRevert(K401Treasury.StaleFeed.selector);
87 distributor.rebase();
88 }
89 
90 /*////////////////////// H-3: equity cannot be managed out //////////////////////*/
91 
92 function test_H3_reserveManagerCannotDrainTheEquityBucket() public {
93 _giveSeats(alice, 1);
94 nvda.mint(address(treasury), 100e18);
95 vm.prank(owner);
96 treasury.addReserveManager(address(this));
97 
98 vm.expectRevert(K401Treasury.EquityIsNotManageable.selector);
99 treasury.manage(address(nvda), 100e18, alice);
100 
101 // Bucket A is still manageable, floor permitting.
102 usdg.mint(address(treasury), 10_000e6);
103 treasury.manage(address(usdg), 1e6, alice);
104 }
105 
106 /*////////////////////// M-3: a clock-out can be cancelled //////////////////////*/
107 
108 function test_M3_cancelClockOutRestoresEarning() public {
109 _primeOracle();
110 _giveSeats(alice, 4);
111 _giveSeats(bob, 4);
112 uint256[] memory aIds = k401.seatsOf(alice);
113 uint256[] memory bIds = k401.seatsOf(bob);
114 
115 vm.prank(alice);
116 staking.clockIn(_ids(aIds[0]));
117 vm.prank(bob);
118 staking.clockIn(_ids(bIds[0]));
119 assertEq(staking.totalStaked(), 2e18);
120 
121 vm.prank(alice);
122 staking.clockOut(_ids(aIds[0])); // phase 1: stops earning
123 assertEq(staking.totalStaked(), 1e18, "principal removed on request");
124 
125 vm.prank(alice);
126 staking.cancelClockOut(_ids(aIds[0]));
127 assertEq(staking.totalStaked(), 2e18, "principal restored");
128 assertEq(staking.unlockAt(aIds[0]), 0, "exit cleared");
129 
130 // She earns again, pro-rata, from here.
131 _skip(8 hours + 1);
132 pair.sync();
133 oracle.checkpoint();
134 distributor.rebase();
135 assertApproxEqRel(staking.pending(alice), staking.pending(bob), 0.0001e18);
136 }
137 
138 function test_M3_cancelRequiresAPendingExit() public {
139 _giveSeats(alice, 2);
140 uint256[] memory ids = k401.seatsOf(alice);
141 vm.prank(alice);
142 staking.clockIn(_ids(ids[0]));
143 
144 vm.prank(alice);
145 vm.expectRevert(K401Staking.NotExiting.selector);
146 staking.cancelClockOut(_ids(ids[0]));
147 }
148 
149 /*////////////////////// UI must not break on a halt //////////////////////*/
150 
151 /// @dev `preview()` and the Lens are the read path the frontend polls. They must
152 /// report "oracle/feed not usable" rather than revert, or the UI goes blank
153 /// exactly when something is wrong and users most need to see state.
154 function test_previewDoesNotRevertWhenAFeedIsFrozen() public {
155 _primeOracle();
156 _giveSeats(alice, 2);
157 nvda.mint(address(treasury), 5e18);
158 nvdaFeed.setUpdatedAt(_now() - (treasury.MAX_FEED_AGE() + 1));
159 
160 (,,, bool ok) = distributor.preview();
161 assertFalse(ok, "preview reports the halt instead of reverting");
162 }
163}
164 

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