Skip to content

Commit

Permalink
feat: add process_claims function (#193)
Browse files Browse the repository at this point in the history
This PR adds the missing `process_claims` function to the writer and
tests for both claim functions.
  • Loading branch information
MegaRedHand authored Jan 8, 2025
1 parent 45dfdeb commit 9c88c1c
Show file tree
Hide file tree
Showing 3 changed files with 203 additions and 21 deletions.
124 changes: 115 additions & 9 deletions crates/chainio/clients/elcontracts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ pub mod writer;
#[cfg(test)]
pub(crate) mod test_utils {
use alloy::hex::FromHex;
use alloy_primitives::{address, Address, Bytes, FixedBytes, U256};
use alloy_primitives::{address, keccak256, Address, Bytes, FixedBytes, U256, U8};
use alloy_sol_types::SolValue;
use eigen_logging::get_test_logger;
use eigen_testing_utils::anvil_constants::{
get_allocation_manager_address, get_avs_directory_address, get_delegation_manager_address,
get_registry_coordinator_address, get_rewards_coordinator_address,
get_erc20_mock_strategy, get_registry_coordinator_address, get_rewards_coordinator_address,
get_strategy_manager_address,
};
use eigen_utils::{
Expand All @@ -29,12 +30,19 @@ pub(crate) mod test_utils {
EarnerTreeMerkleLeaf, RewardsMerkleClaim, TokenTreeMerkleLeaf,
},
},
mockerc20::MockERC20,
};
use std::str::FromStr;

use crate::{reader::ELChainReader, writer::ELChainWriter};

pub const OPERATOR_ADDRESS: Address = address!("70997970C51812dc3A010C7d01b50e0d17dc79C8");
pub const OPERATOR_PRIVATE_KEY: &str =
"0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d";

pub const ANVIL_FIRST_ADDRESS: Address = address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266");
pub const ANVIL_FIRST_PRIVATE_KEY: &str =
"ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";

pub async fn build_el_chain_reader(http_endpoint: String) -> ELChainReader {
let delegation_manager_address =
Expand All @@ -53,10 +61,6 @@ pub(crate) mod test_utils {
.unwrap()
}

pub const ANVIL_FIRST_ADDRESS: Address = address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266");
pub const ANVIL_FIRST_PRIVATE_KEY: &str =
"ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";

pub async fn new_test_writer(http_endpoint: String, private_key: String) -> ELChainWriter {
let el_chain_reader = build_el_chain_reader(http_endpoint.clone()).await;
let strategy_manager = get_strategy_manager_address(http_endpoint.clone()).await;
Expand Down Expand Up @@ -86,7 +90,9 @@ pub(crate) mod test_utils {
)
}

pub async fn new_claim(http_endpoint: &str) -> (FixedBytes<32>, RewardsMerkleClaim) {
// Using test data taken from
// https://github.com/Layr-Labs/eigenlayer-contracts/blob/a888a1cd1479438dda4b138245a69177b125a973/src/test/test-data/rewardsCoordinator/processClaimProofs_MaxEarnerAndLeafIndices.json
pub async fn new_test_claim(http_endpoint: &str) -> (FixedBytes<32>, RewardsMerkleClaim) {
let el_chain_reader = build_el_chain_reader(http_endpoint.to_string()).await;

let earner_address = address!("25a1b7322f9796b26a4bec125913b34c292b28d6");
Expand All @@ -111,8 +117,6 @@ pub(crate) mod test_utils {
}],
};

// Using test data taken from
// https://github.com/Layr-Labs/eigenlayer-contracts/blob/a888a1cd1479438dda4b138245a69177b125a973/src/test/test-data/rewardsCoordinator/processClaimProofs_MaxEarnerAndLeafIndices.json
let root = FixedBytes::from_hex(
"37550707c80f3d8907c467999730e52127ab89be3f17a5017a3f1ffb73a1445f",
)
Expand All @@ -137,4 +141,106 @@ pub(crate) mod test_utils {

(root, claim)
}

/// The claim can be submitted from [`ANVIL_FIRST_PRIVATE_KEY`]
pub async fn new_claim(
http_endpoint: &str,
cumulative_earnings: U256,
) -> (FixedBytes<32>, RewardsMerkleClaim) {
let signer = get_signer(ANVIL_FIRST_PRIVATE_KEY, http_endpoint);
let rewards_coordinator_address =
get_rewards_coordinator_address(http_endpoint.to_string()).await;

let el_chain_reader = build_el_chain_reader(http_endpoint.to_string()).await;
let mock_strategy = get_erc20_mock_strategy(http_endpoint.to_string()).await;
let (_, token_address) = el_chain_reader
.get_strategy_and_underlying_token(mock_strategy)
.await
.unwrap();

// Initialize the rewards coordinator bindings
let rewards_coordinator = IRewardsCoordinator::new(rewards_coordinator_address, &signer);

// Mint tokens for the rewards coordinator
let token = MockERC20::new(token_address, &signer);
let receipt = token
.mint(rewards_coordinator_address, cumulative_earnings)
.send()
.await
.unwrap()
.get_receipt()
.await
.unwrap();
assert!(receipt.status());

// Generate token tree leaf
// For the tree structure, see https://github.com/Layr-Labs/eigenlayer-contracts/blob/a888a1cd1479438dda4b138245a69177b125a973/docs/core/RewardsCoordinator.md#rewards-merkle-tree-structure
let earner_address = ANVIL_FIRST_ADDRESS;
let token_leaves = vec![TokenTreeMerkleLeaf {
token: token_address,
cumulativeEarnings: cumulative_earnings,
}];
// Hash token tree leaf to get root
let encoded_token_leaf = [
// uint8 internal constant TOKEN_LEAF_SALT = 1;
U8::from(1).to_be_bytes_vec(),
token_leaves[0].token.abi_encode_packed(),
token_leaves[0].cumulativeEarnings.abi_encode_packed(),
]
.concat();
let earner_token_root = keccak256(encoded_token_leaf);

// Generate earner tree leaf
let earner_leaf = EarnerTreeMerkleLeaf {
earner: earner_address,
earnerTokenRoot: earner_token_root,
};
// Hash earner tree leaf to get root
let encoded_earner_leaf = [
// uint8 internal constant EARNER_LEAF_SALT = 0;
U8::from(0).to_be_bytes_vec(),
earner_leaf.earner.abi_encode_packed(),
earner_leaf.earnerTokenRoot.abi_encode_packed(),
]
.concat();
let earner_tree_root = keccak256(encoded_earner_leaf);

// Fetch the next root index from contract
let next_root_index = el_chain_reader
.get_distribution_roots_length()
.await
.unwrap();
// Construct the claim
let claim = RewardsMerkleClaim {
rootIndex: next_root_index.try_into().unwrap(),
earnerIndex: 0,
// Empty proof because leaf == root
earnerTreeProof: vec![].into(),
earnerLeaf: earner_leaf,
tokenIndices: vec![0],
tokenTreeProofs: vec![
// Empty proof because leaf == root
vec![].into(),
],
tokenLeaves: token_leaves,
};

let root = earner_tree_root;

// Fetch the current timestamp to increase it
let curr_rewards_calculation_end_timestamp = el_chain_reader
.curr_rewards_calculation_end_timestamp()
.await
.unwrap();

let submit_tx = rewards_coordinator
.submitRoot(root, curr_rewards_calculation_end_timestamp + 1)
.send()
.await
.unwrap();
let submit_status = submit_tx.get_receipt().await.unwrap().status();
assert!(submit_status);

(root, claim)
}
}
12 changes: 6 additions & 6 deletions crates/chainio/clients/elcontracts/src/reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1373,7 +1373,7 @@ pub struct AllocationInfo {
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::{build_el_chain_reader, new_claim, OPERATOR_ADDRESS};
use crate::test_utils::{build_el_chain_reader, new_test_claim, OPERATOR_ADDRESS};
use alloy::providers::Provider;
use alloy::{eips::eip1898::BlockNumberOrTag::Number, rpc::types::BlockTransactionsKind};
use alloy_primitives::{address, keccak256, Address, FixedBytes, U256};
Expand Down Expand Up @@ -1491,7 +1491,7 @@ mod tests {

assert_eq!(distribution_roots_length_ret, U256::from(0));

_ = new_claim(&http_endpoint).await;
_ = new_test_claim(&http_endpoint).await;

let distribution_roots_length_ret = el_chain_reader
.get_distribution_roots_length()
Expand All @@ -1513,7 +1513,7 @@ mod tests {

assert_eq!(end_timestamp, 0);

_ = new_claim(&http_endpoint).await;
_ = new_test_claim(&http_endpoint).await;

let end_timestamp = el_chain_reader
.curr_rewards_calculation_end_timestamp()
Expand All @@ -1535,7 +1535,7 @@ mod tests {
// The root starts being zero
assert_eq!(distribution_root.root, FixedBytes::ZERO);

let (root, _) = new_claim(&http_endpoint).await;
let (root, _) = new_test_claim(&http_endpoint).await;

let distribution_root = el_chain_reader
.get_current_claimable_distribution_root()
Expand All @@ -1549,7 +1549,7 @@ mod tests {
async fn test_get_root_index_from_hash() {
let (_container, http_endpoint, _ws_endpoint) = start_anvil_container().await;
let el_chain_reader = build_el_chain_reader(http_endpoint.to_string()).await;
let (root, _) = new_claim(&http_endpoint).await;
let (root, _) = new_test_claim(&http_endpoint).await;

let index = el_chain_reader
.get_root_index_from_hash(root)
Expand Down Expand Up @@ -1586,7 +1586,7 @@ mod tests {
let (_container, http_endpoint, _ws_endpoint) = start_anvil_container().await;
let el_chain_reader = build_el_chain_reader(http_endpoint.to_string()).await;

let (_, claim) = new_claim(&http_endpoint).await;
let (_, claim) = new_test_claim(&http_endpoint).await;

let valid_claim = el_chain_reader.check_claim(claim.clone()).await.unwrap();
assert!(valid_claim);
Expand Down
88 changes: 82 additions & 6 deletions crates/chainio/clients/elcontracts/src/writer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,8 +235,8 @@ impl ELChainWriter {
///
/// # Arguments
///
/// * `earnerAddress` - The address of the earner for whom to process the claim.
/// * `claim` - The RewardsMerkleClaim object containing the claim.
/// * `earnerAddress` - The address of the earner for whom to process the claim.
///
/// # Returns
///
Expand All @@ -247,8 +247,8 @@ impl ELChainWriter {
/// * `ElContractsError` - if the call to the contract fails. Also fails if no root has been submitted yet.
pub async fn process_claim(
&self,
earner_address: Address,
claim: RewardsMerkleClaim,
earner_address: Address,
) -> Result<FixedBytes<32>, ElContractsError> {
let provider = get_signer(&self.signer, &self.provider);

Expand All @@ -261,6 +261,41 @@ impl ELChainWriter {
Ok(*tx.tx_hash())
}

/// Process multiple claim for rewards for a given earner address. Checks the claim against a given root
/// (determined by the root_index on the claim). Earnings are cumulative so earners can claim to
/// the latest distribution root and the contract will compute the difference between their earning
/// and claimed amounts. The difference is transferred to the earner address.
/// If a claimer has not been set (see [`set_claimer_for`]), only the earner can claim. Otherwise, only
/// the claimer can claim.
///
/// # Arguments
///
/// * `claims` - A [`Vec`] of RewardsMerkleClaim objects containing the claims.
/// * `earnerAddress` - The address of the earner for whom to process the claims.
///
/// # Returns
///
/// * `Result<FixedBytes<32>, ElContractsError>` - The transaction hash if the claim is sent, otherwise an error.
///
/// # Errors
///
/// * `ElContractsError` - if the call to the contract fails. Also fails if no root has been submitted yet.
pub async fn process_claims(
&self,
claims: Vec<RewardsMerkleClaim>,
earner_address: Address,
) -> Result<FixedBytes<32>, ElContractsError> {
let provider = get_signer(&self.signer, &self.provider);

let contract_rewards_coordinator =
IRewardsCoordinator::new(self.rewards_coordinator, &provider);

let process_claim_call = contract_rewards_coordinator.processClaims(claims, earner_address);

let tx = process_claim_call.send().await?;
Ok(*tx.tx_hash())
}

/// Sets the split of a specific `operator` for a specific `avs`
///
/// # Arguments
Expand Down Expand Up @@ -690,7 +725,8 @@ mod test_utils {}
#[cfg(test)]
mod tests {
use crate::test_utils::{
build_el_chain_reader, new_test_writer, ANVIL_FIRST_ADDRESS, ANVIL_FIRST_PRIVATE_KEY,
build_el_chain_reader, new_claim, new_test_writer, ANVIL_FIRST_ADDRESS,
ANVIL_FIRST_PRIVATE_KEY, OPERATOR_ADDRESS, OPERATOR_PRIVATE_KEY,
};
use alloy::providers::Provider;
use alloy_primitives::{address, aliases::U96, Address, U256};
Expand Down Expand Up @@ -830,6 +866,47 @@ mod tests {
assert!(receipt.status());
}

#[tokio::test]
async fn test_process_claim() {
let (_container, http_endpoint, _ws_endpoint) = start_anvil_container().await;
let el_chain_writer = new_test_writer(
http_endpoint.to_string(),
ANVIL_FIRST_PRIVATE_KEY.to_string(),
)
.await;

let (_root, claim) = new_claim(&http_endpoint, U256::from(42)).await;

let tx_hash = el_chain_writer
.process_claim(claim, ANVIL_FIRST_ADDRESS)
.await
.unwrap();

let receipt = wait_transaction(&http_endpoint, tx_hash).await.unwrap();
assert!(receipt.status());
}

#[tokio::test]
async fn test_process_claims() {
let (_container, http_endpoint, _ws_endpoint) = start_anvil_container().await;
let el_chain_writer = new_test_writer(
http_endpoint.to_string(),
ANVIL_FIRST_PRIVATE_KEY.to_string(),
)
.await;

let (_root, claim0) = new_claim(&http_endpoint, U256::from(42)).await;
let (_root, claim1) = new_claim(&http_endpoint, U256::from(4256)).await;

let tx_hash = el_chain_writer
.process_claims(vec![claim0, claim1], ANVIL_FIRST_ADDRESS)
.await
.unwrap();

let receipt = wait_transaction(&http_endpoint, tx_hash).await.unwrap();
assert!(receipt.status());
}

#[tokio::test]
async fn test_add_and_remove_pending_admin() {
let (_container, http_endpoint, _ws_endpoint) = start_anvil_container().await;
Expand Down Expand Up @@ -1094,9 +1171,8 @@ mod tests {
let operator_set_id = 1;
create_operator_set(http_endpoint.as_str(), avs_address, operator_set_id).await;

let operator_addr = address!("70997970C51812dc3A010C7d01b50e0d17dc79C8");
let operator_private_key =
"0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d";
let operator_addr = OPERATOR_ADDRESS;
let operator_private_key = OPERATOR_PRIVATE_KEY;
let el_chain_writer =
new_test_writer(http_endpoint.clone(), operator_private_key.to_string()).await;
let bls_key = BlsKeyPair::new("1".to_string()).unwrap();
Expand Down

0 comments on commit 9c88c1c

Please sign in to comment.