diff --git a/crates/chainio/clients/elcontracts/src/lib.rs b/crates/chainio/clients/elcontracts/src/lib.rs index 6b43a6e4..af549a48 100644 --- a/crates/chainio/clients/elcontracts/src/lib.rs +++ b/crates/chainio/clients/elcontracts/src/lib.rs @@ -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::{ @@ -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 = @@ -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; @@ -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"); @@ -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", ) @@ -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) + } } diff --git a/crates/chainio/clients/elcontracts/src/reader.rs b/crates/chainio/clients/elcontracts/src/reader.rs index 0c1cd101..e05a8a3b 100644 --- a/crates/chainio/clients/elcontracts/src/reader.rs +++ b/crates/chainio/clients/elcontracts/src/reader.rs @@ -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}; @@ -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() @@ -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() @@ -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() @@ -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) @@ -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); diff --git a/crates/chainio/clients/elcontracts/src/writer.rs b/crates/chainio/clients/elcontracts/src/writer.rs index f8964019..c2e56b1b 100644 --- a/crates/chainio/clients/elcontracts/src/writer.rs +++ b/crates/chainio/clients/elcontracts/src/writer.rs @@ -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 /// @@ -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, ElContractsError> { let provider = get_signer(&self.signer, &self.provider); @@ -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, 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, + earner_address: Address, + ) -> Result, 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 @@ -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}; @@ -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; @@ -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();