-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathProtocolStakingInvariantTest.t.sol
More file actions
144 lines (127 loc) · 6.45 KB
/
ProtocolStakingInvariantTest.t.sol
File metadata and controls
144 lines (127 loc) · 6.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;
import {Test} from "forge-std/Test.sol";
import {ProtocolStakingHarness} from "./harness/ProtocolStakingHarness.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {ZamaERC20} from "token/contracts/ZamaERC20.sol";
import {ProtocolStakingHandler} from "./handlers/ProtocolStakingHandler.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
// Invariant fuzz test for ProtocolStaking
contract ProtocolStakingInvariantTest is Test {
ProtocolStakingHarness internal protocolStaking;
ZamaERC20 internal zama;
ProtocolStakingHandler internal handler;
address internal governor = address(1);
address internal manager = address(2);
address internal admin = address(3);
uint256 internal constant ACTOR_COUNT = 5;
uint256 internal constant INITIAL_TOTAL_SUPPLY = 1_000_000 ether;
uint256 internal constant INITIAL_REWARD_RATE = 1e18; // 1 token/second
uint48 internal constant INITIAL_UNSTAKE_COOLDOWN_PERIOD = 1 seconds;
function setUp() public {
address[] memory actorsList = new address[](ACTOR_COUNT);
for (uint256 i = 0; i < ACTOR_COUNT; i++) {
actorsList[i] = address(uint160(4 + i));
}
// Deploy ZamaERC20, mint to all actors, admin is DEFAULT_ADMIN
uint256 initialActorBalance = INITIAL_TOTAL_SUPPLY / ACTOR_COUNT;
address[] memory receivers = new address[](ACTOR_COUNT);
uint256[] memory amounts = new uint256[](ACTOR_COUNT);
for (uint256 i = 0; i < ACTOR_COUNT; i++) {
receivers[i] = actorsList[i];
amounts[i] = initialActorBalance;
}
zama = new ZamaERC20("Zama", "ZAMA", receivers, amounts, admin);
// Deploy ProtocolStaking behind ERC1967 proxy
ProtocolStakingHarness impl = new ProtocolStakingHarness();
bytes memory initData = abi.encodeCall(
protocolStaking.initialize,
(
"Staked ZAMA",
"stZAMA",
"1",
address(zama),
governor,
manager,
INITIAL_UNSTAKE_COOLDOWN_PERIOD,
INITIAL_REWARD_RATE
)
);
ERC1967Proxy proxy = new ERC1967Proxy(address(impl), initData);
protocolStaking = ProtocolStakingHarness(address(proxy));
// Grant MINTER_ROLE on Zama to ProtocolStaking
vm.startPrank(admin);
zama.grantRole(zama.MINTER_ROLE(), address(protocolStaking));
vm.stopPrank();
// Approve ProtocolStaking for all actors
for (uint256 i = 0; i < ACTOR_COUNT; i++) {
vm.prank(actorsList[i]);
zama.approve(address(protocolStaking), type(uint256).max);
}
// Deploy handler with actors list
handler = new ProtocolStakingHandler(protocolStaking, zama, manager, actorsList);
targetContract(address(handler));
bytes4[] memory selectors = new bytes4[](12);
selectors[0] = ProtocolStakingHandler.warp.selector;
selectors[1] = ProtocolStakingHandler.setRewardRate.selector;
selectors[2] = ProtocolStakingHandler.addEligibleAccount.selector;
selectors[3] = ProtocolStakingHandler.removeEligibleAccount.selector;
selectors[4] = ProtocolStakingHandler.stake.selector;
selectors[5] = ProtocolStakingHandler.unstake.selector;
selectors[6] = ProtocolStakingHandler.claimRewards.selector;
selectors[7] = ProtocolStakingHandler.release.selector;
selectors[8] = ProtocolStakingHandler.unstakeThenWarp.selector;
selectors[9] = ProtocolStakingHandler.stakeEquivalenceScenario.selector;
selectors[10] = ProtocolStakingHandler.unstakeEquivalenceScenario.selector;
selectors[11] = ProtocolStakingHandler.setUnstakeCooldownPeriod.selector;
targetSelector(FuzzSelector({addr: address(handler), selectors: selectors}));
}
function invariant_TotalStakedWeightEqualsEligibleWeights() public view {
assertEq(
protocolStaking.totalStakedWeight(),
handler.computeExpectedTotalWeight(),
"totalStakedWeight does not match sum of eligible weights"
);
}
function invariant_TotalSupplyBoundedByRewardRate() public view {
assertLe(
zama.totalSupply(),
// TODO: Occasional Off-by-one error in the ghost total supply calculation, need to locate the source of the error
// adding small buffer of 1 wei to account for this for now
handler.ghost_initialTotalSupply() + handler.ghost_accumulatedRewardCapacity() + 1,
"totalSupply exceeds piecewise rewardRate bound"
);
}
function invariant_RewardDebtConservation() public view {
uint256 tolerance = handler.REWARD_DEBT_CONSERVATION_TOLERANCE();
int256 lhs = handler.computeRewardDebtLHS();
// When the system is empty, net debt across all users should net out to 0
// Σ _paid[account] + Σ earned(account) = 0
// Using ApproxEqAbs per contract comment: "Accounting rounding may have a marginal impact on earned rewards (dust)."
if (protocolStaking.totalStakedWeight() == 0) {
assertApproxEqAbs(lhs, 0, tolerance, "Net reward debt must be 0 when no one is staked");
return;
}
int256 rhs = handler.computeRewardDebtRHS();
assertApproxEqAbs(lhs, rhs, tolerance, "reward debt conservation");
}
function invariant_PendingWithdrawalsSolvency() public view {
address token = protocolStaking.stakingToken();
uint256 balance = IERC20(token).balanceOf(address(protocolStaking));
uint256 sumAwaitingRelease;
for (uint256 i = 0; i < handler.actorsLength(); i++) {
sumAwaitingRelease += protocolStaking.awaitingRelease(handler.actorAt(i));
}
assertGe(balance, sumAwaitingRelease, "pending withdrawals solvency");
}
function invariant_StakedFundsSolvency() public view {
for (uint256 i = 0; i < handler.actorsLength(); i++) {
address account = handler.actorAt(i);
uint256 totalStaked = handler.ghost_totalStaked(account);
uint256 balance = protocolStaking.balanceOf(account);
uint256 awaiting = protocolStaking.awaitingRelease(account);
uint256 released = handler.ghost_totalReleased(account);
assertEq(totalStaked, balance + awaiting + released, "staked funds solvency");
}
}
}