diff --git a/contracts/Reliquary.sol b/contracts/Reliquary.sol index d650fad..d5c0e18 100644 --- a/contracts/Reliquary.sol +++ b/contracts/Reliquary.sol @@ -794,7 +794,7 @@ contract Reliquary is IReliquary, Multicall, ERC721, AccessControlEnumerable, Re } } - // @dev Deposit LP tokens to earn THE. + // @dev Deposit LP tokens to earn gauge rewards. function updatePoolWithGaugeDeposit(uint256 _pid) public { if (paused) revert Reliquary__PAUSED(); DoubleStakingLogic.updatePoolWithGaugeDeposit(poolInfo, _pid); @@ -808,8 +808,8 @@ contract Reliquary is IReliquary, Multicall, ERC721, AccessControlEnumerable, Re DoubleStakingLogic.enableGauge(voter, poolInfo, _pid); } - function disableGauge(uint256 _pid, bool _claimRewards) public onlyRole(OPERATOR) { - DoubleStakingLogic.disableGauge(voter, poolInfo, _pid, gaugeRewardReceiver, _claimRewards); + function disableGauge(uint256 _pid, address[] calldata _claimRewardsTokens) public onlyRole(OPERATOR) { + DoubleStakingLogic.disableGauge(voter, poolInfo, _pid, gaugeRewardReceiver, _claimRewardsTokens); } function setGaugeReceiver(address _gaugeRewardReceiver) public onlyRole(OPERATOR) { @@ -817,9 +817,9 @@ contract Reliquary is IReliquary, Multicall, ERC721, AccessControlEnumerable, Re gaugeRewardReceiver = _gaugeRewardReceiver; } - function claimGaugeRewards(uint256 _pid) public { + function claimGaugeRewards(uint256 _pid, address[] calldata _rewardTokens) public { if (paused) revert Reliquary__PAUSED(); - DoubleStakingLogic.claimGaugeRewards(voter, poolInfo, gaugeRewardReceiver, _pid); + DoubleStakingLogic.claimGaugeRewards(voter, poolInfo, gaugeRewardReceiver, _pid, _rewardTokens); } function pause() external onlyRole(GUARDIAN) { diff --git a/contracts/interfaces/IGauge.sol b/contracts/interfaces/IGauge.sol index ef1d270..2e73884 100644 --- a/contracts/interfaces/IGauge.sol +++ b/contracts/interfaces/IGauge.sol @@ -1,108 +1,37 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.13; interface IGauge { - error NotAlive(); - error NotAuthorized(); - error NotVoter(); - error NotTeam(); - error RewardRateTooHigh(); - error ZeroAmount(); - error ZeroRewardRate(); + function deposit(uint256 amount, uint256 tokenId) external; + + function withdraw(uint256 amount) external; - event Deposit(address indexed from, address indexed to, uint256 amount); - event Withdraw(address indexed from, uint256 amount); - event NotifyReward(address indexed from, uint256 amount); - event ClaimFees(address indexed from, uint256 claimed0, uint256 claimed1); - event ClaimRewards(address indexed from, uint256 amount); + function notifyRewardAmount(address token, uint amount) external; - /// @notice Address of the pool LP token which is deposited (staked) for rewards - function stakingToken() external view returns (address); + function getReward(address account, address[] memory tokens) external; - /// @notice Address of the token (VELO v2) rewarded to stakers - function rewardToken() external view returns (address); + function claimFees() external returns (uint claimed0, uint claimed1); - /// @notice Address of the FeesVotingReward contract linked to the gauge - function feesVotingReward() external view returns (address); + function left(address token) external view returns (uint); - /// @notice Address of Velodrome v2 Voter - function voter() external view returns (address); + function isForPair() external view returns (bool); - /// @notice Address of Velodrome v2 Team - function team() external view returns (address); + function whitelistNotifiedRewards(address token) external; - /// @notice Returns if gauge is linked to a legitimate Velodrome pool - function isPool() external view returns (bool); + function removeRewardWhitelist(address token) external; - /// @notice Timestamp end of current rewards period - function periodFinish() external view returns (uint256); + function rewardsListLength() external view returns (uint256); - /// @notice Current reward rate of rewardToken to distribute per second - function rewardRate() external view returns (uint256); + function rewards(uint256 index) external view returns (address); - /// @notice Most recent timestamp contract has updated state - function lastUpdateTime() external view returns (uint256); + function earned( + address token, + address account + ) external view returns (uint256); - /// @notice Most recent stored value of rewardPerToken - function rewardPerTokenStored() external view returns (uint256); - - /// @notice Amount of stakingToken deposited for rewards - function totalSupply() external view returns (uint256); - - /// @notice Get the amount of stakingToken deposited by an account function balanceOf(address) external view returns (uint256); - /// @notice Cached rewardPerTokenStored for an account based on their most recent action - function userRewardPerTokenPaid(address) external view returns (uint256); - - /// @notice Cached amount of rewardToken earned for an account - function rewards(address) external view returns (uint256); - - /// @notice View to see the rewardRate given the timestamp of the start of the epoch - function rewardRateByEpoch(uint256) external view returns (uint256); - - /// @notice Cached amount of fees generated from the Pool linked to the Gauge of token0 - function fees0() external view returns (uint256); - - /// @notice Cached amount of fees generated from the Pool linked to the Gauge of token1 - function fees1() external view returns (uint256); - - /// @notice Get the current reward rate per unit of stakingToken deposited - function rewardPerToken() external view returns (uint256 _rewardPerToken); - - /// @notice Returns the last time the reward was modified or periodFinish if the reward has ended - function lastTimeRewardApplicable() external view returns (uint256 _time); - - /// @notice Returns accrued balance to date from last claim / first deposit. - function earned(address _account) external view returns (uint256 _earned); - - /// @notice Total amount of rewardToken to distribute for the current rewards period - function left() external view returns (uint256 _left); - - /// @notice Retrieve rewards for an address. - /// @dev Throws if not called by same address or voter. - /// @param _account . - function getReward(address _account) external; - - /// @notice Deposit LP tokens into gauge for msg.sender - /// @param _amount . - function deposit(uint256 _amount) external; - - /// @notice Deposit LP tokens into gauge for any user - /// @param _amount . - /// @param _recipient Recipient to give balance to - function deposit(uint256 _amount, address _recipient) external; - - /// @notice Withdraw LP tokens for user - /// @param _amount . - function withdraw(uint256 _amount) external; - - /// @dev Notifies gauge of gauge rewards. Assumes gauge reward tokens is 18 decimals. - /// If not 18 decimals, rewardRate may have rounding issues. - function notifyRewardAmount(uint256 amount) external; + function derivedBalances(address) external view returns (uint256); - /// @dev Notifies gauge of gauge rewards without distributing its fees. - /// Assumes gauge reward tokens is 18 decimals. - /// If not 18 decimals, rewardRate may have rounding issues. - function notifyRewardWithoutClaim(uint256 amount) external; + function rewardRate(address) external view returns (uint256); } \ No newline at end of file diff --git a/contracts/interfaces/IVoter.sol b/contracts/interfaces/IVoter.sol index 6a1a429..74e9382 100644 --- a/contracts/interfaces/IVoter.sol +++ b/contracts/interfaces/IVoter.sol @@ -1,268 +1,119 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity =0.7.6 || ^0.8.13; +pragma abicoder v2; interface IVoter { - error AlreadyVotedOrDeposited(); - error DistributeWindow(); - error FactoryPathNotApproved(); - error GaugeAlreadyKilled(); - error GaugeAlreadyRevived(); - error GaugeExists(); - error GaugeDoesNotExist(address _pool); - error GaugeNotAlive(address _gauge); - error InactiveManagedNFT(); - error MaximumVotingNumberTooLow(); - error NonZeroVotes(); - error NotAPool(); - error NotApprovedOrOwner(); - error NotGovernor(); - error NotEmergencyCouncil(); - error NotMinter(); - error NotWhitelistedNFT(); - error NotWhitelistedToken(); - error SameValue(); - error SpecialVotingWindow(); - error TooManyPools(); - error UnequalLengths(); - error ZeroBalance(); - error ZeroAddress(); - - event GaugeCreated( - address indexed poolFactory, - address indexed votingRewardsFactory, - address indexed gaugeFactory, - address pool, - address bribeVotingReward, - address feeVotingReward, - address gauge, - address creator - ); - event GaugeKilled(address indexed gauge); - event GaugeRevived(address indexed gauge); - event Voted( - address indexed voter, - address indexed pool, - uint256 indexed tokenId, - uint256 weight, - uint256 totalWeight, - uint256 timestamp - ); - event Abstained( - address indexed voter, - address indexed pool, - uint256 indexed tokenId, - uint256 weight, - uint256 totalWeight, - uint256 timestamp - ); - event NotifyReward(address indexed sender, address indexed reward, uint256 amount); - event DistributeReward(address indexed sender, address indexed gauge, uint256 amount); - event WhitelistToken(address indexed whitelister, address indexed token, bool indexed _bool); - event WhitelistNFT(address indexed whitelister, uint256 indexed tokenId, bool indexed _bool); - - /// @notice Store trusted forwarder address to pass into factories - function forwarder() external view returns (address); - - /// @notice The ve token that governs these contracts - function ve() external view returns (address); - - /// @notice Factory registry for valid pool / gauge / rewards factories - function factoryRegistry() external view returns (address); - - /// @notice Address of Minter.sol - function minter() external view returns (address); + function claimRewards( + address[] memory _gauges, + address[][] memory _tokens + ) external; - /// @notice Standard OZ IGovernor using ve for vote weights. - function governor() external view returns (address); + function _ve() external view returns (address); - /// @notice Custom Epoch Governor using ve for vote weights. - function epochGovernor() external view returns (address); + function governor() external view returns (address); - /// @notice credibly neutral party similar to Curve's Emergency DAO function emergencyCouncil() external view returns (address); - /// @dev Total Voting Weights - function totalWeight() external view returns (uint256); + function attachTokenToGauge(uint256 _tokenId, address account) external; - /// @dev Most number of pools one voter can vote for at once - function maxVotingNum() external view returns (uint256); + function detachTokenFromGauge(uint256 _tokenId, address account) external; - // mappings - /// @dev Pool => Gauge - function gauges(address pool) external view returns (address); + function emitDeposit( + uint256 _tokenId, + address account, + uint256 amount + ) external; - /// @dev Gauge => Pool - function poolForGauge(address gauge) external view returns (address); + function emitWithdraw( + uint256 _tokenId, + address account, + uint256 amount + ) external; - /// @dev Gauge => Fees Voting Reward - function gaugeToFees(address gauge) external view returns (address); + function isWhitelisted(address token) external view returns (bool); - /// @dev Gauge => Bribes Voting Reward - function gaugeToBribe(address gauge) external view returns (address); + function notifyRewardAmount(uint256 amount) external; - /// @dev Pool => Weights - function weights(address pool) external view returns (uint256); + function distribute(address _gauge) external; - /// @dev NFT => Pool => Votes - function votes(uint256 tokenId, address pool) external view returns (uint256); + function gauges(address pool) external view returns (address gauge); - /// @dev NFT => Total voting weight of NFT - function usedWeights(uint256 tokenId) external view returns (uint256); + function feeDistributers( + address gauge + ) external view returns (address feeDistributor); - /// @dev Nft => Timestamp of last vote (ensures single vote per epoch) - function lastVoted(uint256 tokenId) external view returns (uint256); + function gaugefactory() external view returns (address); - /// @dev Address => Gauge - function isGauge(address) external view returns (bool); + function feeDistributorFactory() external view returns (address); - /// @dev Token => Whitelisted status - function isWhitelistedToken(address token) external view returns (bool); + function minter() external view returns (address); - /// @dev TokenId => Whitelisted status - function isWhitelistedNFT(uint256 tokenId) external view returns (bool); + function factory() external view returns (address); - /// @dev Gauge => Liveness status - function isAlive(address gauge) external view returns (bool); + function length() external view returns (uint256); - /// @dev Gauge => Amount claimable - function claimable(address gauge) external view returns (uint256); + function pools(uint256) external view returns (address); - /// @notice Number of pools with a Gauge - function length() external view returns (uint256); + function isAlive(address) external view returns (bool); + + function stale(uint256 tokenId) external view returns (bool); + + function partnerNFT(uint256 tokenId) external view returns (bool); + + function setXRamRatio(uint256 _xRamRatio) external; + + function setGaugeXRamRatio( + address[] calldata _gauges, + uint256[] calldata _xRamRatios + ) external; + + function resetGaugeXRamRatio(address[] calldata _gauges) external; + + function whitelist(address _token) external; + + function forbid(address _token, bool _status) external; - /// @notice Called by Minter to distribute weekly emissions rewards for disbursement amongst gauges. - /// @dev Assumes totalWeight != 0 (Will never be zero as long as users are voting). - /// Throws if not called by minter. - /// @param _amount Amount of rewards to distribute. - function notifyRewardAmount(uint256 _amount) external; - - /// @dev Utility to distribute to gauges of pools in range _start to _finish. - /// @param _start Starting index of gauges to distribute to. - /// @param _finish Ending index of gauges to distribute to. - function distribute(uint256 _start, uint256 _finish) external; - - /// @dev Utility to distribute to gauges of pools in array. - /// @param _gauges Array of gauges to distribute to. - function distribute(address[] memory _gauges) external; - - /// @notice Called by users to update voting balances in voting rewards contracts. - /// @param _tokenId Id of veNFT whose balance you wish to update. - function poke(uint256 _tokenId) external; - - /// @notice Called by users to vote for pools. Votes distributed proportionally based on weights. - /// Can only vote or deposit into a managed NFT once per epoch. - /// Can only vote for gauges that have not been killed. - /// @dev Weights are distributed proportional to the sum of the weights in the array. - /// Throws if length of _poolVote and _weights do not match. - /// @param _tokenId Id of veNFT you are voting with. - /// @param _poolVote Array of pools you are voting for. - /// @param _weights Weights of pools. - function vote(uint256 _tokenId, address[] calldata _poolVote, uint256[] calldata _weights) external; - - /// @notice Called by users to reset voting state. Required if you wish to make changes to - /// veNFT state (e.g. merge, split, deposit into managed etc). - /// Cannot reset in the same epoch that you voted in. - /// Can vote or deposit into a managed NFT again after reset. - /// @param _tokenId Id of veNFT you are reseting. - function reset(uint256 _tokenId) external; - - /// @notice Called by users to deposit into a managed NFT. - /// Can only vote or deposit into a managed NFT once per epoch. - /// Note that NFTs deposited into a managed NFT will be re-locked - /// to the maximum lock time on withdrawal. - /// @dev Throws if not approved or owner. - /// Throws if managed NFT is inactive. - /// Throws if depositing within privileged window (one hour prior to epoch flip). - function depositManaged(uint256 _tokenId, uint256 _mTokenId) external; - - /// @notice Called by users to withdraw from a managed NFT. - /// Cannot do it in the same epoch that you deposited into a managed NFT. - /// Can vote or deposit into a managed NFT again after withdrawing. - /// Note that the NFT withdrawn is re-locked to the maximum lock time. - function withdrawManaged(uint256 _tokenId) external; - - /// @notice Claim emissions from gauges. - /// @param _gauges Array of gauges to collect emissions from. - function claimRewards(address[] memory _gauges) external; - - /// @notice Claim bribes for a given NFT. - /// @dev Utility to help batch bribe claims. - /// @param _bribes Array of BribeVotingReward contracts to collect from. - /// @param _tokens Array of tokens that are used as bribes. - /// @param _tokenId Id of veNFT that you wish to claim bribes for. - function claimBribes(address[] memory _bribes, address[][] memory _tokens, uint256 _tokenId) external; - - /// @notice Claim fees for a given NFT. - /// @dev Utility to help batch fee claims. - /// @param _fees Array of FeesVotingReward contracts to collect from. - /// @param _tokens Array of tokens that are used as fees. - /// @param _tokenId Id of veNFT that you wish to claim fees for. - function claimFees(address[] memory _fees, address[][] memory _tokens, uint256 _tokenId) external; - - /// @notice Set new governor. - /// @dev Throws if not called by governor. - /// @param _governor . - function setGovernor(address _governor) external; - - /// @notice Set new epoch based governor. - /// @dev Throws if not called by governor. - /// @param _epochGovernor . - function setEpochGovernor(address _epochGovernor) external; - - /// @notice Set new emergency council. - /// @dev Throws if not called by emergency council. - /// @param _emergencyCouncil . - function setEmergencyCouncil(address _emergencyCouncil) external; - - /// @notice Set maximum number of gauges that can be voted for. - /// @dev Throws if not called by governor. - /// Throws if _maxVotingNum is too low. - /// Throws if the values are the same. - /// @param _maxVotingNum . - function setMaxVotingNum(uint256 _maxVotingNum) external; - - /// @notice Whitelist (or unwhitelist) token for use in bribes. - /// @dev Throws if not called by governor. - /// @param _token . - /// @param _bool . - function whitelistToken(address _token, bool _bool) external; - - /// @notice Whitelist (or unwhitelist) token id for voting in last hour prior to epoch flip. - /// @dev Throws if not called by governor. - /// Throws if already whitelisted. - /// @param _tokenId . - /// @param _bool . - function whitelistNFT(uint256 _tokenId, bool _bool) external; - - /// @notice Create a new gauge (unpermissioned). - /// @dev Governor can create a new gauge for a pool with any address. - /// @param _poolFactory . - /// @param _pool . - function createGauge(address _poolFactory, address _pool) external returns (address); - - /// @notice Kills a gauge. The gauge will not receive any new emissions and cannot be deposited into. - /// Can still withdraw from gauge. - /// @dev Throws if not called by emergency council. - /// Throws if gauge already killed. - /// @param _gauge . function killGauge(address _gauge) external; - /// @notice Revives a killed gauge. Gauge will can receive emissions and deposits again. - /// @dev Throws if not called by emergency council. - /// Throws if gauge is not killed. - /// @param _gauge . function reviveGauge(address _gauge) external; - /// @dev Update claims to emissions for an array of gauges. - /// @param _gauges Array of gauges to update emissions for. - function updateFor(address[] memory _gauges) external; + function whitelistOperator() external view returns (address); + + function gaugeXRamRatio(address gauge) external view returns (uint256); + + function clawBackUnusedEmissions(address[] calldata _gauges) external; + + function resetVotes(uint256[] calldata tokenIds) external; + + function syncLegacyGaugeRewards(address[] calldata _gauges) external; + + function whitelistGaugeRewards( + address[] calldata _gauges, + address[] calldata _rewards + ) external; + + function removeGaugeRewards( + address[] calldata _gauges, + address[] calldata _rewards + ) external; + + function base() external view returns (address ram); + + function xRamAddress() external view returns (address _xRamAddress); + + function addClGaugeReward(address gauge, address reward) external; + + function removeClGaugeReward(address gauge, address reward) external; + + function designateStale(uint256 _tokenId, bool _status) external; + + function customGaugeForPool( + address pool + ) external view returns (address customGauge); + + function designatePartnerNFT(uint256 _tokenId, bool _status) external; - /// @dev Update claims to emissions for gauges based on their pool id as stored in Voter. - /// @param _start Starting index of pools. - /// @param _end Ending index of pools. - function updateFor(uint256 _start, uint256 _end) external; + function isGauge(address gauge) external view returns (bool _isGauge); - /// @dev Update claims to emissions for single gauge - /// @param _gauge . - function updateFor(address _gauge) external; + function detachPartner(uint256 _tokenId) external; } \ No newline at end of file diff --git a/contracts/libraries/DoubleStakingLogic.sol b/contracts/libraries/DoubleStakingLogic.sol index 6898d43..f7ff170 100644 --- a/contracts/libraries/DoubleStakingLogic.sol +++ b/contracts/libraries/DoubleStakingLogic.sol @@ -10,7 +10,7 @@ import "../interfaces/IReliquary.sol"; library DoubleStakingLogic { using SafeERC20 for IERC20; - // @dev Deposit LP tokens to earn THE. + // @dev Deposit LP tokens to earn gauge rewards. function updatePoolWithGaugeDeposit( PoolInfo[] storage poolInfo, uint256 _pid @@ -27,7 +27,7 @@ library DoubleStakingLogic { poolToken.approve(pool.gauge, type(uint256).max); } // Deposit the LP in the gauge - IGauge(pool.gauge).deposit(balance); + IGauge(pool.gauge).deposit(balance, 0); } } } @@ -62,7 +62,7 @@ library DoubleStakingLogic { PoolInfo[] storage poolInfo, uint256 _pid, address rewardReceiver, - bool _claimRewards + address[] calldata _claimRewardsTokens ) public { address gauge = voter.gauges(poolInfo[_pid].poolToken); if (gauge != address(0)) { @@ -70,10 +70,17 @@ library DoubleStakingLogic { withdrawFromGauge(poolInfo, _pid, balance); // claim rewards before disabling gauge - if (_claimRewards) { - IGauge(gauge).getReward(address(this)); - IERC20 rewardToken = IERC20(IGauge(gauge).rewardToken()); - rewardToken.safeTransfer(rewardReceiver, rewardToken.balanceOf(address(this))); + if (_claimRewardsTokens.length > 0) { + uint256[] memory balancesBefore = new uint256[](_claimRewardsTokens.length); + for (uint256 i = 0; i < _claimRewardsTokens.length; i++) { + balancesBefore[i] = IERC20(_claimRewardsTokens[i]).balanceOf(address(this)); + } + + IGauge(gauge).getReward(address(this), _claimRewardsTokens); + for (uint256 i = 0; i < _claimRewardsTokens.length; i++) { + IERC20 rewardToken = IERC20(_claimRewardsTokens[i]); + rewardToken.safeTransfer(rewardReceiver, rewardToken.balanceOf(address(this)) - balancesBefore[i]); + } } // revoke allowance @@ -87,17 +94,28 @@ library DoubleStakingLogic { IVoter voter, PoolInfo[] storage poolInfo, address rewardReceiver, - uint256 _pid + uint256 _pid, + address[] calldata _rewardTokens ) public { IGauge gauge = IGauge(poolInfo[_pid].gauge); if (address(gauge) != address(0)) { // claim the rewards address[] memory gauges = new address[](1); gauges[0] = poolInfo[_pid].gauge; - voter.claimRewards(gauges); + address[][] memory _rewardTokensArray = new address[][](1); + _rewardTokensArray[0] = _rewardTokens; + + uint256[] memory balancesBefore = new uint256[](_rewardTokens.length); + for (uint256 i = 0; i < _rewardTokens.length; i++) { + balancesBefore[i] = IERC20(_rewardTokens[i]).balanceOf(address(this)); + } + + voter.claimRewards(gauges, _rewardTokensArray); - IERC20 rewardToken = IERC20(gauge.rewardToken()); - rewardToken.safeTransfer(rewardReceiver, rewardToken.balanceOf(address(this))); + for (uint256 i = 0; i < _rewardTokens.length; i++) { + IERC20 rewardToken = IERC20(_rewardTokens[i]); + rewardToken.safeTransfer(rewardReceiver, rewardToken.balanceOf(address(this)) - balancesBefore[i]); + } } } } diff --git a/foundry.toml b/foundry.toml index bf14720..a5a1f96 100644 --- a/foundry.toml +++ b/foundry.toml @@ -5,6 +5,7 @@ libs = ['lib'] test = 'test/foundry' cache_path = 'forge-cache' script = 'scripts' +evm_version = 'shanghai' solc_version = "0.8.23" optimizer = true @@ -24,6 +25,7 @@ fs_permissions = [{ access = "read", path = "./"}] fantom = "https://rpcapi.fantom.network" optimism = "https://opt-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY}" mode = "https://mainnet.mode.network" +arbitrum = "https://arb1.arbitrum.io/rpc" [profile.test.optimizer_details.yulDetails] # Reduces compile times but produces poorly optimized code diff --git a/test/foundry/Reliquary.t.sol b/test/foundry/Reliquary.t.sol index 2d9f5c9..beefe96 100644 --- a/test/foundry/Reliquary.t.sol +++ b/test/foundry/Reliquary.t.sol @@ -42,14 +42,14 @@ contract GaugeRewardsTest is ERC721Holder, Test { int256[] public coeff = [int256(100e18), int256(1e18), int256(5e15), int256(-1e13), int256(5e9)]; function setUp() public { - vm.createSelectFork("mode"); + vm.createSelectFork("arbitrum"); - lpToken0 = IERC20Metadata(0x4200000000000000000000000000000000000006); // weth - lpToken1 = IERC20Metadata(0xd988097fb8612cc24eeC14542bC03424c656005f); // usdc + lpToken0 = IERC20Metadata(0x82aF49447D8a07e3bd95BD0d56f35241523fBab1); // weth + lpToken1 = IERC20Metadata(0x40301951Af3f80b8C1744ca77E55111dd3c1dba1); // neadram - voter = IVoter(0xD2F998a46e4d9Dd57aF1a28EBa8C34E7dD3851D7); + voter = IVoter(0xAAA2564DEb34763E3d05162ed3f5C2658691f499); vm.label(address(voter), "Voter"); - rewardToken = IERC20Metadata(0xDfc7C877a950e49D2610114102175A06C2e3167a); + rewardToken = IERC20Metadata(0xAAA6C1E32C55A7Bfa8066A6FAE9b42650F262418); vm.label(address(rewardToken), "Reward Token"); hoax(address(this)); @@ -68,7 +68,7 @@ contract GaugeRewardsTest is ERC721Holder, Test { oath.mint(address(reliquary), 100_000_000 ether); - poolToken = IERC20Metadata(0xCc16Bfda354353B2E03214d2715F514706Be044C); + poolToken = IERC20Metadata(0x1542D005D7b73c53a75D4Cd98a1a6bF3DC27842B); nftDescriptor = address(new NFTDescriptor(address(reliquary))); // poolToken.mint(address(this), 100_000_000 ether); @@ -385,14 +385,6 @@ contract GaugeRewardsTest is ERC721Holder, Test { } } - function testGaugeReward() public { - uint256 amount = 1 ether; - uint256 relicId = reliquary.createRelicAndDeposit(address(this), 0, amount); - skip(1 days); - reliquary.update(relicId, address(this)); - reliquary.claimGaugeRewards(0); - console.log("reward: ", rewardToken.balanceOf(gaugeReceiver)); - } // function testDepositBonusRewarder() public { // DepositBonusRewarder rewarder = new DepositBonusRewarder( diff --git a/test/foundry/ReliquaryGauges.t.sol b/test/foundry/ReliquaryGauges.t.sol new file mode 100644 index 0000000..b74de55 --- /dev/null +++ b/test/foundry/ReliquaryGauges.t.sol @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; +import "contracts/Reliquary.sol"; +import "contracts/nft_descriptors/NFTDescriptor.sol"; +import "contracts/curves/LinearCurve.sol"; +import "contracts/curves/LinearPlateauCurve.sol"; +import "contracts/rewarders/RollingRewarder.sol"; +import "contracts/rewarders/ParentRollingRewarder.sol"; +import "openzeppelin-contracts/contracts/token/ERC721/utils/ERC721Holder.sol"; +import "./mocks/ERC20Mock.sol"; +import "./mocks/VoterMock.sol"; +import "./mocks/GaugeMock.sol"; + +contract GaugesTest is ERC721Holder, Test { + using Strings for address; + using Strings for uint256; + + Reliquary public reliquary; + LinearCurve public linearCurve; + LinearPlateauCurve public linearPlateauCurve; + ERC20Mock public oath; + ERC20Mock public suppliedToken; + ParentRollingRewarder public parentRewarder; + + address voter; + address gaugeReceiver; + + uint256 public nbChildRewarder = 1; + RollingRewarder[] public childRewarders; + ERC20Mock[] public rewardTokens; + + address public nftDescriptor; + + //! here we set emission rate at 0 to simulate a pure collateral Ethos reward without any oath incentives. + uint256 public emissionRate = 0; + uint256 public initialMint = 100_000_000 ether; + uint256 public initialDistributionPeriod = 7 days; + + // Linear function config (to config) + uint256 public slope = 100; // Increase of multiplier every second + uint256 public minMultiplier = 365 days * 100; // Arbitrary (but should be coherent with slope) + uint256 public plateau = 100 days; + + address public gauge; + + function setUp() public { + oath = new ERC20Mock(18); + voter = address(new VoterMock()); + gaugeReceiver = makeAddr("gaugeReceiver"); + + reliquary = new Reliquary(address(oath), emissionRate, gaugeReceiver, voter, "Reliquary Deposit", "RELIC"); + linearPlateauCurve = new LinearPlateauCurve(slope, minMultiplier, plateau); + linearCurve = new LinearCurve(slope, minMultiplier); + + oath.mint(address(reliquary), initialMint); + + suppliedToken = new ERC20Mock(6); + vm.label(address(suppliedToken), "Supplied Token"); + gauge = address(new GaugeMock(address(suppliedToken))); + VoterMock(voter).setGauge(address(suppliedToken), gauge); + + nftDescriptor = address(new NFTDescriptor(address(reliquary))); + + parentRewarder = new ParentRollingRewarder(); + + reliquary.grantRole(keccak256("OPERATOR"), address(this)); + + deal(address(suppliedToken), address(this), 1); + suppliedToken.approve(address(reliquary), 1); // approve 1 wei to bootstrap the pool + reliquary.addPool( + 100, + address(suppliedToken), + address(parentRewarder), + linearPlateauCurve, + "ETH Pool", + nftDescriptor, + true, + address(this) + ); + + for (uint256 i = 0; i < nbChildRewarder; i++) { + address rewardTokenTemp = address(new ERC20Mock(18)); + address rewarderTemp = parentRewarder.createChild(rewardTokenTemp); + rewardTokens.push(ERC20Mock(rewardTokenTemp)); + childRewarders.push(RollingRewarder(rewarderTemp)); + ERC20Mock(rewardTokenTemp).mint(address(this), initialMint); + ERC20Mock(rewardTokenTemp).approve(address(reliquary), type(uint256).max); + ERC20Mock(rewardTokenTemp).approve(address(rewarderTemp), type(uint256).max); + } + + suppliedToken.mint(address(this), initialMint); + suppliedToken.approve(address(reliquary), type(uint256).max); + } + + function testGaugeReward(uint256 rewardAmount) public { + rewardAmount = bound(rewardAmount, 0, type(uint256).max / 2); + uint256 amount = 1 ether; + uint256 relicId = reliquary.createRelicAndDeposit(address(this), 0, amount); + skip(1 days); + + address[] memory gaugeRewardTokens = new address[](1); + gaugeRewardTokens[0] = address(oath); + uint256[] memory gaugeRewardAmounts = new uint256[](1); + gaugeRewardAmounts[0] = rewardAmount; + + VoterMock(voter).setPendingRewards(gauge, gaugeRewardTokens, gaugeRewardAmounts); + oath.mint(address(voter), rewardAmount); + + reliquary.update(relicId, address(this)); + reliquary.claimGaugeRewards(0, gaugeRewardTokens); + assertEq(oath.balanceOf(gaugeReceiver), rewardAmount); + } + + function testGaugeTransferRewardNotFullBalance(uint256 rewardAmount) public { + rewardAmount = bound(rewardAmount, 0, type(uint256).max / 2); + uint256 amount = 1 ether; + uint256 relicId = reliquary.createRelicAndDeposit(address(this), 0, amount); + skip(1 days); + + address[] memory gaugeRewardTokens = new address[](1); + gaugeRewardTokens[0] = address(oath); + uint256[] memory gaugeRewardAmounts = new uint256[](1); + gaugeRewardAmounts[0] = rewardAmount; + VoterMock(voter).setPendingRewards(gauge, gaugeRewardTokens, gaugeRewardAmounts); + oath.mint(address(voter), rewardAmount); + + reliquary.update(relicId, address(this)); + uint256 balanceBefore = oath.balanceOf(address(reliquary)); + + gaugeRewardTokens[0] = address(oath); + reliquary.claimGaugeRewards(0, gaugeRewardTokens); + + assertEq(oath.balanceOf(gaugeReceiver), rewardAmount); + assertEq(oath.balanceOf(address(reliquary)), balanceBefore); + } + + function testDisableGaugeClaimRewards(uint256 rewardAmount) public { + rewardAmount = bound(rewardAmount, 0, type(uint256).max / 2); + uint256 amount = 1 ether; + uint256 relicId = reliquary.createRelicAndDeposit(address(this), 0, amount); + skip(1 days); + + address[] memory gaugeRewardTokens = new address[](1); + gaugeRewardTokens[0] = address(oath); + uint256[] memory gaugeRewardAmounts = new uint256[](1); + gaugeRewardAmounts[0] = rewardAmount; + GaugeMock(gauge).setReward(address(reliquary), rewardAmount); + oath.mint(address(gauge), rewardAmount); + + reliquary.update(relicId, address(this)); + uint256 balanceBefore = oath.balanceOf(address(reliquary)); + + gaugeRewardTokens[0] = address(oath); + reliquary.disableGauge(0, gaugeRewardTokens); + + assertEq(oath.balanceOf(gaugeReceiver), rewardAmount); + assertEq(oath.balanceOf(address(reliquary)), balanceBefore); + } +} diff --git a/test/foundry/mocks/GaugeMock.sol b/test/foundry/mocks/GaugeMock.sol new file mode 100644 index 0000000..26a52c8 --- /dev/null +++ b/test/foundry/mocks/GaugeMock.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract GaugeMock { + address public token; + + constructor(address _token) { + token = _token; + } + + mapping (address => uint256) public balanceOf; + mapping (address => uint256) public rewardOf; + + function deposit(uint256 amount, uint256) external { + IERC20(token).transferFrom(msg.sender, address(this), amount); + balanceOf[msg.sender] += amount; + } + + function withdraw(uint256 amount) external { + require(balanceOf[msg.sender] >= amount, "GaugeMock: insufficient balance"); + IERC20(token).transfer(msg.sender, amount); + balanceOf[msg.sender] -= amount; + } + + function getReward(address account, address[] memory tokens) external { + for (uint256 i = 0; i < tokens.length; i++) { + IERC20(tokens[i]).transfer(account, rewardOf[account]); + } + rewardOf[account] = 0; + } + + function setReward(address account, uint256 amount) external { + rewardOf[account] = amount; + } +} \ No newline at end of file diff --git a/test/foundry/mocks/VoterMock.sol b/test/foundry/mocks/VoterMock.sol index 1d09371..3f6a46e 100644 --- a/test/foundry/mocks/VoterMock.sol +++ b/test/foundry/mocks/VoterMock.sol @@ -1,12 +1,41 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.15; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + contract VoterMock { mapping(address => address) public gauges; + mapping(address => mapping(address => uint256)) public pendingRewards; constructor() {} function setGauge(address poolToken, address gauge) public { gauges[poolToken] = gauge; } + + function isAlive(address) public pure returns (bool) { + return true; + } + + function claimRewards( + address[] memory _gauges, + address[][] memory _tokens + ) external { + for (uint256 i = 0; i < _gauges.length; i++) { + for (uint256 j = 0; j < _tokens[i].length; j++) { + IERC20(_tokens[i][j]).transfer(msg.sender, pendingRewards[_gauges[i]][_tokens[i][j]]); + pendingRewards[_gauges[i]][_tokens[i][j]] = 0; + } + } + } + + function setPendingRewards( + address gauge, + address[] memory tokens, + uint256[] memory amounts + ) external { + for (uint256 i = 0; i < tokens.length; i++) { + pendingRewards[gauge][tokens[i]] = amounts[i]; + } + } } \ No newline at end of file