use alloy::{
network::TransactionBuilder,
primitives::{address, bytes, Address, Bytes, U256},
providers::{ext::TraceApi, ProviderBuilder},
rpc::types::{trace::parity::TraceType, TransactionRequest},
};
use eyre::{bail, OptionExt, Result};
use std::{collections::VecDeque, fmt::Debug};
#[derive(Debug, Clone)]
pub struct Introspector {
pub contract_address: Address,
rpc_url: String,
}
#[derive(Debug, Clone, Default, Eq, PartialEq)]
pub struct IntrospectResult {
pub balance_slot: Option<U256>,
pub allowance_slot: Option<U256>,
pub token_approvals_slot: Option<U256>,
pub operator_approvals_slot: Option<U256>,
pub erc_1155_balance_slot: Option<U256>,
}
const INTROSPECT_ADDRESS: Address = address!("000000000000000000696c6f76656f7474657273");
impl Introspector {
pub fn try_new(contract_address: Address, rpc_url: impl Into<String>) -> Self {
Self { contract_address, rpc_url: rpc_url.into() }
}
pub async fn run(&self) -> Result<IntrospectResult> {
let balance_slot_future = self.get_balance_slot();
let allowance_slot_future = self.get_allowance_slot();
let token_approvals_slot_future = self.get_token_approvals_slot();
let operator_approvals_slot_future = self.get_operator_approvals_slot();
let erc_1155_balance_slot_future = self.get_erc_1155_balance_slot();
let (
balance_slot_result,
allowance_slot_result,
token_approvals_slot_result,
operator_approvals_slot_result,
erc_1155_balance_slot_result,
) = futures::future::join5(
balance_slot_future,
allowance_slot_future,
token_approvals_slot_future,
operator_approvals_slot_future,
erc_1155_balance_slot_future,
)
.await;
let maybe_erc_20 = balance_slot_result.is_ok() && allowance_slot_result.is_ok();
let maybe_erc_721 = balance_slot_result.is_ok() &&
token_approvals_slot_result.is_ok() &&
operator_approvals_slot_result.is_ok();
let maybe_erc_1155 =
operator_approvals_slot_result.is_ok() && erc_1155_balance_slot_result.is_ok();
if maybe_erc_20 {
Ok(IntrospectResult {
balance_slot: balance_slot_result.ok(),
allowance_slot: allowance_slot_result.ok(),
..Default::default()
})
} else if maybe_erc_721 {
Ok(IntrospectResult {
balance_slot: balance_slot_result.ok(),
token_approvals_slot: token_approvals_slot_result.ok(),
operator_approvals_slot: operator_approvals_slot_result.ok(),
..Default::default()
})
} else if maybe_erc_1155 {
Ok(IntrospectResult {
operator_approvals_slot: operator_approvals_slot_result.ok(),
erc_1155_balance_slot: erc_1155_balance_slot_result.ok(),
..Default::default()
})
} else {
Ok(IntrospectResult {
balance_slot: balance_slot_result.ok(),
allowance_slot: allowance_slot_result.ok(),
token_approvals_slot: token_approvals_slot_result.ok(),
operator_approvals_slot: operator_approvals_slot_result.ok(),
erc_1155_balance_slot: erc_1155_balance_slot_result.ok(),
})
}
}
pub async fn get_balance_slot(&self) -> Result<U256> {
let calldata =
bytes!("70a08231000000000000000000000000000000000000000000696c6f76656f7474657273"); return self.extract_slot(calldata).await;
}
pub async fn get_allowance_slot(&self) -> Result<U256> {
let calldata =
bytes!("dd62ed3e000000000000000000000000000000000000000000696c6f76656f7474657273000000000000000000000000000000000000000000696c6f76656f7474657273"); return self.extract_slot(calldata).await;
}
pub async fn get_token_approvals_slot(&self) -> Result<U256> {
let calldata =
bytes!("081812fc000000000000000000000000000000000000000000696c6f76656f7474657273"); return self.extract_slot(calldata).await;
}
pub async fn get_operator_approvals_slot(&self) -> Result<U256> {
let calldata =
bytes!("e985e9c5000000000000000000000000000000000000000000696c6f76656f7474657273000000000000000000000000000000000000000000696c6f76656f7474657273"); return self.extract_slot(calldata).await;
}
pub async fn get_erc_1155_balance_slot(&self) -> Result<U256> {
let calldata =
bytes!("00fdd58e000000000000000000000000000000000000000000696c6f76656f7474657273000000000000000000000000000000000000000000696c6f76656f7474657273"); return self.extract_slot(calldata).await;
}
async fn extract_slot(&self, calldata: Bytes) -> Result<U256> {
let provider = ProviderBuilder::new().on_builtin(&self.rpc_url).await?;
let mut tx = TransactionRequest::default()
.with_from(INTROSPECT_ADDRESS)
.with_to(self.contract_address)
.with_input(calldata.clone());
tx.input.data = Some(calldata);
let result = provider
.trace_call(&tx, &[TraceType::VmTrace, TraceType::Trace])
.await?
.vm_trace
.ok_or_eyre("vm trace not found")?
.ops;
let mut stack: VecDeque<U256> = VecDeque::new();
let mut memory: Vec<u8> = Vec::new();
for instruction in result {
if let Some(executed) = &instruction.ex {
if let Some(memory_delta) = &executed.mem {
let offset = memory_delta.off;
let data = memory_delta.data.to_vec();
if memory.len() < offset + data.len() {
memory.resize(offset + data.len(), 0);
}
memory[offset..offset + data.len()].copy_from_slice(&data);
}
stack.extend(executed.push.iter());
}
if let Some(opcode) = &instruction.op {
if ["KECCAK256", "SHA3"].contains(&opcode.as_str()) {
let _ = stack.pop_back().ok_or_eyre("stack underflow")?; let offset: usize =
stack.pop_back().ok_or_eyre("stack underflow")?.try_into()?;
let size: usize = stack.pop_back().ok_or_eyre("stack underflow")?.try_into()?;
let data = memory[offset..offset + size].to_vec();
if data.len() == 64 && &data[12..32] == INTROSPECT_ADDRESS.as_slice() {
let balance_slot: U256 = U256::from_be_slice(&data[32..64]);
return Ok(balance_slot);
}
}
}
}
bail!("failed to get balance slot")
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloy::primitives::address;
#[tokio::test]
async fn test_introspect_erc20() {
let rpc_url = std::env::var("RPC_URL").unwrap_or_else(|_| {
println!("RPC_URL not set, skipping test");
std::process::exit(0);
});
let contract_address = address!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2");
let introspector = Introspector::try_new(contract_address, rpc_url);
let res = introspector.run().await.expect("failed to run introspection");
assert_eq!(
res,
IntrospectResult {
balance_slot: Some(U256::from(3)),
allowance_slot: Some(U256::from(4)),
..Default::default()
}
);
}
#[tokio::test]
async fn test_introspect_erc721() {
let rpc_url = std::env::var("RPC_URL").unwrap_or_else(|_| {
println!("RPC_URL not set, skipping test");
std::process::exit(0);
});
let contract_address = address!("bc4ca0eda7647a8ab7c2061c2e118a18a936f13d");
let introspector = Introspector::try_new(contract_address, rpc_url);
let res = introspector.run().await.expect("failed to run introspection");
assert_eq!(
res,
IntrospectResult {
balance_slot: Some(U256::from(1)),
token_approvals_slot: Some(U256::from(3)),
operator_approvals_slot: Some(U256::from(5)),
..Default::default()
}
);
}
#[tokio::test]
async fn test_introspect_erc1155() {
let rpc_url = std::env::var("RPC_URL").unwrap_or_else(|_| {
println!("RPC_URL not set, skipping test");
std::process::exit(0);
});
let contract_address = address!("c552292732f7a9a4a494da557b47bc01e01722df");
let introspector = Introspector::try_new(contract_address, rpc_url);
let res = introspector.run().await.expect("failed to run introspection");
assert_eq!(
res,
IntrospectResult {
operator_approvals_slot: Some(U256::from(1)),
erc_1155_balance_slot: Some(U256::from(0)),
..Default::default()
}
);
}
}