From 627b073e9f611afbd1f1731267c9d5d8ed99a5e2 Mon Sep 17 00:00:00 2001 From: toyvo Date: Thu, 18 Apr 2024 22:26:50 -0400 Subject: [PATCH] test coverage --- Makefile | 22 +- src/BaseGauge.sol | 55 ++--- src/Bribe.sol | 30 +-- src/FluxToken.sol | 4 +- src/RewardsDistributor.sol | 4 +- src/Voter.sol | 38 +--- src/VotingEscrow.sol | 29 +-- src/gauges/PassthroughGauge.sol | 9 +- src/interfaces/IBaseGauge.sol | 6 - src/interfaces/IVoter.sol | 18 -- src/test/AlchemixGovernor.t.sol | 24 +++ src/test/FluxToken.t.sol | 143 +++++++++++++ src/test/Minter.t.sol | 19 ++ src/test/RevenueHandler.t.sol | 38 ++++ src/test/RewardPoolManager.t.sol | 167 +++++++++++++++ src/test/Voting.t.sol | 348 ++++++++++++++++++++++++++++++- src/test/VotingEscrow.t.sol | 152 +++++++++----- 17 files changed, 916 insertions(+), 190 deletions(-) create mode 100644 src/test/RewardPoolManager.t.sol diff --git a/Makefile b/Makefile index aaac73b..76c0190 100644 --- a/Makefile +++ b/Makefile @@ -74,4 +74,24 @@ test_file_block_debug :; FOUNDRY_PROFILE=$(PROFILE) forge test $(FORK_URL) $(MAT test_file_debug_test :; FOUNDRY_PROFILE=$(PROFILE) forge test $(FORK_URL) $(MATCH_PATH) $(MATCH_TEST) -vvv # runs single test within file with added verbosity for failing test from a given block: "make test_file_block_debug_test FILE=Minter TEST=testUnwrap" -test_file_block_debug_test :; FOUNDRY_PROFILE=$(PROFILE) forge test $(FORK_URL) $(MATCH_PATH) $(MATCH_TEST) $(FORK_BLOCK) -vvv \ No newline at end of file +test_file_block_debug_test :; FOUNDRY_PROFILE=$(PROFILE) forge test $(FORK_URL) $(MATCH_PATH) $(MATCH_TEST) $(FORK_BLOCK) -vvv + +# | File | % Lines | % Statements | % Branches | % Funcs | +# |----------------------------|--------------------|--------------------|------------------|------------------| +# | src/RewardsDistributor.sol | 90.00% (126/140) | 88.44% (176/199) | 63.64% (42/66) | 80.00% (12/15) | +# | src/VotingEscrow.sol | 90.34% (449/497) | 91.06% (560/615) | 67.05% (177/264) | 82.50% (66/80) | + + +# | File | % Lines | % Statements | % Branches | % Funcs | +# |----------------------------|--------------------|--------------------|------------------|------------------| +# | src/Bribe.sol | 94.52% (138/146) | 95.63% (175/183) | 78.38% (58/74) | 100.00% (19/19) | +# | src/Minter.sol | 100.00% (48/48) | 100.00% (61/61) | 88.46% (23/26) | 100.00% (9/9) | +# | src/Voter.sol | 97.14% (170/175) | 96.53% (195/202) | 83.02% (88/106) | 93.33% (28/30) | +# | src/FluxToken.sol | 97.06% (66/68) | 97.40% (75/77) | 90.32% (56/62) | 100.00% (19/19) | +# | src/RevenueHandler.sol | 100.00% (94/94) | 100.00% (115/115) | 93.18% (41/44) | 100.00% (15/15) | +# | src/RewardPoolManager.sol | 100.00% (47/47) | 100.00% (55/55) | 94.12% (32/34) | 100.00% (13/13) | +# | src/BaseGauge.sol | 100.00% (13/13) | 100.00% (13/13) | 100.00% (14/14) | 80.00% (4/5) | +# | src/AlchemixGovernor.sol | 100.00% (16/16) | 100.00% (17/17) | 100.00% (12/12) | 100.00% (6/6) | +# | src/BribeFactory.sol | 100.00% (1/1) | 100.00% (2/2) | 100.00% (0/0) | 100.00% (1/1) | +# | src/GaugeFactory.sol | 100.00% (2/2) | 100.00% (4/4) | 100.00% (0/0) | 100.00% (2/2) | +# | src/PassthroughGauge.sol | 100.00% (2/2) | 100.00% (2/2) | 100.00% (0/0) | 100.00% (1/1) | \ No newline at end of file diff --git a/src/BaseGauge.sol b/src/BaseGauge.sol index 6bc7261..dbddef0 100644 --- a/src/BaseGauge.sol +++ b/src/BaseGauge.sol @@ -6,6 +6,7 @@ import "src/interfaces/IBribe.sol"; import "src/interfaces/IBaseGauge.sol"; import "src/interfaces/IVoter.sol"; import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; /** * @title Base Gauge @@ -13,17 +14,27 @@ import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; * @notice Gauges are used to incentivize pools, they emit or passthrough reward tokens */ abstract contract BaseGauge is IBaseGauge { - uint256 internal constant DURATION = 2 weeks; // Rewards released over voting period + using SafeERC20 for IERC20; + + /// @notice Rewards released over voting period + uint256 internal constant DURATION = 2 weeks; uint256 internal constant BRIBE_LAG = 1 days; uint256 internal constant MAX_REWARD_TOKENS = 16; - address public ve; // veALCX token used for gauges - address public bribe; // Address of bribe contract - address public voter; // Address of voter contract + /// @notice veALCX token used for gauges + address public ve; + /// @notice Address of bribe contract + address public bribe; + /// @notice Address of voter contract + address public voter; + /// @notice Address of admin address public admin; + /// @notice Address of pending admin address public pendingAdmin; - address public receiver; // Address that receives the ALCX rewards - address public rewardToken; // Address of the reward token + /// @notice Address that receives the ALCX rewards + address public receiver; + /// @notice Address of the reward token + address public rewardToken; // Re-entrancy check uint256 internal _unlocked = 1; @@ -34,22 +45,6 @@ abstract contract BaseGauge is IBaseGauge { _unlocked = 1; } - /* - View functions - */ - - /// @inheritdoc IBaseGauge - function getVotingStage(uint256 timestamp) external pure returns (VotingStage) { - uint256 modTime = timestamp % (1 weeks); - if (modTime < BRIBE_LAG) { - return VotingStage.BribesPhase; - } else if (modTime >= BRIBE_LAG && modTime < (BRIBE_LAG + DURATION)) { - return VotingStage.VotesPhase; - } else { - return VotingStage.RewardsPhase; - } - } - /* External functions */ @@ -76,7 +71,7 @@ abstract contract BaseGauge is IBaseGauge { function notifyRewardAmount(uint256 _amount) external lock { require(msg.sender == voter, "not voter"); require(_amount > 0, "zero amount"); - _safeTransferFrom(rewardToken, msg.sender, address(this), _amount); + IERC20(rewardToken).safeTransferFrom(msg.sender, address(this), _amount); emit NotifyReward(msg.sender, rewardToken, _amount); @@ -87,20 +82,6 @@ abstract contract BaseGauge is IBaseGauge { Internal functions */ - function _safeTransfer(address token, address to, uint256 value) internal { - require(token.code.length > 0); - (bool success, bytes memory data) = token.call(abi.encodeWithSelector(IERC20.transfer.selector, to, value)); - require(success && (data.length == 0 || abi.decode(data, (bool)))); - } - - function _safeTransferFrom(address token, address from, address to, uint256 value) internal { - require(token.code.length > 0); - (bool success, bytes memory data) = token.call( - abi.encodeWithSelector(IERC20.transferFrom.selector, from, to, value) - ); - require(success && (data.length == 0 || abi.decode(data, (bool)))); - } - /** * @notice Override function to implement passthrough logic * @param _amount Amount of rewards diff --git a/src/Bribe.sol b/src/Bribe.sol index 35fb9cb..09411cb 100644 --- a/src/Bribe.sol +++ b/src/Bribe.sol @@ -1,18 +1,21 @@ // SPDX-License-Identifier: GPL-3 pragma solidity ^0.8.15; - +import "lib/forge-std/src/console2.sol"; import "src/interfaces/IBribe.sol"; import "src/interfaces/IBaseGauge.sol"; import "src/interfaces/IVoter.sol"; import "src/interfaces/IVotingEscrow.sol"; import "src/libraries/Math.sol"; import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; /** * @title Bribe * @notice Implementation of bribe contract to be used with gauges */ contract Bribe is IBribe { + using SafeERC20 for IERC20; + /// @notice Rewards released over voting period uint256 internal constant DURATION = 2 weeks; /// @notice Duration of time when bribes are accepted @@ -107,7 +110,7 @@ contract Bribe is IBribe { /// @inheritdoc IBribe function notifyRewardAmount(address token, uint256 amount) external lock { - require(amount > 0); + require(amount > 0, "reward amount must be greater than 0"); // If the token has been whitelisted by the voter contract, add it to the rewards list require(IVoter(voter).isWhitelisted(token), "bribe tokens must be whitelisted"); @@ -117,7 +120,8 @@ contract Bribe is IBribe { uint256 adjustedTstamp = getEpochStart(block.timestamp); uint256 epochRewards = tokenRewardsPerEpoch[token][adjustedTstamp]; - _safeTransferFrom(token, msg.sender, address(this), amount); + IERC20(token).safeTransferFrom(msg.sender, address(this), amount); + tokenRewardsPerEpoch[token][adjustedTstamp] = epochRewards + amount; periodFinish[token] = adjustedTstamp + DURATION; @@ -126,7 +130,7 @@ contract Bribe is IBribe { /// @inheritdoc IBribe function addRewardToken(address token) external { - require(msg.sender == gauge); + require(msg.sender == gauge, "not being set by a gauge"); _addRewardToken(token); } @@ -135,7 +139,6 @@ contract Bribe is IBribe { require(msg.sender == voter, "Only voter can execute"); require(IVoter(voter).isWhitelisted(newToken), "New token must be whitelisted"); require(rewards[oldTokenIndex] == oldToken, "Old token mismatch"); - require(newToken != address(0), "New token cannot be zero address"); // Check that the newToken does not already exist in the rewards array for (uint256 i = 0; i < rewards.length; i++) { @@ -193,7 +196,6 @@ contract Bribe is IBribe { if (votingCheckpoints[nCheckpoints - 1].timestamp < timestamp) { return (nCheckpoints - 1); } - // Check implicit zero balance if (votingCheckpoints[0].timestamp > timestamp) { return 0; @@ -291,7 +293,7 @@ contract Bribe is IBribe { _writeCheckpoint(tokenId, balanceOf[tokenId]); - _safeTransfer(tokens[i], _owner, _reward); + IERC20(tokens[i]).safeTransfer(_owner, _reward); emit ClaimRewards(_owner, tokens[i], _reward); } @@ -384,18 +386,4 @@ contract Bribe is IBribe { function _bribeStart(uint256 timestamp) internal pure returns (uint256) { return timestamp - (timestamp % (DURATION)); } - - function _safeTransfer(address token, address to, uint256 value) internal { - require(token.code.length > 0); - (bool success, bytes memory data) = token.call(abi.encodeWithSelector(IERC20.transfer.selector, to, value)); - require(success && (data.length == 0 || abi.decode(data, (bool)))); - } - - function _safeTransferFrom(address token, address from, address to, uint256 value) internal { - require(token.code.length > 0); - (bool success, bytes memory data) = token.call( - abi.encodeWithSelector(IERC20.transferFrom.selector, from, to, value) - ); - require(success && (data.length == 0 || abi.decode(data, (bool)))); - } } diff --git a/src/FluxToken.sol b/src/FluxToken.sol index efc2b9a..9df10b6 100644 --- a/src/FluxToken.sol +++ b/src/FluxToken.sol @@ -213,7 +213,7 @@ contract FluxToken is ERC20("Flux", "FLUX"), IFluxToken { // Given an amount of eth, calculate how much FLUX it would earn in a year if it were deposited into veALCX function getClaimableFlux(uint256 _amount, address _nft) public view returns (uint256 claimableFlux) { - uint256 bpt = _calculateBPT(_amount); + uint256 bpt = calculateBPT(_amount); uint256 veMul = IVotingEscrow(veALCX).MULTIPLIER(); uint256 veMax = IVotingEscrow(veALCX).MAXTIME(); @@ -229,7 +229,7 @@ contract FluxToken is ERC20("Flux", "FLUX"), IFluxToken { } } - function _calculateBPT(uint256 _amount) public view returns (uint256 bptOut) { + function calculateBPT(uint256 _amount) public view returns (uint256 bptOut) { bptOut = _amount * bptMultiplier; } } diff --git a/src/RewardsDistributor.sol b/src/RewardsDistributor.sol index 8861571..8c5d7c0 100644 --- a/src/RewardsDistributor.sol +++ b/src/RewardsDistributor.sol @@ -138,7 +138,7 @@ contract RewardsDistributor is IRewardsDistributor, ReentrancyGuard { /// @inheritdoc IRewardsDistributor function checkpointToken() external { - assert(msg.sender == depositor); + require(msg.sender == depositor, "only depositor"); _checkpointToken(); } @@ -211,7 +211,7 @@ contract RewardsDistributor is IRewardsDistributor, ReentrancyGuard { /// @dev Once off event on contract initialize function setDepositor(address _depositor) external { - require(msg.sender == depositor); + require(msg.sender == depositor, "only depositor"); depositor = _depositor; emit DepositorUpdated(_depositor); } diff --git a/src/Voter.sol b/src/Voter.sol index 60074d7..11dad6f 100644 --- a/src/Voter.sol +++ b/src/Voter.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3 pragma solidity ^0.8.15; - +import "lib/forge-std/src/console2.sol"; import "src/interfaces/IBribeFactory.sol"; import "src/interfaces/IBribe.sol"; import "src/interfaces/IBaseGauge.sol"; @@ -28,7 +28,7 @@ contract Voter is IVoter { address public immutable bribefactory; uint256 internal constant BPS = 10_000; - uint256 internal constant MAX_BOOST = 10000; + uint256 internal constant MAX_BOOST = 10_000; uint256 internal constant MIN_BOOST = 0; /// @notice Rewards are released over this duration (2 weeks) uint256 internal constant DURATION = 2 weeks; @@ -167,7 +167,7 @@ contract Voter is IVoter { /// @inheritdoc IVoter function swapReward(address gaugeAddress, uint256 tokenIndex, address oldToken, address newToken) external { - require(msg.sender == admin); + require(msg.sender == admin, "only admin can swap reward tokens"); IBribe(bribes[gaugeAddress]).swapOutRewardToken(tokenIndex, oldToken, newToken); } @@ -231,8 +231,8 @@ contract Voter is IVoter { uint256[] calldata _weights, uint256 _boost ) external onlyNewEpoch(_tokenId) { - require(IVotingEscrow(veALCX).isApprovedOrOwner(msg.sender, _tokenId)); - require(_poolVote.length == _weights.length); + require(IVotingEscrow(veALCX).isApprovedOrOwner(msg.sender, _tokenId), "not approved or owner"); + require(_poolVote.length == _weights.length, "pool vote and weights mismatch"); require(_poolVote.length > 0, "no pools voted for"); require(_poolVote.length <= pools.length, "invalid pools"); require( @@ -251,6 +251,7 @@ contract Voter is IVoter { /// @inheritdoc IVoter function whitelist(address _token) public { require(msg.sender == admin, "not admin"); + require(_token != address(0), "cannot be zero address"); _whitelist(_token); } @@ -262,8 +263,8 @@ contract Voter is IVoter { /// @inheritdoc IVoter function createGauge(address _pool, GaugeType _gaugeType) external returns (address) { + require(msg.sender == admin, "not admin"); require(gauges[_pool] == address(0x0), "exists"); - require(msg.sender == admin, "only admin creates gauges"); address _bribe = IBribeFactory(bribefactory).createBribe(); @@ -327,23 +328,6 @@ contract Voter is IVoter { } } - /// @inheritdoc IVoter - function updateForRange(uint256 start, uint256 end) public { - for (uint256 i = start; i < end; i++) { - _updateFor(gauges[pools[i]]); - } - } - - /// @inheritdoc IVoter - function updateAll() external { - updateForRange(0, pools.length); - } - - /// @inheritdoc IVoter - function updateGauge(address _gauge) external { - _updateFor(_gauge); - } - /// @inheritdoc IVoter function claimBribes(address[] memory _bribes, address[][] memory _tokens, uint256 _tokenId) external { require(IVotingEscrow(veALCX).isApprovedOrOwner(msg.sender, _tokenId)); @@ -446,8 +430,8 @@ contract Voter is IVoter { require(isAlive[_gauge], "cannot vote for dead gauge"); uint256 _poolWeight = (_weights[i] * totalPower) / _totalVoteWeight; - require(votes[_tokenId][_pool] == 0); - require(_poolWeight != 0); + require(votes[_tokenId][_pool] == 0, "already voted for pool"); + require(_poolWeight != 0, "cannot vote with zero weight"); _updateFor(_gauge); poolVote[_tokenId].push(_pool); @@ -471,13 +455,13 @@ contract Voter is IVoter { } function _whitelist(address _token) internal { - require(!isWhitelisted[_token]); + require(!isWhitelisted[_token], "token already whitelisted"); isWhitelisted[_token] = true; emit Whitelisted(msg.sender, _token); } function _removeFromWhitelist(address _token) internal { - require(isWhitelisted[_token]); + require(isWhitelisted[_token], "token not whitelisted"); isWhitelisted[_token] = false; emit RemovedFromWhitelist(msg.sender, _token); } diff --git a/src/VotingEscrow.sol b/src/VotingEscrow.sol index 77264b5..0fb6515 100644 --- a/src/VotingEscrow.sol +++ b/src/VotingEscrow.sol @@ -501,13 +501,13 @@ contract VotingEscrow is IERC721, IERC721Metadata, IVotes, IVotingEscrow { function approve(address _approved, uint256 _tokenId) public { address owner = idToOwner[_tokenId]; // Throws if `_tokenId` is not a valid token - require(owner != address(0)); + require(owner != address(0), "owner not found"); // Throws if `_approved` is the current owner require(_approved != owner, "Approved is already owner"); // Check requirements bool senderIsOwner = (owner == msg.sender); bool senderIsApprovedForAll = (ownerToOperators[owner])[msg.sender]; - require(senderIsOwner || senderIsApprovedForAll); + require(senderIsOwner || senderIsApprovedForAll, "sender is not owner or approved"); // Set the approval idToApprovals[_tokenId] = _approved; emit Approval(owner, _approved, _tokenId); @@ -523,7 +523,7 @@ contract VotingEscrow is IERC721, IERC721Metadata, IVotes, IVotingEscrow { */ function setApprovalForAll(address _operator, bool _approved) external { // Throws if `_operator` is the `msg.sender` - require(_operator != msg.sender); + require(_operator != msg.sender, "operator cannot be sender"); ownerToOperators[msg.sender][_operator] = _approved; emit ApprovalForAll(msg.sender, _operator, _approved); } @@ -533,7 +533,7 @@ contract VotingEscrow is IERC721, IERC721Metadata, IVotes, IVotingEscrow { * @param delegatee The address to delegate votes to */ function delegate(address delegatee) public { - if (delegatee == address(0)) delegatee = msg.sender; + require(delegatee != address(0), "cannot delegate to zero address"); return _delegate(msg.sender, delegatee); } @@ -571,13 +571,13 @@ contract VotingEscrow is IERC721, IERC721Metadata, IVotes, IVotingEscrow { /// @inheritdoc IVotingEscrow function voting(uint256 _tokenId) external { - require(msg.sender == voter); + require(msg.sender == voter, "not voter"); voted[_tokenId] = true; } /// @inheritdoc IVotingEscrow function abstain(uint256 _tokenId) external { - require(msg.sender == voter); + require(msg.sender == voter, "not voter"); voted[_tokenId] = false; } @@ -618,8 +618,8 @@ contract VotingEscrow is IERC721, IERC721Metadata, IVotes, IVotingEscrow { function merge(uint256 _from, uint256 _to) external { require(!voted[_from], "voting in progress for token"); require(_from != _to, "must be different tokens"); - require(_isApprovedOrOwner(msg.sender, _from)); - require(_isApprovedOrOwner(msg.sender, _to)); + require(_isApprovedOrOwner(msg.sender, _from), "not approved or owner"); + require(_isApprovedOrOwner(msg.sender, _to), "not approved or owner"); LockedBalance memory _locked0 = locked[_from]; LockedBalance memory _locked1 = locked[_to]; @@ -657,8 +657,8 @@ contract VotingEscrow is IERC721, IERC721Metadata, IVotes, IVotingEscrow { /// @inheritdoc IVotingEscrow function updateLock(uint256 _tokenId) external { - require(isMaxLocked(_tokenId), "not max locked"); require(msg.sender == voter, "not voter"); + require(isMaxLocked(_tokenId), "not max locked"); locked[_tokenId].end = ((block.timestamp + MAXTIME) / WEEK) * WEEK; } @@ -712,7 +712,7 @@ contract VotingEscrow is IERC721, IERC721Metadata, IVotes, IVotingEscrow { * @param _maxLockEnabled Is max lock being enabled */ function updateUnlockTime(uint256 _tokenId, uint256 _lockDuration, bool _maxLockEnabled) external nonreentrant { - require(_isApprovedOrOwner(msg.sender, _tokenId)); + require(_isApprovedOrOwner(msg.sender, _tokenId), "not approved or owner"); LockedBalance memory _locked = locked[_tokenId]; @@ -739,7 +739,7 @@ contract VotingEscrow is IERC721, IERC721Metadata, IVotes, IVotingEscrow { * @dev Only possible if the lock has expired */ function withdraw(uint256 _tokenId) public nonreentrant { - require(_isApprovedOrOwner(msg.sender, _tokenId)); + require(_isApprovedOrOwner(msg.sender, _tokenId), "not approved or owner"); require(!voted[_tokenId], "voting in progress for token"); LockedBalance memory _locked = locked[_tokenId]; @@ -757,7 +757,10 @@ contract VotingEscrow is IERC721, IERC721Metadata, IVotes, IVotingEscrow { _checkpoint(_tokenId, _locked, LockedBalance(0, 0, false, 0)); // Withdraws BPT from reward pool - require(IRewardPoolManager(rewardPoolManager).withdrawFromRewardPool(value)); + require( + IRewardPoolManager(rewardPoolManager).withdrawFromRewardPool(value), + "withdraw from reward pool failed" + ); require(IERC20(BPT).transfer(ownerOf(_tokenId), value)); @@ -773,7 +776,7 @@ contract VotingEscrow is IERC721, IERC721Metadata, IVotes, IVotingEscrow { /// @inheritdoc IVotingEscrow function startCooldown(uint256 _tokenId) external { - require(_isApprovedOrOwner(msg.sender, _tokenId)); + require(_isApprovedOrOwner(msg.sender, _tokenId), "not approved or owner"); LockedBalance memory _locked = locked[_tokenId]; diff --git a/src/gauges/PassthroughGauge.sol b/src/gauges/PassthroughGauge.sol index 77dfeac..7be0eda 100644 --- a/src/gauges/PassthroughGauge.sol +++ b/src/gauges/PassthroughGauge.sol @@ -13,6 +13,8 @@ import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; * @dev If custom distribution logic is necessary create additional contract */ contract PassthroughGauge is BaseGauge { + using SafeERC20 for IERC20; + constructor(address _receiver, address _bribe, address _ve, address _voter) { receiver = _receiver; bribe = _bribe; @@ -35,10 +37,9 @@ contract PassthroughGauge is BaseGauge { * @param _amount Amount of rewards */ function _passthroughRewards(uint256 _amount) internal override { - uint256 rewardBalance = IERC20(rewardToken).balanceOf(address(this)); - require(rewardBalance >= _amount, "insufficient rewards"); - - _safeTransfer(rewardToken, receiver, _amount); + // Gauge will always have _amount + // It is transfered in BaseGauge.notifyRewardAmount + IERC20(rewardToken).safeTransfer(receiver, _amount); emit Passthrough(msg.sender, rewardToken, _amount, receiver); } diff --git a/src/interfaces/IBaseGauge.sol b/src/interfaces/IBaseGauge.sol index 21514c0..c8fb687 100644 --- a/src/interfaces/IBaseGauge.sol +++ b/src/interfaces/IBaseGauge.sol @@ -54,12 +54,6 @@ interface IBaseGauge { function acceptAdmin() external; - /** - * @notice Get the current voting stage - * @param timestamp The timestamp to check the voting stage - */ - function getVotingStage(uint256 timestamp) external pure returns (VotingStage); - /** * @notice Distribute the appropriate rewards to a gauge * @dev This function may require different implementation depending on destination diff --git a/src/interfaces/IVoter.sol b/src/interfaces/IVoter.sol index 4be043a..8d05f27 100644 --- a/src/interfaces/IVoter.sol +++ b/src/interfaces/IVoter.sol @@ -156,24 +156,6 @@ interface IVoter { */ function updateFor(address[] memory _gauges) external; - /** - * @notice Update a list of gauges given a range of pool indexes - * @param start start index - * @param end end index - */ - function updateForRange(uint256 start, uint256 end) external; - - /** - * @notice Update all gauges in the pools array - */ - function updateAll() external; - - /** - * @notice Update the gauge for a specific pool - * @param _gauge Address of the gauge to update - */ - function updateGauge(address _gauge) external; - /** * @notice Claim the bribes for a given token id * @param _bribes Array of bribe addresses to claim from diff --git a/src/test/AlchemixGovernor.t.sol b/src/test/AlchemixGovernor.t.sol index 81e2ce9..1a798b5 100644 --- a/src/test/AlchemixGovernor.t.sol +++ b/src/test/AlchemixGovernor.t.sol @@ -381,4 +381,28 @@ contract AlchemixGovernorTest is BaseTest { assertFalse(voter.isWhitelisted(usdc)); } + + function testAdminFunctions() public { + address admin = governor.admin(); + + hevm.expectRevert(abi.encodePacked("not admin")); + governor.setAdmin(devmsig); + + hevm.expectRevert(abi.encodePacked("not admin")); + governor.setVotingDelay(20); + + hevm.expectRevert(abi.encodePacked("not admin")); + governor.setVotingPeriod(20); + + hevm.prank(admin); + governor.setAdmin(devmsig); + + hevm.prank(admin); + hevm.expectRevert(abi.encodePacked("not pending admin")); + governor.acceptAdmin(); + + hevm.prank(devmsig); + governor.acceptAdmin(); + assertEq(governor.admin(), devmsig, "admin should be updated"); + } } diff --git a/src/test/FluxToken.t.sol b/src/test/FluxToken.t.sol index 3e234a6..22876a9 100644 --- a/src/test/FluxToken.t.sol +++ b/src/test/FluxToken.t.sol @@ -8,6 +8,80 @@ contract FluxTokenTest is BaseTest { setupContracts(block.timestamp); } + function testAdminFunctionErrors() external { + address admin = flux.admin(); + + hevm.expectRevert(abi.encodePacked("not admin")); + flux.setAdmin(devmsig); + + hevm.prank(admin); + hevm.expectRevert(abi.encodePacked("not pending admin")); + flux.acceptAdmin(); + + hevm.expectRevert(abi.encodePacked("not admin")); + flux.setVoter(devmsig); + + hevm.expectRevert(abi.encodePacked("not admin")); + flux.setVeALCX(devmsig); + + hevm.expectRevert(abi.encodePacked("not admin")); + flux.setAlchemechNFT(devmsig); + + hevm.expectRevert(abi.encodePacked("not admin")); + flux.setPatronNFT(devmsig); + + hevm.expectRevert(abi.encodePacked("not admin")); + flux.setNftMultiplier(1); + + hevm.expectRevert(abi.encodePacked("not admin")); + flux.setBptMultiplier(1); + } + + function testUpdateToZero() external { + address admin = flux.admin(); + address minter = flux.minter(); + + hevm.prank(admin); + flux.setAdmin(devmsig); + + hevm.startPrank(devmsig); + flux.acceptAdmin(); + + hevm.expectRevert(abi.encodePacked("FluxToken: voter cannot be zero address")); + flux.setVoter(address(0)); + + hevm.expectRevert(abi.encodePacked("FluxToken: veALCX cannot be zero address")); + flux.setVeALCX(address(0)); + + hevm.expectRevert(abi.encodePacked("FluxToken: alchemechNFT cannot be zero address")); + flux.setAlchemechNFT(address(0)); + + hevm.expectRevert(abi.encodePacked("FluxToken: patronNFT cannot be zero address")); + flux.setPatronNFT(address(0)); + + hevm.expectRevert(abi.encodePacked("FluxToken: nftMultiplier cannot be zero")); + flux.setNftMultiplier(0); + + hevm.expectRevert(abi.encodePacked("FluxToken: bptMultiplier cannot be zero")); + flux.setBptMultiplier(0); + + hevm.stopPrank(); + + hevm.prank(minter); + hevm.expectRevert(abi.encodePacked("FluxToken: minter cannot be zero address")); + flux.setMinter(address(0)); + } + + function testSetMultipliersLimit() external { + hevm.startPrank(admin); + + hevm.expectRevert(abi.encodePacked("FluxToken: nftMultiplier cannot be greater than BPS")); + flux.setNftMultiplier(BPS + 1); + + hevm.expectRevert(abi.encodePacked("FluxToken: bptMultiplier cannot be greater than BPS")); + flux.setBptMultiplier(BPS + 1); + } + function testMintFluxFromNFT() external { uint256 tokenId = 4; address ownerOfPatronNFT = IAlEthNFT(patronNFT).ownerOf(tokenId); @@ -36,6 +110,13 @@ contract FluxTokenTest is BaseTest { assertEq(flux.balanceOf(ownerOfAlchemechNFT), alchemechTotal, "owner should have alchemech flux"); } + function testCalculateBPT() external { + uint256 amount = 1000; + uint256 bptCalculation = flux.calculateBPT(amount); + + assertEq(amount * flux.bptMultiplier(), bptCalculation, "should calculate BPT correctly"); + } + function testMintFluxFromNFTErrors() external { uint256 tokenId = 4; address ownerOfPatronNFT = IAlEthNFT(patronNFT).ownerOf(tokenId); @@ -107,4 +188,66 @@ contract FluxTokenTest is BaseTest { assertEq(unclaimedFluxEnd, amountToRagequit, "should have all unclaimed flux"); } + + function testFluxActions() external { + address bribeAddress = voter.bribes(address(sushiGauge)); + + uint256 tokenId1 = createVeAlcx(admin, TOKEN_1, veALCX.MAXTIME(), false); + uint256 tokenId2 = createVeAlcx(beef, TOKEN_1, veALCX.MAXTIME(), false); + + uint256 amount1 = veALCX.claimableFlux(tokenId1); + uint256 amount2 = veALCX.claimableFlux(tokenId2); + + uint256 totalAmount = amount1 + amount2; + + uint256 unclaimedFlux1Start = flux.getUnclaimedFlux(tokenId1); + uint256 unclaimedFlux2Start = flux.getUnclaimedFlux(tokenId2); + + assertEq(unclaimedFlux1Start, 0, "should start with no unclaimed flux"); + assertEq(unclaimedFlux2Start, 0, "should start with no unclaimed flux"); + + address[] memory pools = new address[](1); + pools[0] = sushiPoolAddress; + uint256[] memory weights = new uint256[](1); + weights[0] = 5000; + + address[] memory bribes = new address[](1); + bribes[0] = address(bribeAddress); + address[][] memory tokens = new address[][](2); + tokens[0] = new address[](1); + tokens[0][0] = bal; + + hevm.prank(admin); + voter.vote(tokenId1, pools, weights, 0); + + hevm.prank(beef); + voter.vote(tokenId2, pools, weights, 0); + + // Fast forward epochs + hevm.warp(newEpoch()); + + voter.distribute(); + + hevm.expectRevert(abi.encodePacked("not voter")); + flux.updateFlux(tokenId1, amount1); + + hevm.expectRevert(abi.encodePacked("not voter")); + flux.accrueFlux(tokenId1); + + hevm.prank(address(voter)); + hevm.expectRevert(abi.encodePacked("not enough flux")); + flux.updateFlux(tokenId1, TOKEN_100K); + + hevm.expectRevert(abi.encodePacked("not veALCX")); + flux.mergeFlux(tokenId1, tokenId2); + + hevm.prank(address(veALCX)); + flux.mergeFlux(tokenId1, tokenId2); + + uint256 unclaimedFlux1End = flux.getUnclaimedFlux(tokenId1); + uint256 unclaimedFlux2End = flux.getUnclaimedFlux(tokenId2); + + assertEq(unclaimedFlux1End, 0, "should have no unclaimed flux"); + assertEq(unclaimedFlux2End, totalAmount, "should have all unclaimed flux"); + } } diff --git a/src/test/Minter.t.sol b/src/test/Minter.t.sol index a215740..3162b00 100644 --- a/src/test/Minter.t.sol +++ b/src/test/Minter.t.sol @@ -295,6 +295,11 @@ contract MinterTest is BaseTest { hevm.expectRevert(abi.encodePacked("insufficient balance to compound")); distributor.claim(tokenId, true); + + hevm.stopPrank(); + + hevm.expectRevert(abi.encodePacked("not approved")); + distributor.claim(tokenId, true); } // Compound claiming should revert if user doesn't provide enough weth @@ -320,6 +325,9 @@ contract MinterTest is BaseTest { hevm.expectRevert(abi.encodePacked("insufficient balance to compound")); distributor.claim(tokenId, true); + hevm.expectRevert(abi.encodePacked("Value must be 0 if not compounding")); + distributor.claim{ value: 1 ether }(tokenId, false); + distributor.claim{ value: 100 ether }(tokenId, true); assertGt(admin.balance, 0); assertGt(100 ether, admin.balance); @@ -332,12 +340,19 @@ contract MinterTest is BaseTest { hevm.expectRevert(abi.encodePacked("not initializer")); minter.initialize(); + hevm.prank(address(0)); + hevm.expectRevert(abi.encodePacked("already initialized")); + minter.initialize(); + hevm.expectRevert(abi.encodePacked("not admin")); minter.setAdmin(devmsig); hevm.expectRevert(abi.encodePacked("not admin")); minter.setVeAlcxEmissionsRate(1000); + hevm.expectRevert(abi.encodePacked("not voter")); + minter.updatePeriod(); + hevm.prank(admin); minter.setAdmin(devmsig); @@ -347,6 +362,10 @@ contract MinterTest is BaseTest { hevm.startPrank(devmsig); minter.acceptAdmin(); + + hevm.expectRevert(abi.encodePacked("cannot be greater than 100%")); + minter.setVeAlcxEmissionsRate(10_000 * 2); + minter.setVeAlcxEmissionsRate(1000); hevm.stopPrank(); diff --git a/src/test/RevenueHandler.t.sol b/src/test/RevenueHandler.t.sol index 38039b9..7908478 100644 --- a/src/test/RevenueHandler.t.sol +++ b/src/test/RevenueHandler.t.sol @@ -312,6 +312,22 @@ contract RevenueHandlerTest is BaseTest { assertEq(balAfter, claimable, "should be equal to amount claimed"); } + function testNotEnoughRevenueToClaim() external { + uint256 revAmt = 1000e18; + uint256 tokenId = _setupClaimableNonAlchemicRevenue(revAmt, bal); + uint256 balBefore = IERC20(bal).balanceOf(address(this)); + + assertEq(balBefore, 0, "should have no bal before claiming"); + + uint256 claimable = revenueHandler.claimable(tokenId, bal); + + hevm.prank(address(revenueHandler)); + IERC20(bal).transfer(admin, claimable); + + hevm.expectRevert(abi.encodePacked("Not enough revenue to claim")); + revenueHandler.claim(tokenId, bal, address(0), claimable, address(this)); + } + function testClaimNonApprovedRevenue() external { uint256 revAmt = 1000e18; uint256 tokenId = _setupClaimableNonAlchemicRevenue(revAmt, aura); @@ -606,4 +622,26 @@ contract RevenueHandlerTest is BaseTest { assertEq(revenueHandler.treasuryPct(), newPct); } } + + function testEnableRevenueToken() external { + address revenueToken = revenueHandler.revenueTokens(0); + + hevm.expectRevert(abi.encodePacked("Token enabled")); + revenueHandler.enableRevenueToken(revenueToken); + + revenueHandler.disableRevenueToken(revenueToken); + + hevm.expectRevert(abi.encodePacked("Token disabled")); + revenueHandler.disableRevenueToken(revenueToken); + + revenueHandler.enableRevenueToken(revenueToken); + } + + function testSetTreasury() external { + hevm.expectRevert(abi.encodePacked("treasury cannot be 0x0")); + revenueHandler.setTreasury(address(0)); + + revenueHandler.setTreasury(admin); + assertEq(revenueHandler.treasury(), admin, "treasury should be admin"); + } } diff --git a/src/test/RewardPoolManager.t.sol b/src/test/RewardPoolManager.t.sol new file mode 100644 index 0000000..59d4f74 --- /dev/null +++ b/src/test/RewardPoolManager.t.sol @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.15; + +import "./BaseTest.sol"; + +contract RewardPoolManagerTest is BaseTest { + function setUp() public { + setupContracts(block.timestamp); + } + + function testAdminFunctions() public { + address admin = rewardPoolManager.admin(); + + hevm.expectRevert(abi.encodePacked("not admin")); + rewardPoolManager.setAdmin(devmsig); + + hevm.prank(admin); + hevm.expectRevert(abi.encodePacked("not pending admin")); + rewardPoolManager.acceptAdmin(); + + hevm.expectRevert(abi.encodePacked("not admin")); + rewardPoolManager.setTreasury(devmsig); + + hevm.expectRevert(abi.encodePacked("not admin")); + rewardPoolManager.setRewardPool(devmsig); + + hevm.expectRevert(abi.encodePacked("not admin")); + rewardPoolManager.setPoolToken(devmsig); + + hevm.expectRevert(abi.encodePacked("not admin")); + rewardPoolManager.setVeALCX(devmsig); + + hevm.prank(admin); + rewardPoolManager.setAdmin(devmsig); + + hevm.startPrank(devmsig); + rewardPoolManager.acceptAdmin(); + + rewardPoolManager.setTreasury(devmsig); + + rewardPoolManager.setRewardPool(devmsig); + + rewardPoolManager.setPoolToken(address(usdc)); + + rewardPoolManager.setVeALCX(address(minter)); + + hevm.stopPrank(); + } + + function testDepositIntoRewardPoolError() public { + hevm.expectRevert(abi.encodePacked("must be veALCX")); + rewardPoolManager.depositIntoRewardPool(TOKEN_1); + } + + function testWithdrawFromRewardPool() public { + hevm.expectRevert(abi.encodePacked("must be veALCX")); + rewardPoolManager.withdrawFromRewardPool(1000); + } + + // Test depositing, withdrawing from a rewardPool (Aura pool) + function testRewardPool() public { + // Reward pool should be set + assertEq(rewardPool, rewardPoolManager.rewardPool()); + + deal(bpt, address(rewardPoolManager), TOKEN_1); + + // Initial amount of bal and aura rewards earned + uint256 rewardBalanceBefore1 = IERC20(bal).balanceOf(admin); + uint256 rewardBalanceBefore2 = IERC20(aura).balanceOf(admin); + assertEq(rewardBalanceBefore1, 0, "rewardBalanceBefore1 should be 0"); + assertEq(rewardBalanceBefore2, 0, "rewardBalanceBefore2 should be 0"); + + // Initial BPT balance of rewardPoolManager + uint256 amount = IERC20(bpt).balanceOf(address(rewardPoolManager)); + assertEq(amount, TOKEN_1); + + // Deposit BPT balance into rewardPool + hevm.prank(address(veALCX)); + rewardPoolManager.depositIntoRewardPool(amount); + + uint256 amountAfterDeposit = IERC20(bpt).balanceOf(address(rewardPoolManager)); + assertEq(amountAfterDeposit, 0, "full balance should be deposited"); + + uint256 rewardPoolBalance = IRewardPool4626(rewardPool).balanceOf(address(rewardPoolManager)); + assertEq(rewardPoolBalance, amount, "rewardPool balance should equal amount deposited"); + + // Fast forward to accumulate rewards + hevm.warp(block.timestamp + 2 weeks); + + hevm.expectRevert(abi.encodePacked("not admin")); + rewardPoolManager.claimRewardPoolRewards(); + + hevm.prank(admin); + rewardPoolManager.claimRewardPoolRewards(); + uint256 rewardBalanceAfter1 = IERC20(bal).balanceOf(address(admin)); + uint256 rewardBalanceAfter2 = IERC20(aura).balanceOf(address(admin)); + + // After claiming rewards admin bal balance should increase + assertGt(rewardBalanceAfter1, rewardBalanceBefore1, "should accumulate bal rewards"); + assertGt(rewardBalanceAfter2, rewardBalanceBefore2, "should accumulate aura rewards"); + + hevm.prank(address(veALCX)); + rewardPoolManager.withdrawFromRewardPool(amount); + + // veALCX BPT balance should equal original amount after withdrawing from rewardPool + uint256 amountAfterWithdraw = IERC20(bpt).balanceOf(address(veALCX)); + assertEq(amountAfterWithdraw, amount, "should equal original amount"); + + // Only rewardPoolManager admin can update rewardPool + hevm.expectRevert(abi.encodePacked("not admin")); + rewardPoolManager.setRewardPool(sushiPoolAddress); + + hevm.prank(admin); + rewardPoolManager.setRewardPool(sushiPoolAddress); + + // Reward pool should update + assertEq(sushiPoolAddress, rewardPoolManager.rewardPool(), "rewardPool not updated"); + } + + function testUpdatingRewardPoolTokens() public { + address admin = rewardPoolManager.admin(); + + address[] memory tokens = new address[](2); + tokens[0] = dai; + tokens[1] = usdt; + + hevm.expectRevert(abi.encodePacked("not admin")); + rewardPoolManager.swapOutRewardPoolToken(0, bal, usdc); + + hevm.expectRevert(abi.encodePacked("not admin")); + rewardPoolManager.addRewardPoolTokens(tokens); + + hevm.expectRevert(abi.encodePacked("not admin")); + rewardPoolManager.addRewardPoolToken(dai); + + hevm.startPrank(admin); + + hevm.expectRevert(abi.encodePacked("incorrect token")); + rewardPoolManager.swapOutRewardPoolToken(0, dai, usdc); + + rewardPoolManager.swapOutRewardPoolToken(0, bal, usdc); + assertEq(rewardPoolManager.rewardPoolTokens(0), usdc, "rewardPoolTokens[0] should be usdc"); + + rewardPoolManager.addRewardPoolTokens(tokens); + assertEq(rewardPoolManager.rewardPoolTokens(2), dai, "rewardPoolTokens[2] should be dai"); + assertEq(rewardPoolManager.rewardPoolTokens(3), usdt, "rewardPoolTokens[3] should be usdt"); + } + + function testMaxRewardPoolTokens() public { + address[] memory tokens = new address[](8); + tokens[0] = dai; + tokens[1] = usdt; + tokens[2] = usdc; + tokens[3] = bpt; + tokens[4] = time; + tokens[5] = aleth; + tokens[6] = alusd3crv; + tokens[7] = alusd; + + hevm.prank(admin); + rewardPoolManager.addRewardPoolTokens(tokens); + + hevm.prank(admin); + hevm.expectRevert(abi.encodePacked("too many reward pool tokens")); + rewardPoolManager.addRewardPoolToken(beef); + } +} diff --git a/src/test/Voting.t.sol b/src/test/Voting.t.sol index f3ae124..643631c 100644 --- a/src/test/Voting.t.sol +++ b/src/test/Voting.t.sol @@ -23,6 +23,18 @@ contract VotingTest is BaseTest { voter.distribute(); + deal(address(alcx), address(voter), TOKEN_100K); + + hevm.expectRevert(abi.encodePacked("not voter")); + sushiGauge.notifyRewardAmount(TOKEN_100K); + + hevm.prank(address(voter)); + hevm.expectRevert(abi.encodePacked("zero amount")); + sushiGauge.notifyRewardAmount(0); + + hevm.prank(address(voter)); + sushiGauge.notifyRewardAmount(TOKEN_100K); + uint256 distributorBal2 = alcx.balanceOf(address(distributor)); uint256 voterBal2 = alcx.balanceOf(address(voter)); @@ -83,6 +95,7 @@ contract VotingTest is BaseTest { hevm.startPrank(admin); hevm.expectRevert(abi.encodePacked("no rewards to claim")); voter.claimBribes(bribes, tokens, tokenId); + distributor.claim(tokenId, false); hevm.stopPrank(); @@ -161,12 +174,48 @@ contract VotingTest is BaseTest { pools[0] = alUsdPoolAddress; voter.killGauge(voter.gauges(alUsdPoolAddress)); + hevm.expectRevert(abi.encodePacked("exists")); + voter.createGauge(alUsdPoolAddress, IVoter.GaugeType.Passthrough); + hevm.expectRevert(abi.encodePacked("cannot vote for dead gauge")); voter.vote(tokenId, pools, weights, 0); hevm.stopPrank(); } + function testManageGauge() public { + address emergencyCouncil = voter.emergencyCouncil(); + address gaugeAddress = voter.gauges(alUsdPoolAddress); + + bool isGaugeAlive = voter.isAlive(gaugeAddress); + assertEq(isGaugeAlive, true, "gauge should be alive"); + + hevm.expectRevert(abi.encodePacked("not emergency council")); + voter.killGauge(gaugeAddress); + + hevm.prank(emergencyCouncil); + voter.killGauge(gaugeAddress); + + hevm.prank(emergencyCouncil); + hevm.expectRevert(abi.encodePacked("gauge already dead")); + voter.killGauge(gaugeAddress); + + hevm.prank(beef); + hevm.expectRevert(abi.encodePacked("not emergency council")); + voter.reviveGauge(gaugeAddress); + + hevm.prank(emergencyCouncil); + hevm.expectRevert(abi.encodePacked("invalid gauge")); + voter.reviveGauge(beef); + + hevm.prank(emergencyCouncil); + voter.reviveGauge(gaugeAddress); + + hevm.prank(emergencyCouncil); + hevm.expectRevert(abi.encodePacked("gauge already alive")); + voter.reviveGauge(gaugeAddress); + } + function testNextEpochVoteOrReset() public { uint256 tokenId = createVeAlcx(admin, TOKEN_1, MAXTIME, false); @@ -206,10 +255,16 @@ contract VotingTest is BaseTest { // Next epoch hevm.warp(block.timestamp + nextEpoch); - // Resetting succeeds + hevm.stopPrank(); + + // Resetting fails when not approved or owner + hevm.prank(beef); + hevm.expectRevert(abi.encodePacked("not approved or owner")); voter.reset(tokenId); - hevm.stopPrank(); + // Resetting succeeds + hevm.prank(admin); + voter.reset(tokenId); } // veALCX holders should be able to accrue their unclaimed flux over epochs @@ -402,6 +457,10 @@ contract VotingTest is BaseTest { hevm.expectRevert(abi.encodePacked("bribe tokens must be whitelisted")); IBribe(bribeAddress).notifyRewardAmount(usdc, TOKEN_100K); + // Bribe amount must be greater than 0 + hevm.expectRevert(abi.encodePacked("reward amount must be greater than 0")); + IBribe(bribeAddress).notifyRewardAmount(usdc, 0); + uint256 rewardApplicable = IBribe(bribeAddress).lastTimeRewardApplicable(bal); assertEq(rewardApplicable, block.timestamp, "reward applicable should be current timestamp"); @@ -507,6 +566,45 @@ contract VotingTest is BaseTest { ); } + function testPriorVotingIndexZero() public { + uint256 initialTimestamp = block.timestamp; + + uint256 tokenId1 = createVeAlcx(admin, TOKEN_1, MAXTIME, false); + + address bribeAddress = voter.bribes(address(sushiGauge)); + + // Prior index should be zero when nCheckpoints is zero + uint256 priorIndex = IBribe(bribeAddress).getPriorVotingIndex(initialTimestamp); + assertEq(priorIndex, 0, "prior voting index should be 0"); + + uint256 earned = IBribe(bribeAddress).earned(address(alcx), tokenId1); + assertEq(earned, 0, "earned should be 0 when there are no checkpoints for a token"); + + // Add BAL bribes to sushiGauge + createThirdPartyBribe(bribeAddress, bal, TOKEN_100K); + + address[] memory pools = new address[](1); + pools[0] = sushiPoolAddress; + uint256[] memory weights = new uint256[](1); + weights[0] = 5000; + + address[] memory bribes = new address[](1); + bribes[0] = address(bribeAddress); + address[][] memory tokens = new address[][](2); + tokens[0] = new address[](1); + tokens[0][0] = bal; + + hevm.prank(admin); + voter.vote(tokenId1, pools, weights, 0); + + // Fast forward epochs + hevm.warp(block.timestamp + nextEpoch * 5); + + // Prior index should be zero when timestamp is before first checkpoint timestamp + uint256 priorIndexNow = IBribe(bribeAddress).getPriorVotingIndex(initialTimestamp - nextEpoch * 5); + assertEq(priorIndexNow, 0, "prior voting index should be 0"); + } + // Test impact of voting on bribes earned function testBribeAccounting() public { uint256 tokenId1 = createVeAlcx(admin, TOKEN_1, MAXTIME, false); @@ -1157,6 +1255,11 @@ contract VotingTest is BaseTest { // Mock poking idle tokens to sync voting hevm.stopPrank(); + + // Only admin can poke tokens + hevm.expectRevert(abi.encodePacked("not admin")); + voter.pokeTokens(tokens); + hevm.prank(voter.admin()); voter.pokeTokens(tokens); hevm.startPrank(admin); @@ -1190,7 +1293,62 @@ contract VotingTest is BaseTest { hevm.stopPrank(); } + function testGaugeAdminFunctions() public { + address gaugeAdmin = sushiGauge.admin(); + + hevm.prank(beef); + hevm.expectRevert(abi.encodePacked("not admin")); + sushiGauge.setAdmin(beef); + + hevm.prank(beef); + hevm.expectRevert(abi.encodePacked("not admin")); + sushiGauge.updateReceiver(beef); + + hevm.prank(gaugeAdmin); + sushiGauge.setAdmin(beef); + + hevm.prank(gaugeAdmin); + hevm.expectRevert(abi.encodePacked("not pending admin")); + sushiGauge.acceptAdmin(); + + hevm.prank(beef); + sushiGauge.acceptAdmin(); + + hevm.prank(beef); + hevm.expectRevert(abi.encodePacked("cannot be zero address")); + sushiGauge.updateReceiver(address(0)); + + hevm.prank(beef); + hevm.expectRevert(abi.encodePacked("same receiver")); + sushiGauge.updateReceiver(sushiPoolAddress); + + hevm.prank(beef); + sushiGauge.updateReceiver(admin); + + address newReceiver = sushiGauge.receiver(); + assertEq(newReceiver, admin, "receiver should be updated"); + } + function testAdminFunctions() public { + hevm.expectRevert(abi.encodePacked("not voter")); + veALCX.updateLock(1); + + hevm.prank(address(voter)); + hevm.expectRevert(abi.encodePacked("not max locked")); + veALCX.updateLock(1); + + hevm.expectRevert(abi.encodePacked("not emergency council")); + voter.setEmergencyCouncil(devmsig); + + address emergencyCouncil = voter.emergencyCouncil(); + + hevm.prank(emergencyCouncil); + hevm.expectRevert(abi.encodePacked("cannot be zero address")); + voter.setEmergencyCouncil(address(0)); + + hevm.prank(emergencyCouncil); + voter.setEmergencyCouncil(devmsig); + hevm.expectRevert(abi.encodePacked("not admin")); voter.setMinter(admin); @@ -1200,6 +1358,15 @@ contract VotingTest is BaseTest { hevm.expectRevert(abi.encodePacked("not admin")); voter.setBoostMultiplier(1000); + hevm.expectRevert(abi.encodePacked("not admin")); + voter.whitelist(dai); + + hevm.expectRevert(abi.encodePacked("not admin")); + voter.removeFromWhitelist(dai); + + hevm.expectRevert(abi.encodePacked("not admin")); + voter.createGauge(alUsdPoolAddress, IVoter.GaugeType.Passthrough); + hevm.prank(address(timelockExecutor)); voter.setAdmin(devmsig); @@ -1209,12 +1376,189 @@ contract VotingTest is BaseTest { hevm.startPrank(devmsig); voter.acceptAdmin(); + + hevm.expectRevert(abi.encodePacked("Boost multiplier is out of bounds")); + voter.setBoostMultiplier(10_000 + 1); + voter.setBoostMultiplier(1000); + hevm.expectRevert(abi.encodePacked("cannot be zero address")); + voter.whitelist(address(0)); + + assertEq(voter.isWhitelisted(address(0)), false, "zero address should not be whitelisted"); + + hevm.expectRevert(abi.encodePacked("token not whitelisted")); + voter.removeFromWhitelist(dai); + + voter.whitelist(dai); + + hevm.expectRevert(abi.encodePacked("token already whitelisted")); voter.whitelist(dai); assertEq(voter.isWhitelisted(dai), true, "whitelisting failed"); + voter.removeFromWhitelist(dai); + + assertEq(voter.isWhitelisted(dai), false, "remove whitelisting failed"); + hevm.stopPrank(); } + + function testAddingRewardTokenErrors() public { + address bribeAddress = voter.bribes(address(sushiGauge)); + hevm.prank(admin); + hevm.expectRevert(abi.encodePacked("not being set by a gauge")); + IBribe(bribeAddress).addRewardToken(usdt); + + hevm.prank(address(voter)); + hevm.expectRevert(abi.encodePacked("New token must be whitelisted")); + IBribe(bribeAddress).swapOutRewardToken(0, dai, usdt); + + hevm.prank(address(timelockExecutor)); + voter.whitelist(usdt); + + hevm.prank(address(voter)); + hevm.expectRevert(abi.encodePacked("Old token mismatch")); + IBribe(bribeAddress).swapOutRewardToken(0, usdc, usdt); + + hevm.prank(address(sushiGauge)); + IBribe(bribeAddress).addRewardToken(usdt); + + hevm.prank(address(voter)); + hevm.expectRevert(abi.encodePacked("New token already exists")); + IBribe(bribeAddress).swapOutRewardToken(1, usdt, usdt); + } + + function testSwapOutRewardToken() public { + address bribeAddress = voter.bribes(address(sushiGauge)); + + hevm.prank(beef); + hevm.expectRevert(abi.encodePacked("only admin can swap reward tokens")); + voter.swapReward(address(sushiGauge), 0, dai, usdt); + + hevm.prank(address(timelockExecutor)); + voter.whitelist(usdt); + + hevm.prank(address(sushiGauge)); + IBribe(bribeAddress).addRewardToken(usdt); + + assertEq(IBribe(bribeAddress).rewards(0), address(alcx), "reward token should be alcx"); + assertEq(IBribe(bribeAddress).rewards(1), usdt, "reward token should be usdt"); + + hevm.prank(address(timelockExecutor)); + voter.whitelist(dai); + + hevm.prank(address(voter)); + IBribe(bribeAddress).swapOutRewardToken(1, usdt, dai); + + assertEq(IBribe(bribeAddress).rewards(1), dai, "reward token should have updated to be dai"); + } + + function testNotifyRewardAmountNoVotes() public { + deal(address(alcx), address(minter), TOKEN_100K); + + uint256 minterAlcxBalance = IERC20(alcx).balanceOf(address(minter)); + assertEq(minterAlcxBalance, TOKEN_100K, "minter should have balance"); + + uint256 voterAlcxBalance = IERC20(alcx).balanceOf(address(voter)); + assertEq(voterAlcxBalance, 0, "voter should have no balance"); + + hevm.expectRevert(abi.encodePacked("only minter can send rewards")); + voter.notifyRewardAmount(TOKEN_100K); + + hevm.prank(address(minter)); + // Distributing rewards without any votes should revert + hevm.expectRevert(abi.encodePacked("no votes")); + voter.notifyRewardAmount(TOKEN_100K); + + uint256 minterAlcxBalanceAfter = IERC20(alcx).balanceOf(address(minter)); + assertEq(minterAlcxBalanceAfter, TOKEN_100K, "minter should have the same balance"); + } + + function testNotifyRewardAmount() public { + // Create a token and vote to create votes + uint256 tokenId1 = createVeAlcx(admin, TOKEN_1, 3 weeks, false); + + uint256[] memory tokens = new uint256[](2); + tokens[0] = tokenId1; + + address[] memory pools = new address[](1); + pools[0] = sushiPoolAddress; + uint256[] memory weights = new uint256[](1); + weights[0] = 5000; + + hevm.prank(admin); + voter.vote(tokenId1, pools, weights, 0); + + deal(address(alcx), address(minter), TOKEN_100K); + + uint256 minterAlcxBalance = IERC20(alcx).balanceOf(address(minter)); + assertEq(minterAlcxBalance, TOKEN_100K, "minter should have balance"); + + uint256 voterAlcxBalance = IERC20(alcx).balanceOf(address(voter)); + assertEq(voterAlcxBalance, 0, "voter should have no balance"); + + hevm.startPrank(address(minter)); + alcx.approve(address(voter), TOKEN_100K); + voter.notifyRewardAmount(TOKEN_100K); + hevm.stopPrank(); + + uint256 minterAlcxBalanceAfter = IERC20(alcx).balanceOf(address(minter)); + assertEq(minterAlcxBalanceAfter, 0, "minter should have distributed its balance"); + + uint256 voterAlcxBalanceAfter = IERC20(alcx).balanceOf(address(voter)); + assertEq(voterAlcxBalanceAfter, TOKEN_100K, "voter should have received balance"); + } + + function testSettingGauge() public { + address bribeAddress = voter.bribes(address(sushiGauge)); + + hevm.expectRevert(abi.encodePacked("gauge already set")); + IBribe(bribeAddress).setGauge(devmsig); + } + + function testVotingErrors() public { + uint256 tokenId = createVeAlcx(admin, TOKEN_1, MAXTIME, false); + + address[] memory pools = new address[](0); + + uint256[] memory weights = new uint256[](2); + weights[0] = 5000; + weights[1] = 5000; + + uint256[] memory weights2 = new uint256[](0); + + address[] memory pools2 = new address[](6); + pools2[0] = sushiPoolAddress; + pools2[1] = sushiPoolAddress; + pools2[2] = sushiPoolAddress; + pools2[3] = sushiPoolAddress; + pools2[4] = sushiPoolAddress; + pools2[5] = sushiPoolAddress; + + uint256[] memory weights3 = new uint256[](6); + weights3[0] = 5000; + weights3[1] = 5000; + weights3[2] = 5000; + weights3[3] = 5000; + weights3[4] = 5000; + weights3[5] = 5000; + + address[] memory gauges = new address[](1); + gauges[0] = address(sushiGauge); + + hevm.expectRevert(abi.encodePacked("not approved or owner")); + voter.vote(tokenId, pools, weights, 0); + + hevm.startPrank(admin); + + hevm.expectRevert(abi.encodePacked("pool vote and weights mismatch")); + voter.vote(tokenId, pools, weights, 0); + + hevm.expectRevert(abi.encodePacked("no pools voted for")); + voter.vote(tokenId, pools, weights2, 0); + + hevm.expectRevert(abi.encodePacked("invalid pools")); + voter.vote(tokenId, pools2, weights3, 0); + } } diff --git a/src/test/VotingEscrow.t.sol b/src/test/VotingEscrow.t.sol index ce39526..6e828a3 100644 --- a/src/test/VotingEscrow.t.sol +++ b/src/test/VotingEscrow.t.sol @@ -45,63 +45,6 @@ contract VotingEscrowTest is BaseTest { hevm.stopPrank(); } - // Test depositing, withdrawing from a rewardPool (Aura pool) - function testRewardPool() public { - // Reward pool should be set - assertEq(rewardPool, rewardPoolManager.rewardPool()); - - deal(bpt, address(rewardPoolManager), TOKEN_1); - - // Initial amount of bal and aura rewards earned - uint256 rewardBalanceBefore1 = IERC20(bal).balanceOf(admin); - uint256 rewardBalanceBefore2 = IERC20(aura).balanceOf(admin); - assertEq(rewardBalanceBefore1, 0, "rewardBalanceBefore1 should be 0"); - assertEq(rewardBalanceBefore2, 0, "rewardBalanceBefore2 should be 0"); - - // Initial BPT balance of rewardPoolManager - uint256 amount = IERC20(bpt).balanceOf(address(rewardPoolManager)); - assertEq(amount, TOKEN_1); - - // Deposit BPT balance into rewardPool - hevm.prank(address(veALCX)); - rewardPoolManager.depositIntoRewardPool(amount); - - uint256 amountAfterDeposit = IERC20(bpt).balanceOf(address(rewardPoolManager)); - assertEq(amountAfterDeposit, 0, "full balance should be deposited"); - - uint256 rewardPoolBalance = IRewardPool4626(rewardPool).balanceOf(address(rewardPoolManager)); - assertEq(rewardPoolBalance, amount, "rewardPool balance should equal amount deposited"); - - // Fast forward to accumulate rewards - hevm.warp(block.timestamp + 2 weeks); - - hevm.prank(admin); - rewardPoolManager.claimRewardPoolRewards(); - uint256 rewardBalanceAfter1 = IERC20(bal).balanceOf(address(admin)); - uint256 rewardBalanceAfter2 = IERC20(aura).balanceOf(address(admin)); - - // After claiming rewards admin bal balance should increase - assertGt(rewardBalanceAfter1, rewardBalanceBefore1, "should accumulate bal rewards"); - assertGt(rewardBalanceAfter2, rewardBalanceBefore2, "should accumulate aura rewards"); - - hevm.prank(address(veALCX)); - rewardPoolManager.withdrawFromRewardPool(amount); - - // veALCX BPT balance should equal original amount after withdrawing from rewardPool - uint256 amountAfterWithdraw = IERC20(bpt).balanceOf(address(veALCX)); - assertEq(amountAfterWithdraw, amount, "should equal original amount"); - - // Only rewardPoolManager admin can update rewardPool - hevm.expectRevert(abi.encodePacked("not admin")); - rewardPoolManager.setRewardPool(sushiPoolAddress); - - hevm.prank(admin); - rewardPoolManager.setRewardPool(sushiPoolAddress); - - // Reward pool should update - assertEq(sushiPoolAddress, rewardPoolManager.rewardPool(), "rewardPool not updated"); - } - function testUpdateLockDuration() public { hevm.startPrank(admin); @@ -376,6 +319,15 @@ contract VotingEscrowTest is BaseTest { assertGt(unclaimedFlux3, unclaimedFlux2, "unclaimed flux should be greater for active voter"); } + function testOnlyDepositorFunctions() public { + // Distributor should be set + hevm.expectRevert(abi.encodePacked("only depositor")); + distributor.setDepositor(beef); + + hevm.expectRevert(abi.encodePacked("only depositor")); + distributor.checkpointToken(); + } + // Voting should not impact amount of ALCX rewards earned function testRewardsClaiming() public { uint256 tokenId1 = createVeAlcx(admin, TOKEN_1, MAXTIME, false); @@ -468,6 +420,11 @@ contract VotingEscrowTest is BaseTest { function testMergeTokens() public { uint256 tokenId1 = createVeAlcx(admin, TOKEN_1, MAXTIME, false); uint256 tokenId2 = createVeAlcx(admin, TOKEN_100K, MAXTIME / 2, false); + uint256 tokenId3 = createVeAlcx(beef, TOKEN_100K, MAXTIME / 2, false); + + hevm.prank(beef); + hevm.expectRevert(abi.encodePacked("not approved or owner")); + veALCX.merge(tokenId1, tokenId2); hevm.startPrank(admin); @@ -500,6 +457,9 @@ contract VotingEscrowTest is BaseTest { hevm.expectRevert(abi.encodePacked("must be different tokens")); veALCX.merge(tokenId1, tokenId1); + hevm.expectRevert(abi.encodePacked("not approved or owner")); + veALCX.merge(tokenId1, tokenId3); + veALCX.merge(tokenId1, tokenId2); uint256 unclaimedFluxAfter1 = flux.getUnclaimedFlux(tokenId1); @@ -552,6 +512,12 @@ contract VotingEscrowTest is BaseTest { // Fast forward to lock end of tokenId2 hevm.warp(block.timestamp + THREE_WEEKS); + hevm.expectRevert(abi.encodePacked("not approved or owner")); + veALCX.withdraw(tokenId1); + + hevm.expectRevert(abi.encodePacked("not approved or owner")); + veALCX.startCooldown(tokenId1); + hevm.startPrank(admin); // Should not be able to withdraw BPT @@ -655,6 +621,9 @@ contract VotingEscrowTest is BaseTest { hevm.prank(address(veALCX)); flux.mint(admin, ragequitAmount); + hevm.expectRevert(abi.encodePacked("not approved or owner")); + veALCX.updateUnlockTime(tokenId, 1 days, true); + hevm.startPrank(admin); flux.approve(address(veALCX), ragequitAmount); veALCX.startCooldown(tokenId); @@ -977,4 +946,73 @@ contract VotingEscrowTest is BaseTest { uint256 newVotingPowerBeef = veALCX.getVotes(beef); assertEq(newVotingPowerBeef, balanceOfTokens, "incorrect voting power"); } + + function testAdminFunctions() public { + hevm.expectRevert(abi.encodePacked("not admin")); + veALCX.setTreasury(beef); + + hevm.expectRevert(abi.encodePacked("not admin")); + veALCX.setVoter(beef); + + hevm.expectRevert(abi.encodePacked("not admin")); + veALCX.setRewardsDistributor(beef); + + hevm.expectRevert(abi.encodePacked("not admin")); + veALCX.setfluxMultiplier(0); + + hevm.expectRevert(abi.encodePacked("not admin")); + veALCX.setRewardPoolManager(beef); + + hevm.expectRevert(abi.encodePacked("not admin")); + veALCX.setAdmin(beef); + + hevm.expectRevert(abi.encodePacked("not admin")); + veALCX.setfluxPerVeALCX(0); + + hevm.expectRevert(abi.encodePacked("not voter")); + veALCX.voting(0); + + hevm.expectRevert(abi.encodePacked("not voter")); + veALCX.abstain(0); + + hevm.prank(admin); + veALCX.setAdmin(beef); + + hevm.expectRevert(abi.encodePacked("not pending admin")); + veALCX.acceptAdmin(); + + hevm.prank(admin); + hevm.expectRevert(abi.encodePacked("treasury cannot be 0x0")); + veALCX.setTreasury(address(0)); + + hevm.prank(admin); + hevm.expectRevert(abi.encodePacked("fluxMultiplier must be greater than 0")); + veALCX.setfluxMultiplier(0); + + hevm.prank(admin); + veALCX.setTreasury(beef); + assertEq(veALCX.treasury(), beef, "incorrect treasury"); + + hevm.expectRevert(abi.encodePacked("owner not found")); + veALCX.approve(admin, 100); + + hevm.expectRevert(abi.encodePacked("cannot delegate to zero address")); + veALCX.delegate(address(0)); + + hevm.prank(beef); + veALCX.acceptAdmin(); + + hevm.startPrank(beef); + + veALCX.setfluxPerVeALCX(10); + assertEq(veALCX.fluxPerVeALCX(), 10, "incorrect flux per veALCX"); + + veALCX.setfluxMultiplier(10); + assertEq(veALCX.fluxMultiplier(), 10, "incorrect flux multiplier"); + + veALCX.setClaimFee(1000); + assertEq(veALCX.claimFeeBps(), 1000, "incorrect claim fee"); + + hevm.stopPrank(); + } }