Skip to content

Commit 96185d3

Browse files
authored
Merge pull request #442 from rsksmart/test/FLY-2167
Adding Invariant Tests
2 parents 1f45fe3 + e70bbc4 commit 96185d3

13 files changed

Lines changed: 2223 additions & 281 deletions

foundry.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ remappings = [
3030
[fuzz]
3131
runs = 128
3232

33+
[invariant]
34+
runs = 256
35+
depth = 64
36+
fail_on_revert = false
37+
3338
[rpc_endpoints]
3439
rskRegtest = "${REGTEST_RPC_URL}"
3540
rskTestnet = "${TESTNET_RPC_URL}"

test/invariant/CollateralInvariant.t.sol

Lines changed: 97 additions & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -1,216 +1,152 @@
11
// SPDX-License-Identifier: MIT
22
pragma solidity 0.8.25;
33

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";
97

108
/// @title CollateralManagement Invariant Tests
119
/// @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;
1812
address public punisher;
1913

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-
3214
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();
4017

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
6724
);
6825

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));
7727

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;
8535
targetSelector(
86-
FuzzSelector({addr: address(this), selectors: selectors})
36+
FuzzSelector({addr: address(handler), selectors: selectors})
8737
);
8838
}
8939

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-
12940
// ============ Invariant Tests ============
13041

131-
/// @notice Contract balance should always be >= total collateral obligations
42+
/// @notice Contract balance should always cover all collateral + rewards + penalties
13243
function invariant_ContractSolvent() public view {
13344
uint256 contractBalance = address(collateralManagement).balance;
13445
uint256 totalCollateral = _calculateTotalCollateral();
46+
uint256 totalRewards = _calculateTotalRewards();
47+
uint256 totalPenalties = collateralManagement.getPenalties();
13548

13649
assertGe(
13750
contractBalance,
138-
totalCollateral,
51+
totalCollateral + totalRewards + totalPenalties,
13952
"INVARIANT VIOLATED: Contract is insolvent"
14053
);
14154
}
14255

143-
/// @notice Ghost accounting should match contract state (relaxed check)
56+
/// @notice Contract balance must equal added - withdrawn - rewardsWithdrawn (tight equality)
14457
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;
14759

14860
uint256 contractBalance = address(collateralManagement).balance;
61+
uint256 added = handler.ghost_totalAdded();
62+
uint256 withdrawn = handler.ghost_totalWithdrawn();
63+
uint256 rewardsWithdrawn = handler.ghost_totalRewardsWithdrawn();
14964

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"
15469
);
15570
}
15671

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"
16282
);
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"
17587
);
17688
}
17789
}
17890

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;
18095

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;
187110

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"
190122
);
191-
providers.push(provider);
192-
vm.deal(provider, 10 ether);
193-
return provider;
194123
}
195124

125+
// ============ Helper Functions ============
126+
196127
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);
205133
}
206134
}
207135

136+
function _calculateTotalRewards() internal view returns (uint256) {
137+
return collateralManagement.getRewards(punisher);
138+
}
139+
208140
function invariant_callSummary() public view {
209141
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+
);
214150
console.log("Contract balance:", address(collateralManagement).balance);
215151
console.log("------------------------------------\n");
216152
}

0 commit comments

Comments
 (0)