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: add process_claims function #193

Merged
merged 9 commits into from
Jan 8, 2025
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
Loading