SKIP TO CONTENT

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

  • SYNCING: ……

Registry.t.sol

Tier upgrades, fuse(), stock slots, and that tier survives an NFT transfer.

227 lines8.2 KBSolidity
Source of test/Registry.t.sol, 227 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 {K401SeatRegistry} from "../src/K401SeatRegistry.sol";
7import {Symbol, SeatMode} from "../src/interfaces/IK401Interfaces.sol";
8 
9contract RegistryTest is Base {
10 uint8 internal constant NVDA = uint8(Symbol.NVDA);
11 uint8 internal constant AAPL = uint8(Symbol.AAPL);
12 
13 function test_tierMultiplierCurve() public view {
14 assertEq(registry.tierMultiplier(0), 0);
15 assertEq(registry.tierMultiplier(1), 1.00e18);
16 assertEq(registry.tierMultiplier(7), 3.50e18);
17 assertEq(registry.tierMultiplier(8), 0);
18 // strictly increasing
19 for (uint8 t = 1; t < 7; ++t) {
20 assertLt(registry.tierMultiplier(t), registry.tierMultiplier(t + 1));
21 }
22 }
23 
24 function test_upgradeCostCurveIsExactly25kTimesTwoToTheTier() public view {
25 assertEq(registry.upgradeCost(1), 25_000e18);
26 assertEq(registry.upgradeCost(2), 50_000e18);
27 assertEq(registry.upgradeCost(3), 100_000e18);
28 assertEq(registry.upgradeCost(4), 200_000e18);
29 assertEq(registry.upgradeCost(5), 400_000e18);
30 assertEq(registry.upgradeCost(6), 800_000e18);
31 assertEq(registry.upgradeCost(7), 0, "tier 7 is terminal");
32 }
33 
34 function test_upgradeBurnsSupplyAndRaisesTheTier() public {
35 _fundNoSeats(alice, 100_000e18);
36 uint256 id = _giveSeatNfts(alice, 1)[0];
37 
38 uint256 supplyBefore = k401.totalSupply();
39 uint256 mulBefore = registry.totalVestedMultiplier();
40 
41 vm.prank(alice);
42 (uint8 newTier, uint256 cost) = registry.upgradeTier(id);
43 
44 assertEq(newTier, 2);
45 assertEq(cost, 25_000e18);
46 assertEq(k401.totalSupply(), supplyBefore - 25_000e18, "burned, not transferred");
47 assertEq(registry.tierOf(id), 2);
48 assertEq(registry.multiplierOf(id), 1.25e18);
49 assertEq(registry.totalVestedMultiplier(), mulBefore + 0.25e18);
50 assertEq(mirror.ownerOf(id), alice, "the upgraded Seat is never consumed by its own fee");
51 }
52 
53 function test_upgradeRejectsNonOwnerAndMaxTier() public {
54 // The full 1 -> 7 ladder costs 1,575,000 401K, more than the genesis supply.
55 vm.prank(owner);
56 k401.addMinter(address(this));
57 _fundNoSeats(alice, 100e18);
58 k401.mint(alice, 1_600_000e18);
59 uint256 id = _giveSeatNfts(alice, 1)[0];
60 
61 vm.prank(bob);
62 vm.expectRevert(K401SeatRegistry.NotSeatOwner.selector);
63 registry.upgradeTier(id);
64 
65 vm.startPrank(alice);
66 for (uint256 i; i < 6; ++i) {
67 registry.upgradeTier(id);
68 }
69 assertEq(registry.tierOf(id), 7);
70 assertEq(registry.multiplierOf(id), 3.50e18);
71 vm.expectRevert(K401SeatRegistry.MaxTier.selector);
72 registry.upgradeTier(id);
73 vm.stopPrank();
74 }
75 
76 function test_fuseRequiresExactlyThreeSameTierSeats() public {
77 _giveSeats(alice, 6);
78 uint256[] memory ids = k401.seatsOf(alice);
79 
80 vm.prank(alice);
81 vm.expectRevert(K401SeatRegistry.BadFuseInput.selector);
82 registry.fuse(_ids(ids[0], ids[1]));
83 
84 vm.prank(alice);
85 vm.expectRevert(K401SeatRegistry.DuplicateSeat.selector);
86 registry.fuse(_ids(ids[0], ids[0], ids[1]));
87 
88 vm.prank(bob);
89 vm.expectRevert(K401SeatRegistry.NotSeatOwner.selector);
90 registry.fuse(_ids(ids[0], ids[1], ids[2]));
91 
92 // Mixed tiers are rejected.
93 vm.prank(alice);
94 registry.fuse(_ids(ids[0], ids[1], ids[2])); // ids[0] -> tier 2
95 uint256[] memory after_ = k401.seatsOf(alice);
96 vm.prank(alice);
97 vm.expectRevert(K401SeatRegistry.BadFuseInput.selector);
98 registry.fuse(_ids(after_[0], after_[1], after_[2]));
99 }
100 
101 function test_fuseRefusesClockedInSeats() public {
102 _giveSeats(alice, 4);
103 uint256[] memory ids = k401.seatsOf(alice);
104 vm.prank(alice);
105 staking.clockIn(_ids(ids[1]));
106 
107 vm.prank(alice);
108 vm.expectRevert(K401SeatRegistry.SeatIsOnTheClock.selector);
109 registry.fuse(_ids(ids[0], ids[1], ids[2]));
110 }
111 
112 function test_setStocksRequiresWeightsSummingTo10000() public {
113 _giveSeats(alice, 1);
114 uint256 id = k401.seatsOf(alice)[0];
115 
116 vm.prank(alice);
117 vm.expectRevert(K401SeatRegistry.BadWeights.selector);
118 registry.setStocks(id, [NVDA, AAPL, NVDA], [uint16(5000), 4000, 0]);
119 
120 vm.prank(alice);
121 vm.expectRevert(K401SeatRegistry.BadSymbol.selector);
122 registry.setStocks(id, [NVDA, uint8(Symbol.USDG), NVDA], [uint16(5000), 5000, 0]);
123 
124 vm.prank(alice);
125 registry.setStocks(id, [NVDA, AAPL, NVDA], [uint16(6000), 4000, 0]);
126 
127 (uint8[3] memory syms, uint16[3] memory w) = registry.stocksOf(id);
128 assertEq(syms[0], NVDA);
129 assertEq(w[0], 6000);
130 assertEq(w[1], 4000);
131 assertEq(registry.weightedMultiplier(NVDA), 0.6e18);
132 assertEq(registry.weightedMultiplier(AAPL), 0.4e18);
133 }
134 
135 function test_weightedMultiplierTracksModeAndTier() public {
136 _giveSeats(alice, 2);
137 uint256[] memory ids = k401.seatsOf(alice);
138 assertEq(registry.weightedMultiplier(NVDA), 2e18, "default is 100% NVDA");
139 assertEq(registry.totalVestedMultiplier(), 2e18);
140 
141 // Clocking in makes a Seat stop earning stock.
142 vm.prank(alice);
143 staking.clockIn(_ids(ids[0]));
144 assertEq(registry.weightedMultiplier(NVDA), 1e18);
145 assertEq(registry.totalVestedMultiplier(), 1e18);
146 
147 // Clocking back out restores it.
148 vm.startPrank(alice);
149 staking.clockOut(_ids(ids[0]));
150 vm.stopPrank();
151 _skip(24 hours + 1);
152 vm.prank(alice);
153 staking.clockOut(_ids(ids[0]));
154 assertEq(registry.weightedMultiplier(NVDA), 2e18);
155 }
156 
157 /// @dev The Seat mint/burn hook is the only path into `onSeatTransfer`.
158 function test_onSeatTransferIsTokenOnly() public {
159 vm.expectRevert(K401SeatRegistry.NotAuthorized.selector);
160 registry.onSeatTransfer(address(0), alice, 1);
161 }
162 
163 function test_setModeIsStakingOnly() public {
164 _giveSeats(alice, 1);
165 uint256 id = k401.seatsOf(alice)[0];
166 vm.prank(alice);
167 vm.expectRevert(K401SeatRegistry.NotAuthorized.selector);
168 registry.setMode(id, SeatMode.ON_THE_CLOCK);
169 }
170 
171 function test_burnedSeatIsClearedAndReMintReadsAsTier1() public {
172 _giveSeats(alice, 3);
173 uint256[] memory ids = k401.seatsOf(alice);
174 vm.prank(alice);
175 registry.fuse(_ids(ids[0], ids[1], ids[2]));
176 
177 assertEq(registry.tierOf(ids[1]), 0);
178 assertEq(registry.generationOf(ids[1]), 2, "generation bumped on destroy");
179 
180 // Re-mint: DN404 recycles the id and the Seat comes back as a plain tier 1.
181 _giveSeats(bob, 2);
182 uint256[] memory bobIds = k401.seatsOf(bob);
183 for (uint256 i; i < bobIds.length; ++i) {
184 assertEq(registry.tierOf(bobIds[i]), 1, "recycled ids do not inherit a tier");
185 }
186 }
187 
188 function test_addOnlyConfigCannotBeRevoked() public {
189 vm.startPrank(owner);
190 vm.expectRevert(K401.RemovalDisabled.selector);
191 k401.mapPair(address(pair), false);
192 vm.expectRevert(K401.RemovalDisabled.selector);
193 k401.setWhitelist(address(staking), false);
194 vm.expectRevert(K401.AlreadySet.selector);
195 k401.setStaking(address(0xdead));
196 vm.expectRevert(K401.AlreadySet.selector);
197 k401.setFeeSplitter(address(0xdead));
198 vm.expectRevert(K401.AlreadySet.selector);
199 k401.setSeatRegistry(address(0xdead));
200 vm.stopPrank();
201 }
202 
203 function test_nonOwnerCannotConfigure() public {
204 vm.prank(alice);
205 vm.expectRevert();
206 k401.mapPair(address(0xbeef), true);
207 
208 vm.prank(alice);
209 vm.expectRevert();
210 k401.addMinter(alice);
211 }
212 
213 function test_mintAndBurnAreRoleGated() public {
214 vm.prank(alice);
215 vm.expectRevert(K401.NotAuthorized.selector);
216 k401.mint(alice, 1e18);
217 
218 vm.prank(alice);
219 vm.expectRevert(K401.NotAuthorized.selector);
220 k401.burnFrom(alice, 1e18);
221 
222 vm.prank(alice);
223 vm.expectRevert(K401.NotAuthorized.selector);
224 k401.burnSeats(alice, _ids(1));
225 }
226}
227 

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