* Add integration test for withdrawing 0x stake in catastrophic mode * Add a test for delegating to an external account * Use startPrank for all calls as all from delegator * Better use of prank Co-authored-by: duncancmt <1207590+duncancmt@users.noreply.github.com> * Add the delegation test under normal operation mode' --------- Co-authored-by: duncancmt <1207590+duncancmt@users.noreply.github.com>
455 lines
17 KiB
Solidity
455 lines
17 KiB
Solidity
// SPDX-License-Identifier: Apache-2.0
|
|
/*
|
|
|
|
Copyright 2023 ZeroEx Intl.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
|
|
*/
|
|
|
|
pragma solidity ^0.8.19;
|
|
|
|
import "@openzeppelin/token/ERC20/IERC20.sol";
|
|
import "../mocks/IZeroExMock.sol";
|
|
import "../mocks/IZrxTreasuryMock.sol";
|
|
import "../mocks/IStakingMock.sol";
|
|
import "../mocks/IZrxVaultMock.sol";
|
|
import "../BaseTest.t.sol";
|
|
import "../../src/ZRXWrappedToken.sol";
|
|
import "../../src/ZeroExVotes.sol";
|
|
import "../../src/ZeroExTimelock.sol";
|
|
import "../../src/ZeroExProtocolGovernor.sol";
|
|
import "../../src/ZeroExTreasuryGovernor.sol";
|
|
|
|
contract GovernanceE2ETest is BaseTest {
|
|
uint256 internal mainnetFork;
|
|
string internal MAINNET_RPC_URL = vm.envString("MAINNET_RPC_URL");
|
|
|
|
struct DelegatorPool {
|
|
address delegator;
|
|
bytes32 pool;
|
|
}
|
|
|
|
address internal constant ZRX_TOKEN = 0xE41d2489571d322189246DaFA5ebDe1F4699F498;
|
|
address internal constant MATIC_TOKEN = 0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0;
|
|
address internal constant WCELO_TOKEN = 0xE452E6Ea2dDeB012e20dB73bf5d3863A3Ac8d77a;
|
|
address internal constant WYV_TOKEN = 0x056017c55aE7AE32d12AeF7C679dF83A85ca75Ff;
|
|
|
|
address internal constant EXCHANGE_PROXY = 0xDef1C0ded9bec7F1a1670819833240f027b25EfF;
|
|
address internal constant EXCHANGE_GOVERNOR = 0x618F9C67CE7Bf1a50afa1E7e0238422601b0ff6e;
|
|
address internal constant TREASURY = 0x0bB1810061C2f5b2088054eE184E6C79e1591101;
|
|
address internal constant STAKING = 0xa26e80e7Dea86279c6d778D702Cc413E6CFfA777; // Holds ~$262K in WETH for rewards
|
|
address internal constant ZRX_VAULT = 0xBa7f8b5fB1b19c1211c5d49550fcD149177A5Eaf; // Holds ~$10m in staked ZRX
|
|
address internal constant STAKING_AND_VAULT_OWNER = 0x7D3455421BbC5Ed534a83c88FD80387dc8271392;
|
|
|
|
address internal staker1 = 0x885c327cAD2aebb969dfaAb4c928B73CA17e3887;
|
|
address internal staker2 = 0x03c823e96F6964076C118395F08a2D7edF0f8a8C;
|
|
|
|
address[] internal topStakers = [
|
|
0x5775afA796818ADA27b09FaF5c90d101f04eF600,
|
|
0xE1bdcd3B70e077D2d66ADcbe78be3941F0BF380B,
|
|
0xcCa71809E8870AFEB72c4720d0fe50d5C3230e05,
|
|
0x828FD91d3e3a9FFa6305e78B9aE2Cfbc5B5D9f6B,
|
|
0x4A36C3DA5d367B148d17265e7d7feafcf8fb4a21,
|
|
0xEeff6fd32DeaFe1a9d3258A51c7F952F9FF0B2Ce,
|
|
0x1D0738b927dFCBFBD59A9F0944BbD1860d3B9248,
|
|
0x0C073E7248C1b548a08b27dD3af5D0f39c774280,
|
|
0xA178FF321335BB777A7E21A56376592F69556b9c,
|
|
0xD06CfBb59d2e8918F84D99d981039d7706DCA288
|
|
];
|
|
|
|
// voting power 1500000e18
|
|
address internal voter1 = 0x292c6DAE7417B3D31d8B6e1d2EeA0258d14C4C4b;
|
|
bytes32 internal voter1Pool = 0x0000000000000000000000000000000000000000000000000000000000000030;
|
|
bytes32[] internal voter1_operated_poolIds = [voter1Pool];
|
|
|
|
// voting power 1500000.5e18
|
|
address internal voter2 = 0x4990cE223209FCEc4ec4c1ff6E0E81eebD8Cca08;
|
|
bytes32 internal voter2Pool = 0x0000000000000000000000000000000000000000000000000000000000000031;
|
|
bytes32[] internal voter2_operated_poolIds = [voter2Pool];
|
|
|
|
// voting power 1500000e18
|
|
address internal voter3 = 0x5265Bde27F57E738bE6c1F6AB3544e82cdc92a8f;
|
|
bytes32 internal voter3Pool = 0x0000000000000000000000000000000000000000000000000000000000000032;
|
|
bytes32[] internal voter3_operated_poolIds = [voter3Pool];
|
|
|
|
// voting power 1500000e18
|
|
address internal voter4 = 0xcA9F5049c1Ea8FC78574f94B7Cf5bE5fEE354C31;
|
|
bytes32 internal voter4Pool = 0x0000000000000000000000000000000000000000000000000000000000000034;
|
|
bytes32[] internal voter4_operated_poolIds = [voter4Pool];
|
|
|
|
// voting power 1500000e18
|
|
address internal voter5 = 0xDBB5664a9DBCB98F6365804880e5b277B3155422;
|
|
bytes32 internal voter5Pool = 0x0000000000000000000000000000000000000000000000000000000000000035;
|
|
bytes32[] internal voter5_operated_poolIds = [voter5Pool];
|
|
|
|
// voting power 2291490.952353335e18
|
|
address internal voter6 = 0x9a4Eb1101C0c053505Bd71d2fFa27Ed902DEaD85;
|
|
bytes32 internal voter6Pool = 0x0000000000000000000000000000000000000000000000000000000000000029;
|
|
bytes32[] internal voter6_operated_poolIds = [voter6Pool];
|
|
|
|
// voting power 4575984.325e18
|
|
address internal voter7 = 0x9564177EC8052C92752a488a71769F710aA0A41D;
|
|
bytes32 internal voter7Pool = 0x0000000000000000000000000000000000000000000000000000000000000025;
|
|
bytes32[] internal voter7_operated_poolIds = [voter7Pool];
|
|
|
|
IERC20 internal token;
|
|
IERC20 internal maticToken;
|
|
IERC20 internal wceloToken;
|
|
IERC20 internal wyvToken;
|
|
|
|
IZeroExMock internal exchange;
|
|
IZrxTreasuryMock internal treasury;
|
|
IZrxVaultMock internal vault;
|
|
IStakingMock internal staking;
|
|
IERC20 internal weth;
|
|
|
|
ZRXWrappedToken internal wToken;
|
|
ZeroExVotes internal votes;
|
|
ZeroExTimelock internal protocolTimelock;
|
|
ZeroExTimelock internal treasuryTimelock;
|
|
ZeroExProtocolGovernor internal protocolGovernor;
|
|
ZeroExTreasuryGovernor internal treasuryGovernor;
|
|
|
|
function setUp() public {
|
|
mainnetFork = vm.createFork(MAINNET_RPC_URL);
|
|
vm.selectFork(mainnetFork);
|
|
|
|
token = IERC20(ZRX_TOKEN);
|
|
maticToken = IERC20(MATIC_TOKEN);
|
|
wceloToken = IERC20(WCELO_TOKEN);
|
|
wyvToken = IERC20(WYV_TOKEN);
|
|
|
|
exchange = IZeroExMock(payable(EXCHANGE_PROXY));
|
|
treasury = IZrxTreasuryMock(TREASURY);
|
|
vault = IZrxVaultMock(ZRX_VAULT);
|
|
staking = IStakingMock(STAKING);
|
|
weth = IERC20(staking.getWethContract());
|
|
|
|
address protocolGovernorAddress;
|
|
address treasuryGovernorAddress;
|
|
(
|
|
wToken,
|
|
votes,
|
|
protocolTimelock,
|
|
treasuryTimelock,
|
|
protocolGovernorAddress,
|
|
treasuryGovernorAddress
|
|
) = setupGovernance(token);
|
|
|
|
protocolGovernor = ZeroExProtocolGovernor(payable(protocolGovernorAddress));
|
|
treasuryGovernor = ZeroExTreasuryGovernor(payable(treasuryGovernorAddress));
|
|
}
|
|
|
|
function testProtocolGovernanceMigration() public {
|
|
// initially the zrx exchange is owned by the legacy exchange governor
|
|
assertEq(exchange.owner(), EXCHANGE_GOVERNOR);
|
|
|
|
// transfer ownership to new protocol governor
|
|
vm.prank(EXCHANGE_GOVERNOR);
|
|
exchange.transferOwnership(address(protocolGovernor));
|
|
assertEq(exchange.owner(), address(protocolGovernor));
|
|
}
|
|
|
|
function testTreasuryGovernanceMigration() public {
|
|
// Create a proposal to migrate to new governor
|
|
|
|
uint256 currentEpoch = staking.currentEpoch();
|
|
uint256 executionEpoch = currentEpoch + 2;
|
|
|
|
vm.startPrank(voter3);
|
|
|
|
IZrxTreasuryMock.ProposedAction[] memory actions = new IZrxTreasuryMock.ProposedAction[](4);
|
|
|
|
// Transfer MATIC
|
|
uint256 maticBalance = maticToken.balanceOf(address(treasury));
|
|
actions[0] = IZrxTreasuryMock.ProposedAction({
|
|
target: MATIC_TOKEN,
|
|
data: abi.encodeCall(maticToken.transfer, (address(treasuryGovernor), maticBalance)),
|
|
value: 0
|
|
});
|
|
|
|
// Transfer ZRX
|
|
uint256 zrxBalance = token.balanceOf(address(treasury));
|
|
actions[1] = IZrxTreasuryMock.ProposedAction({
|
|
target: ZRX_TOKEN,
|
|
data: abi.encodeCall(token.transfer, (address(treasuryGovernor), zrxBalance)),
|
|
value: 0
|
|
});
|
|
|
|
// Transfer wCELO
|
|
uint256 wceloBalance = wceloToken.balanceOf(address(treasury));
|
|
actions[2] = IZrxTreasuryMock.ProposedAction({
|
|
target: WCELO_TOKEN,
|
|
data: abi.encodeCall(wceloToken.transfer, (address(treasuryGovernor), wceloBalance)),
|
|
value: 0
|
|
});
|
|
|
|
// Transfer WYV
|
|
uint256 wyvBalance = wyvToken.balanceOf(address(treasury));
|
|
actions[3] = IZrxTreasuryMock.ProposedAction({
|
|
target: WYV_TOKEN,
|
|
data: abi.encodeCall(wyvToken.transfer, (address(treasuryGovernor), wyvBalance)),
|
|
value: 0
|
|
});
|
|
|
|
uint256 proposalId = treasury.propose(
|
|
actions,
|
|
executionEpoch,
|
|
"Z-5 Migrate to new treasury governor",
|
|
voter3_operated_poolIds
|
|
);
|
|
|
|
// Once a proposal is created, it becomes open for voting at the epoch after next (currentEpoch + 2)
|
|
// and is open for the voting period (currently set to 3 days).
|
|
uint256 epochDurationInSeconds = staking.epochDurationInSeconds(); // Currently set to 604800 seconds = 7 days
|
|
uint256 currentEpochEndTime = staking.currentEpochStartTimeInSeconds() + epochDurationInSeconds;
|
|
|
|
vm.warp(currentEpochEndTime + 1);
|
|
staking.endEpoch();
|
|
vm.warp(block.timestamp + epochDurationInSeconds + 1);
|
|
staking.endEpoch();
|
|
|
|
vm.stopPrank();
|
|
// quorum is 10,000,000e18 so reach that via the following votes
|
|
vm.prank(voter1);
|
|
treasury.castVote(proposalId, true, voter1_operated_poolIds);
|
|
vm.stopPrank();
|
|
|
|
vm.prank(voter2);
|
|
treasury.castVote(proposalId, true, voter2_operated_poolIds);
|
|
vm.stopPrank();
|
|
|
|
vm.prank(voter3);
|
|
treasury.castVote(proposalId, true, voter3_operated_poolIds);
|
|
vm.stopPrank();
|
|
|
|
vm.prank(voter4);
|
|
treasury.castVote(proposalId, true, voter4_operated_poolIds);
|
|
vm.stopPrank();
|
|
|
|
vm.prank(voter5);
|
|
treasury.castVote(proposalId, true, voter5_operated_poolIds);
|
|
vm.stopPrank();
|
|
|
|
vm.prank(voter6);
|
|
treasury.castVote(proposalId, true, voter6_operated_poolIds);
|
|
vm.stopPrank();
|
|
|
|
vm.prank(voter7);
|
|
treasury.castVote(proposalId, true, voter7_operated_poolIds);
|
|
vm.stopPrank();
|
|
|
|
vm.warp(block.timestamp + 3 days + 1);
|
|
|
|
// Execute proposal
|
|
treasury.execute(proposalId, actions);
|
|
|
|
// Assert value of treasury has correctly transferred
|
|
uint256 maticBalanceNewTreasury = maticToken.balanceOf(address(treasuryGovernor));
|
|
assertEq(maticBalanceNewTreasury, maticBalance);
|
|
|
|
uint256 zrxBalanceNewTreasury = token.balanceOf(address(treasuryGovernor));
|
|
assertEq(zrxBalanceNewTreasury, zrxBalance);
|
|
|
|
uint256 wceloBalanceNewTreasury = wceloToken.balanceOf(address(treasuryGovernor));
|
|
assertEq(wceloBalanceNewTreasury, wceloBalance);
|
|
|
|
uint256 wyvBalanceNewTreasury = wyvToken.balanceOf(address(treasuryGovernor));
|
|
assertEq(wyvBalanceNewTreasury, wyvBalance);
|
|
}
|
|
|
|
// Test entering catastrophic failure mode on the zrx vault to decomission v3 staking
|
|
function testCatastrophicFailureModeOnStaking() public {
|
|
DelegatorPool[5] memory delegatorPools = [
|
|
DelegatorPool(
|
|
0x0ee1F33A2EB0da738FdF035C48d62d75e996a3bd,
|
|
0x0000000000000000000000000000000000000000000000000000000000000016
|
|
),
|
|
DelegatorPool(
|
|
0xcAb3d8cBBb3dA1bDabfB003B9C828B27a821717f,
|
|
0x0000000000000000000000000000000000000000000000000000000000000017
|
|
),
|
|
DelegatorPool(
|
|
0x7f88b00Db27a500fBfA7EbC9c3CaA2Dea6F59d5b,
|
|
0x0000000000000000000000000000000000000000000000000000000000000014
|
|
),
|
|
DelegatorPool(
|
|
0xcE266E6123B682f7A7388097e2155b5379D9AC78,
|
|
0x0000000000000000000000000000000000000000000000000000000000000014
|
|
),
|
|
DelegatorPool(
|
|
0xBa4f44E774158408E2DC6c5cb65BC995F0a89180, // pool operator
|
|
0x0000000000000000000000000000000000000000000000000000000000000017
|
|
)
|
|
];
|
|
|
|
// Enter catastrophic failure mode on the zrx vault
|
|
vm.prank(STAKING_AND_VAULT_OWNER);
|
|
vault.enterCatastrophicFailure();
|
|
vm.stopPrank();
|
|
|
|
// Stakes can still be withdrawn
|
|
// staker1 withdraws
|
|
uint256 stake1 = vault.balanceOf(staker1);
|
|
uint256 balance1 = token.balanceOf(staker1);
|
|
assertGt(stake1, 0);
|
|
|
|
vm.prank(staker1);
|
|
vault.withdrawAllFrom(staker1);
|
|
vm.stopPrank();
|
|
|
|
assertEq(vault.balanceOf(staker1), 0);
|
|
assertEq(token.balanceOf(staker1), stake1 + balance1);
|
|
|
|
// staker2 withdraws
|
|
uint256 stake2 = vault.balanceOf(staker2);
|
|
uint256 balance2 = token.balanceOf(staker2);
|
|
assertGt(stake2, 0);
|
|
|
|
vm.prank(staker2);
|
|
vault.withdrawAllFrom(staker2);
|
|
vm.stopPrank();
|
|
|
|
assertEq(vault.balanceOf(staker2), 0);
|
|
assertEq(token.balanceOf(staker2), stake2 + balance2);
|
|
|
|
// Test top stakers can withdraw
|
|
for (uint256 i = 0; i < topStakers.length; i++) {
|
|
address staker = topStakers[i];
|
|
uint256 stake = vault.balanceOf(staker);
|
|
uint256 balance = token.balanceOf(staker);
|
|
assertGt(stake, 0);
|
|
|
|
vm.prank(staker);
|
|
vault.withdrawAllFrom(staker);
|
|
vm.stopPrank();
|
|
|
|
assertEq(vault.balanceOf(staker), 0);
|
|
assertEq(token.balanceOf(staker), stake + balance);
|
|
}
|
|
|
|
// Delegator can withdraw rewards
|
|
for (uint256 i = 0; i < delegatorPools.length; i++) {
|
|
address delegator = delegatorPools[i].delegator;
|
|
bytes32 pool = delegatorPools[i].pool;
|
|
uint256 reward = staking.computeRewardBalanceOfDelegator(pool, delegator);
|
|
assertGt(reward, 0);
|
|
uint256 balanceBeforeReward = weth.balanceOf(delegator);
|
|
|
|
vm.prank(delegator);
|
|
staking.withdrawDelegatorRewards(pool);
|
|
vm.stopPrank();
|
|
|
|
assertEq(weth.balanceOf(delegator), balanceBeforeReward + reward);
|
|
}
|
|
}
|
|
|
|
function testSwitchDelegationInCatastrophicMode() public {
|
|
// Enter catastrophic failure mode on the zrx vault
|
|
vm.prank(STAKING_AND_VAULT_OWNER);
|
|
vault.enterCatastrophicFailure();
|
|
|
|
// 0x delegator
|
|
address delegator = 0x5775afA796818ADA27b09FaF5c90d101f04eF600;
|
|
|
|
uint256 stake = vault.balanceOf(delegator);
|
|
uint256 balance = token.balanceOf(delegator);
|
|
assertGt(stake, 0);
|
|
|
|
// Withdraw stake all at once
|
|
vm.startPrank(delegator);
|
|
vault.withdrawAllFrom(delegator);
|
|
|
|
assertEq(vault.balanceOf(delegator), 0);
|
|
assertEq(token.balanceOf(delegator), stake + balance);
|
|
|
|
// delegate 1M ZRX to 0x4990cE223209FCEc4ec4c1ff6E0E81eebD8Cca08
|
|
vm.roll(block.number + 1);
|
|
address delegate = 0x4990cE223209FCEc4ec4c1ff6E0E81eebD8Cca08;
|
|
uint256 amountToDelegate = 1000000e18;
|
|
|
|
// Approve the wrapped token and deposit 1m ZRX
|
|
token.approve(address(wToken), amountToDelegate);
|
|
wToken.depositFor(delegator, amountToDelegate);
|
|
|
|
assertEq(wToken.balanceOf(delegator), amountToDelegate);
|
|
|
|
vm.roll(block.number + 1);
|
|
wToken.delegate(delegate);
|
|
vm.stopPrank();
|
|
|
|
assertEq(votes.getVotes(delegate), amountToDelegate);
|
|
assertEq(votes.getQuadraticVotes(delegate), amountToDelegate);
|
|
}
|
|
|
|
function testSwitchDelegationInNormalOperationMode() public {
|
|
// 0x delegator
|
|
address delegator = 0x5775afA796818ADA27b09FaF5c90d101f04eF600;
|
|
uint256 balance = token.balanceOf(delegator);
|
|
|
|
// Undelegate stake from pool 0x35
|
|
vm.startPrank(delegator);
|
|
staking.moveStake(
|
|
IStructs.StakeInfo(
|
|
IStructs.StakeStatus.DELEGATED,
|
|
0x0000000000000000000000000000000000000000000000000000000000000035
|
|
),
|
|
IStructs.StakeInfo(IStructs.StakeStatus.UNDELEGATED, bytes32(0)),
|
|
3000000000000000000000000
|
|
);
|
|
|
|
// Undelegate stake from pool 0x38
|
|
IStructs.StoredBalance memory storedBalance38 = staking.getStakeDelegatedToPoolByOwner(
|
|
delegator,
|
|
0x0000000000000000000000000000000000000000000000000000000000000038
|
|
);
|
|
staking.moveStake(
|
|
IStructs.StakeInfo(
|
|
IStructs.StakeStatus.DELEGATED,
|
|
0x0000000000000000000000000000000000000000000000000000000000000038
|
|
),
|
|
IStructs.StakeInfo(IStructs.StakeStatus.UNDELEGATED, bytes32(0)),
|
|
storedBalance38.currentEpochBalance
|
|
);
|
|
|
|
// Warp past an epochs and unstake
|
|
uint256 epochEndTime = staking.getCurrentEpochEarliestEndTimeInSeconds();
|
|
vm.warp(epochEndTime + 1);
|
|
staking.endEpoch();
|
|
|
|
staking.unstake(6000000000000000000000000);
|
|
vm.stopPrank();
|
|
|
|
assertEq(token.balanceOf(delegator), balance + 2 * 3000000000000000000000000);
|
|
|
|
// delegate 1M ZRX to 0x4990cE223209FCEc4ec4c1ff6E0E81eebD8Cca08
|
|
vm.roll(block.number + 1);
|
|
address delegate = 0x4990cE223209FCEc4ec4c1ff6E0E81eebD8Cca08;
|
|
uint256 amountToDelegate = 1000000e18;
|
|
|
|
// Approve the wrapped token and deposit 1m ZRX
|
|
token.approve(address(wToken), amountToDelegate);
|
|
wToken.depositFor(delegator, amountToDelegate);
|
|
|
|
assertEq(wToken.balanceOf(delegator), amountToDelegate);
|
|
|
|
vm.roll(block.number + 1);
|
|
wToken.delegate(delegate);
|
|
vm.stopPrank();
|
|
|
|
assertEq(votes.getVotes(delegate), amountToDelegate);
|
|
assertEq(votes.getQuadraticVotes(delegate), amountToDelegate);
|
|
}
|
|
}
|