use std::collections::HashMap;
use alloy_primitives::{Address, Log, U256, keccak256};
use alloy_sol_types::SolValue;
use crate::events::{EventDecoder, StateView};
use crate::inspector::TransferInspector;
use crate::mapping_probe::TrackedMapping;
use crate::state_update::{SlotDelta, StateUpdate};
pub struct Erc20TransferDecoder {
balance_slots: HashMap<Address, U256>,
tracked: HashMap<Address, TrackedMapping>,
default_balance_slot: U256,
}
impl Erc20TransferDecoder {
pub fn new(default_balance_slot: U256) -> Self {
Self {
balance_slots: HashMap::new(),
tracked: HashMap::new(),
default_balance_slot,
}
}
pub fn with_token(mut self, token: Address, balance_slot: U256) -> Self {
self.balance_slots.insert(token, balance_slot);
self
}
pub fn with_tracked(mut self, tracked: TrackedMapping) -> Self {
self.tracked.insert(tracked.contract, tracked);
self
}
fn entry_slot(&self, token: Address, owner: Address) -> U256 {
if let Some(tracked) = self.tracked.get(&token)
&& let Some(slot) = tracked.slot_for(owner.into_word())
{
return U256::from_be_bytes(slot.0);
}
balance_key(owner, self.balance_slot(token))
}
fn balance_slot(&self, token: Address) -> U256 {
self.balance_slots
.get(&token)
.copied()
.unwrap_or(self.default_balance_slot)
}
}
fn balance_key(owner: Address, mapping_slot: U256) -> U256 {
U256::from_be_bytes(keccak256((owner, mapping_slot).abi_encode()).0)
}
impl EventDecoder for Erc20TransferDecoder {
fn decode(&self, log: &Log, _view: &dyn StateView) -> Vec<StateUpdate> {
let Some(transfer) = TransferInspector::parse_transfer(log) else {
return Vec::new();
};
let mut updates = Vec::with_capacity(2);
if transfer.from != Address::ZERO {
updates.push(StateUpdate::slot_delta(
transfer.token,
self.entry_slot(transfer.token, transfer.from),
SlotDelta::Sub(transfer.value),
));
}
if transfer.to != Address::ZERO {
updates.push(StateUpdate::slot_delta(
transfer.token,
self.entry_slot(transfer.token, transfer.to),
SlotDelta::Add(transfer.value),
));
}
updates
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mapping_probe::{SlotLayout, TrackedMapping};
use alloy_primitives::{B256, Log, address};
struct NoState;
impl StateView for NoState {
fn storage(&self, _address: Address, _slot: U256) -> Option<U256> {
None
}
}
fn transfer_log(token: Address, from: Address, to: Address, value: U256) -> Log {
let sig = keccak256("Transfer(address,address,uint256)");
Log::new(
token,
vec![sig, from.into_word(), to.into_word()],
value.to_be_bytes_vec().into(),
)
.unwrap()
}
const TOKEN: Address = address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
const HOLDER: Address = address!("00000000000000000000000000000000000000A1");
#[test]
fn mint_skips_zero_address_from_leg() {
let decoder = Erc20TransferDecoder::new(U256::from(3u64));
let log = transfer_log(TOKEN, Address::ZERO, HOLDER, U256::from(100u64));
let updates = decoder.decode(&log, &NoState);
assert_eq!(
updates,
vec![StateUpdate::slot_delta(
TOKEN,
balance_key(HOLDER, U256::from(3u64)),
SlotDelta::Add(U256::from(100u64)),
)],
"mint emits only the recipient leg; no zero-address write"
);
}
#[test]
fn burn_skips_zero_address_to_leg() {
let decoder = Erc20TransferDecoder::new(U256::from(3u64));
let log = transfer_log(TOKEN, HOLDER, Address::ZERO, U256::from(40u64));
let updates = decoder.decode(&log, &NoState);
assert_eq!(
updates,
vec![StateUpdate::slot_delta(
TOKEN,
balance_key(HOLDER, U256::from(3u64)),
SlotDelta::Sub(U256::from(40u64)),
)],
"burn emits only the sender leg; no zero-address write"
);
}
#[test]
fn zero_to_zero_emits_nothing() {
let decoder = Erc20TransferDecoder::new(U256::from(3u64));
let log = transfer_log(TOKEN, Address::ZERO, Address::ZERO, U256::from(5u64));
assert!(decoder.decode(&log, &NoState).is_empty());
}
#[test]
fn layout_aware_uses_tracked_order_and_still_skips_zero() {
let tracked = TrackedMapping::new(TOKEN, U256::from(2u64), SlotLayout::VyperMapping);
let decoder = Erc20TransferDecoder::new(U256::from(3u64)).with_tracked(tracked);
let log = transfer_log(TOKEN, Address::ZERO, HOLDER, U256::from(7u64));
let updates = decoder.decode(&log, &NoState);
let expected_slot = {
let mut pre = [0u8; 64];
pre[31] = 2;
pre[32..64].copy_from_slice(HOLDER.into_word().as_slice());
B256::from(keccak256(pre))
};
assert_eq!(
updates,
vec![StateUpdate::slot_delta(
TOKEN,
U256::from_be_bytes(expected_slot.0),
SlotDelta::Add(U256::from(7u64)),
)]
);
}
}