Skip to content

Commit

Permalink
feat: process multiple claims (#756)
Browse files Browse the repository at this point in the history
* feat: `processClaim` multiple claims

* fix: review reconciliations
  • Loading branch information
0xClandestine authored Sep 20, 2024
1 parent 1881ea6 commit 531ba93
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 41 deletions.
81 changes: 57 additions & 24 deletions src/contracts/core/RewardsCoordinator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
}
}

Expand Down Expand Up @@ -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;
Expand Down
16 changes: 15 additions & 1 deletion src/contracts/interfaces/IRewardsCoordinator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
40 changes: 24 additions & 16 deletions src/test/unit/RewardsCoordinatorUnit.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);

Expand Down

0 comments on commit 531ba93

Please sign in to comment.