|
1 | 1 | // SPDX-License-Identifier: MIT |
2 | 2 | pragma solidity 0.8.25; |
3 | 3 |
|
4 | | -import {Test, console} from "forge-std/Test.sol"; |
5 | | -import {CollateralManagementContract} from "../../src/CollateralManagement.sol"; |
6 | | -import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; |
7 | | -import {PauseRegistry} from "../../src/PauseRegistry.sol"; |
8 | | -import {IPauseRegistry} from "../../src/interfaces/IPauseRegistry.sol"; |
| 4 | +import {console} from "forge-std/Test.sol"; |
| 5 | +import {CollateralTestBase} from "../collateral/CollateralTestBase.sol"; |
| 6 | +import {CollateralHandler} from "./handlers/CollateralHandler.sol"; |
9 | 7 |
|
10 | 8 | /// @title CollateralManagement Invariant Tests |
11 | 9 | /// @notice Tests critical invariants for the CollateralManagement contract |
12 | | -contract CollateralInvariantTest is Test { |
13 | | - CollateralManagementContract public collateralManagement; |
14 | | - |
15 | | - address public owner; |
16 | | - address public adder; |
17 | | - address public slasher; |
| 10 | +contract CollateralInvariantTest is CollateralTestBase { |
| 11 | + CollateralHandler public handler; |
18 | 12 | address public punisher; |
19 | 13 |
|
20 | | - // Track providers for invariant checks |
21 | | - address[] public providers; |
22 | | - |
23 | | - // Ghost variables |
24 | | - uint256 public ghost_totalAdded; |
25 | | - uint256 public ghost_totalSlashed; |
26 | | - uint256 public ghost_totalWithdrawn; |
27 | | - |
28 | | - uint256 constant MIN_COLLATERAL = 0.6 ether; |
29 | | - uint256 constant RESIGN_DELAY = 500; |
30 | | - uint256 constant REWARD_PERCENTAGE = 1000; |
31 | | - |
32 | 14 | function setUp() public { |
33 | | - owner = makeAddr("owner"); |
34 | | - adder = makeAddr("adder"); |
35 | | - slasher = makeAddr("slasher"); |
36 | | - punisher = makeAddr("punisher"); |
37 | | - |
38 | | - vm.deal(owner, 100 ether); |
39 | | - vm.deal(adder, 100 ether); |
| 15 | + deployCollateralManagement(); |
| 16 | + setupRoles(); |
40 | 17 |
|
41 | | - // Deploy PauseRegistry first |
42 | | - PauseRegistry prImpl = new PauseRegistry(); |
43 | | - ERC1967Proxy prProxy = new ERC1967Proxy( |
44 | | - address(prImpl), |
45 | | - abi.encodeCall(prImpl.initialize, (0, owner)) |
46 | | - ); |
47 | | - IPauseRegistry pauseRegistry = IPauseRegistry( |
48 | | - payable(address(prProxy)) |
49 | | - ); |
50 | | - |
51 | | - // Deploy CollateralManagement |
52 | | - CollateralManagementContract impl = new CollateralManagementContract(); |
53 | | - bytes memory initData = abi.encodeCall( |
54 | | - CollateralManagementContract.initialize, |
55 | | - ( |
56 | | - owner, |
57 | | - 30, |
58 | | - MIN_COLLATERAL, |
59 | | - RESIGN_DELAY, |
60 | | - REWARD_PERCENTAGE, |
61 | | - pauseRegistry |
62 | | - ) |
63 | | - ); |
64 | | - ERC1967Proxy proxy = new ERC1967Proxy(address(impl), initData); |
65 | | - collateralManagement = CollateralManagementContract( |
66 | | - payable(address(proxy)) |
| 18 | + punisher = makeAddr("punisher"); |
| 19 | + handler = new CollateralHandler( |
| 20 | + collateralManagement, |
| 21 | + adder, |
| 22 | + slasher, |
| 23 | + punisher |
67 | 24 | ); |
68 | 25 |
|
69 | | - // Grant roles |
70 | | - bytes32 adderRole = collateralManagement.COLLATERAL_ADDER(); |
71 | | - bytes32 slasherRole = collateralManagement.COLLATERAL_SLASHER(); |
72 | | - |
73 | | - vm.startPrank(owner); |
74 | | - collateralManagement.grantRole(adderRole, adder); |
75 | | - collateralManagement.grantRole(slasherRole, slasher); |
76 | | - vm.stopPrank(); |
| 26 | + targetContract(address(handler)); |
77 | 27 |
|
78 | | - // Target this contract for invariant testing |
79 | | - targetContract(address(this)); |
80 | | - |
81 | | - // Exclude setUp from being called during invariant testing |
82 | | - bytes4[] memory selectors = new bytes4[](2); |
83 | | - selectors[0] = this.addCollateral.selector; |
84 | | - selectors[1] = this.resignAndWithdraw.selector; |
| 28 | + bytes4[] memory selectors = new bytes4[](6); |
| 29 | + selectors[0] = handler.addPegInCollateral.selector; |
| 30 | + selectors[1] = handler.addPegOutCollateral.selector; |
| 31 | + selectors[2] = handler.slashPegIn.selector; |
| 32 | + selectors[3] = handler.slashPegOut.selector; |
| 33 | + selectors[4] = handler.resignAndWithdraw.selector; |
| 34 | + selectors[5] = handler.withdrawRewards.selector; |
85 | 35 | targetSelector( |
86 | | - FuzzSelector({addr: address(this), selectors: selectors}) |
| 36 | + FuzzSelector({addr: address(handler), selectors: selectors}) |
87 | 37 | ); |
88 | 38 | } |
89 | 39 |
|
90 | | - // ============ Handler Functions ============ |
91 | | - |
92 | | - function addCollateral(uint256 providerSeed, uint256 amount) external { |
93 | | - amount = bound(amount, MIN_COLLATERAL, 10 ether); |
94 | | - address provider = _getOrCreateProvider(providerSeed); |
95 | | - |
96 | | - vm.deal(adder, amount); |
97 | | - vm.prank(adder); |
98 | | - collateralManagement.addPegInCollateralTo{value: amount}(provider); |
99 | | - |
100 | | - ghost_totalAdded += amount; |
101 | | - } |
102 | | - |
103 | | - function resignAndWithdraw(uint256 providerSeed) external { |
104 | | - if (providers.length == 0) return; |
105 | | - |
106 | | - address provider = providers[providerSeed % providers.length]; |
107 | | - uint256 pegInCollateral = collateralManagement.getPegInCollateral( |
108 | | - provider |
109 | | - ); |
110 | | - |
111 | | - if (pegInCollateral == 0) return; |
112 | | - |
113 | | - // Resign first |
114 | | - vm.prank(provider); |
115 | | - try collateralManagement.resign() {} catch { |
116 | | - return; |
117 | | - } |
118 | | - |
119 | | - // Advance blocks past delay |
120 | | - vm.roll(block.number + RESIGN_DELAY + 1); |
121 | | - |
122 | | - // Withdraw |
123 | | - vm.prank(provider); |
124 | | - try collateralManagement.withdrawCollateral() { |
125 | | - ghost_totalWithdrawn += pegInCollateral; |
126 | | - } catch {} |
127 | | - } |
128 | | - |
129 | 40 | // ============ Invariant Tests ============ |
130 | 41 |
|
131 | | - /// @notice Contract balance should always be >= total collateral obligations |
| 42 | + /// @notice Contract balance should always cover all collateral + rewards + penalties |
132 | 43 | function invariant_ContractSolvent() public view { |
133 | 44 | uint256 contractBalance = address(collateralManagement).balance; |
134 | 45 | uint256 totalCollateral = _calculateTotalCollateral(); |
| 46 | + uint256 totalRewards = _calculateTotalRewards(); |
| 47 | + uint256 totalPenalties = collateralManagement.getPenalties(); |
135 | 48 |
|
136 | 49 | assertGe( |
137 | 50 | contractBalance, |
138 | | - totalCollateral, |
| 51 | + totalCollateral + totalRewards + totalPenalties, |
139 | 52 | "INVARIANT VIOLATED: Contract is insolvent" |
140 | 53 | ); |
141 | 54 | } |
142 | 55 |
|
143 | | - /// @notice Ghost accounting should match contract state (relaxed check) |
| 56 | + /// @notice Contract balance must equal added - withdrawn - rewardsWithdrawn (tight equality) |
144 | 57 | function invariant_GhostAccountingConsistent() public view { |
145 | | - // If nothing has been added via our handlers, skip this check |
146 | | - if (ghost_totalAdded == 0) return; |
| 58 | + if (handler.ghost_totalAdded() == 0) return; |
147 | 59 |
|
148 | 60 | uint256 contractBalance = address(collateralManagement).balance; |
| 61 | + uint256 added = handler.ghost_totalAdded(); |
| 62 | + uint256 withdrawn = handler.ghost_totalWithdrawn(); |
| 63 | + uint256 rewardsWithdrawn = handler.ghost_totalRewardsWithdrawn(); |
149 | 64 |
|
150 | | - // Contract balance should be reasonable - not more than we added |
151 | | - assertTrue( |
152 | | - contractBalance <= ghost_totalAdded + 1 ether, |
153 | | - "INVARIANT VIOLATED: Contract has more than deposited" |
| 65 | + assertEq( |
| 66 | + contractBalance, |
| 67 | + added - withdrawn - rewardsWithdrawn, |
| 68 | + "INVARIANT VIOLATED: Contract balance != added - withdrawn - rewardsWithdrawn" |
154 | 69 | ); |
155 | 70 | } |
156 | 71 |
|
157 | | - /// @notice No provider should have negative collateral (underflow) |
158 | | - function invariant_NoNegativeCollateral() public view { |
159 | | - for (uint256 i = 0; i < providers.length; i++) { |
160 | | - uint256 pegInCollateral = collateralManagement.getPegInCollateral( |
161 | | - providers[i] |
| 72 | + /// @notice Provider collateral should not exceed total added through handler |
| 73 | + function invariant_CollateralBoundedByTotalAdded() public view { |
| 74 | + uint256 added = handler.ghost_totalAdded(); |
| 75 | + uint256 count = handler.getProviderCount(); |
| 76 | + for (uint256 i = 0; i < count; i++) { |
| 77 | + address provider = handler.getProvider(i); |
| 78 | + assertLe( |
| 79 | + collateralManagement.getPegInCollateral(provider), |
| 80 | + added, |
| 81 | + "INVARIANT VIOLATED: PegIn collateral exceeds total added" |
162 | 82 | ); |
163 | | - uint256 pegOutCollateral = collateralManagement.getPegOutCollateral( |
164 | | - providers[i] |
165 | | - ); |
166 | | - |
167 | | - // Check for underflow - values near max uint256 indicate underflow |
168 | | - assertTrue( |
169 | | - pegInCollateral < 1_000_000 ether, |
170 | | - "INVARIANT VIOLATED: PegIn collateral underflowed" |
171 | | - ); |
172 | | - assertTrue( |
173 | | - pegOutCollateral < 1_000_000 ether, |
174 | | - "INVARIANT VIOLATED: PegOut collateral underflowed" |
| 83 | + assertLe( |
| 84 | + collateralManagement.getPegOutCollateral(provider), |
| 85 | + added, |
| 86 | + "INVARIANT VIOLATED: PegOut collateral exceeds total added" |
175 | 87 | ); |
176 | 88 | } |
177 | 89 | } |
178 | 90 |
|
179 | | - // ============ Helper Functions ============ |
| 91 | + /// @notice Rewards + penalties must equal total slashed |
| 92 | + function invariant_RewardsPlusPenaltiesEqualSlashed() public view { |
| 93 | + uint256 slashed = handler.ghost_totalSlashed(); |
| 94 | + if (slashed == 0) return; |
180 | 95 |
|
181 | | - function _getOrCreateProvider( |
182 | | - uint256 seed |
183 | | - ) internal returns (address provider) { |
184 | | - if (providers.length > 0 && seed % 3 != 0) { |
185 | | - return providers[seed % providers.length]; |
186 | | - } |
| 96 | + uint256 totalRewards = _calculateTotalRewards() + |
| 97 | + handler.ghost_totalRewardsWithdrawn(); |
| 98 | + uint256 totalPenalties = collateralManagement.getPenalties(); |
| 99 | + |
| 100 | + assertEq( |
| 101 | + totalRewards + totalPenalties, |
| 102 | + slashed, |
| 103 | + "INVARIANT VIOLATED: Rewards + penalties != total slashed" |
| 104 | + ); |
| 105 | + } |
| 106 | + |
| 107 | + /// @notice Sum of all collateral + penalties + rewards must equal added - withdrawn - rewardsWithdrawn |
| 108 | + function invariant_FullConservation() public view { |
| 109 | + if (handler.ghost_totalAdded() == 0) return; |
187 | 110 |
|
188 | | - provider = address( |
189 | | - uint160(uint256(keccak256(abi.encode(seed, providers.length)))) |
| 111 | + uint256 totalCollateral = _calculateTotalCollateral(); |
| 112 | + uint256 rewards = _calculateTotalRewards(); |
| 113 | + uint256 penalties = collateralManagement.getPenalties(); |
| 114 | + uint256 added = handler.ghost_totalAdded(); |
| 115 | + uint256 withdrawn = handler.ghost_totalWithdrawn(); |
| 116 | + uint256 rewardsWithdrawn = handler.ghost_totalRewardsWithdrawn(); |
| 117 | + |
| 118 | + assertEq( |
| 119 | + totalCollateral + rewards + penalties, |
| 120 | + added - withdrawn - rewardsWithdrawn, |
| 121 | + "INVARIANT VIOLATED: Conservation of value failed" |
190 | 122 | ); |
191 | | - providers.push(provider); |
192 | | - vm.deal(provider, 10 ether); |
193 | | - return provider; |
194 | 123 | } |
195 | 124 |
|
| 125 | + // ============ Helper Functions ============ |
| 126 | + |
196 | 127 | function _calculateTotalCollateral() internal view returns (uint256 total) { |
197 | | - for (uint256 i = 0; i < providers.length; i++) { |
198 | | - uint256 pegInCollateral = collateralManagement.getPegInCollateral( |
199 | | - providers[i] |
200 | | - ); |
201 | | - uint256 pegOutCollateral = collateralManagement.getPegOutCollateral( |
202 | | - providers[i] |
203 | | - ); |
204 | | - total += pegInCollateral + pegOutCollateral; |
| 128 | + uint256 count = handler.getProviderCount(); |
| 129 | + for (uint256 i = 0; i < count; i++) { |
| 130 | + address provider = handler.getProvider(i); |
| 131 | + total += collateralManagement.getPegInCollateral(provider); |
| 132 | + total += collateralManagement.getPegOutCollateral(provider); |
205 | 133 | } |
206 | 134 | } |
207 | 135 |
|
| 136 | + function _calculateTotalRewards() internal view returns (uint256) { |
| 137 | + return collateralManagement.getRewards(punisher); |
| 138 | + } |
| 139 | + |
208 | 140 | function invariant_callSummary() public view { |
209 | 141 | console.log("\n--- Collateral Invariant Summary ---"); |
210 | | - console.log("Providers:", providers.length); |
211 | | - console.log("Total added:", ghost_totalAdded); |
212 | | - console.log("Total slashed:", ghost_totalSlashed); |
213 | | - console.log("Total withdrawn:", ghost_totalWithdrawn); |
| 142 | + console.log("Providers:", handler.getProviderCount()); |
| 143 | + console.log("Total added:", handler.ghost_totalAdded()); |
| 144 | + console.log("Total slashed:", handler.ghost_totalSlashed()); |
| 145 | + console.log("Total withdrawn:", handler.ghost_totalWithdrawn()); |
| 146 | + console.log( |
| 147 | + "Total rewards withdrawn:", |
| 148 | + handler.ghost_totalRewardsWithdrawn() |
| 149 | + ); |
214 | 150 | console.log("Contract balance:", address(collateralManagement).balance); |
215 | 151 | console.log("------------------------------------\n"); |
216 | 152 | } |
|
0 commit comments