SKIP TO CONTENT

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

  • SYNCING: ……

Staking.t.sol

Clock in/out, index accrual, materialize() and the 24h cooldown.

234 lines8.0 KBSolidity
Source of test/Staking.t.sol, 234 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 {K401Staking} from "../src/K401Staking.sol";
7import {SeatMode} from "../src/interfaces/IK401Interfaces.sol";
8 
9contract StakingTest is Base {
10 function _rebaseOnce() internal returns (uint256 minted) {
11 _skip(8 hours + 1);
12 pair.sync();
13 oracle.checkpoint();
14 vm.prank(keeper);
15 (minted,) = distributor.rebase();
16 }
17 
18 function test_clockInLocksTheSeatInPlace() public {
19 _giveSeats(alice, 3);
20 uint256[] memory ids = k401.seatsOf(alice);
21 
22 vm.prank(alice);
23 staking.clockIn(_ids(ids[0]));
24 
25 // The Seat did NOT move and was NOT burned.
26 assertEq(mirror.ownerOf(ids[0]), alice, "Seat stays in the wallet");
27 assertEq(k401.balanceOf(alice), 3e18, "liquid balance untouched");
28 assertEq(k401.seatBalanceOf(alice), 3);
29 assertTrue(k401.isSeatLocked(ids[0]));
30 assertEq(k401.lockedSeats(alice), 1);
31 assertEq(uint8(registry.modeOf(ids[0])), uint8(SeatMode.ON_THE_CLOCK));
32 assertEq(staking.totalStaked(), 1e18);
33 }
34 
35 function test_lockedSeatCannotBeTransferred() public {
36 _giveSeats(alice, 3);
37 uint256[] memory ids = k401.seatsOf(alice);
38 vm.prank(alice);
39 staking.clockIn(_ids(ids[0]));
40 
41 vm.prank(alice);
42 vm.expectRevert(K401.SeatIsLocked.selector);
43 mirror.transferFrom(alice, bob, ids[0]);
44 }
45 
46 function test_lockedBalanceCannotBeMovedOutAsErc20() public {
47 _giveSeats(alice, 3);
48 uint256[] memory ids = k401.seatsOf(alice);
49 vm.prank(alice);
50 staking.clockIn(_ids(ids[0], ids[1]));
51 
52 // 2 Seats locked -> only 1e18 is movable.
53 vm.prank(alice);
54 k401.transfer(bob, 1e18);
55 assertEq(k401.balanceOf(alice), 2e18);
56 
57 vm.prank(alice);
58 vm.expectRevert(K401.LockedBalance.selector);
59 k401.transfer(bob, 1);
60 }
61 
62 /// @dev The pinning scheme must keep the *specific* locked ids alive, not just the count.
63 function test_lockedIdsSurviveLifoBurnOrdering() public {
64 _giveSeats(alice, 5);
65 uint256[] memory ids = k401.seatsOf(alice);
66 
67 // Lock the LAST id, which is the first one DN404's LIFO burn would reach.
68 vm.prank(alice);
69 staking.clockIn(_ids(ids[4]));
70 
71 vm.prank(alice);
72 k401.transfer(bob, 4e18);
73 
74 assertEq(k401.seatBalanceOf(alice), 1);
75 assertEq(mirror.ownerOf(ids[4]), alice, "the locked id specifically survived");
76 assertTrue(k401.isSeatLocked(ids[4]));
77 }
78 
79 function test_clockOutIsTwoPhaseWithA24hCooldown() public {
80 _giveSeats(alice, 2);
81 uint256[] memory ids = k401.seatsOf(alice);
82 vm.prank(alice);
83 staking.clockIn(_ids(ids[0]));
84 
85 // phase 1: request
86 vm.prank(alice);
87 staking.clockOut(_ids(ids[0]));
88 assertEq(staking.totalStaked(), 0, "stops earning immediately");
89 assertTrue(k401.isSeatLocked(ids[0]), "still locked during the cooldown");
90 assertEq(staking.unlockAt(ids[0]), _now() + 24 hours);
91 
92 // too early
93 _skip(23 hours);
94 vm.prank(alice);
95 vm.expectRevert(K401Staking.CooldownActive.selector);
96 staking.clockOut(_ids(ids[0]));
97 
98 // phase 2: finalise
99 _skip(1 hours + 1);
100 vm.prank(alice);
101 staking.clockOut(_ids(ids[0]));
102 assertFalse(k401.isSeatLocked(ids[0]));
103 assertEq(uint8(registry.modeOf(ids[0])), uint8(SeatMode.VESTED));
104 assertEq(staking.stakedCountOf(alice), 0);
105 
106 vm.prank(alice);
107 mirror.transferFrom(alice, bob, ids[0]);
108 assertEq(mirror.ownerOf(ids[0]), bob);
109 }
110 
111 function test_indexAccountingSplitsProRata() public {
112 _primeOracle();
113 _giveSeats(alice, 30);
114 _giveSeats(bob, 10);
115 
116 uint256[] memory aIds = k401.seatsOf(alice);
117 uint256[] memory bIds = k401.seatsOf(bob);
118 uint256[] memory a3 = new uint256[](3);
119 for (uint256 i; i < 3; ++i) {
120 a3[i] = aIds[i];
121 }
122 vm.prank(alice);
123 staking.clockIn(a3);
124 vm.prank(bob);
125 staking.clockIn(_ids(bIds[0]));
126 
127 uint256 minted = _rebaseOnce();
128 uint256 toStaking = minted - (minted * 50) / 10_000;
129 
130 // alice has 3 of 4 staked Seats.
131 assertApproxEqRel(staking.pending(alice), (toStaking * 3) / 4, 0.0001e18);
132 assertApproxEqRel(staking.pending(bob), toStaking / 4, 0.0001e18);
133 assertGt(staking.index(), 1e18);
134 }
135 
136 function test_materializeMintsWholeSeatsAndCarriesTheRemainder() public {
137 _primeOracle();
138 _giveSeats(alice, 200);
139 uint256[] memory ids = k401.seatsOf(alice);
140 uint256[] memory many = new uint256[](150);
141 for (uint256 i; i < 150; ++i) {
142 many[i] = ids[i];
143 }
144 vm.prank(alice);
145 staking.clockIn(many);
146 
147 _rebaseOnce();
148 
149 uint256 pendingBefore = staking.pending(alice);
150 assertGe(pendingBefore, 1e18, "need at least one whole Seat for this test");
151 uint256 balBefore = k401.balanceOf(alice);
152 uint256 seatsBefore = k401.seatBalanceOf(alice);
153 
154 vm.prank(alice);
155 uint256 got = staking.materialize();
156 
157 assertEq(got % 1e18, 0, "only whole Seats are materialised");
158 assertEq(k401.balanceOf(alice), balBefore + got);
159 assertEq(k401.seatBalanceOf(alice), seatsBefore + got / 1e18);
160 assertEq(staking.pending(alice), pendingBefore - got, "remainder carries forward");
161 assertLt(staking.pending(alice), 1e18);
162 }
163 
164 function test_materializeRevertsWithNothingWhole() public {
165 _giveSeats(alice, 1);
166 uint256[] memory ids = k401.seatsOf(alice);
167 vm.prank(alice);
168 staking.clockIn(_ids(ids[0]));
169 
170 vm.prank(alice);
171 vm.expectRevert(K401Staking.NothingToMaterialize.selector);
172 staking.materialize();
173 }
174 
175 /// @dev A user who clocks in AFTER an emission must not retroactively earn any of it.
176 function test_noRetroactiveEarningsForLateStakers() public {
177 _primeOracle();
178 _giveSeats(alice, 20);
179 _giveSeats(bob, 20);
180 uint256[] memory aIds = k401.seatsOf(alice);
181 uint256[] memory bIds = k401.seatsOf(bob);
182 
183 vm.prank(alice);
184 staking.clockIn(_ids(aIds[0]));
185 _rebaseOnce();
186 uint256 alicePending = staking.pending(alice);
187 assertGt(alicePending, 0);
188 
189 vm.prank(bob);
190 staking.clockIn(_ids(bIds[0]));
191 assertEq(staking.pending(bob), 0, "no retroactive claim on past epochs");
192 
193 // Alice's already-accrued amount is not diluted by bob joining.
194 assertEq(staking.pending(alice), alicePending);
195 }
196 
197 /// @dev First-depositor / rounding: the very first staker cannot inflate the index.
198 function test_firstDepositorCannotSkewTheIndex() public {
199 _primeOracle();
200 assertEq(staking.index(), 1e18);
201 _giveSeats(alice, 5);
202 uint256[] memory ids = k401.seatsOf(alice);
203 vm.prank(alice);
204 staking.clockIn(_ids(ids[0]));
205 assertEq(staking.index(), 1e18, "clocking in does not move the index");
206 assertEq(staking.pending(alice), 0);
207 
208 _rebaseOnce();
209 // The sole staker earns the whole epoch and nothing more.
210 assertApproxEqAbs(staking.pending(alice), k401.balanceOf(address(staking)), 1e6);
211 }
212 
213 function test_notifyEmissionIsDistributorOnly() public {
214 vm.expectRevert(K401Staking.NotAuthorized.selector);
215 staking.notifyEmission(1e18);
216 }
217 
218 function test_lockSeatIsStakingOnly() public {
219 _giveSeats(alice, 1);
220 uint256[] memory ids = k401.seatsOf(alice);
221 vm.prank(alice);
222 vm.expectRevert(K401.NotAuthorized.selector);
223 k401.lockSeat(alice, ids[0]);
224 }
225 
226 function test_cannotClockInSomeoneElsesSeat() public {
227 _giveSeats(alice, 1);
228 uint256[] memory ids = k401.seatsOf(alice);
229 vm.prank(bob);
230 vm.expectRevert(K401Staking.NotSeatOwner.selector);
231 staking.clockIn(_ids(ids[0]));
232 }
233}
234 

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