diff --git a/src/BribeInitiative.sol b/src/BribeInitiative.sol index da762b68..00ea8b60 100644 --- a/src/BribeInitiative.sol +++ b/src/BribeInitiative.sol @@ -101,10 +101,10 @@ contract BribeInitiative is IInitiative, IBribeInitiative { ); (uint88 totalLQTY, uint32 totalAverageTimestamp) = _decodeLQTYAllocation(totalLQTYAllocation.value); - uint240 totalVotes = governance.lqtyToVotes(totalLQTY, block.timestamp, totalAverageTimestamp); + uint240 totalVotes = governance.lqtyToVotes(totalLQTY, uint32(block.timestamp), totalAverageTimestamp); if (totalVotes != 0) { (uint88 lqty, uint32 averageTimestamp) = _decodeLQTYAllocation(lqtyAllocation.value); - uint240 votes = governance.lqtyToVotes(lqty, block.timestamp, averageTimestamp); + uint240 votes = governance.lqtyToVotes(lqty, uint32(block.timestamp), averageTimestamp); boldAmount = uint256(bribe.boldAmount) * uint256(votes) / uint256(totalVotes); bribeTokenAmount = uint256(bribe.bribeTokenAmount) * uint256(votes) / uint256(totalVotes); } diff --git a/src/Governance.sol b/src/Governance.sol index 6898a8e1..f6ec5242 100644 --- a/src/Governance.sol +++ b/src/Governance.sol @@ -86,7 +86,7 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance bold = IERC20(_bold); require(_config.minClaim <= _config.minAccrual, "Gov: min-claim-gt-min-accrual"); REGISTRATION_FEE = _config.registrationFee; - + // Registration threshold must be below 100% of votes require(_config.registrationThresholdFactor < WAD, "Gov: registration-config"); REGISTRATION_THRESHOLD_FACTOR = _config.registrationThresholdFactor; @@ -239,12 +239,12 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance } /// @inheritdoc IGovernance - function lqtyToVotes(uint88 _lqtyAmount, uint256 _currentTimestamp, uint32 _averageTimestamp) + function lqtyToVotes(uint88 _lqtyAmount, uint32 _currentTimestamp, uint32 _averageTimestamp) public pure - returns (uint240) + returns (uint120) { - return uint240(_lqtyAmount) * _averageAge(uint32(_currentTimestamp), _averageTimestamp); + return uint120(_lqtyAmount) * uint120(_averageAge(_currentTimestamp, _averageTimestamp)); } /// @inheritdoc IGovernance @@ -287,29 +287,19 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance if (initiativeSnapshot.forEpoch < currentEpoch - 1) { uint256 votingThreshold = calculateVotingThreshold(); uint32 start = epochStart(); - uint240 votes = + uint120 votes = lqtyToVotes(initiativeState.voteLQTY, start, initiativeState.averageStakingTimestampVoteLQTY); - uint240 vetos = + uint120 vetos = lqtyToVotes(initiativeState.vetoLQTY, start, initiativeState.averageStakingTimestampVetoLQTY); // if the votes didn't meet the voting threshold then no votes qualify /// @audit TODO TEST THIS /// The change means that all logic for votes and rewards must be done in `getInitiativeState` - initiativeSnapshot.votes = uint224(votes); /// @audit TODO: We should change this to check the treshold, we should instead use the snapshot to just report all the valid data + initiativeSnapshot.votes = uint120(votes); /// @audit TODO: We should change this to check the treshold, we should instead use the snapshot to just report all the valid data - initiativeSnapshot.vetos = uint224(vetos); /// @audit TODO: Overflow + order of operations + initiativeSnapshot.vetos = uint120(vetos); /// @audit TODO: Overflow + order of operations initiativeSnapshot.forEpoch = currentEpoch - 1; - /// @audit Conditional - /// If we meet the threshold then we increase this - /// TODO: Either simplify, or use this for the state machine as well - if( - initiativeSnapshot.votes > initiativeSnapshot.vetos && - initiativeSnapshot.votes >= votingThreshold - ) { - // initiativeSnapshot.lastCountedEpoch = currentEpoch - 1; /// @audit This updating makes it so that we lose track | TODO: Find a better way - } - votesForInitiativeSnapshot[_initiative] = initiativeSnapshot; emit SnapshotVotesForInitiative(_initiative, initiativeSnapshot.votes, initiativeSnapshot.forEpoch); } @@ -409,7 +399,7 @@ contract Governance is Multicall, UserProxyFactory, ReentrancyGuard, IGovernance // an initiative can be registered if the registrant has more voting power (LQTY * age) // than the registration threshold derived from the previous epoch's total global votes require( - lqtyToVotes(uint88(stakingV1.stakes(userProxyAddress)), block.timestamp, userState.averageStakingTimestamp) + lqtyToVotes(uint88(stakingV1.stakes(userProxyAddress)), uint32(block.timestamp), userState.averageStakingTimestamp) >= snapshot.votes * REGISTRATION_THRESHOLD_FACTOR / WAD, "Governance: insufficient-lqty" ); diff --git a/src/interfaces/IGovernance.sol b/src/interfaces/IGovernance.sol index 146f3da3..51d1b5f3 100644 --- a/src/interfaces/IGovernance.sol +++ b/src/interfaces/IGovernance.sol @@ -9,16 +9,39 @@ import {PermitParams} from "../utils/Types.sol"; interface IGovernance { event DepositLQTY(address user, uint256 depositedLQTY); - event WithdrawLQTY(address user, uint256 withdrawnLQTY, uint256 accruedLUSD, uint256 accruedETH); + event WithdrawLQTY( + address user, + uint256 withdrawnLQTY, + uint256 accruedLUSD, + uint256 accruedETH + ); event SnapshotVotes(uint240 votes, uint16 forEpoch); - event SnapshotVotesForInitiative(address initiative, uint240 votes, uint16 forEpoch); + event SnapshotVotesForInitiative( + address initiative, + uint240 votes, + uint16 forEpoch + ); - event RegisterInitiative(address initiative, address registrant, uint16 atEpoch); + event RegisterInitiative( + address initiative, + address registrant, + uint16 atEpoch + ); event UnregisterInitiative(address initiative, uint16 atEpoch); - event AllocateLQTY(address user, address initiative, int256 deltaVoteLQTY, int256 deltaVetoLQTY, uint16 atEpoch); - event ClaimForInitiative(address initiative, uint256 bold, uint256 forEpoch); + event AllocateLQTY( + address user, + address initiative, + int256 deltaVoteLQTY, + int256 deltaVetoLQTY, + uint16 atEpoch + ); + event ClaimForInitiative( + address initiative, + uint256 bold, + uint256 forEpoch + ); struct Configuration { uint128 registrationFee; @@ -51,7 +74,10 @@ interface IGovernance { function EPOCH_DURATION() external view returns (uint256 epochDuration); /// @notice Voting period of an epoch in seconds (e.g. 6 days) /// @return epochVotingCutoff Epoch voting cutoff - function EPOCH_VOTING_CUTOFF() external view returns (uint256 epochVotingCutoff); + function EPOCH_VOTING_CUTOFF() + external + view + returns (uint256 epochVotingCutoff); /// @notice Minimum BOLD amount that has to be claimed, if an initiative doesn't have enough votes to meet the /// criteria then it's votes a excluded from the vote count and distribution /// @return minClaim Minimum claim amount @@ -65,49 +91,64 @@ interface IGovernance { function REGISTRATION_FEE() external view returns (uint256 registrationFee); /// @notice Share of all votes that are necessary to register a new initiative /// @return registrationThresholdFactor Threshold factor - function REGISTRATION_THRESHOLD_FACTOR() external view returns (uint256 registrationThresholdFactor); + function REGISTRATION_THRESHOLD_FACTOR() + external + view + returns (uint256 registrationThresholdFactor); /// @notice Multiple of the voting threshold in vetos that are necessary to unregister an initiative /// @return unregistrationThresholdFactor Unregistration threshold factor - function UNREGISTRATION_THRESHOLD_FACTOR() external view returns (uint256 unregistrationThresholdFactor); + function UNREGISTRATION_THRESHOLD_FACTOR() + external + view + returns (uint256 unregistrationThresholdFactor); /// @notice Number of epochs an initiative has to exist before it can be unregistered /// @return registrationWarmUpPeriod Number of epochs - function REGISTRATION_WARM_UP_PERIOD() external view returns (uint256 registrationWarmUpPeriod); + function REGISTRATION_WARM_UP_PERIOD() + external + view + returns (uint256 registrationWarmUpPeriod); /// @notice Number of epochs an initiative has to be inactive before it can be unregistered /// @return unregistrationAfterEpochs Number of epochs - function UNREGISTRATION_AFTER_EPOCHS() external view returns (uint256 unregistrationAfterEpochs); + function UNREGISTRATION_AFTER_EPOCHS() + external + view + returns (uint256 unregistrationAfterEpochs); /// @notice Share of all votes that are necessary for an initiative to be included in the vote count /// @return votingThresholdFactor Voting threshold factor - function VOTING_THRESHOLD_FACTOR() external view returns (uint256 votingThresholdFactor); + function VOTING_THRESHOLD_FACTOR() + external + view + returns (uint256 votingThresholdFactor); /// @notice Returns the amount of BOLD accrued since last epoch (last snapshot) /// @return boldAccrued BOLD accrued function boldAccrued() external view returns (uint256 boldAccrued); struct VoteSnapshot { - uint240 votes; // Votes at epoch transition + uint120 votes; // Votes at epoch transition uint16 forEpoch; // Epoch for which the votes are counted } struct InitiativeVoteSnapshot { - uint224 votes; // Votes at epoch transition + uint120 votes; // Votes at epoch transition uint16 forEpoch; // Epoch for which the votes are counted - uint16 lastCountedEpoch; // Epoch at which which the votes where counted last in the global snapshot - uint224 vetos; // Vetos at epoch transition + uint120 vetos; // Vetos at epoch transition } /// @notice Returns the vote count snapshot of the previous epoch /// @return votes Number of votes /// @return forEpoch Epoch for which the votes are counted - function votesSnapshot() external view returns (uint240 votes, uint16 forEpoch); + function votesSnapshot() + external + view + returns (uint120 votes, uint16 forEpoch); /// @notice Returns the vote count snapshot for an initiative of the previous epoch /// @param _initiative Address of the initiative /// @return votes Number of votes /// @return forEpoch Epoch for which the votes are counted - /// @return lastCountedEpoch Epoch at which which the votes where counted last in the global snapshot - function votesForInitiativeSnapshot(address _initiative) - external - view - returns (uint224 votes, uint16 forEpoch, uint16 lastCountedEpoch, uint224 vetos); + function votesForInitiativeSnapshot( + address _initiative + ) external view returns (uint120 votes, uint16 forEpoch, uint120 vetos); struct Allocation { uint88 voteLQTY; // LQTY allocated vouching for the initiative @@ -137,7 +178,12 @@ interface IGovernance { /// @param _user Address of the user /// @return allocatedLQTY LQTY allocated by the user /// @return averageStakingTimestamp Average timestamp at which LQTY was staked (deposited) by the user - function userStates(address _user) external view returns (uint88 allocatedLQTY, uint32 averageStakingTimestamp); + function userStates( + address _user + ) + external + view + returns (uint88 allocatedLQTY, uint32 averageStakingTimestamp); /// @notice Returns the initiative's state /// @param _initiative Address of the initiative /// @return voteLQTY LQTY allocated vouching for the initiative @@ -145,7 +191,9 @@ interface IGovernance { /// @return averageStakingTimestampVoteLQTY // Average staking timestamp of the voting LQTY for the initiative /// @return averageStakingTimestampVetoLQTY // Average staking timestamp of the vetoing LQTY for the initiative /// @return lastEpochClaim // Last epoch at which rewards were claimed - function initiativeStates(address _initiative) + function initiativeStates( + address _initiative + ) external view returns ( @@ -158,22 +206,30 @@ interface IGovernance { /// @notice Returns the global state /// @return countedVoteLQTY Total LQTY that is included in vote counting /// @return countedVoteLQTYAverageTimestamp Average timestamp: derived initiativeAllocation.averageTimestamp - function globalState() external view returns (uint88 countedVoteLQTY, uint32 countedVoteLQTYAverageTimestamp); + function globalState() + external + view + returns ( + uint88 countedVoteLQTY, + uint32 countedVoteLQTYAverageTimestamp + ); /// @notice Returns the amount of voting and vetoing LQTY a user allocated to an initiative /// @param _user Address of the user /// @param _initiative Address of the initiative /// @return voteLQTY LQTY allocated vouching for the initiative /// @return vetoLQTY LQTY allocated vetoing the initiative /// @return atEpoch Epoch at which the allocation was last updated - function lqtyAllocatedByUserToInitiative(address _user, address _initiative) - external - view - returns (uint88 voteLQTY, uint88 vetoLQTY, uint16 atEpoch); + function lqtyAllocatedByUserToInitiative( + address _user, + address _initiative + ) external view returns (uint88 voteLQTY, uint88 vetoLQTY, uint16 atEpoch); /// @notice Returns when an initiative was registered /// @param _initiative Address of the initiative /// @return atEpoch Epoch at which the initiative was registered - function registeredInitiatives(address _initiative) external view returns (uint16 atEpoch); + function registeredInitiatives( + address _initiative + ) external view returns (uint16 atEpoch); /*////////////////////////////////////////////////////////////// STAKING @@ -186,7 +242,10 @@ interface IGovernance { /// @notice Deposits LQTY via Permit /// @param _lqtyAmount Amount of LQTY to deposit /// @param _permitParams Permit parameters - function depositLQTYViaPermit(uint88 _lqtyAmount, PermitParams memory _permitParams) external; + function depositLQTYViaPermit( + uint88 _lqtyAmount, + PermitParams memory _permitParams + ) external; /// @notice Withdraws LQTY and claims any accrued LUSD and ETH rewards from StakingV1 /// @param _lqtyAmount Amount of LQTY to withdraw function withdrawLQTY(uint88 _lqtyAmount) external; @@ -194,7 +253,9 @@ interface IGovernance { /// @param _rewardRecipient Address that will receive the rewards /// @return accruedLUSD Amount of LUSD accrued /// @return accruedETH Amount of ETH accrued - function claimFromStakingV1(address _rewardRecipient) external returns (uint256 accruedLUSD, uint256 accruedETH); + function claimFromStakingV1( + address _rewardRecipient + ) external returns (uint256 accruedLUSD, uint256 accruedETH); /*////////////////////////////////////////////////////////////// VOTING @@ -208,30 +269,42 @@ interface IGovernance { function epochStart() external view returns (uint32 epochStart); /// @notice Returns the number of seconds that have gone by since the current epoch started /// @return secondsWithinEpoch Seconds within the current epoch - function secondsWithinEpoch() external view returns (uint32 secondsWithinEpoch); + function secondsWithinEpoch() + external + view + returns (uint32 secondsWithinEpoch); /// @notice Returns the number of votes per LQTY for a user /// @param _lqtyAmount Amount of LQTY to convert to votes /// @param _currentTimestamp Current timestamp /// @param _averageTimestamp Average timestamp at which the LQTY was staked /// @return votes Number of votes - function lqtyToVotes(uint88 _lqtyAmount, uint256 _currentTimestamp, uint32 _averageTimestamp) - external - pure - returns (uint240); + function lqtyToVotes( + uint88 _lqtyAmount, + uint32 _currentTimestamp, + uint32 _averageTimestamp + ) external pure returns (uint120); /// @notice Voting threshold is the max. of either: /// - 4% of the total voting LQTY in the previous epoch /// - or the minimum number of votes necessary to claim at least MIN_CLAIM BOLD /// @return votingThreshold Voting threshold - function calculateVotingThreshold() external view returns (uint256 votingThreshold); + function calculateVotingThreshold() + external + view + returns (uint256 votingThreshold); /// @notice Snapshots votes for the previous epoch and accrues funds for the current epoch /// @param _initiative Address of the initiative /// @return voteSnapshot Vote snapshot /// @return initiativeVoteSnapshot Vote snapshot of the initiative - function snapshotVotesForInitiative(address _initiative) + function snapshotVotesForInitiative( + address _initiative + ) external - returns (VoteSnapshot memory voteSnapshot, InitiativeVoteSnapshot memory initiativeVoteSnapshot); + returns ( + VoteSnapshot memory voteSnapshot, + InitiativeVoteSnapshot memory initiativeVoteSnapshot + ); /// @notice Registers a new initiative /// @param _initiative Address of the initiative @@ -247,11 +320,16 @@ interface IGovernance { /// @param _initiatives Addresses of the initiatives to allocate to /// @param _deltaLQTYVotes Delta LQTY to allocate to the initiatives as votes /// @param _deltaLQTYVetos Delta LQTY to allocate to the initiatives as vetos - function allocateLQTY(address[] memory _initiatives, int88[] memory _deltaLQTYVotes, int88[] memory _deltaLQTYVetos) - external; + function allocateLQTY( + address[] memory _initiatives, + int88[] memory _deltaLQTYVotes, + int88[] memory _deltaLQTYVetos + ) external; /// @notice Splits accrued funds according to votes received between all initiatives /// @param _initiative Addresse of the initiative /// @return claimed Amount of BOLD claimed - function claimForInitiative(address _initiative) external returns (uint256 claimed); + function claimForInitiative( + address _initiative + ) external returns (uint256 claimed); } diff --git a/test/Governance.t.sol b/test/Governance.t.sol index 84846c8f..58bff4ac 100644 --- a/test/Governance.t.sol +++ b/test/Governance.t.sol @@ -28,7 +28,10 @@ contract GovernanceInternal is Governance { address[] memory _initiatives ) Governance(_lqty, _lusd, _stakingV1, _bold, _config, _initiatives) {} - function averageAge(uint32 _currentTimestamp, uint32 _averageTimestamp) external pure returns (uint32) { + function averageAge( + uint32 _currentTimestamp, + uint32 _averageTimestamp + ) external pure returns (uint32) { return _averageAge(_currentTimestamp, _averageTimestamp); } @@ -38,19 +41,29 @@ contract GovernanceInternal is Governance { uint88 _prevLQTYBalance, uint88 _newLQTYBalance ) external view returns (uint32) { - return _calculateAverageTimestamp( - _prevOuterAverageTimestamp, _newInnerAverageTimestamp, _prevLQTYBalance, _newLQTYBalance - ); + return + _calculateAverageTimestamp( + _prevOuterAverageTimestamp, + _newInnerAverageTimestamp, + _prevLQTYBalance, + _newLQTYBalance + ); } } contract GovernanceTest is Test { - IERC20 private constant lqty = IERC20(address(0x6DEA81C8171D0bA574754EF6F8b412F2Ed88c54D)); - IERC20 private constant lusd = IERC20(address(0x5f98805A4E8be255a32880FDeC7F6728C6568bA0)); - address private constant stakingV1 = address(0x4f9Fbb3f1E99B56e0Fe2892e623Ed36A76Fc605d); - address private constant user = address(0xF977814e90dA44bFA03b6295A0616a897441aceC); - address private constant user2 = address(0x10C9cff3c4Faa8A60cB8506a7A99411E6A199038); - address private constant lusdHolder = address(0xcA7f01403C4989d2b1A9335A2F09dD973709957c); + IERC20 private constant lqty = + IERC20(address(0x6DEA81C8171D0bA574754EF6F8b412F2Ed88c54D)); + IERC20 private constant lusd = + IERC20(address(0x5f98805A4E8be255a32880FDeC7F6728C6568bA0)); + address private constant stakingV1 = + address(0x4f9Fbb3f1E99B56e0Fe2892e623Ed36A76Fc605d); + address private constant user = + address(0xF977814e90dA44bFA03b6295A0616a897441aceC); + address private constant user2 = + address(0x10C9cff3c4Faa8A60cB8506a7A99411E6A199038); + address private constant lusdHolder = + address(0xcA7f01403C4989d2b1A9335A2F09dD973709957c); uint128 private constant REGISTRATION_FEE = 1e18; uint128 private constant REGISTRATION_THRESHOLD_FACTOR = 0.01e18; @@ -76,7 +89,12 @@ contract GovernanceTest is Test { baseInitiative1 = address( new BribeInitiative( - address(vm.computeCreateAddress(address(this), vm.getNonce(address(this)) + 3)), + address( + vm.computeCreateAddress( + address(this), + vm.getNonce(address(this)) + 3 + ) + ), address(lusd), address(lqty) ) @@ -84,7 +102,12 @@ contract GovernanceTest is Test { baseInitiative2 = address( new BribeInitiative( - address(vm.computeCreateAddress(address(this), vm.getNonce(address(this)) + 2)), + address( + vm.computeCreateAddress( + address(this), + vm.getNonce(address(this)) + 2 + ) + ), address(lusd), address(lqty) ) @@ -92,7 +115,12 @@ contract GovernanceTest is Test { baseInitiative3 = address( new BribeInitiative( - address(vm.computeCreateAddress(address(this), vm.getNonce(address(this)) + 1)), + address( + vm.computeCreateAddress( + address(this), + vm.getNonce(address(this)) + 1 + ) + ), address(lusd), address(lqty) ) @@ -145,8 +173,14 @@ contract GovernanceTest is Test { } // should not revert under any input - function test_averageAge(uint32 _currentTimestamp, uint32 _timestamp) public { - uint32 averageAge = governanceInternal.averageAge(_currentTimestamp, _timestamp); + function test_averageAge( + uint32 _currentTimestamp, + uint32 _timestamp + ) public { + uint32 averageAge = governanceInternal.averageAge( + _currentTimestamp, + _timestamp + ); if (_timestamp == 0 || _currentTimestamp < _timestamp) { assertEq(averageAge, 0); } else { @@ -161,12 +195,16 @@ contract GovernanceTest is Test { uint88 _prevLQTYBalance, uint88 _newLQTYBalance ) public { - uint32 highestTimestamp = (_prevOuterAverageTimestamp > _newInnerAverageTimestamp) + uint32 highestTimestamp = (_prevOuterAverageTimestamp > + _newInnerAverageTimestamp) ? _prevOuterAverageTimestamp : _newInnerAverageTimestamp; if (highestTimestamp > block.timestamp) vm.warp(highestTimestamp); governanceInternal.calculateAverageTimestamp( - _prevOuterAverageTimestamp, _newInnerAverageTimestamp, _prevLQTYBalance, _newLQTYBalance + _prevOuterAverageTimestamp, + _newInnerAverageTimestamp, + _prevLQTYBalance, + _newLQTYBalance ); } @@ -176,26 +214,93 @@ contract GovernanceTest is Test { vm.startPrank(user); + // should not revert if the user doesn't have a UserProxy deployed yet + address userProxy = governance.deriveUserProxyAddress(user); + lqty.approve(address(userProxy), 1e18); + // vm.expectEmit("DepositLQTY", abi.encode(user, 1e18)); + // deploy and deposit 1 LQTY + governance.depositLQTY(1e18); + assertEq(UserProxy(payable(userProxy)).staked(), 1e18); + (uint88 allocatedLQTY, uint32 averageStakingTimestamp) = governance + .userStates(user); + assertEq(allocatedLQTY, 0); + // first deposit should have an averageStakingTimestamp if block.timestamp + assertEq(averageStakingTimestamp, block.timestamp); + + vm.warp(block.timestamp + timeIncrease); + + lqty.approve(address(userProxy), 1e18); + governance.depositLQTY(1e18); + assertEq(UserProxy(payable(userProxy)).staked(), 2e18); + (allocatedLQTY, averageStakingTimestamp) = governance.userStates(user); + assertEq(allocatedLQTY, 0); + // subsequent deposits should have a stake weighted average + assertEq(averageStakingTimestamp, block.timestamp - timeIncrease / 2); + + // withdraw 0.5 half of LQTY + vm.warp(block.timestamp + timeIncrease); + + vm.startPrank(user); + + governance.withdrawLQTY(1e18); + assertEq(UserProxy(payable(userProxy)).staked(), 1e18); + (allocatedLQTY, averageStakingTimestamp) = governance.userStates(user); + assertEq(allocatedLQTY, 0); + assertEq( + averageStakingTimestamp, + (block.timestamp - timeIncrease) - timeIncrease / 2 + ); + + // withdraw remaining LQTY + governance.withdrawLQTY(1e18); + assertEq(UserProxy(payable(userProxy)).staked(), 0); + (allocatedLQTY, averageStakingTimestamp) = governance.userStates(user); + assertEq(allocatedLQTY, 0); + assertEq( + averageStakingTimestamp, + (block.timestamp - timeIncrease) - timeIncrease / 2 + ); + + vm.stopPrank(); + } + + function test_depositLQTY_withdrawLQTY_reverts_zero_deposit() public { + uint256 timeIncrease = 86400 * 30; + vm.warp(block.timestamp + timeIncrease); + + vm.startPrank(user); + // should revert with a 0 amount vm.expectRevert("Governance: zero-lqty-amount"); governance.depositLQTY(0); + } + + function test_depositLQTY_withdrawLQTY_reverts_depositing_more_than_allowance() public { + uint256 timeIncrease = 86400 * 30; + vm.warp(block.timestamp + timeIncrease); + + vm.startPrank(user); // should revert if the `_lqtyAmount` > `lqty.allowance(msg.sender, userProxy)` vm.expectRevert("ERC20: transfer amount exceeds allowance"); governance.depositLQTY(1e18); + } - // should revert if the `_lqtyAmount` > `lqty.balanceOf(msg.sender)` - vm.expectRevert("ERC20: transfer amount exceeds balance"); - governance.depositLQTY(type(uint88).max); + function test_depositLQTY_withdrawLQTY_reverts_withdrawing_no_proxy() public { + uint256 timeIncrease = 86400 * 30; + vm.warp(block.timestamp + timeIncrease); + + vm.startPrank(user); // should not revert if the user doesn't have a UserProxy deployed yet address userProxy = governance.deriveUserProxyAddress(user); lqty.approve(address(userProxy), 1e18); - // vm.expectEmit("DepositLQTY", abi.encode(user, 1e18)); + // deploy and deposit 1 LQTY governance.depositLQTY(1e18); assertEq(UserProxy(payable(userProxy)).staked(), 1e18); - (uint88 allocatedLQTY, uint32 averageStakingTimestamp) = governance.userStates(user); + (uint88 allocatedLQTY, uint32 averageStakingTimestamp) = governance + .userStates(user); assertEq(allocatedLQTY, 0); // first deposit should have an averageStakingTimestamp if block.timestamp assertEq(averageStakingTimestamp, block.timestamp); @@ -217,6 +322,39 @@ contract GovernanceTest is Test { vm.expectRevert("Governance: user-proxy-not-deployed"); governance.withdrawLQTY(1e18); vm.stopPrank(); + } + + function test_depositLQTY_withdrawLQTY_reverts_withdrawing_more_than_deposited() public { + uint256 timeIncrease = 86400 * 30; + vm.warp(block.timestamp + timeIncrease); + + vm.startPrank(user); + + // should not revert if the user doesn't have a UserProxy deployed yet + address userProxy = governance.deriveUserProxyAddress(user); + lqty.approve(address(userProxy), 1e18); + // vm.expectEmit("DepositLQTY", abi.encode(user, 1e18)); + // deploy and deposit 1 LQTY + governance.depositLQTY(1e18); + assertEq(UserProxy(payable(userProxy)).staked(), 1e18); + (uint88 allocatedLQTY, uint32 averageStakingTimestamp) = governance + .userStates(user); + assertEq(allocatedLQTY, 0); + // first deposit should have an averageStakingTimestamp if block.timestamp + assertEq(averageStakingTimestamp, block.timestamp); + + vm.warp(block.timestamp + timeIncrease); + + lqty.approve(address(userProxy), 1e18); + governance.depositLQTY(1e18); + assertEq(UserProxy(payable(userProxy)).staked(), 2e18); + (allocatedLQTY, averageStakingTimestamp) = governance.userStates(user); + assertEq(allocatedLQTY, 0); + // subsequent deposits should have a stake weighted average + assertEq(averageStakingTimestamp, block.timestamp - timeIncrease / 2); + + // withdraw 0.5 half of LQTY + vm.warp(block.timestamp + timeIncrease); vm.startPrank(user); @@ -227,14 +365,20 @@ contract GovernanceTest is Test { assertEq(UserProxy(payable(userProxy)).staked(), 1e18); (allocatedLQTY, averageStakingTimestamp) = governance.userStates(user); assertEq(allocatedLQTY, 0); - assertEq(averageStakingTimestamp, (block.timestamp - timeIncrease) - timeIncrease / 2); + assertEq( + averageStakingTimestamp, + (block.timestamp - timeIncrease) - timeIncrease / 2 + ); // withdraw remaining LQTY governance.withdrawLQTY(1e18); assertEq(UserProxy(payable(userProxy)).staked(), 0); (allocatedLQTY, averageStakingTimestamp) = governance.userStates(user); assertEq(allocatedLQTY, 0); - assertEq(averageStakingTimestamp, (block.timestamp - timeIncrease) - timeIncrease / 2); + assertEq( + averageStakingTimestamp, + (block.timestamp - timeIncrease) - timeIncrease / 2 + ); vm.stopPrank(); } @@ -244,7 +388,9 @@ contract GovernanceTest is Test { vm.warp(block.timestamp + timeIncrease); vm.startPrank(user); - VmSafe.Wallet memory wallet = vm.createWallet(uint256(keccak256(bytes("1")))); + VmSafe.Wallet memory wallet = vm.createWallet( + uint256(keccak256(bytes("1"))) + ); lqty.transfer(wallet.addr, 1e18); vm.stopPrank(); vm.startPrank(wallet.addr); @@ -303,9 +449,80 @@ contract GovernanceTest is Test { // deploy and deposit 1 LQTY governance.depositLQTYViaPermit(1e18, permitParams); assertEq(UserProxy(payable(userProxy)).staked(), 1e18); - (uint88 allocatedLQTY, uint32 averageStakingTimestamp) = governance.userStates(wallet.addr); + (uint88 allocatedLQTY, uint32 averageStakingTimestamp) = governance + .userStates(wallet.addr); + assertEq(allocatedLQTY, 0); + assertEq(averageStakingTimestamp, block.timestamp); + } + + function test_depositLQTYViaPermit_withdrawLQTY() public { + uint256 timeIncrease = 86400 * 30; + vm.warp(block.timestamp + timeIncrease); + + vm.startPrank(user); + VmSafe.Wallet memory wallet = vm.createWallet( + uint256(keccak256(bytes("1"))) + ); + lqty.transfer(wallet.addr, 1e18); + vm.stopPrank(); + vm.startPrank(wallet.addr); + + // check address + address userProxy = governance.deriveUserProxyAddress(wallet.addr); + + PermitParams memory permitParams = PermitParams({ + owner: wallet.addr, + spender: address(userProxy), + value: 1e18, + deadline: block.timestamp + 86400, + v: 0, + r: "", + s: "" + }); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign( + wallet.privateKey, + keccak256( + abi.encodePacked( + "\x19\x01", + ILQTY(address(lqty)).domainSeparator(), + keccak256( + abi.encode( + 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9, + permitParams.owner, + permitParams.spender, + permitParams.value, + 0, + permitParams.deadline + ) + ) + ) + ) + ); + + permitParams.v = v; + permitParams.r = r; + permitParams.s = s; + + vm.startPrank(wallet.addr); + + // deploy and deposit 1 LQTY + governance.depositLQTYViaPermit(1e18, permitParams); + assertEq(UserProxy(payable(userProxy)).staked(), 1e18); + (uint88 allocatedLQTY, uint32 averageStakingTimestamp) = governance + .userStates(wallet.addr); assertEq(allocatedLQTY, 0); assertEq(averageStakingTimestamp, block.timestamp); + + // owner withdraws their LQTY + uint256 lqtyBalanceBefore = lqty.balanceOf(wallet.addr); + governance.withdrawLQTY(1e18); + uint256 lqtyBalanceAfter = lqty.balanceOf(wallet.addr); + + assertEq(UserProxy(payable(userProxy)).staked(), 0); + assertEq(lqtyBalanceAfter - lqtyBalanceBefore, 1e18, "owner does not receive lqty"); + + vm.stopPrank(); } function test_claimFromStakingV1() public { @@ -331,6 +548,14 @@ contract GovernanceTest is Test { assertEq(UserProxy(payable(userProxy)).staked(), 1e18); } + function test_claimFromStakingV1_reverts_proxy_not_deployed() public { + uint256 timeIncrease = 86400 * 30; + vm.warp(block.timestamp + timeIncrease); + + vm.expectRevert("Governance: user-proxy-not-deployed"); + governance.claimFromStakingV1(address(this)); + } + // should return the correct epoch for a given block.timestamp function test_epoch() public { assertEq(governance.epoch(), 1); @@ -358,6 +583,23 @@ contract GovernanceTest is Test { assertEq(governance.epochStart(), block.timestamp - 1); } + function test_epochStart_other_epochs() public { + // check that initial epoch timestamp is correct + assertEq(governance.epochStart(), block.timestamp, "first epoch start check is incorrect"); + + // warp to the next epoch + vm.warp(block.timestamp + governance.EPOCH_DURATION()); + + // block.timestamp + EPOCH_DURATION EPOCH_DURATION block.timestamp + EPOCH_DURATION + // | epoch 1 | + uint32 initialEpochStart = governance.epochStart(); + assertEq(initialEpochStart, block.timestamp, "second epoch start check is incorrect"); // block.timestamp is currently at the start of epoch 1 + + // warp to the end of this epoch + vm.warp(block.timestamp + governance.EPOCH_DURATION() - 1); + assertEq(governance.epochStart(), initialEpochStart, "third epoch start check is incorrect"); // block.timestamp is currently at the start of epoch 1 + } + // should not revert under any block.timestamp >= EPOCH_START function test_epochStart_fuzz(uint32 _timestamp) public { vm.warp(_timestamp); @@ -384,10 +626,19 @@ contract GovernanceTest is Test { } // should not revert under any input - function test_lqtyToVotes(uint88 _lqtyAmount, uint256 _currentTimestamp, uint32 _averageTimestamp) public { - governance.lqtyToVotes(_lqtyAmount, _currentTimestamp, _averageTimestamp); + function test_lqtyToVotes( + uint88 _lqtyAmount, + uint32 _currentTimestamp, + uint32 _averageTimestamp + ) public { + governance.lqtyToVotes( + _lqtyAmount, + _currentTimestamp, + _averageTimestamp + ); } + // check that votingThreshold is is high enough such that MIN_CLAIM is met function test_calculateVotingThreshold() public { governance = new Governance( address(lqty), @@ -411,26 +662,40 @@ contract GovernanceTest is Test { ); // is 0 when the previous epochs votes are 0 - assertEq(governance.calculateVotingThreshold(), 0); + assertEq(governance.calculateVotingThreshold(), 0, "threshold"); + + // 1. user stakes liquity + uint88 lqtyAmount = 1e18; + _stakeLQTY(user, lqtyAmount); + + // 2. user allocates in epoch 1 for initiative to be active + vm.warp(block.timestamp + EPOCH_DURATION); // warp to first epoch + + _allocateLQTY(user, lqtyAmount); + + // 3. warp to second epoch and snapshot + vm.warp(block.timestamp + EPOCH_DURATION); + governance.snapshotVotesForInitiative(baseInitiative1); + + (uint120 votes, uint16 forEpoch) = governance.votesSnapshot(); + assertGt(votes, 0, "no votes allocated for the epoch"); + assertEq(2, forEpoch, "expected forEpoch is incorrect"); - // check that votingThreshold is is high enough such that MIN_CLAIM is met - IGovernance.VoteSnapshot memory snapshot = IGovernance.VoteSnapshot(1e18, 1); + uint256 boldAccrued = 1000e18; vm.store( address(governance), - bytes32(uint256(2)), - bytes32(abi.encodePacked(uint16(snapshot.forEpoch), uint240(snapshot.votes))) + bytes32(uint256(1)), + bytes32(abi.encode(boldAccrued)) ); - (uint240 votes, uint16 forEpoch) = governance.votesSnapshot(); - assertEq(votes, 1e18); - assertEq(forEpoch, 1); - - uint256 boldAccrued = 1000e18; - vm.store(address(governance), bytes32(uint256(1)), bytes32(abi.encode(boldAccrued))); - assertEq(governance.boldAccrued(), 1000e18); + assertEq(governance.boldAccrued(), 1000e18, "bold accrue"); - assertEq(governance.calculateVotingThreshold(), MIN_CLAIM / 1000); + assertGe(governance.calculateVotingThreshold(), MIN_CLAIM / 1000); + } - // check that votingThreshold is 4% of votes of previous epoch + // check that votingThreshold is 4% of votes of previous epoch + function test_calculateVotingThreshold_percentage_of_previous_epoch() + public + { governance = new Governance( address(lqty), address(lusd), @@ -452,26 +717,41 @@ contract GovernanceTest is Test { initialInitiatives ); - snapshot = IGovernance.VoteSnapshot(10000e18, 1); + // 1. user stakes liquity + + uint88 lqtyAmount = 10000e18; + _stakeLQTY(user, lqtyAmount); + + // 2. user allocates for an epoch + vm.warp(block.timestamp + EPOCH_DURATION); // warp to first epoch + + _allocateLQTY(user, lqtyAmount); + + // 3. warp to second epoch and snapshot + vm.warp(block.timestamp + EPOCH_DURATION); + + governance.snapshotVotesForInitiative(baseInitiative1); + (uint120 votes, uint16 forEpoch) = governance.votesSnapshot(); + + uint256 boldAccrued = 1000e18; vm.store( address(governance), - bytes32(uint256(2)), - bytes32(abi.encodePacked(uint16(snapshot.forEpoch), uint240(snapshot.votes))) + bytes32(uint256(1)), + bytes32(abi.encode(boldAccrued)) ); - (votes, forEpoch) = governance.votesSnapshot(); - assertEq(votes, 10000e18); - assertEq(forEpoch, 1); + assertEq(governance.boldAccrued(), 1000e18, "bold accrued"); - boldAccrued = 1000e18; - vm.store(address(governance), bytes32(uint256(1)), bytes32(abi.encode(boldAccrued))); - assertEq(governance.boldAccrued(), 1000e18); - - assertEq(governance.calculateVotingThreshold(), 10000e18 * 0.04); + assertEq( + governance.calculateVotingThreshold(), + 10000e18 * 0.04, + "voting threshold not correct" + ); } // should not revert under any state function test_calculateVotingThreshold_fuzz( - uint128 _votes, + uint88 _lqtyAmount, + uint120 _votes, uint16 _forEpoch, uint88 _boldAccrued, uint128 _votingThresholdFactor, @@ -499,63 +779,130 @@ contract GovernanceTest is Test { initialInitiatives ); - IGovernance.VoteSnapshot memory snapshot = IGovernance.VoteSnapshot(_votes, _forEpoch); + // NOTE: replacing the previous implementation with depositing and allocating for more realistic flow + // will need to clamp values to ensure no reverts because of input validation + + // 1. user stakes liquity + _lqtyAmount = uint88( + bound(uint256(_lqtyAmount), 1, lqty.balanceOf(user)) + ); + _stakeLQTY(user, _lqtyAmount); + + // 2. user allocates for an epoch + vm.warp(block.timestamp + EPOCH_DURATION); + + _allocateLQTY(user, _lqtyAmount); + + // 3. warp to next epoch and snapshot + vm.warp(block.timestamp + EPOCH_DURATION); + + (uint120 votes, uint16 forEpoch) = governance.votesSnapshot(); + + // 3. bold is donated to the governance contract to simulate accrual vm.store( address(governance), - bytes32(uint256(2)), - bytes32(abi.encodePacked(uint16(snapshot.forEpoch), uint240(snapshot.votes))) + bytes32(uint256(1)), + bytes32(abi.encode(_boldAccrued)) + ); + assertEq( + governance.boldAccrued(), + _boldAccrued, + "boldAccrued is inconsistent" ); - (uint240 votes, uint16 forEpoch) = governance.votesSnapshot(); - assertEq(votes, _votes); - assertEq(forEpoch, _forEpoch); - - vm.store(address(governance), bytes32(uint256(1)), bytes32(abi.encode(_boldAccrued))); - assertEq(governance.boldAccrued(), _boldAccrued); governance.calculateVotingThreshold(); } function test_registerInitiative() public { - vm.startPrank(user); + vm.prank(lusdHolder); + lusd.transfer(user, 2e18); + vm.startPrank(user); address userProxy = governance.deployUserProxy(); - IGovernance.VoteSnapshot memory snapshot = IGovernance.VoteSnapshot(1e18, 1); - vm.store( - address(governance), - bytes32(uint256(2)), - bytes32(abi.encodePacked(uint16(snapshot.forEpoch), uint240(snapshot.votes))) - ); - (uint240 votes,) = governance.votesSnapshot(); - assertEq(votes, 1e18); + lusd.approve(address(governance), 2e18); + + lqty.approve(address(userProxy), 1e18); + governance.depositLQTY(1e18); + + vm.warp(block.timestamp + 365 days); + + governance.registerInitiative(baseInitiative3); + uint16 atEpoch = governance.registeredInitiatives(baseInitiative3); + assertEq(atEpoch, governance.epoch()); + + vm.stopPrank(); + } + + function test_registerInitiative_reverts_insufficient_fee_balance() public { + vm.startPrank(user); + + address userProxy = governance.deployUserProxy(); // should revert if the `REGISTRATION_FEE` > `lqty.balanceOf(msg.sender)` vm.expectRevert("ERC20: transfer amount exceeds balance"); governance.registerInitiative(baseInitiative3); + } - vm.startPrank(lusdHolder); + function test_registerInitiative_reverts_not_enough_voting_power() public { + vm.prank(user); + address userProxy = governance.deployUserProxy(); + + vm.prank(lusdHolder); lusd.transfer(user, 2e18); - vm.stopPrank(); - vm.startPrank(user); + _stakeLQTY(user2, 2e18); + // warp to epoch 1 + vm.warp(block.timestamp + EPOCH_DURATION); + + _allocateLQTY(user2, 2e18); + + vm.startPrank(user); lusd.approve(address(governance), 2e18); + // warp to epoch 2 + vm.warp(block.timestamp + EPOCH_DURATION); + // should revert if the registrant doesn't have enough voting power vm.expectRevert("Governance: insufficient-lqty"); governance.registerInitiative(baseInitiative3); + } - // should revert if the `REGISTRATION_FEE` > `lqty.allowance(msg.sender, governance)` - vm.expectRevert("ERC20: transfer amount exceeds allowance"); - governance.depositLQTY(1e18); + function test_registerInitiative_reverts_zero_address() public { + vm.prank(user); + address userProxy = governance.deployUserProxy(); + + vm.prank(lusdHolder); + lusd.transfer(user, 2e18); + + vm.startPrank(user); + lusd.approve(address(governance), 2e18); lqty.approve(address(userProxy), 1e18); governance.depositLQTY(1e18); + vm.warp(block.timestamp + 365 days); // should revert if `_initiative` is zero vm.expectRevert("Governance: zero-address"); governance.registerInitiative(address(0)); + } + + function test_registerInitiative_reverts_already_registered() public { + vm.prank(user); + address userProxy = governance.deployUserProxy(); + + vm.prank(lusdHolder); + lusd.transfer(user, 2e18); + + vm.startPrank(user); + lusd.approve(address(governance), 2e18); + + lqty.approve(address(userProxy), 1e18); + governance.depositLQTY(1e18); + + vm.warp(block.timestamp + 365 days); governance.registerInitiative(baseInitiative3); uint16 atEpoch = governance.registeredInitiatives(baseInitiative3); @@ -568,39 +915,59 @@ contract GovernanceTest is Test { vm.stopPrank(); } - // TODO: Broken: Fix it by simplifying most likely function test_unregisterInitiative() public { - vm.startPrank(user); + vm.prank(lusdHolder); + lusd.transfer(user, 1e18); + vm.startPrank(user); address userProxy = governance.deployUserProxy(); - IGovernance.VoteSnapshot memory snapshot = IGovernance.VoteSnapshot(1e18, 1); - vm.store( - address(governance), - bytes32(uint256(2)), - bytes32(abi.encodePacked(uint16(snapshot.forEpoch), uint240(snapshot.votes))) - ); - (uint240 votes, uint16 forEpoch) = governance.votesSnapshot(); - assertEq(votes, 1e18); - assertEq(forEpoch, 1); + lusd.approve(address(governance), 1e18); + lqty.approve(address(userProxy), 1e18); + governance.depositLQTY(1e18); + vm.warp(block.timestamp + 365 days); + + governance.registerInitiative(baseInitiative3); + uint16 atEpoch = governance.registeredInitiatives(baseInitiative3); + assertEq(atEpoch, governance.epoch()); + + vm.warp(block.timestamp + 365 days); + + governance.unregisterInitiative(baseInitiative3); vm.stopPrank(); + } - vm.startPrank(lusdHolder); + function test_unregisterInitiative_reverts_not_registered() public { + vm.prank(lusdHolder); lusd.transfer(user, 1e18); - vm.stopPrank(); vm.startPrank(user); + address userProxy = governance.deployUserProxy(); lusd.approve(address(governance), 1e18); lqty.approve(address(userProxy), 1e18); governance.depositLQTY(1e18); + vm.warp(block.timestamp + 365 days); // should revert if the initiative isn't registered vm.expectRevert("Governance: initiative-not-registered"); governance.unregisterInitiative(baseInitiative3); - + } + + function test_unregisterInitiative_reverts_in_warm_up() public { + vm.prank(lusdHolder); + lusd.transfer(user, 1e18); + + vm.startPrank(user); + address userProxy = governance.deployUserProxy(); + + lusd.approve(address(governance), 1e18); + lqty.approve(address(userProxy), 1e18); + governance.depositLQTY(1e18); + vm.warp(block.timestamp + 365 days); + governance.registerInitiative(baseInitiative3); uint16 atEpoch = governance.registeredInitiatives(baseInitiative3); assertEq(atEpoch, governance.epoch()); @@ -608,59 +975,103 @@ contract GovernanceTest is Test { // should revert if the initiative is still in the registration warm up period vm.expectRevert("Governance: initiative-in-warm-up"); governance.unregisterInitiative(baseInitiative3); + } + + function test_unregisterInitiative_reverts_has_votes_allocated() public { + vm.prank(lusdHolder); + lusd.transfer(user, 1e18); + + vm.startPrank(user); + address userProxy = governance.deployUserProxy(); + + lusd.approve(address(governance), 2e18); + lqty.approve(address(userProxy), 2e18); + governance.depositLQTY(2e18); + vm.warp(block.timestamp + 365 days); + + governance.registerInitiative(baseInitiative3); + uint16 atEpoch = governance.registeredInitiatives(baseInitiative3); + assertEq(atEpoch, governance.epoch()); + + vm.warp(block.timestamp + EPOCH_DURATION); + + // user allocates to the initiative to make it unregisterable + address[] memory initiatives = new address[](1); + initiatives[0] = baseInitiative3; + int88[] memory deltaLQTYVotes = new int88[](1); + deltaLQTYVotes[0] = int88(1e18); + int88[] memory deltaLQTYVetos = new int88[](1); + + governance.allocateLQTY(initiatives, deltaLQTYVotes, deltaLQTYVetos); vm.warp(block.timestamp + 365 days); // should revert if the initiative is still active or the vetos don't meet the threshold /// @audit TO REVIEW, this never got any votes, so it seems correct to remove // No votes = can be kicked - // vm.expectRevert("Governance: cannot-unregister-initiative"); - // governance.unregisterInitiative(baseInitiative3); + vm.expectRevert("Governance: cannot-unregister-initiative"); + governance.unregisterInitiative(baseInitiative3); + } - snapshot = IGovernance.VoteSnapshot(1e18, governance.epoch() - 1); - vm.store( - address(governance), - bytes32(uint256(2)), - bytes32(abi.encodePacked(uint16(snapshot.forEpoch), uint240(snapshot.votes))) - ); - (votes, forEpoch) = governance.votesSnapshot(); - assertEq(votes, 1e18); - assertEq(forEpoch, governance.epoch() - 1); + function test_unregisterInitiative_allocate_in_removal_epoch() public { + vm.prank(lusdHolder); + lusd.transfer(user, 1e18); - IGovernance.InitiativeVoteSnapshot memory initiativeSnapshot = - IGovernance.InitiativeVoteSnapshot(0, governance.epoch() - 1, 0, 0); - vm.store( - address(governance), - keccak256(abi.encode(baseInitiative3, uint256(3))), - bytes32( - abi.encodePacked( - uint16(initiativeSnapshot.lastCountedEpoch), - uint16(initiativeSnapshot.forEpoch), - uint224(initiativeSnapshot.votes) - ) - ) - ); - (uint224 votes_, uint16 forEpoch_, uint16 lastCountedEpoch, ) = - governance.votesForInitiativeSnapshot(baseInitiative3); - assertEq(votes_, 0); - assertEq(forEpoch_, governance.epoch() - 1); - assertEq(lastCountedEpoch, 0); + vm.startPrank(user); + address userProxy = governance.deployUserProxy(); - governance.unregisterInitiative(baseInitiative3); + lusd.approve(address(governance), 2e18); + lqty.approve(address(userProxy), 2e18); + governance.depositLQTY(2e18); + vm.warp(block.timestamp + 365 days); - vm.stopPrank(); + governance.registerInitiative(baseInitiative3); + uint16 atEpoch = governance.registeredInitiatives(baseInitiative3); + assertEq(atEpoch, governance.epoch()); - vm.startPrank(lusdHolder); - lusd.transfer(user, 1e18); - vm.stopPrank(); + vm.warp(block.timestamp + 365 days); + + // user allocates to the initiative but it can still be unregistered + address[] memory initiatives = new address[](1); + initiatives[0] = baseInitiative3; + int88[] memory deltaLQTYVotes = new int88[](1); + deltaLQTYVotes[0] = int88(1e18); + int88[] memory deltaLQTYVetos = new int88[](1); + governance.allocateLQTY(initiatives, deltaLQTYVotes, deltaLQTYVetos); + + vm.expectRevert("Governance: cannot-unregister-initiative"); + governance.unregisterInitiative(baseInitiative3); + } + + function test_registerInitiative_reverts_initiative_previously_registered() + public + { + vm.prank(lusdHolder); + lusd.transfer(user, 2e18); vm.startPrank(user); + address userProxy = governance.deployUserProxy(); + + lusd.approve(address(governance), 1e18); + lqty.approve(address(userProxy), 1e18); + governance.depositLQTY(1e18); + vm.warp(block.timestamp + 365 days); + + governance.registerInitiative(baseInitiative3); + uint16 atEpoch = governance.registeredInitiatives(baseInitiative3); + assertEq(atEpoch, governance.epoch()); + + vm.warp(block.timestamp + 365 days); + + governance.unregisterInitiative(baseInitiative3); lusd.approve(address(governance), 1e18); + vm.expectRevert("Governance: initiative-already-registered"); governance.registerInitiative(baseInitiative3); - } + vm.stopPrank(); + } // Test: You can always remove allocation // forge test --match-test test_crit_accounting_mismatch -vv @@ -685,7 +1096,7 @@ contract GovernanceTest is Test { governance.allocateLQTY(initiatives, deltaLQTYVotes, deltaLQTYVetos); - (uint256 allocatedLQTY,) = governance.userStates(user); + (uint256 allocatedLQTY, ) = governance.userStates(user); assertEq(allocatedLQTY, 1_000e18); ( @@ -693,34 +1104,60 @@ contract GovernanceTest is Test { , uint32 averageStakingTimestampVoteLQTY1, , + ) = governance.initiativeStates(baseInitiative1); - ( - uint88 voteLQTY2, - , - , - , - ) = governance.initiativeStates(baseInitiative2); + (uint88 voteLQTY2, , , , ) = governance.initiativeStates( + baseInitiative2 + ); // Get power at time of vote - uint256 votingPower = governance.lqtyToVotes(voteLQTY1, block.timestamp, averageStakingTimestampVoteLQTY1); + uint256 votingPower = governance.lqtyToVotes( + voteLQTY1, + uint32(block.timestamp), + averageStakingTimestampVoteLQTY1 + ); assertGt(votingPower, 0, "Non zero power"); - + /// @audit TODO Fully digest and explain the bug // Warp to end so we check the threshold against future threshold - + { vm.warp(block.timestamp + governance.EPOCH_DURATION()); - (IGovernance.VoteSnapshot memory snapshot, IGovernance.InitiativeVoteSnapshot memory initiativeVoteSnapshot1) = governance.snapshotVotesForInitiative(baseInitiative1); - (, IGovernance.InitiativeVoteSnapshot memory initiativeVoteSnapshot2) = governance.snapshotVotesForInitiative(baseInitiative2); + ( + IGovernance.VoteSnapshot memory snapshot, + IGovernance.InitiativeVoteSnapshot + memory initiativeVoteSnapshot1 + ) = governance.snapshotVotesForInitiative(baseInitiative1); + ( + , + IGovernance.InitiativeVoteSnapshot + memory initiativeVoteSnapshot2 + ) = governance.snapshotVotesForInitiative(baseInitiative2); uint256 threshold = governance.calculateVotingThreshold(); - assertLt(initiativeVoteSnapshot1.votes, threshold, "it didn't get rewards"); - - uint256 votingPowerWithProjection = governance.lqtyToVotes(voteLQTY1, governance.epochStart() + governance.EPOCH_DURATION(), averageStakingTimestampVoteLQTY1); - assertLt(votingPower, threshold, "Current Power is not enough - Desynch A"); - assertLt(votingPowerWithProjection, threshold, "Future Power is also not enough - Desynch B"); + assertLt( + initiativeVoteSnapshot1.votes, + threshold, + "it didn't get rewards" + ); + + uint256 votingPowerWithProjection = governance.lqtyToVotes( + voteLQTY1, + uint32(governance.epochStart() + governance.EPOCH_DURATION()), + averageStakingTimestampVoteLQTY1 + ); + assertLt( + votingPower, + threshold, + "Current Power is not enough - Desynch A" + ); + assertLt( + votingPowerWithProjection, + threshold, + "Future Power is also not enough - Desynch B" + ); // assertEq(counted1, counted2, "both counted"); } @@ -750,20 +1187,31 @@ contract GovernanceTest is Test { governance.allocateLQTY(initiatives, deltaLQTYVotes, deltaLQTYVetos); - // Warp to end so we check the threshold against future threshold - + { vm.warp(block.timestamp + governance.EPOCH_DURATION()); - (IGovernance.VoteSnapshot memory snapshot, IGovernance.InitiativeVoteSnapshot memory initiativeVoteSnapshot1) = governance.snapshotVotesForInitiative(baseInitiative1); + ( + IGovernance.VoteSnapshot memory snapshot, + IGovernance.InitiativeVoteSnapshot + memory initiativeVoteSnapshot1 + ) = governance.snapshotVotesForInitiative(baseInitiative1); uint256 threshold = governance.calculateVotingThreshold(); - assertLt(initiativeVoteSnapshot1.votes, threshold, "it didn't get rewards"); + assertLt( + initiativeVoteSnapshot1.votes, + threshold, + "it didn't get rewards" + ); } // Roll for - vm.warp(block.timestamp + governance.UNREGISTRATION_AFTER_EPOCHS() * governance.EPOCH_DURATION()); + vm.warp( + block.timestamp + + governance.UNREGISTRATION_AFTER_EPOCHS() * + governance.EPOCH_DURATION() + ); governance.unregisterInitiative(baseInitiative1); // @audit Warmup is not necessary @@ -780,13 +1228,20 @@ contract GovernanceTest is Test { int88[] memory removeDeltaLQTYVetos = new int88[](1); /// @audit the next call MUST not revert - this is a critical bug - governance.allocateLQTY(removeInitiatives, removeDeltaLQTYVotes, removeDeltaLQTYVetos); + governance.allocateLQTY( + removeInitiatives, + removeDeltaLQTYVotes, + removeDeltaLQTYVetos + ); // Security Check | TODO: MORE INVARIANTS // I should not be able to remove votes again vm.expectRevert(); // TODO: This is a panic - governance.allocateLQTY(removeInitiatives, removeDeltaLQTYVotes, removeDeltaLQTYVetos); - + governance.allocateLQTY( + removeInitiatives, + removeDeltaLQTYVotes, + removeDeltaLQTYVetos + ); address[] memory reAddInitiatives = new address[](1); reAddInitiatives[0] = baseInitiative1; @@ -796,7 +1251,11 @@ contract GovernanceTest is Test { /// @audit This MUST revert, an initiative should not be re-votable once disabled vm.expectRevert("Governance: initiative-not-active"); - governance.allocateLQTY(reAddInitiatives, reAddDeltaLQTYVotes, reAddDeltaLQTYVetos); + governance.allocateLQTY( + reAddInitiatives, + reAddDeltaLQTYVotes, + reAddDeltaLQTYVetos + ); } // Just pass a negative value and see what happens @@ -821,7 +1280,8 @@ contract GovernanceTest is Test { int88[] memory deltaLQTYVetos = new int88[](2); governance.allocateLQTY(initiatives, deltaLQTYVotes, deltaLQTYVetos); - (uint88 allocatedB4Test,,) = governance.lqtyAllocatedByUserToInitiative(user, baseInitiative1); + (uint88 allocatedB4Test, , ) = governance + .lqtyAllocatedByUserToInitiative(user, baseInitiative1); console.log("allocatedB4Test", allocatedB4Test); vm.warp(block.timestamp + governance.EPOCH_DURATION()); @@ -835,16 +1295,27 @@ contract GovernanceTest is Test { removeDeltaLQTYVotes[0] = int88(-1e18); int88[] memory removeDeltaLQTYVetos = new int88[](1); - (uint88 allocatedB4Removal,,) = governance.lqtyAllocatedByUserToInitiative(user, baseInitiative1); + (uint88 allocatedB4Removal, , ) = governance + .lqtyAllocatedByUserToInitiative(user, baseInitiative1); console.log("allocatedB4Removal", allocatedB4Removal); - governance.allocateLQTY(removeInitiatives, removeDeltaLQTYVotes, removeDeltaLQTYVetos); - (uint88 allocatedAfterRemoval,,) = governance.lqtyAllocatedByUserToInitiative(user, baseInitiative1); + governance.allocateLQTY( + removeInitiatives, + removeDeltaLQTYVotes, + removeDeltaLQTYVetos + ); + (uint88 allocatedAfterRemoval, , ) = governance + .lqtyAllocatedByUserToInitiative(user, baseInitiative1); console.log("allocatedAfterRemoval", allocatedAfterRemoval); vm.expectRevert(); - governance.allocateLQTY(removeInitiatives, removeDeltaLQTYVotes, removeDeltaLQTYVetos); - (uint88 allocatedAfter,,) = governance.lqtyAllocatedByUserToInitiative(user, baseInitiative1); + governance.allocateLQTY( + removeInitiatives, + removeDeltaLQTYVotes, + removeDeltaLQTYVetos + ); + (uint88 allocatedAfter, , ) = governance + .lqtyAllocatedByUserToInitiative(user, baseInitiative1); console.log("allocatedAfter", allocatedAfter); } @@ -853,9 +1324,7 @@ contract GovernanceTest is Test { /// Ensure chunks above 1 wei /// Go ahead and remove /// Ensure that at the end you remove 100% - function test_fuzz_canRemoveExtact() public { - - } + function test_fuzz_canRemoveExtact() public {} function test_allocateLQTY_single() public { vm.startPrank(user); @@ -865,9 +1334,10 @@ contract GovernanceTest is Test { lqty.approve(address(userProxy), 1e18); governance.depositLQTY(1e18); - (uint88 allocatedLQTY, uint32 averageStakingTimestampUser) = governance.userStates(user); + (uint88 allocatedLQTY, uint32 averageStakingTimestampUser) = governance + .userStates(user); assertEq(allocatedLQTY, 0); - (uint88 countedVoteLQTY,) = governance.globalState(); + (uint88 countedVoteLQTY, ) = governance.globalState(); assertEq(countedVoteLQTY, 0); address[] memory initiatives = new address[](1); @@ -883,15 +1353,15 @@ contract GovernanceTest is Test { vm.warp(block.timestamp + 365 days); governance.allocateLQTY(initiatives, deltaLQTYVotes, deltaLQTYVetos); - (allocatedLQTY,) = governance.userStates(user); + (allocatedLQTY, ) = governance.userStates(user); assertEq(allocatedLQTY, 1e18); ( uint88 voteLQTY, uint88 vetoLQTY, uint32 averageStakingTimestampVoteLQTY, - uint32 averageStakingTimestampVetoLQTY - , + uint32 averageStakingTimestampVetoLQTY, + ) = governance.initiativeStates(baseInitiative1); // should update the `voteLQTY` and `vetoLQTY` variables assertEq(voteLQTY, 1e18); @@ -902,12 +1372,13 @@ contract GovernanceTest is Test { assertEq(averageStakingTimestampVoteLQTY, averageStakingTimestampUser); assertEq(averageStakingTimestampVetoLQTY, 0); // should remove or add the initiatives voting LQTY from the counter - - (countedVoteLQTY,) = governance.globalState(); + + (countedVoteLQTY, ) = governance.globalState(); assertEq(countedVoteLQTY, 1e18); uint16 atEpoch; - (voteLQTY, vetoLQTY, atEpoch) = governance.lqtyAllocatedByUserToInitiative(user, baseInitiative1); + (voteLQTY, vetoLQTY, atEpoch) = governance + .lqtyAllocatedByUserToInitiative(user, baseInitiative1); // should update the allocation mapping from user to initiative assertEq(voteLQTY, 1e18); assertEq(vetoLQTY, 0); @@ -917,7 +1388,7 @@ contract GovernanceTest is Test { // should snapshot the global and initiatives votes if there hasn't been a snapshot in the current epoch yet (, uint16 forEpoch) = governance.votesSnapshot(); assertEq(forEpoch, governance.epoch() - 1); - (, forEpoch, ,) = governance.votesForInitiativeSnapshot(baseInitiative1); + (, forEpoch, ) = governance.votesForInitiativeSnapshot(baseInitiative1); assertEq(forEpoch, governance.epoch() - 1); vm.stopPrank(); @@ -932,7 +1403,10 @@ contract GovernanceTest is Test { governance.depositLQTY(1e18); (, uint32 averageAge) = governance.userStates(user2); - assertEq(governance.lqtyToVotes(1e18, block.timestamp, averageAge), 0); + assertEq( + governance.lqtyToVotes(1e18, uint32(block.timestamp), averageAge), + 0 + ); deltaLQTYVetos[0] = 1e18; @@ -944,23 +1418,32 @@ contract GovernanceTest is Test { governance.allocateLQTY(initiatives, deltaLQTYVotes, deltaLQTYVetos); // should update the user's allocated LQTY balance - (allocatedLQTY,) = governance.userStates(user2); + (allocatedLQTY, ) = governance.userStates(user2); assertEq(allocatedLQTY, 1e18); - (voteLQTY, vetoLQTY, averageStakingTimestampVoteLQTY, averageStakingTimestampVetoLQTY, ) = - governance.initiativeStates(baseInitiative1); + ( + voteLQTY, + vetoLQTY, + averageStakingTimestampVoteLQTY, + averageStakingTimestampVetoLQTY, + + ) = governance.initiativeStates(baseInitiative1); assertEq(voteLQTY, 2e18); assertEq(vetoLQTY, 0); assertEq(averageStakingTimestampVoteLQTY, block.timestamp - 365 days); assertGt(averageStakingTimestampVoteLQTY, averageStakingTimestampUser); assertEq(averageStakingTimestampVetoLQTY, 0); - // should revert if the user doesn't have enough unallocated LQTY available vm.expectRevert("Governance: insufficient-unallocated-lqty"); governance.withdrawLQTY(1e18); - vm.warp(block.timestamp + EPOCH_DURATION - governance.secondsWithinEpoch() - 1); + vm.warp( + block.timestamp + + EPOCH_DURATION - + governance.secondsWithinEpoch() - + 1 + ); initiatives[0] = baseInitiative1; deltaLQTYVotes[0] = 1e18; @@ -972,22 +1455,62 @@ contract GovernanceTest is Test { deltaLQTYVotes[0] = -1e18; governance.allocateLQTY(initiatives, deltaLQTYVotes, deltaLQTYVetos); - (allocatedLQTY,) = governance.userStates(user2); + (allocatedLQTY, ) = governance.userStates(user2); assertEq(allocatedLQTY, 0); - (countedVoteLQTY,) = governance.globalState(); + (countedVoteLQTY, ) = governance.globalState(); assertEq(countedVoteLQTY, 1e18); - (voteLQTY, vetoLQTY, averageStakingTimestampVoteLQTY, averageStakingTimestampVetoLQTY, ) = - governance.initiativeStates(baseInitiative1); + ( + voteLQTY, + vetoLQTY, + averageStakingTimestampVoteLQTY, + averageStakingTimestampVetoLQTY, + + ) = governance.initiativeStates(baseInitiative1); assertEq(voteLQTY, 1e18); assertEq(vetoLQTY, 0); assertEq(averageStakingTimestampVoteLQTY, averageStakingTimestampUser); assertEq(averageStakingTimestampVetoLQTY, 0); - vm.stopPrank(); } + function test_allocateLQTY_to_unregistered_initiative() public { + vm.startPrank(user); + + address userProxy = governance.deployUserProxy(); + + lqty.approve(address(userProxy), 1e18); + governance.depositLQTY(1e18); + + (uint88 allocatedLQTY, uint32 averageStakingTimestampUser) = governance + .userStates(user); + assertEq(allocatedLQTY, 0); + (uint88 countedVoteLQTY, ) = governance.globalState(); + assertEq(countedVoteLQTY, 0); + + address baseInitiative4 = address( + new BribeInitiative( + address(governance), + address(lusd), + address(lqty) + ) + ); + + address[] memory initiatives = new address[](1); + initiatives[0] = baseInitiative4; + int88[] memory deltaLQTYVotes = new int88[](1); + deltaLQTYVotes[0] = 1e18; //this should be 0 + int88[] memory deltaLQTYVetos = new int88[](1); + + vm.warp(block.timestamp + 365 days); + vm.expectRevert("Governance: initiative-not-active"); + governance.allocateLQTY(initiatives, deltaLQTYVotes, deltaLQTYVetos); + + (allocatedLQTY, ) = governance.userStates(user); + assertEq(allocatedLQTY, 0); + } + function test_allocateLQTY_multiple() public { vm.startPrank(user); @@ -996,9 +1519,9 @@ contract GovernanceTest is Test { lqty.approve(address(userProxy), 2e18); governance.depositLQTY(2e18); - (uint88 allocatedLQTY,) = governance.userStates(user); + (uint88 allocatedLQTY, ) = governance.userStates(user); assertEq(allocatedLQTY, 0); - (uint88 countedVoteLQTY,) = governance.globalState(); + (uint88 countedVoteLQTY, ) = governance.globalState(); assertEq(countedVoteLQTY, 0); address[] memory initiatives = new address[](2); @@ -1013,9 +1536,9 @@ contract GovernanceTest is Test { governance.allocateLQTY(initiatives, deltaLQTYVotes, deltaLQTYVetos); - (allocatedLQTY,) = governance.userStates(user); + (allocatedLQTY, ) = governance.userStates(user); assertEq(allocatedLQTY, 2e18); - (countedVoteLQTY,) = governance.globalState(); + (countedVoteLQTY, ) = governance.globalState(); assertEq(countedVoteLQTY, 2e18); ( @@ -1023,24 +1546,38 @@ contract GovernanceTest is Test { uint88 vetoLQTY, uint32 averageStakingTimestampVoteLQTY, uint32 averageStakingTimestampVetoLQTY, + ) = governance.initiativeStates(baseInitiative1); assertEq(voteLQTY, 1e18); assertEq(vetoLQTY, 0); - (voteLQTY, vetoLQTY, averageStakingTimestampVoteLQTY, averageStakingTimestampVetoLQTY, ) = - governance.initiativeStates(baseInitiative2); + ( + voteLQTY, + vetoLQTY, + averageStakingTimestampVoteLQTY, + averageStakingTimestampVetoLQTY, + + ) = governance.initiativeStates(baseInitiative2); assertEq(voteLQTY, 1e18); assertEq(vetoLQTY, 0); } - function test_allocateLQTY_fuzz_deltaLQTYVotes(uint88 _deltaLQTYVotes) public { - vm.assume(_deltaLQTYVotes > 0 && _deltaLQTYVotes < uint88(type(int88).max)); + function test_allocateLQTY_fuzz_deltaLQTYVotes( + uint88 _deltaLQTYVotes + ) public { + vm.assume( + _deltaLQTYVotes > 0 && _deltaLQTYVotes < uint88(type(int88).max) + ); vm.startPrank(user); address userProxy = governance.deployUserProxy(); - vm.store(address(lqty), keccak256(abi.encode(user, 0)), bytes32(abi.encode(uint256(_deltaLQTYVotes)))); + vm.store( + address(lqty), + keccak256(abi.encode(user, 0)), + bytes32(abi.encode(uint256(_deltaLQTYVotes))) + ); lqty.approve(address(userProxy), _deltaLQTYVotes); governance.depositLQTY(_deltaLQTYVotes); @@ -1057,14 +1594,22 @@ contract GovernanceTest is Test { vm.stopPrank(); } - function test_allocateLQTY_fuzz_deltaLQTYVetos(uint88 _deltaLQTYVetos) public { - vm.assume(_deltaLQTYVetos > 0 && _deltaLQTYVetos < uint88(type(int88).max)); + function test_allocateLQTY_fuzz_deltaLQTYVetos( + uint88 _deltaLQTYVetos + ) public { + vm.assume( + _deltaLQTYVetos > 0 && _deltaLQTYVetos < uint88(type(int88).max) + ); vm.startPrank(user); address userProxy = governance.deployUserProxy(); - vm.store(address(lqty), keccak256(abi.encode(user, 0)), bytes32(abi.encode(uint256(_deltaLQTYVetos)))); + vm.store( + address(lqty), + keccak256(abi.encode(user, 0)), + bytes32(abi.encode(uint256(_deltaLQTYVetos))) + ); lqty.approve(address(userProxy), _deltaLQTYVetos); governance.depositLQTY(_deltaLQTYVetos); @@ -1109,18 +1654,26 @@ contract GovernanceTest is Test { deltaVoteLQTY[1] = 500e18; int88[] memory deltaVetoLQTY = new int88[](2); governance.allocateLQTY(initiatives, deltaVoteLQTY, deltaVetoLQTY); - (uint88 allocatedLQTY,) = governance.userStates(user); + (uint88 allocatedLQTY, ) = governance.userStates(user); assertEq(allocatedLQTY, 1000e18); vm.warp(block.timestamp + governance.EPOCH_DURATION() + 1); // should compute the claim and transfer it to the initiative - assertEq(governance.claimForInitiative(baseInitiative1), 5000e18, "first claim"); + assertEq( + governance.claimForInitiative(baseInitiative1), + 5000e18, + "first claim" + ); // 2nd claim = 0 assertEq(governance.claimForInitiative(baseInitiative1), 0); - assertEq(governance.claimForInitiative(baseInitiative2), 5000e18, "first claim 2"); + assertEq( + governance.claimForInitiative(baseInitiative2), + 5000e18, + "first claim 2" + ); assertEq(governance.claimForInitiative(baseInitiative2), 0); assertEq(lusd.balanceOf(baseInitiative2), 5000e18); @@ -1147,17 +1700,21 @@ contract GovernanceTest is Test { assertEq(governance.claimForInitiative(baseInitiative1), 9950e18); assertEq(governance.claimForInitiative(baseInitiative1), 0); - assertEq(lusd.balanceOf(baseInitiative1), 14950e18); - (Governance.InitiativeStatus status, , uint256 claimable) = governance.getInitiativeState(baseInitiative2); + (Governance.InitiativeStatus status, , uint256 claimable) = governance + .getInitiativeState(baseInitiative2); console.log("res", uint8(status)); console.log("claimable", claimable); - (uint224 votes, , , uint224 vetos) = governance.votesForInitiativeSnapshot(baseInitiative2); + (uint224 votes, , uint224 vetos) = governance + .votesForInitiativeSnapshot(baseInitiative2); console.log("snapshot votes", votes); console.log("snapshot vetos", vetos); - console.log("governance.calculateVotingThreshold()", governance.calculateVotingThreshold()); + console.log( + "governance.calculateVotingThreshold()", + governance.calculateVotingThreshold() + ); assertEq(governance.claimForInitiative(baseInitiative2), 0, "zero 2"); assertEq(governance.claimForInitiative(baseInitiative2), 0, "zero 3"); @@ -1196,7 +1753,7 @@ contract GovernanceTest is Test { deltaVoteLQTY[1] = 500e18; int88[] memory deltaVetoLQTY = new int88[](2); governance.allocateLQTY(initiatives, deltaVoteLQTY, deltaVetoLQTY); - (uint88 allocatedLQTY,) = governance.userStates(user); + (uint88 allocatedLQTY, ) = governance.userStates(user); assertEq(allocatedLQTY, 1000e18); vm.warp(block.timestamp + governance.EPOCH_DURATION() + 1); @@ -1250,7 +1807,10 @@ contract GovernanceTest is Test { uint88 lqtyAmount = 1000e18; uint256 lqtyBalance = lqty.balanceOf(user); - lqty.approve(address(governance.deriveUserProxyAddress(user)), lqtyAmount); + lqty.approve( + address(governance.deriveUserProxyAddress(user)), + lqtyAmount + ); bytes[] memory data = new bytes[](7); address[] memory initiatives = new address[](1); @@ -1265,20 +1825,34 @@ contract GovernanceTest is Test { data[0] = abi.encodeWithSignature("deployUserProxy()"); data[1] = abi.encodeWithSignature("depositLQTY(uint88)", lqtyAmount); data[2] = abi.encodeWithSignature( - "allocateLQTY(address[],int88[],int88[])", initiatives, deltaVoteLQTY, deltaVetoLQTY + "allocateLQTY(address[],int88[],int88[])", + initiatives, + deltaVoteLQTY, + deltaVetoLQTY ); data[3] = abi.encodeWithSignature("userStates(address)", user); - data[4] = abi.encodeWithSignature("snapshotVotesForInitiative(address)", baseInitiative1); + data[4] = abi.encodeWithSignature( + "snapshotVotesForInitiative(address)", + baseInitiative1 + ); data[5] = abi.encodeWithSignature( - "allocateLQTY(address[],int88[],int88[])", initiatives, deltaVoteLQTY_, deltaVetoLQTY + "allocateLQTY(address[],int88[],int88[])", + initiatives, + deltaVoteLQTY_, + deltaVetoLQTY ); data[6] = abi.encodeWithSignature("withdrawLQTY(uint88)", lqtyAmount); bytes[] memory response = governance.multicall(data); - (uint88 allocatedLQTY,) = abi.decode(response[3], (uint88, uint32)); + (uint88 allocatedLQTY, ) = abi.decode(response[3], (uint88, uint32)); assertEq(allocatedLQTY, lqtyAmount); - (IGovernance.VoteSnapshot memory votes, IGovernance.InitiativeVoteSnapshot memory votesForInitiative) = - abi.decode(response[4], (IGovernance.VoteSnapshot, IGovernance.InitiativeVoteSnapshot)); + ( + IGovernance.VoteSnapshot memory votes, + IGovernance.InitiativeVoteSnapshot memory votesForInitiative + ) = abi.decode( + response[4], + (IGovernance.VoteSnapshot, IGovernance.InitiativeVoteSnapshot) + ); assertEq(votes.votes + votesForInitiative.votes, 0); assertEq(lqty.balanceOf(user), lqtyBalance); @@ -1288,97 +1862,37 @@ contract GovernanceTest is Test { function test_nonReentrant() public { MockInitiative mockInitiative = new MockInitiative(address(governance)); - vm.startPrank(user); - + vm.prank(user); address userProxy = governance.deployUserProxy(); - IGovernance.VoteSnapshot memory snapshot = IGovernance.VoteSnapshot(1e18, 1); - vm.store( - address(governance), - bytes32(uint256(2)), - bytes32(abi.encodePacked(uint16(snapshot.forEpoch), uint240(snapshot.votes))) - ); - (uint240 votes, uint16 forEpoch) = governance.votesSnapshot(); - assertEq(votes, 1e18); - assertEq(forEpoch, 1); - vm.startPrank(lusdHolder); lusd.transfer(user, 2e18); vm.stopPrank(); - vm.startPrank(user); - + vm.prank(user); lusd.approve(address(governance), 2e18); - lqty.approve(address(userProxy), 1e18); - governance.depositLQTY(1e18); + _stakeLQTY(user, 1e18); + vm.warp(block.timestamp + 365 days); + vm.startPrank(user); governance.registerInitiative(address(mockInitiative)); - uint16 atEpoch = governance.registeredInitiatives(address(mockInitiative)); + uint16 atEpoch = governance.registeredInitiatives( + address(mockInitiative) + ); assertEq(atEpoch, governance.epoch()); + vm.stopPrank(); vm.warp(block.timestamp + 365 days); - address[] memory initiatives = new address[](1); - initiatives[0] = address(mockInitiative); - int88[] memory deltaLQTYVotes = new int88[](1); - int88[] memory deltaLQTYVetos = new int88[](1); - governance.allocateLQTY(initiatives, deltaLQTYVotes, deltaLQTYVetos); - - // check that votingThreshold is is high enough such that MIN_CLAIM is met - snapshot = IGovernance.VoteSnapshot(1, governance.epoch() - 1); - vm.store( - address(governance), - bytes32(uint256(2)), - bytes32(abi.encodePacked(uint16(snapshot.forEpoch), uint240(snapshot.votes))) - ); - (votes, forEpoch) = governance.votesSnapshot(); - assertEq(votes, 1); - assertEq(forEpoch, governance.epoch() - 1); - - IGovernance.InitiativeVoteSnapshot memory initiativeSnapshot = - IGovernance.InitiativeVoteSnapshot(1, governance.epoch() - 1, governance.epoch() - 1, 0); - vm.store( - address(governance), - keccak256(abi.encode(address(mockInitiative), uint256(3))), - bytes32( - abi.encodePacked( - uint16(initiativeSnapshot.lastCountedEpoch), - uint16(initiativeSnapshot.forEpoch), - uint224(initiativeSnapshot.votes) - ) - ) - ); - (uint224 votes_, uint16 forEpoch_, uint16 lastCountedEpoch, ) = - governance.votesForInitiativeSnapshot(address(mockInitiative)); - assertEq(votes_, 1); - assertEq(forEpoch_, governance.epoch() - 1); - assertEq(lastCountedEpoch, governance.epoch() - 1); + _allocateLQTY(user, 0); governance.claimForInitiative(address(mockInitiative)); vm.warp(block.timestamp + governance.EPOCH_DURATION()); - initiativeSnapshot = IGovernance.InitiativeVoteSnapshot(0, governance.epoch() - 1, 0, 0); - vm.store( - address(governance), - keccak256(abi.encode(address(mockInitiative), uint256(3))), - bytes32( - abi.encodePacked( - uint16(initiativeSnapshot.lastCountedEpoch), - uint16(initiativeSnapshot.forEpoch), - uint224(initiativeSnapshot.votes) - ) - ) - ); - (votes_, forEpoch_, lastCountedEpoch, ) = governance.votesForInitiativeSnapshot(address(mockInitiative)); - assertEq(votes_, 0, "votes"); - assertEq(forEpoch_, governance.epoch() - 1, "forEpoch_"); - assertEq(lastCountedEpoch, 0, "lastCountedEpoch"); - - vm.warp(block.timestamp + governance.EPOCH_DURATION() * 4); - + // NOTE: reentrancy guard revert doesn't bubble up so can't be caught with expectRevert governance.unregisterInitiative(address(mockInitiative)); } @@ -1411,4 +1925,26 @@ contract GovernanceTest is Test { vm.stopPrank(); } + + function _stakeLQTY(address staker, uint88 amount) internal { + vm.startPrank(staker); + address userProxy = governance.deriveUserProxyAddress(staker); + lqty.approve(address(userProxy), amount); + + governance.depositLQTY(amount); + vm.stopPrank(); + } + + function _allocateLQTY(address allocator, uint88 amount) internal { + vm.startPrank(allocator); + + address[] memory initiatives = new address[](1); + initiatives[0] = baseInitiative1; + int88[] memory deltaLQTYVotes = new int88[](1); + deltaLQTYVotes[0] = int88(amount); + int88[] memory deltaLQTYVetos = new int88[](1); + + governance.allocateLQTY(initiatives, deltaLQTYVotes, deltaLQTYVetos); + vm.stopPrank(); + } } diff --git a/test/mocks/MockGovernance.sol b/test/mocks/MockGovernance.sol index 3ed3e752..c5f3466e 100644 --- a/test/mocks/MockGovernance.sol +++ b/test/mocks/MockGovernance.sol @@ -21,11 +21,11 @@ contract MockGovernance { return _currentTimestamp - _averageTimestamp; } - function lqtyToVotes(uint88 _lqtyAmount, uint256 _currentTimestamp, uint32 _averageTimestamp) + function lqtyToVotes(uint88 _lqtyAmount, uint32 _currentTimestamp, uint32 _averageTimestamp) public pure - returns (uint240) + returns (uint120) { - return uint240(_lqtyAmount) * _averageAge(uint32(_currentTimestamp), _averageTimestamp); + return uint120(_lqtyAmount) * _averageAge(_currentTimestamp, _averageTimestamp); } } diff --git a/zzz_TEMP_TO_FIX.MD b/zzz_TEMP_TO_FIX.MD index ab90ca91..85926310 100644 --- a/zzz_TEMP_TO_FIX.MD +++ b/zzz_TEMP_TO_FIX.MD @@ -1,5 +1,14 @@ -[FAIL. Reason: revert: Governance: claim-not-met] test_claimForInitiative() (gas: 1198986) +Failing tests: +Encountered 5 failing tests in test/Governance.t.sol:GovernanceTest +[FAIL. Reason: votes: 0 != 1000000000000000000] test_calculateVotingThreshold() (gas: 5023391) +[FAIL. Reason: assertion failed: 0 != 6027; counterexample: calldata=0xcc3b9002000000000000000000000000000000000000000000000000000000000000178b0000000000000000000000000000000000000000000000000000000000003c43000000000000000000000000000000000000000000000000000000000000470d0000000000000000000000000000000000000000000000000000000000003d0f0000000000000000000000000000000000000000000000000000000000000920 args=[6027, 15427 [1.542e4], 18189 [1.818e4], 15631 [1.563e4], 2336]] test_calculateVotingThreshold_fuzz(uint120,uint16,uint88,uint128,uint88) (runs: 0, μ: 0, ~: 0) +[FAIL. Reason: assertion failed: 0 != 1000000000000000000] test_nonReentrant() (gas: 389879) +[FAIL. Reason: assertion failed: 0 != 1000000000000000000] test_registerInitiative() (gas: 53601) +[FAIL. Reason: assertion failed: 0 != 1000000000000000000] test_unregisterInitiative() (gas: 53585) -Fails because of Governance: claim-not-met -TODO: Discuss if we should return 0 in those scenarios \ No newline at end of file +All of these tests use vm.store, which is out of wahck + +IMO rewrite to do it the proper way + +Can debug the slots to make sure we're not doing any weird thing as wel \ No newline at end of file