Registry.t.sol
Tier upgrades, fuse(), stock slots, and that tier survives an NFT transfer.
227 lines8.2 KBSolidity
| 1 | // SPDX-License-Identifier: MIT |
| 2 | pragma solidity ^0.8.24; |
| 3 | |
| 4 | import {Base} from "./Base.t.sol"; |
| 5 | import {K401} from "../src/K401.sol"; |
| 6 | import {K401SeatRegistry} from "../src/K401SeatRegistry.sol"; |
| 7 | import {Symbol, SeatMode} from "../src/interfaces/IK401Interfaces.sol"; |
| 8 | |
| 9 | contract 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.