From fb14d0fa92f60429e09fe9313ffb193045b10ac5 Mon Sep 17 00:00:00 2001 From: simplyoptimistic <111120814+simplyoptimistic@users.noreply.github.com> Date: Mon, 15 Jan 2024 23:02:02 +1100 Subject: [PATCH] feat: implement get reward with address, used by voter only (#124) --- contracts/core/interfaces/IVoter.sol | 4 ++ contracts/gauge/CLGauge.sol | 16 +++++ contracts/gauge/interfaces/ICLGauge.sol | 5 ++ contracts/test/MockVoter.sol | 7 ++ test/unit/concrete/CLGauge/getReward.t.sol | 78 ++++++++++++++++++++++ 5 files changed, 110 insertions(+) diff --git a/contracts/core/interfaces/IVoter.sol b/contracts/core/interfaces/IVoter.sol index 8b3849b..d737b55 100644 --- a/contracts/core/interfaces/IVoter.sol +++ b/contracts/core/interfaces/IVoter.sol @@ -29,6 +29,10 @@ interface IVoter { function emergencyCouncil() external view returns (address); + /// @notice Claim emissions from gauges. + /// @param _gauges Array of gauges to collect emissions from. + function claimRewards(address[] memory _gauges) external; + /// @notice Claim fees for a given NFT. /// @dev Utility to help batch fee claims. /// @param _fees Array of FeesVotingReward contracts to collect from. diff --git a/contracts/gauge/CLGauge.sol b/contracts/gauge/CLGauge.sol index f7e2a9e..44c9880 100644 --- a/contracts/gauge/CLGauge.sol +++ b/contracts/gauge/CLGauge.sol @@ -130,6 +130,22 @@ contract CLGauge is ICLGauge, ERC721Holder, ReentrancyGuard { return claimable; } + /// @inheritdoc ICLGauge + function getReward(address account) external override nonReentrant { + require(msg.sender == address(voter), "NV"); + + uint256[] memory tokenIds = _stakes[account].values(); + uint256 length = tokenIds.length; + uint256 tokenId; + int24 tickLower; + int24 tickUpper; + for (uint256 i = 0; i < length; i++) { + tokenId = tokenIds[i]; + (,,,,, tickLower, tickUpper,,,,,) = nft.positions(tokenId); + _getReward(tickLower, tickUpper, tokenId, account); + } + } + /// @inheritdoc ICLGauge function getReward(uint256 tokenId) external override nonReentrant { require(_stakes[msg.sender].contains(tokenId), "NA"); diff --git a/contracts/gauge/interfaces/ICLGauge.sol b/contracts/gauge/interfaces/ICLGauge.sol index 34822f1..5d008ee 100644 --- a/contracts/gauge/interfaces/ICLGauge.sol +++ b/contracts/gauge/interfaces/ICLGauge.sol @@ -98,6 +98,11 @@ interface ICLGauge { /// @return The amount of claimable reward function earned(address account, uint256 tokenId) external view returns (uint256); + /// @notice Retrieve rewards for all tokens owned by an account + /// @dev Throws if not called by the voter + /// @param account The account of the user + function getReward(address account) external; + /// @notice Retrieve rewards for a tokenId /// @dev Throws if not called by the position owner /// @param tokenId The tokenId of the position diff --git a/contracts/test/MockVoter.sol b/contracts/test/MockVoter.sol index 96bc4e1..f41c68f 100644 --- a/contracts/test/MockVoter.sol +++ b/contracts/test/MockVoter.sol @@ -78,4 +78,11 @@ contract MockVoter is IVoter { } function vote(uint256 _tokenId, address[] calldata _poolVote, uint256[] calldata _weights) external override {} + + function claimRewards(address[] memory _gauges) external override { + uint256 _length = _gauges.length; + for (uint256 i = 0; i < _length; i++) { + ICLGauge(_gauges[i]).getReward(msg.sender); + } + } } diff --git a/test/unit/concrete/CLGauge/getReward.t.sol b/test/unit/concrete/CLGauge/getReward.t.sol index 525c7aa..240ad58 100644 --- a/test/unit/concrete/CLGauge/getReward.t.sol +++ b/test/unit/concrete/CLGauge/getReward.t.sol @@ -33,6 +33,13 @@ contract GetRewardTest is CLGaugeTest { skipToNextEpoch(0); } + function labelContracts() internal override { + super.labelContracts(); + + vm.label({account: address(pool), newLabel: "Pool"}); + vm.label({account: address(gauge), newLabel: "Gauge"}); + } + function test_RevertIf_CallerIsNotOwner() public { uint256 tokenId = nftCallee.mintNewFullRangePositionForUserWith60TickSpacing(TOKEN_1, TOKEN_1, users.alice); @@ -502,4 +509,75 @@ contract GetRewardTest is CLGaugeTest { // gauge should have 0 rewards left (not counting dust) assertLe(gaugeRewardTokenBalance, 1e6); } + + function test_RevertIf_CallerIsNotVoter() public { + uint256 tokenId = nftCallee.mintNewFullRangePositionForUserWith60TickSpacing(TOKEN_1, TOKEN_1, users.alice); + + nft.approve(address(gauge), tokenId); + gauge.deposit(tokenId); + + vm.startPrank(users.charlie); + vm.expectRevert(abi.encodePacked("NV")); + gauge.getReward(users.alice); + } + + function test_GetRewardAsVoterWithSingleDeposit() public { + uint256 tokenId = nftCallee.mintNewFullRangePositionForUserWith60TickSpacing(TOKEN_1, TOKEN_1, users.alice); + + nft.approve(address(gauge), tokenId); + gauge.deposit(tokenId); + + uint256 reward = TOKEN_1; + addRewardToGauge(address(voter), address(gauge), reward); + + skip(2 days); + + vm.startPrank(users.alice); + + address[] memory gauges = new address[](1); + gauges[0] = address(gauge); + + vm.expectEmit(true, true, false, true, address(gauge)); + emit ClaimRewards(users.alice, 285714285714259199); + voter.claimRewards(gauges); + + uint256 aliceRewardBalance = rewardToken.balanceOf(users.alice); + // alice should have 2 days worth of rewards + assertApproxEqAbs(aliceRewardBalance, reward / 7 * 2, 1e5); + assertEq(gauge.rewards(tokenId), 0); + assertEq(gauge.lastUpdateTime(tokenId), 777600); + } + + function test_GetRewardAsVoterWithMultipleDeposits() public { + uint256 tokenId = nftCallee.mintNewFullRangePositionForUserWith60TickSpacing(TOKEN_1, TOKEN_1, users.alice); + uint256 tokenId2 = nftCallee.mintNewFullRangePositionForUserWith60TickSpacing(TOKEN_1, TOKEN_1, users.alice); + + nft.approve(address(gauge), tokenId); + gauge.deposit(tokenId); + nft.approve(address(gauge), tokenId2); + gauge.deposit(tokenId2); + + uint256 reward = TOKEN_1; + addRewardToGauge(address(voter), address(gauge), reward); + + skip(2 days); + + vm.startPrank(users.alice); + + address[] memory gauges = new address[](1); + gauges[0] = address(gauge); + + vm.expectEmit(true, true, false, true, address(gauge)); + emit ClaimRewards(users.alice, 142857142857129599); + emit ClaimRewards(users.alice, 142857142857129599); + voter.claimRewards(gauges); + + uint256 aliceRewardBalance = rewardToken.balanceOf(users.alice); + // alice should have 2 days worth of rewards + assertApproxEqAbs(aliceRewardBalance, reward / 7 * 2, 1e5); + assertEq(gauge.rewards(tokenId), 0); + assertEq(gauge.lastUpdateTime(tokenId), 777600); + assertEq(gauge.rewards(tokenId2), 0); + assertEq(gauge.lastUpdateTime(tokenId2), 777600); + } }