Base.t.sol
Shared test fixture: deploys the system with mocks and funds the actors.
271 lines10.4 KBSolidity
| 1 | // SPDX-License-Identifier: MIT |
| 2 | pragma solidity ^0.8.24; |
| 3 | |
| 4 | import {Test, console2} from "forge-std/Test.sol"; |
| 5 | |
| 6 | import {K401} from "../src/K401.sol"; |
| 7 | import {K401Treasury} from "../src/K401Treasury.sol"; |
| 8 | import {K401Oracle} from "../src/K401Oracle.sol"; |
| 9 | import {K401Distributor} from "../src/K401Distributor.sol"; |
| 10 | import {K401Staking} from "../src/K401Staking.sol"; |
| 11 | import {K401SeatRegistry} from "../src/K401SeatRegistry.sol"; |
| 12 | import {K401Seat6551, K401Seat6551Account} from "../src/K401Seat6551.sol"; |
| 13 | import {ERC6551Registry} from "../src/ERC6551Registry.sol"; |
| 14 | import {K401FeeSplitter} from "../src/K401FeeSplitter.sol"; |
| 15 | import {K401StockDesk} from "../src/K401StockDesk.sol"; |
| 16 | import {K401BondDepository} from "../src/K401BondDepository.sol"; |
| 17 | import {K401Buyback} from "../src/K401Buyback.sol"; |
| 18 | import {K401Lens} from "../src/K401Lens.sol"; |
| 19 | import {Symbol, SeatMode} from "../src/interfaces/IK401Interfaces.sol"; |
| 20 | |
| 21 | import {MockERC20} from "./mocks/MockERC20.sol"; |
| 22 | import {MockPriceFeed} from "./mocks/MockPriceFeed.sol"; |
| 23 | import {MockUniV2Pair, MockUniV2Router} from "./mocks/MockUniV2.sol"; |
| 24 | |
| 25 | interface IMirror { |
| 26 | function ownerOf(uint256) external view returns (address); |
| 27 | function balanceOf(address) external view returns (uint256); |
| 28 | function totalSupply() external view returns (uint256); |
| 29 | function transferFrom(address, address, uint256) external; |
| 30 | function approve(address, uint256) external; |
| 31 | function setApprovalForAll(address, bool) external; |
| 32 | } |
| 33 | |
| 34 | /// @dev Full-protocol fixture: real contracts, mock USDG / equities / feeds / UniV2. |
| 35 | abstract contract Base is Test { |
| 36 | // ---- actors ---- |
| 37 | address internal owner = makeAddr("owner"); |
| 38 | address internal team = makeAddr("team"); |
| 39 | address internal lpReceiver = makeAddr("lpReceiver"); |
| 40 | address internal alice = makeAddr("alice"); |
| 41 | address internal bob = makeAddr("bob"); |
| 42 | address internal carol = makeAddr("carol"); |
| 43 | address internal keeper = makeAddr("keeper"); |
| 44 | |
| 45 | // ---- protocol ---- |
| 46 | K401 internal k401; |
| 47 | IMirror internal mirror; |
| 48 | K401Treasury internal treasury; |
| 49 | K401Oracle internal oracle; |
| 50 | K401SeatRegistry internal registry; |
| 51 | K401Staking internal staking; |
| 52 | K401Distributor internal distributor; |
| 53 | ERC6551Registry internal tbaRegistry; |
| 54 | K401Seat6551Account internal seatAccountImpl; |
| 55 | K401Seat6551 internal seat6551; |
| 56 | K401StockDesk internal stockDesk; |
| 57 | K401FeeSplitter internal splitter; |
| 58 | K401BondDepository internal bonds; |
| 59 | K401Buyback internal buyback; |
| 60 | K401Lens internal lens; |
| 61 | |
| 62 | // ---- externals ---- |
| 63 | MockERC20 internal usdg; |
| 64 | MockERC20 internal nvda; |
| 65 | MockERC20 internal aapl; |
| 66 | MockPriceFeed internal nvdaFeed; |
| 67 | MockPriceFeed internal aaplFeed; |
| 68 | MockUniV2Pair internal pair; // 401K / USDG |
| 69 | MockUniV2Pair internal nvdaPair; // USDG / NVDA |
| 70 | MockUniV2Pair internal aaplPair; // USDG / AAPL |
| 71 | MockUniV2Router internal router; |
| 72 | |
| 73 | uint8 internal constant USDG_DECIMALS = 6; |
| 74 | uint256 internal constant INITIAL_SUPPLY = 1_000_000e18; |
| 75 | |
| 76 | function setUp() public virtual { |
| 77 | vm.warp(1_700_000_000); |
| 78 | |
| 79 | usdg = new MockERC20("Global Dollar", "USDG", USDG_DECIMALS); |
| 80 | nvda = new MockERC20("Tokenized NVDA", "tNVDA", 18); |
| 81 | aapl = new MockERC20("Tokenized AAPL", "tAAPL", 18); |
| 82 | nvdaFeed = new MockPriceFeed(8, 100e8, "NVDA/USD"); |
| 83 | aaplFeed = new MockPriceFeed(8, 200e8, "AAPL/USD"); |
| 84 | |
| 85 | // 1. Token. Owner holds the initial supply. |
| 86 | k401 = new K401(uint96(INITIAL_SUPPLY), owner, owner); |
| 87 | mirror = IMirror(k401.mirrorERC721()); |
| 88 | |
| 89 | // 2. Pair + router. |
| 90 | pair = new MockUniV2Pair(address(k401), address(usdg)); |
| 91 | nvdaPair = new MockUniV2Pair(address(usdg), address(nvda)); |
| 92 | aaplPair = new MockUniV2Pair(address(usdg), address(aapl)); |
| 93 | router = new MockUniV2Router(); |
| 94 | router.setPair(address(k401), address(usdg), address(pair)); |
| 95 | router.setPair(address(usdg), address(nvda), address(nvdaPair)); |
| 96 | router.setPair(address(usdg), address(aapl), address(aaplPair)); |
| 97 | |
| 98 | // 3. Core modules. |
| 99 | treasury = new K401Treasury(address(k401), address(usdg), owner); |
| 100 | oracle = new K401Oracle(address(pair), address(k401), address(usdg)); |
| 101 | registry = new K401SeatRegistry(address(k401), owner); |
| 102 | staking = new K401Staking(address(k401), address(registry), owner); |
| 103 | distributor = new K401Distributor(address(k401), address(oracle), address(treasury), address(staking), owner); |
| 104 | |
| 105 | // 4. ERC-6551. |
| 106 | tbaRegistry = new ERC6551Registry(); |
| 107 | seatAccountImpl = new K401Seat6551Account(); |
| 108 | seat6551 = new K401Seat6551(address(tbaRegistry), address(seatAccountImpl), address(mirror)); |
| 109 | |
| 110 | // 5. Revenue plumbing. |
| 111 | stockDesk = new K401StockDesk(address(usdg), address(oracle), address(registry), address(seat6551), owner); |
| 112 | splitter = new K401FeeSplitter(address(k401), address(usdg), owner); |
| 113 | bonds = new K401BondDepository(address(k401), address(oracle), address(treasury), owner); |
| 114 | buyback = new K401Buyback(address(k401), address(usdg), address(treasury), address(oracle), owner); |
| 115 | |
| 116 | // 6. Wiring. |
| 117 | vm.startPrank(owner); |
| 118 | k401.setSeatRegistry(address(registry)); |
| 119 | k401.setStaking(address(staking)); |
| 120 | k401.setFeeSplitter(address(splitter)); |
| 121 | k401.mapPair(address(pair), true); |
| 122 | k401.setWhitelist(address(staking), true); |
| 123 | k401.setWhitelist(address(treasury), true); |
| 124 | k401.setWhitelist(address(stockDesk), true); |
| 125 | k401.setWhitelist(address(bonds), true); |
| 126 | k401.setWhitelist(address(buyback), true); |
| 127 | k401.setWhitelist(address(distributor), true); |
| 128 | // The deployer seeds POL and distributes the genesis allocation fee-free. |
| 129 | k401.setWhitelist(owner, true); |
| 130 | k401.addMinter(address(treasury)); |
| 131 | k401.addMinter(address(distributor)); |
| 132 | k401.addBurner(address(registry)); |
| 133 | k401.addBurner(address(buyback)); |
| 134 | |
| 135 | registry.setStaking(address(staking)); |
| 136 | registry.setStockDesk(address(stockDesk)); |
| 137 | staking.setDistributor(address(distributor)); |
| 138 | |
| 139 | treasury.addDepositor(address(bonds)); |
| 140 | treasury.addPolToken(address(pair)); |
| 141 | treasury.addEquityToken(address(nvda), address(nvdaFeed)); |
| 142 | treasury.addEquityToken(address(aapl), address(aaplFeed)); |
| 143 | |
| 144 | stockDesk.setRouter(address(router)); |
| 145 | stockDesk.listSymbol(uint8(Symbol.NVDA), address(nvda), address(nvdaFeed)); |
| 146 | stockDesk.listSymbol(uint8(Symbol.AAPL), address(aapl), address(aaplFeed)); |
| 147 | |
| 148 | splitter.wire( |
| 149 | address(router), address(stockDesk), address(treasury), lpReceiver, address(buyback), team |
| 150 | ); |
| 151 | vm.stopPrank(); |
| 152 | |
| 153 | lens = new K401Lens( |
| 154 | address(k401), |
| 155 | address(treasury), |
| 156 | address(oracle), |
| 157 | address(distributor), |
| 158 | address(staking), |
| 159 | address(registry), |
| 160 | address(seat6551), |
| 161 | address(stockDesk), |
| 162 | address(bonds) |
| 163 | ); |
| 164 | |
| 165 | _seedLiquidity(); |
| 166 | // 1.20 USDG of RFV per token: floor intact, 200k of mint headroom, |
| 167 | // and NAV (1.20) below the pool price (2.00) so there is a real premium. |
| 168 | _fundTreasury(1_200_000e6); |
| 169 | _seedEquityPools(); |
| 170 | } |
| 171 | |
| 172 | /*////////////////////////////////////////////////////////////// |
| 173 | HELPERS |
| 174 | //////////////////////////////////////////////////////////////*/ |
| 175 | |
| 176 | /// @dev Seeds the 401K/USDG pair at ~2.00 USDG per 401K. |
| 177 | function _seedLiquidity() internal { |
| 178 | vm.startPrank(owner); |
| 179 | k401.transfer(address(pair), 100_000e18); |
| 180 | vm.stopPrank(); |
| 181 | usdg.mint(address(pair), 200_000e6); |
| 182 | pair.mint(owner); |
| 183 | } |
| 184 | |
| 185 | function _seedEquityPools() internal { |
| 186 | // NVDA @ $100, AAPL @ $200 with deep-enough mock pools for the batch tests. |
| 187 | usdg.mint(address(nvdaPair), 10_000_000e6); |
| 188 | nvda.mint(address(nvdaPair), 100_000e18); |
| 189 | nvdaPair.mint(owner); |
| 190 | |
| 191 | usdg.mint(address(aaplPair), 10_000_000e6); |
| 192 | aapl.mint(address(aaplPair), 50_000e18); |
| 193 | aaplPair.mint(owner); |
| 194 | } |
| 195 | |
| 196 | function _fundTreasury(uint256 usdgAmount) internal { |
| 197 | usdg.mint(address(treasury), usdgAmount); |
| 198 | } |
| 199 | |
| 200 | /// @dev `vm.warp(block.timestamp + x)` is unsafe under via-ir (the TIMESTAMP opcode |
| 201 | /// gets CSE'd), so always read the clock back through the cheatcode. |
| 202 | function _skip(uint256 secs) internal { |
| 203 | vm.warp(vm.getBlockTimestamp() + secs); |
| 204 | } |
| 205 | |
| 206 | function _now() internal view returns (uint256) { |
| 207 | return vm.getBlockTimestamp(); |
| 208 | } |
| 209 | |
| 210 | /// @dev Moves time forward and rolls two oracle windows so `consult()` is valid. |
| 211 | function _primeOracle() internal { |
| 212 | _skip(31 minutes); |
| 213 | pair.sync(); |
| 214 | oracle.checkpoint(); |
| 215 | _skip(31 minutes); |
| 216 | pair.sync(); |
| 217 | oracle.checkpoint(); |
| 218 | } |
| 219 | |
| 220 | function _giveSeats(address to, uint256 count) internal { |
| 221 | vm.prank(owner); |
| 222 | k401.transfer(to, count * 1e18); |
| 223 | } |
| 224 | |
| 225 | /// @dev Fund an address with 401K without minting Seats (skipNFT), for gas-sane |
| 226 | /// tests of the 25_000 401K upgrade cost. |
| 227 | function _fundNoSeats(address to, uint256 amount) internal { |
| 228 | vm.prank(to); |
| 229 | k401.setSkipNFT(true); |
| 230 | vm.prank(owner); |
| 231 | k401.transfer(to, amount); |
| 232 | } |
| 233 | |
| 234 | /// @dev Give `to` exactly `count` Seat NFTs by minting them elsewhere and moving the |
| 235 | /// ids across with an ERC721 transfer (which preserves tokenIds). |
| 236 | function _giveSeatNfts(address to, uint256 count) internal returns (uint256[] memory ids) { |
| 237 | address mule = address(uint160(uint256(keccak256(abi.encode("mule", to, count, _now()))))); |
| 238 | vm.prank(owner); |
| 239 | k401.transfer(mule, count * 1e18); |
| 240 | ids = k401.seatsOf(mule); |
| 241 | vm.startPrank(mule); |
| 242 | for (uint256 i; i < ids.length; ++i) { |
| 243 | mirror.transferFrom(mule, to, ids[i]); |
| 244 | } |
| 245 | vm.stopPrank(); |
| 246 | } |
| 247 | |
| 248 | function _ids(uint256 a) internal pure returns (uint256[] memory out) { |
| 249 | out = new uint256[](1); |
| 250 | out[0] = a; |
| 251 | } |
| 252 | |
| 253 | function _ids(uint256 a, uint256 b) internal pure returns (uint256[] memory out) { |
| 254 | out = new uint256[](2); |
| 255 | out[0] = a; |
| 256 | out[1] = b; |
| 257 | } |
| 258 | |
| 259 | function _ids(uint256 a, uint256 b, uint256 c) internal pure returns (uint256[] memory out) { |
| 260 | out = new uint256[](3); |
| 261 | out[0] = a; |
| 262 | out[1] = b; |
| 263 | out[2] = c; |
| 264 | } |
| 265 | |
| 266 | function _syms(uint8 a) internal pure returns (uint8[] memory out) { |
| 267 | out = new uint8[](1); |
| 268 | out[0] = a; |
| 269 | } |
| 270 | } |
| 271 |
Click any line number to deep-link to it — the target line highlights on load.