Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: unified programmatic rewards #701

Open
wants to merge 4 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 80 additions & 24 deletions src/contracts/core/RewardsCoordinator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ contract RewardsCoordinator is
uint8 internal constant PAUSED_SUBMIT_DISABLE_ROOTS = 3;
/// @dev Index for flag that pauses calling rewardAllStakersAndOperators
uint8 internal constant PAUSED_REWARD_ALL_STAKERS_AND_OPERATORS = 4;
/// @dev Index for flag that pauses rewardParticipants
uint8 internal constant PAUSED_REWARD_PARTICIPANTS = 4;

/// @dev Salt for the earner leaf, meant to distinguish from tokenLeaf since they have the same sized data
uint8 internal constant EARNER_LEAF_SALT = 0;
Expand Down Expand Up @@ -204,13 +206,34 @@ contract RewardsCoordinator is
rewardsSubmission.token.safeTransferFrom(msg.sender, address(this), rewardsSubmission.amount);
}
}

/*
* @notice Facilitates the distribution of rewards to participants through the UniPRInt V1.1 system.
* @dev The function leverages AVS-defined logic and external data for reward calculations.
* @param token The address of the ERC20 token used for rewards.
* @param amount The total amount of tokens to be rewarded.
* @param contentHash The keccak256 hash of the JSON manifest file that lists the earners and their rewards.
* @param contentURI The URI where the JSON manifest file is publicly accessible.
*/
function rewardParticipants(
IERC20 token,
uint256 amount,
bytes32 contentHash,
string calldata contentURI
) external onlyWhenNotPaused(PAUSED_REWARD_PARTICIPANTS) {
// Pull `token` of `amount` from caller to this contract.
token.safeTransferFrom(msg.sender, address(this), amount);

// Emit event indicating a direct reward payment.
emit DirectRewardPayment(msg.sender, token, amount, contentHash, contentURI);
}

/**
* @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 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 @@ -221,30 +244,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 @@ -500,6 +520,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
46 changes: 45 additions & 1 deletion src/contracts/interfaces/IRewardsCoordinator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,21 @@ interface IRewardsCoordinator {
uint256 claimedAmount
);

/// @notice Emitted when a direct reward payment is initiated through the UniPRInt V1.1 system.
/// @dev Supports programmatic rewards for AVS performance, EIGEN incentives, and more.
/// @param avs The address of the AVS managing this reward.
/// @param token The address of the ERC20 token used for payment.
/// @param amount The total amount of tokens transferred to the RewardsCoordinator.
/// @param contentHash The keccak256 hash of the JSON manifest file listing the earners and amounts.
/// @param contentURI The publicly addressable URI pointing to the JSON manifest file.
event DirectRewardPayment(
address indexed avs,
IERC20 token,
uint256 amount,
bytes32 contentHash,
string contentURI
ypatil12 marked this conversation as resolved.
Show resolved Hide resolved
);

/**
*
* VIEW FUNCTIONS
Expand Down Expand Up @@ -282,13 +297,28 @@ interface IRewardsCoordinator {
* @param rewardsSubmissions The rewards submissions being created
*/
function createRewardsForAllEarners(RewardsSubmission[] calldata rewardsSubmissions) external;

/**
* @notice Facilitates the distribution of rewards to participants through the UniPRInt V1.1 system.
* @dev The function leverages AVS-defined logic and external data for reward calculations.
* @param token The address of the ERC20 token used for rewards.
* @param totalAmount The total amount of tokens to be transferred to the RewardsCoordinator.
* @param contentHash The keccak256 hash of the JSON manifest file that lists the earners and their rewards.
* @param contentURI The URI where the JSON manifest file is publicly accessible.
*/
function rewardParticipants(
IERC20 token,
uint256 totalAmount,
bytes32 contentHash,
string calldata contentURI
) 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 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 @@ -297,6 +327,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
Loading
Loading