diff --git a/src/contracts/core/RewardsCoordinator.sol b/src/contracts/core/RewardsCoordinator.sol index 29cb9db5b..98050f762 100644 --- a/src/contracts/core/RewardsCoordinator.sol +++ b/src/contracts/core/RewardsCoordinator.sol @@ -235,7 +235,7 @@ contract RewardsCoordinator is * Earnings are cumulative so earners don't have to claim against all distribution roots they have earnings for, * they can simply claim against the latest root and the contract will calculate the difference between * their cumulativeEarnings and cumulativeClaimed. This difference is then transferred to recipient address. - * @param claim The RewardsMerkleClaim to be processed. + * @param claim The RewardsMerkleClaims to be processed. * Contains the root index, earner, token leaves, and required proofs * @param recipient The address recipient that receives the ERC20 rewards * @dev only callable by the valid claimer, that is @@ -246,30 +246,27 @@ contract RewardsCoordinator is RewardsMerkleClaim calldata claim, address recipient ) external onlyWhenNotPaused(PAUSED_PROCESS_CLAIM) nonReentrant { - DistributionRoot memory root = _distributionRoots[claim.rootIndex]; - _checkClaim(claim, root); - // If claimerFor earner is not set, claimer is by default the earner. Else set to claimerFor - address earner = claim.earnerLeaf.earner; - address claimer = claimerFor[earner]; - if (claimer == address(0)) { - claimer = earner; - } - require(msg.sender == claimer, "RewardsCoordinator.processClaim: caller is not valid claimer"); - for (uint256 i = 0; i < claim.tokenIndices.length; i++) { - TokenTreeMerkleLeaf calldata tokenLeaf = claim.tokenLeaves[i]; - - uint256 currCumulativeClaimed = cumulativeClaimed[earner][tokenLeaf.token]; - require( - tokenLeaf.cumulativeEarnings > currCumulativeClaimed, - "RewardsCoordinator.processClaim: cumulativeEarnings must be gt than cumulativeClaimed" - ); - - // Calculate amount to claim and update cumulativeClaimed - uint256 claimAmount = tokenLeaf.cumulativeEarnings - currCumulativeClaimed; - cumulativeClaimed[earner][tokenLeaf.token] = tokenLeaf.cumulativeEarnings; + _processClaim(claim, recipient); + } - tokenLeaf.token.safeTransfer(recipient, claimAmount); - emit RewardsClaimed(root.root, earner, claimer, recipient, tokenLeaf.token, claimAmount); + /** + * @notice Claim rewards against a given root (read from _distributionRoots[claim.rootIndex]). + * Earnings are cumulative so earners don't have to claim against all distribution roots they have earnings for, + * they can simply claim against the latest root and the contract will calculate the difference between + * their cumulativeEarnings and cumulativeClaimed. This difference is then transferred to recipient address. + * @param claims The array of RewardsMerkleClaims to be processed. + * Contains the root index, earner, token leaves, and required proofs + * @param recipient The address recipient that receives the ERC20 rewards + * @dev only callable by the valid claimer, that is + * if claimerFor[claim.earner] is address(0) then only the earner can claim, otherwise only + * claimerFor[claim.earner] can claim the rewards. + */ + function processClaims( + RewardsMerkleClaim[] calldata claims, + address recipient + ) external onlyWhenNotPaused(PAUSED_PROCESS_CLAIM) nonReentrant { + for (uint256 i; i < claims.length; ++i) { + _processClaim(claims[i], recipient); } } @@ -525,6 +522,42 @@ contract RewardsCoordinator is ); } + /** + * @notice Internal helper to process reward claims. + * @param claim The RewardsMerkleClaims to be processed. + * @param recipient The address recipient that receives the ERC20 rewards + */ + function _processClaim( + RewardsMerkleClaim calldata claim, + address recipient + ) internal { + DistributionRoot memory root = _distributionRoots[claim.rootIndex]; + _checkClaim(claim, root); + // If claimerFor earner is not set, claimer is by default the earner. Else set to claimerFor + address earner = claim.earnerLeaf.earner; + address claimer = claimerFor[earner]; + if (claimer == address(0)) { + claimer = earner; + } + require(msg.sender == claimer, "RewardsCoordinator.processClaim: caller is not valid claimer"); + for (uint256 i = 0; i < claim.tokenIndices.length; i++) { + TokenTreeMerkleLeaf calldata tokenLeaf = claim.tokenLeaves[i]; + + uint256 currCumulativeClaimed = cumulativeClaimed[earner][tokenLeaf.token]; + require( + tokenLeaf.cumulativeEarnings > currCumulativeClaimed, + "RewardsCoordinator.processClaim: cumulativeEarnings must be gt than cumulativeClaimed" + ); + + // Calculate amount to claim and update cumulativeClaimed + uint256 claimAmount = tokenLeaf.cumulativeEarnings - currCumulativeClaimed; + cumulativeClaimed[earner][tokenLeaf.token] = tokenLeaf.cumulativeEarnings; + + tokenLeaf.token.safeTransfer(recipient, claimAmount); + emit RewardsClaimed(root.root, earner, claimer, recipient, tokenLeaf.token, claimAmount); + } + } + function _setActivationDelay(uint32 _activationDelay) internal { emit ActivationDelaySet(activationDelay, _activationDelay); activationDelay = _activationDelay; diff --git a/src/contracts/interfaces/IRewardsCoordinator.sol b/src/contracts/interfaces/IRewardsCoordinator.sol index b4cca1c49..76deb5a7d 100644 --- a/src/contracts/interfaces/IRewardsCoordinator.sol +++ b/src/contracts/interfaces/IRewardsCoordinator.sol @@ -322,7 +322,7 @@ interface IRewardsCoordinator { * Earnings are cumulative so earners don't have to claim against all distribution roots they have earnings for, * they can simply claim against the latest root and the contract will calculate the difference between * their cumulativeEarnings and cumulativeClaimed. This difference is then transferred to recipient address. - * @param claim The RewardsMerkleClaim to be processed. + * @param claim The RewardsMerkleClaims to be processed. * Contains the root index, earner, token leaves, and required proofs * @param recipient The address recipient that receives the ERC20 rewards * @dev only callable by the valid claimer, that is @@ -331,6 +331,20 @@ interface IRewardsCoordinator { */ function processClaim(RewardsMerkleClaim calldata claim, address recipient) external; + /** + * @notice Claim rewards against a given root (read from _distributionRoots[claim.rootIndex]). + * Earnings are cumulative so earners don't have to claim against all distribution roots they have earnings for, + * they can simply claim against the latest root and the contract will calculate the difference between + * their cumulativeEarnings and cumulativeClaimed. This difference is then transferred to recipient address. + * @param claims The array of RewardsMerkleClaims to be processed. + * Contains the root index, earner, token leaves, and required proofs + * @param recipient The address recipient that receives the ERC20 rewards + * @dev only callable by the valid claimer, that is + * if claimerFor[claim.earner] is address(0) then only the earner can claim, otherwise only + * claimerFor[claim.earner] can claim the rewards. + */ + function processClaims(RewardsMerkleClaim[] calldata claims, address recipient) external; + /** * @notice Creates a new distribution root. activatedAt is set to block.timestamp + activationDelay * @param root The merkle root of the distribution diff --git a/src/test/unit/RewardsCoordinatorUnit.t.sol b/src/test/unit/RewardsCoordinatorUnit.t.sol index 27551a476..32e54fc40 100644 --- a/src/test/unit/RewardsCoordinatorUnit.t.sol +++ b/src/test/unit/RewardsCoordinatorUnit.t.sol @@ -1448,6 +1448,13 @@ contract RewardsCoordinatorUnitTests_processClaim is RewardsCoordinatorUnitTests mockTokenBytecode = address(mockToken).code; } + function _toClaims( + IRewardsCoordinator.RewardsMerkleClaim memory claim + ) internal pure returns (IRewardsCoordinator.RewardsMerkleClaim[] memory claims) { + claims = new IRewardsCoordinator.RewardsMerkleClaim[](1); + claims[0] = claim; + } + /// @notice Claim against latest submitted root, rootIndex 3 /// Limit fuzz runs to speed up tests since these require reading from JSON /// forge-config: default.fuzz.runs = 10 @@ -1484,7 +1491,8 @@ contract RewardsCoordinatorUnitTests_processClaim is RewardsCoordinatorUnitTests uint256[] memory tokenBalancesBefore = _getClaimTokenBalances(claimer, claim); _assertRewardsClaimedEvents(distributionRoot.root, claim, claimer); - rewardsCoordinator.processClaim(claim, claimer); + + rewardsCoordinator.processClaims(_toClaims(claim), claimer); uint256[] memory tokenBalancesAfter = _getClaimTokenBalances(claimer, claim); @@ -1534,7 +1542,7 @@ contract RewardsCoordinatorUnitTests_processClaim is RewardsCoordinatorUnitTests uint256[] memory tokenBalancesBefore = _getClaimTokenBalances(claimer, claim); _assertRewardsClaimedEvents(distributionRoot.root, claim, claimer); - rewardsCoordinator.processClaim(claim, claimer); + rewardsCoordinator.processClaims(_toClaims(claim), claimer); uint256[] memory tokenBalancesAfter = _getClaimTokenBalances(claimer, claim); @@ -1586,7 +1594,7 @@ contract RewardsCoordinatorUnitTests_processClaim is RewardsCoordinatorUnitTests uint256[] memory tokenBalancesBefore = _getClaimTokenBalances(claimer, claim); _assertRewardsClaimedEvents(distributionRoot.root, claim, claimer); - rewardsCoordinator.processClaim(claim, claimer); + rewardsCoordinator.processClaims(_toClaims(claim), claimer); uint256[] memory tokenBalancesAfter = _getClaimTokenBalances(claimer, claim); @@ -1619,7 +1627,7 @@ contract RewardsCoordinatorUnitTests_processClaim is RewardsCoordinatorUnitTests uint256[] memory tokenBalancesBefore = _getClaimTokenBalances(claimer, claim); _assertRewardsClaimedEvents(distributionRoot.root, claim, claimer); - rewardsCoordinator.processClaim(claim, claimer); + rewardsCoordinator.processClaims(_toClaims(claim), claimer); uint256[] memory tokenBalancesAfter = _getClaimTokenBalances(claimer, claim); @@ -1652,7 +1660,7 @@ contract RewardsCoordinatorUnitTests_processClaim is RewardsCoordinatorUnitTests uint256[] memory tokenBalancesBefore = _getClaimTokenBalances(claimer, claim); _assertRewardsClaimedEvents(distributionRoot.root, claim, claimer); - rewardsCoordinator.processClaim(claim, claimer); + rewardsCoordinator.processClaims(_toClaims(claim), claimer); uint256[] memory tokenBalancesAfter = _getClaimTokenBalances(claimer, claim); @@ -1698,7 +1706,7 @@ contract RewardsCoordinatorUnitTests_processClaim is RewardsCoordinatorUnitTests // rootIndex in claim is 0, which is disabled IRewardsCoordinator.RewardsMerkleClaim memory claim; cheats.expectRevert("RewardsCoordinator._checkClaim: root is disabled"); - rewardsCoordinator.processClaim(claim, claimer); + rewardsCoordinator.processClaims(_toClaims(claim), claimer); cheats.stopPrank(); } @@ -1739,7 +1747,7 @@ contract RewardsCoordinatorUnitTests_processClaim is RewardsCoordinatorUnitTests uint256[] memory tokenBalancesBefore = _getClaimTokenBalances(claimer, claim); _assertRewardsClaimedEvents(distributionRoot.root, claim, claimer); - rewardsCoordinator.processClaim(claim, claimer); + rewardsCoordinator.processClaims(_toClaims(claim), claimer); uint256[] memory tokenBalancesAfter = _getClaimTokenBalances(claimer, claim); @@ -1767,7 +1775,7 @@ contract RewardsCoordinatorUnitTests_processClaim is RewardsCoordinatorUnitTests assertTrue(rewardsCoordinator.checkClaim(claim), "RewardsCoordinator.checkClaim: claim not valid"); cheats.expectRevert("RewardsCoordinator.processClaim: cumulativeEarnings must be gt than cumulativeClaimed"); - rewardsCoordinator.processClaim(claim, claimer); + rewardsCoordinator.processClaims(_toClaims(claim), claimer); cheats.stopPrank(); } @@ -1809,7 +1817,7 @@ contract RewardsCoordinatorUnitTests_processClaim is RewardsCoordinatorUnitTests assertFalse(rewardsCoordinator.checkClaim(claim), "RewardsCoordinator.checkClaim: claim not valid"); cheats.expectRevert("RewardsCoordinator._verifyTokenClaim: invalid token claim proof"); - rewardsCoordinator.processClaim(claim, claimer); + rewardsCoordinator.processClaims(_toClaims(claim), claimer); cheats.stopPrank(); } @@ -1850,7 +1858,7 @@ contract RewardsCoordinatorUnitTests_processClaim is RewardsCoordinatorUnitTests assertFalse(rewardsCoordinator.checkClaim(claim), "RewardsCoordinator.checkClaim: claim not valid"); cheats.expectRevert("RewardsCoordinator._verifyEarnerClaimProof: invalid earner claim proof"); - rewardsCoordinator.processClaim(claim, claimer); + rewardsCoordinator.processClaims(_toClaims(claim), claimer); cheats.stopPrank(); } @@ -1888,7 +1896,7 @@ contract RewardsCoordinatorUnitTests_processClaim is RewardsCoordinatorUnitTests ).with_key(address(claim.tokenLeaves[0].token)).checked_write(type(uint256).max); cheats.startPrank(claimer); cheats.expectRevert("RewardsCoordinator.processClaim: cumulativeEarnings must be gt than cumulativeClaimed"); - rewardsCoordinator.processClaim(claim, claimer); + rewardsCoordinator.processClaims(_toClaims(claim), claimer); cheats.stopPrank(); } @@ -1927,7 +1935,7 @@ contract RewardsCoordinatorUnitTests_processClaim is RewardsCoordinatorUnitTests claim.tokenIndices[0] = claim.tokenIndices[0] | uint32(1 << (numShift + proofLength / 32)); cheats.startPrank(claimer); cheats.expectRevert("RewardsCoordinator._verifyTokenClaim: invalid tokenLeafIndex"); - rewardsCoordinator.processClaim(claim, claimer); + rewardsCoordinator.processClaims(_toClaims(claim), claimer); cheats.stopPrank(); } @@ -1966,7 +1974,7 @@ contract RewardsCoordinatorUnitTests_processClaim is RewardsCoordinatorUnitTests claim.earnerIndex = claim.earnerIndex | uint32(1 << (numShift + proofLength / 32)); cheats.startPrank(claimer); cheats.expectRevert("RewardsCoordinator._verifyEarnerClaimProof: invalid earnerLeafIndex"); - rewardsCoordinator.processClaim(claim, claimer); + rewardsCoordinator.processClaims(_toClaims(claim), claimer); cheats.stopPrank(); } @@ -2021,7 +2029,7 @@ contract RewardsCoordinatorUnitTests_processClaim is RewardsCoordinatorUnitTests "TokenIndex not set to max value" ); _assertRewardsClaimedEvents(distributionRoot.root, claim, claimer); - rewardsCoordinator.processClaim(claim, claimer); + rewardsCoordinator.processClaims(_toClaims(claim), claimer); uint256[] memory tokenBalancesAfter = _getClaimTokenBalances(claimer, claim); @@ -2076,7 +2084,7 @@ contract RewardsCoordinatorUnitTests_processClaim is RewardsCoordinatorUnitTests assertEq(claim.tokenIndices[0], 0, "TokenIndex should be 0"); assertEq(claim.tokenTreeProofs[0].length, 0, "TokenTreeProof should be empty"); _assertRewardsClaimedEvents(distributionRoot.root, claim, claimer); - rewardsCoordinator.processClaim(claim, claimer); + rewardsCoordinator.processClaims(_toClaims(claim), claimer); uint256[] memory tokenBalancesAfter = _getClaimTokenBalances(claimer, claim); @@ -2134,7 +2142,7 @@ contract RewardsCoordinatorUnitTests_processClaim is RewardsCoordinatorUnitTests assertEq(claim.earnerIndex, 0, "EarnerIndex should be 0"); assertEq(claim.earnerTreeProof.length, 0, "EarnerTreeProof should be empty"); _assertRewardsClaimedEvents(distributionRoot.root, claim, claimer); - rewardsCoordinator.processClaim(claim, claimer); + rewardsCoordinator.processClaims(_toClaims(claim), claimer); uint256[] memory tokenBalancesAfter = _getClaimTokenBalances(claimer, claim);