1use alloy::{
7 network::TransactionBuilder,
8 primitives::{address, bytes, Address, Bytes, U256},
9 providers::{ext::TraceApi, ProviderBuilder},
10 rpc::types::{trace::parity::TraceType, TransactionRequest},
11};
12use eyre::{bail, OptionExt, Result};
13use std::{collections::VecDeque, fmt::Debug};
14
15#[derive(Debug, Clone)]
18pub struct Introspector {
19 pub contract_address: Address,
21 rpc_url: String,
24}
25
26#[derive(Debug, Clone, Default, Eq, PartialEq)]
28pub struct IntrospectResult {
29 pub balance_slot: Option<U256>,
31 pub allowance_slot: Option<U256>,
33 pub token_approvals_slot: Option<U256>,
35 pub operator_approvals_slot: Option<U256>,
37 pub erc_1155_balance_slot: Option<U256>,
39}
40
41const INTROSPECT_ADDRESS: Address = address!("000000000000000000696c6f76656f7474657273");
42
43impl Introspector {
44 pub fn try_new(contract_address: Address, rpc_url: impl Into<String>) -> Self {
46 Self { contract_address, rpc_url: rpc_url.into() }
47 }
48
49 pub async fn run(&self) -> Result<IntrospectResult> {
58 let balance_slot_future = self.get_balance_slot();
59 let allowance_slot_future = self.get_allowance_slot();
60 let token_approvals_slot_future = self.get_token_approvals_slot();
61 let operator_approvals_slot_future = self.get_operator_approvals_slot();
62 let erc_1155_balance_slot_future = self.get_erc_1155_balance_slot();
63
64 let (
66 balance_slot_result,
67 allowance_slot_result,
68 token_approvals_slot_result,
69 operator_approvals_slot_result,
70 erc_1155_balance_slot_result,
71 ) = futures::future::join5(
72 balance_slot_future,
73 allowance_slot_future,
74 token_approvals_slot_future,
75 operator_approvals_slot_future,
76 erc_1155_balance_slot_future,
77 )
78 .await;
79
80 let maybe_erc_20 = balance_slot_result.is_ok() && allowance_slot_result.is_ok();
81 let maybe_erc_721 = balance_slot_result.is_ok() &&
82 token_approvals_slot_result.is_ok() &&
83 operator_approvals_slot_result.is_ok();
84 let maybe_erc_1155 =
85 operator_approvals_slot_result.is_ok() && erc_1155_balance_slot_result.is_ok();
86
87 if maybe_erc_20 {
88 Ok(IntrospectResult {
89 balance_slot: balance_slot_result.ok(),
90 allowance_slot: allowance_slot_result.ok(),
91 ..Default::default()
92 })
93 } else if maybe_erc_721 {
94 Ok(IntrospectResult {
95 balance_slot: balance_slot_result.ok(),
96 token_approvals_slot: token_approvals_slot_result.ok(),
97 operator_approvals_slot: operator_approvals_slot_result.ok(),
98 ..Default::default()
99 })
100 } else if maybe_erc_1155 {
101 Ok(IntrospectResult {
102 operator_approvals_slot: operator_approvals_slot_result.ok(),
103 erc_1155_balance_slot: erc_1155_balance_slot_result.ok(),
104 ..Default::default()
105 })
106 } else {
107 Ok(IntrospectResult {
108 balance_slot: balance_slot_result.ok(),
109 allowance_slot: allowance_slot_result.ok(),
110 token_approvals_slot: token_approvals_slot_result.ok(),
111 operator_approvals_slot: operator_approvals_slot_result.ok(),
112 erc_1155_balance_slot: erc_1155_balance_slot_result.ok(),
113 })
114 }
115 }
116
117 pub async fn get_balance_slot(&self) -> Result<U256> {
119 let calldata =
120 bytes!("70a08231000000000000000000000000000000000000000000696c6f76656f7474657273"); return self.extract_slot(calldata).await;
122 }
123
124 pub async fn get_allowance_slot(&self) -> Result<U256> {
126 let calldata =
127 bytes!("dd62ed3e000000000000000000000000000000000000000000696c6f76656f7474657273000000000000000000000000000000000000000000696c6f76656f7474657273"); return self.extract_slot(calldata).await;
129 }
130
131 pub async fn get_token_approvals_slot(&self) -> Result<U256> {
133 let calldata =
134 bytes!("081812fc000000000000000000000000000000000000000000696c6f76656f7474657273"); return self.extract_slot(calldata).await;
136 }
137
138 pub async fn get_operator_approvals_slot(&self) -> Result<U256> {
141 let calldata =
142 bytes!("e985e9c5000000000000000000000000000000000000000000696c6f76656f7474657273000000000000000000000000000000000000000000696c6f76656f7474657273"); return self.extract_slot(calldata).await;
144 }
145
146 pub async fn get_erc_1155_balance_slot(&self) -> Result<U256> {
149 let calldata =
150 bytes!("00fdd58e000000000000000000000000000000000000000000696c6f76656f7474657273000000000000000000000000000000000000000000696c6f76656f7474657273"); return self.extract_slot(calldata).await;
152 }
153
154 async fn extract_slot(&self, calldata: Bytes) -> Result<U256> {
168 let provider = ProviderBuilder::new().on_builtin(&self.rpc_url).await?;
169
170 let mut tx = TransactionRequest::default()
171 .with_from(INTROSPECT_ADDRESS)
172 .with_to(self.contract_address)
173 .with_input(calldata.clone());
174 tx.input.data = Some(calldata);
175
176 let result = provider
177 .trace_call(&tx, &[TraceType::VmTrace, TraceType::Trace])
178 .await?
179 .vm_trace
180 .ok_or_eyre("vm trace not found")?
181 .ops;
182
183 let mut stack: VecDeque<U256> = VecDeque::new();
186 let mut memory: Vec<u8> = Vec::new();
187
188 for instruction in result {
189 if let Some(executed) = &instruction.ex {
190 if let Some(memory_delta) = &executed.mem {
191 let offset = memory_delta.off;
192 let data = memory_delta.data.to_vec();
193
194 if memory.len() < offset + data.len() {
196 memory.resize(offset + data.len(), 0);
197 }
198
199 memory[offset..offset + data.len()].copy_from_slice(&data);
201 }
202
203 stack.extend(executed.push.iter());
204 }
205
206 if let Some(opcode) = &instruction.op {
207 if ["KECCAK256", "SHA3"].contains(&opcode.as_str()) {
210 let _ = stack.pop_back().ok_or_eyre("stack underflow")?; let offset: usize =
212 stack.pop_back().ok_or_eyre("stack underflow")?.try_into()?;
213 let size: usize = stack.pop_back().ok_or_eyre("stack underflow")?.try_into()?;
214
215 let data = memory[offset..offset + size].to_vec();
216
217 if data.len() == 64 && &data[12..32] == INTROSPECT_ADDRESS.as_slice() {
219 let balance_slot: U256 = U256::from_be_slice(&data[32..64]);
221 return Ok(balance_slot);
222 }
223 }
224 }
225 }
226
227 bail!("failed to get balance slot")
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234 use alloy::primitives::address;
235
236 #[tokio::test]
237 async fn test_introspect_erc20() {
238 let rpc_url = std::env::var("RPC_URL").unwrap_or_else(|_| {
239 println!("RPC_URL not set, skipping test");
240 std::process::exit(0);
241 });
242
243 let contract_address = address!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2");
244 let introspector = Introspector::try_new(contract_address, rpc_url);
245
246 let res = introspector.run().await.expect("failed to run introspection");
247 assert_eq!(
248 res,
249 IntrospectResult {
250 balance_slot: Some(U256::from(3)),
251 allowance_slot: Some(U256::from(4)),
252 ..Default::default()
253 }
254 );
255 }
256
257 #[tokio::test]
258 async fn test_introspect_erc721() {
259 let rpc_url = std::env::var("RPC_URL").unwrap_or_else(|_| {
260 println!("RPC_URL not set, skipping test");
261 std::process::exit(0);
262 });
263
264 let contract_address = address!("bc4ca0eda7647a8ab7c2061c2e118a18a936f13d");
265 let introspector = Introspector::try_new(contract_address, rpc_url);
266
267 let res = introspector.run().await.expect("failed to run introspection");
268 assert_eq!(
269 res,
270 IntrospectResult {
271 balance_slot: Some(U256::from(1)),
272 token_approvals_slot: Some(U256::from(3)),
273 operator_approvals_slot: Some(U256::from(5)),
274 ..Default::default()
275 }
276 );
277 }
278
279 #[tokio::test]
280 async fn test_introspect_erc1155() {
281 let rpc_url = std::env::var("RPC_URL").unwrap_or_else(|_| {
282 println!("RPC_URL not set, skipping test");
283 std::process::exit(0);
284 });
285
286 let contract_address = address!("c552292732f7a9a4a494da557b47bc01e01722df");
287 let introspector = Introspector::try_new(contract_address, rpc_url);
288
289 let res = introspector.run().await.expect("failed to run introspection");
290 assert_eq!(
291 res,
292 IntrospectResult {
293 operator_approvals_slot: Some(U256::from(1)),
294 erc_1155_balance_slot: Some(U256::from(0)),
295 ..Default::default()
296 }
297 );
298 }
299}