use std::collections::HashMap;
use async_trait::async_trait;
use tracing::{debug, info};
use tycho_common::{
models::{Address, BlockHash},
traits::AllowanceSlotDetector,
Bytes,
};
use crate::services::entrypoint_tracer::slot_detector::{
SlotDetectionStrategy, SlotDetector, SlotDetectorError,
};
#[derive(Clone)]
pub struct AllowanceStrategy;
impl SlotDetectionStrategy for AllowanceStrategy {
type CacheKey = (Address, Address, Address);
type Params = (Address, Address);
fn cache_key(token: &Address, params: &Self::Params) -> Self::CacheKey {
let (owner, spender) = params;
(token.clone(), owner.clone(), spender.clone())
}
fn encode_calldata(params: &Self::Params) -> Bytes {
let (owner, spender) = params;
let mut calldata = vec![0xdd, 0x62, 0xed, 0x3e];
calldata.extend_from_slice(&[0u8; 12]);
calldata.extend_from_slice(owner.as_ref());
calldata.extend_from_slice(&[0u8; 12]);
calldata.extend_from_slice(spender.as_ref());
Bytes::from(calldata)
}
}
pub type EVMAllowanceSlotDetector = SlotDetector<AllowanceStrategy>;
#[async_trait]
impl AllowanceSlotDetector for EVMAllowanceSlotDetector {
type Error = SlotDetectorError;
async fn detect_allowance_slots(
&self,
tokens: &[Address],
owner: Address,
spender: Address,
block_hash: BlockHash,
) -> HashMap<Address, Result<(Address, Bytes), Self::Error>> {
info!("Starting allowance slot detection for {} tokens", tokens.len());
let filtered_tokens = tokens
.iter()
.filter_map(|token| {
if !token.is_zero() {
Some(token.clone())
} else {
debug!("Skipping zero token: {token}");
None
}
})
.collect::<Vec<_>>();
if filtered_tokens.is_empty() {
return HashMap::new();
}
let params = (owner, spender);
let results = self
.detect_slots_chunked(&filtered_tokens, ¶ms, &block_hash)
.await;
info!("Allowance slot detection completed. Found results for {} tokens", results.len());
results
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use rstest::rstest;
use super::*;
use crate::{
rpc::EthereumRpcClient,
test_fixtures::{USDC_STR, USDT_STR, WETH_STR},
};
const BLOCK_HASH: &str = "0x658814e4cb074359f10dd71237cc57b1ae6791fc9de59fde570e724bd884cbb0";
#[test]
fn test_encode_allowance_calldata() {
let owner = Address::from([0x11u8; 20]);
let spender = Address::from([0x22u8; 20]);
let calldata = AllowanceStrategy::encode_calldata(&(owner.clone(), spender.clone()));
assert_eq!(&calldata[0..4], &[0xdd, 0x62, 0xed, 0x3e]);
assert_eq!(calldata.len(), 68);
assert_eq!(&calldata[4..16], &[0u8; 12]);
assert_eq!(&calldata[16..36], owner.as_ref());
assert_eq!(&calldata[36..48], &[0u8; 12]);
assert_eq!(&calldata[48..68], spender.as_ref());
}
#[tokio::test]
#[ignore = "require RPC connection"]
async fn test_detect_truf_allowance_slot() {
let rpc_url = std::env::var("RPC_URL").expect("RPC_URL must be set");
let rpc = EthereumRpcClient::new(&rpc_url).expect("failed to create RPC client");
let detector = EVMAllowanceSlotDetector::new(&rpc).with_max_token_batch_size(5);
let token = Address::from_str("0x38c2a4a7330b22788374b8ff70bba513c8d848ca").unwrap();
let owner = Address::from_str("0xcd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2").unwrap();
let spender = Address::from_str("0xfd0b31d2e955fa55e3fa641fe90e08b677188d35").unwrap();
let block_hash = BlockHash::from_str(
"0x23efd28b949cff1bea0cce77277d4e113793ff029c0c9815a36b6528eaa187ca",
)
.unwrap();
let results = detector
.detect_allowance_slots(std::slice::from_ref(&token), owner, spender, block_hash)
.await;
match results.get(&token) {
Some(Ok((storage_addr, slot))) => {
assert_eq!(storage_addr, &token);
let expected_slot = Bytes::from_str(
"0x4e4b5f80f87725e40fd825bd7b26188e05acd6dbf57e82d1bd0f2bd067293504",
)
.unwrap();
assert_eq!(slot, &expected_slot);
}
Some(Err(e)) => panic!("Failed to detect slot: {e:?}"),
None => panic!("No result returned for TRUF"),
}
}
#[rstest]
#[case(
"f847a638E44186F3287ee9F8cAF73FF4d4B80784",
"fD0b31d2E955fA55e3fa641Fe90e08b677188d35",
"ZeroAllowanceUser"
)]
#[case(
"fD0b31d2E955fA55e3fa641Fe90e08b677188d35",
"BA12222222228d8Ba445958a75a0704d566BF2C8",
"NonZeroAllowanceUser"
)]
#[tokio::test]
#[ignore = "require RPC connection"]
async fn test_detect_allowance_slots_integration(
#[case] owner_address_hex: &str,
#[case] spender_address_hex: &str,
#[case] user_name: &str,
) {
let rpc_url = std::env::var("RPC_URL").expect("RPC_URL must be set");
let rpc = EthereumRpcClient::new(&rpc_url).expect("Failed to create RPC client");
println!("Using RPC URL: {}", rpc_url);
let weth = Address::from_str(WETH_STR).expect("Invalid WETH address");
let usdc = Address::from_str(USDC_STR).expect("Invalid USDC address");
let usdt = Address::from_str(USDT_STR).expect("Invalid USDT address");
let owner_address = Address::from_str(owner_address_hex).expect("Invalid owner address");
let spender_address =
Address::from_str(spender_address_hex).expect("Invalid spender address");
let tokens = vec![weth.clone(), usdc.clone(), usdt.clone()];
let block_hash = BlockHash::from_str(BLOCK_HASH).expect("Invalid block hash");
println!("Block hash: {block_hash}");
let detector = EVMAllowanceSlotDetector::new(&rpc).with_max_token_batch_size(5);
let results = detector
.detect_allowance_slots(&tokens, owner_address, spender_address, block_hash)
.await;
assert!(!results.is_empty(), "Expected results for at least one token, but got none");
if let Some(weth_result) = results.get(&weth) {
match weth_result {
Ok((storage_addr, slot)) => {
println!(
"WETH slot detected for {user_name} - Storage: {storage_addr}, Slot: {slot}",
);
}
Err(e) => panic!("Failed to detect WETH allowance slot for {}: {}", user_name, e),
}
} else {
panic!("No result for WETH token for {}", user_name);
}
if let Some(usdc_result) = results.get(&usdc) {
match usdc_result {
Ok((storage_addr, slot)) => {
println!(
"USDC slot detected for {user_name} - Storage: {storage_addr}, Slot: {slot}",
);
}
Err(e) => panic!("Failed to detect USDC allowance slot for {}: {}", user_name, e),
}
} else {
panic!("No result for USDC token for {}", user_name);
}
if let Some(usdt_result) = results.get(&usdt) {
match usdt_result {
Ok((storage_addr, slot)) => {
println!(
"USDT slot detected for {user_name} - Storage: {storage_addr}, Slot: {slot}",
);
assert_eq!(storage_addr, &usdt, "Storage address should match token address");
}
Err(e) => panic!("Failed to detect USDT allowance slot for {}: {}", user_name, e),
}
} else {
panic!("No result for USDT token for {}", user_name);
}
}
}