use std::collections::HashMap;
use std::fmt;
use alloy_primitives::{Address, B256, U256, keccak256};
use revm::Inspector;
use revm::interpreter::interpreter_types::{Jumps, MemoryTr, StackTr};
use revm::interpreter::{Interpreter, InterpreterTypes};
const OP_KECCAK256: u8 = 0x20;
const OP_SLOAD: u8 = 0x54;
const MAX_PREIMAGE_LEN: usize = 4096;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SlotLayout {
SolidityMapping,
VyperMapping,
Nested,
PackedSeed {
seed: U256,
},
ArrayPointer,
Opaque,
}
impl fmt::Display for SlotLayout {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SlotLayout::SolidityMapping => f.write_str("mapping(key‖slot) [Solidity]"),
SlotLayout::VyperMapping => f.write_str("mapping(slot‖key) [Vyper]"),
SlotLayout::Nested => f.write_str("nested mapping"),
SlotLayout::PackedSeed { seed } => write!(f, "packed(addr‖seed={seed:#x}) [Solady]"),
SlotLayout::ArrayPointer => f.write_str("array/base pointer"),
SlotLayout::Opaque => f.write_str("opaque"),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Confidence {
Low,
Heuristic,
Medium,
High,
}
impl fmt::Display for Confidence {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{self:?}")
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct HashSlotAccess {
pub slot: B256,
pub value: U256,
pub keys: Vec<B256>,
pub base_slot: U256,
pub layout: SlotLayout,
pub depth: usize,
pub confidence: Confidence,
}
impl HashSlotAccess {
pub fn keyed_by(&self, k: B256) -> bool {
self.keys.iter().any(|key| word_matches(*key, k))
}
pub fn as_tracked(&self, contract: Address) -> Option<TrackedMapping> {
if self.keys.len() != 1 {
return None;
}
match self.layout {
SlotLayout::SolidityMapping
| SlotLayout::VyperMapping
| SlotLayout::PackedSeed { .. } => Some(TrackedMapping {
contract,
base_slot: self.base_slot,
layout: self.layout,
}),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TrackedMapping {
pub contract: Address,
pub base_slot: U256,
pub layout: SlotLayout,
}
impl TrackedMapping {
pub fn new(contract: Address, base_slot: U256, layout: SlotLayout) -> Self {
Self {
contract,
base_slot,
layout,
}
}
pub fn slot_for(&self, key: B256) -> Option<B256> {
match self.layout {
SlotLayout::SolidityMapping => {
let mut pre = [0u8; 64];
pre[0..32].copy_from_slice(key.as_slice());
pre[32..64].copy_from_slice(self.base_slot_word().as_slice());
Some(keccak256(pre))
}
SlotLayout::VyperMapping => {
let mut pre = [0u8; 64];
pre[0..32].copy_from_slice(self.base_slot_word().as_slice());
pre[32..64].copy_from_slice(key.as_slice());
Some(keccak256(pre))
}
SlotLayout::PackedSeed { seed } => {
let mut pre = [0u8; 32];
pre[0..20].copy_from_slice(&Address::from_word(key).into_array());
let seed_be = seed.to_be_bytes::<32>();
pre[20..32].copy_from_slice(&seed_be[20..32]);
Some(keccak256(pre))
}
_ => None,
}
}
pub fn slots_for(&self, keys: impl IntoIterator<Item = B256>) -> Vec<(B256, B256)> {
keys.into_iter()
.filter_map(|k| self.slot_for(k).map(|s| (k, s)))
.collect()
}
fn base_slot_word(&self) -> B256 {
B256::from(self.base_slot.to_be_bytes::<32>())
}
}
pub type TrackedBalances = (TrackedMapping, Vec<(Address, B256)>);
#[derive(Clone, Debug)]
struct SloadRecord {
slot: B256,
value: U256,
}
#[derive(Clone, Debug, Default)]
pub struct HashStorageProbe {
preimages: HashMap<B256, Vec<u8>>,
reads: Vec<SloadRecord>,
pending: Option<B256>,
}
impl HashStorageProbe {
pub fn new() -> Self {
Self::default()
}
pub fn preimage_count(&self) -> usize {
self.preimages.len()
}
pub fn hashed_read_count(&self) -> usize {
self.reads
.iter()
.filter(|r| self.preimages.contains_key(&r.slot))
.count()
}
pub fn slots_returning(&self, value: U256) -> Vec<B256> {
let mut seen = std::collections::HashSet::new();
self.reads
.iter()
.filter(|r| r.value == value)
.map(|r| r.slot)
.filter(|slot| seen.insert(*slot))
.collect()
}
pub fn accesses(&self, known: &[B256]) -> Vec<HashSlotAccess> {
self.reads
.iter()
.filter(|r| self.preimages.contains_key(&r.slot))
.map(|r| resolve(r.slot, r.value, &self.preimages, known))
.collect()
}
}
impl<CTX, INTR: InterpreterTypes> Inspector<CTX, INTR> for HashStorageProbe {
fn step(&mut self, interp: &mut Interpreter<INTR>, _ctx: &mut CTX) {
match interp.bytecode.opcode() {
OP_KECCAK256 => {
let (offset, size) = {
let s = interp.stack.data();
let n = s.len();
if n < 2 {
return;
}
(s[n - 1], s[n - 2])
};
let (Some(offset), Some(size)) = (to_usize(offset), to_usize(size)) else {
return;
};
if size == 0 || size > MAX_PREIMAGE_LEN {
return;
}
let preimage = read_mem(interp, offset, size);
self.preimages.insert(keccak256(&preimage), preimage);
}
OP_SLOAD => {
if let Some(k) = interp.stack.data().last() {
self.pending = Some(word_from(*k));
}
}
_ => {}
}
}
fn step_end(&mut self, interp: &mut Interpreter<INTR>, _ctx: &mut CTX) {
if let Some(slot) = self.pending.take() {
if let Some(value) = interp.stack.data().last().copied() {
self.reads.push(SloadRecord { slot, value });
}
}
}
}
fn resolve(
slot: B256,
value: U256,
pre: &HashMap<B256, Vec<u8>>,
known: &[B256],
) -> HashSlotAccess {
let mut keys: Vec<B256> = Vec::new();
let mut confidence = Confidence::High;
let mut cur = slot;
let (base_slot, layout) = loop {
let Some(p) = pre.get(&cur) else {
let l = if keys.is_empty() {
SlotLayout::Opaque
} else {
SlotLayout::Nested
};
break (U256::from_be_slice(cur.as_slice()), l);
};
match p.len() {
64 => {
let aw = B256::from_slice(&p[0..32]);
let bw = B256::from_slice(&p[32..64]);
let a_parent = pre.contains_key(&aw);
let b_parent = pre.contains_key(&bw);
if a_parent ^ b_parent {
let (parent, key) = if a_parent { (aw, bw) } else { (bw, aw) };
keys.push(key);
cur = parent;
continue;
}
let (slot_word, key_word, key_first, conf) =
split_key_slot(&p[0..32], &p[32..64], known);
confidence = confidence.min(conf);
keys.push(key_word);
let layout = if keys.len() > 1 {
SlotLayout::Nested
} else if key_first {
SlotLayout::SolidityMapping
} else {
SlotLayout::VyperMapping
};
break (U256::from_be_slice(slot_word.as_slice()), layout);
}
32 => {
let hi = Address::from_slice(&p[0..20]);
if hi != Address::ZERO && known.iter().any(|k| Address::from_word(*k) == hi) {
keys.push(hi.into_word());
confidence = confidence.min(Confidence::Medium);
let seed = U256::from_be_slice(&p[20..32]);
break (seed, SlotLayout::PackedSeed { seed });
}
confidence = confidence.min(Confidence::Heuristic);
let l = if keys.is_empty() {
SlotLayout::ArrayPointer
} else {
SlotLayout::Nested
};
break (U256::from_be_slice(&p[0..32]), l);
}
_ => {
confidence = Confidence::Low;
break (U256::from_be_slice(cur.as_slice()), SlotLayout::Opaque);
}
}
};
let depth = keys.len().max(1);
HashSlotAccess {
slot,
value,
keys,
base_slot,
layout,
depth,
confidence,
}
}
fn split_key_slot(a: &[u8], b: &[u8], known: &[B256]) -> (B256, B256, bool, Confidence) {
let aw = B256::from_slice(a);
let bw = B256::from_slice(b);
let a_known = known.iter().any(|k| word_matches(*k, aw));
let b_known = known.iter().any(|k| word_matches(*k, bw));
match (a_known, b_known) {
(true, false) => (bw, aw, true, Confidence::High), (false, true) => (aw, bw, false, Confidence::High), _ => {
let (sa, sb) = (sig(a), sig(b));
if sa < sb {
(aw, bw, false, Confidence::Medium)
} else if sb < sa {
(bw, aw, true, Confidence::Medium)
} else {
(aw, bw, false, Confidence::Low)
}
}
}
}
fn read_mem<INTR: InterpreterTypes>(
interp: &Interpreter<INTR>,
offset: usize,
len: usize,
) -> Vec<u8> {
let mut out = vec![0u8; len];
let size = interp.memory.size();
if offset >= size || len == 0 {
return out;
}
let avail = (size - offset).min(len);
let chunk = interp.memory.slice_len(offset, avail);
out[..avail].copy_from_slice(&chunk[..]);
out
}
fn to_usize(x: U256) -> Option<usize> {
let limbs = x.as_limbs();
if limbs[1] | limbs[2] | limbs[3] != 0 {
return None;
}
usize::try_from(limbs[0]).ok()
}
fn word_from(x: U256) -> B256 {
B256::from(x.to_be_bytes::<32>())
}
fn word_matches(a: B256, b: B256) -> bool {
a == b || Address::from_word(a) == Address::from_word(b)
}
fn sig(bytes: &[u8]) -> usize {
match bytes.iter().position(|&x| x != 0) {
Some(p) => bytes.len() - p,
None => 0,
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloy_primitives::address;
fn sol_slot(key: Address, base: u64) -> B256 {
let mut pre = [0u8; 64];
pre[0..32].copy_from_slice(key.into_word().as_slice());
pre[63] = base as u8;
keccak256(pre)
}
fn vyper_slot(base: u64, key: Address) -> B256 {
let mut pre = [0u8; 64];
pre[31] = base as u8;
pre[32..64].copy_from_slice(key.into_word().as_slice());
keccak256(pre)
}
fn solady_slot(key: Address, seed: u32) -> B256 {
let mut pre = [0u8; 32];
pre[0..20].copy_from_slice(&key.into_array());
pre[28..32].copy_from_slice(&seed.to_be_bytes());
keccak256(pre)
}
fn probe_with(preimages: Vec<Vec<u8>>, reads: Vec<(B256, U256)>) -> HashStorageProbe {
let mut p = HashStorageProbe::new();
for pre in preimages {
p.preimages.insert(keccak256(&pre), pre);
}
for (slot, value) in reads {
p.reads.push(SloadRecord { slot, value });
}
p
}
#[test]
fn slots_returning_matches_value_and_dedups() {
let key = address!("00000000000000000000000000000000000000A1");
let mut pre = vec![0u8; 64];
pre[0..32].copy_from_slice(key.into_word().as_slice());
pre[63] = 3;
let hashed_slot = keccak256(&pre);
let plain_slot = B256::from(U256::from(2u64).to_be_bytes::<32>());
let mut probe = probe_with(vec![pre], vec![]);
probe.reads.push(SloadRecord {
slot: hashed_slot,
value: U256::from(100u64),
});
probe.reads.push(SloadRecord {
slot: hashed_slot,
value: U256::from(100u64),
});
probe.reads.push(SloadRecord {
slot: plain_slot,
value: U256::from(100u64),
});
probe.reads.push(SloadRecord {
slot: plain_slot,
value: U256::from(7u64),
});
assert_eq!(
probe.slots_returning(U256::from(100u64)),
vec![hashed_slot, plain_slot]
);
assert_eq!(probe.hashed_read_count(), 2); assert_eq!(probe.accesses(&[key.into_word()]).len(), 2); }
#[test]
fn resolves_solidity_mapping() {
let key = address!("00000000000000000000000000000000000000A1");
let mut pre = vec![0u8; 64];
pre[0..32].copy_from_slice(key.into_word().as_slice());
pre[63] = 3;
let slot = keccak256(&pre);
let probe = probe_with(vec![pre], vec![(slot, U256::from(42u64))]);
let a = &probe.accesses(&[key.into_word()])[0];
assert_eq!(a.layout, SlotLayout::SolidityMapping);
assert_eq!(a.base_slot, U256::from(3u64));
assert_eq!(a.keys, vec![key.into_word()]);
assert_eq!(a.confidence, Confidence::High);
}
#[test]
fn resolves_vyper_order_without_known_key() {
let key = address!("28C6c06298d514Db089934071355E5743bf21d60");
let mut pre = vec![0u8; 64];
pre[31] = 2;
pre[32..64].copy_from_slice(key.into_word().as_slice());
let slot = keccak256(&pre);
let probe = probe_with(vec![pre], vec![(slot, U256::from(1u64))]);
let a = &probe.accesses(&[])[0];
assert_eq!(a.layout, SlotLayout::VyperMapping);
assert_eq!(a.base_slot, U256::from(2u64));
assert_eq!(a.confidence, Confidence::Medium);
}
#[test]
fn resolves_solady_packed() {
let key = address!("00000000000000000000000000000000000000A1");
let seed = 0x87a2_11a2u32;
let mut pre = vec![0u8; 32];
pre[0..20].copy_from_slice(&key.into_array());
pre[28..32].copy_from_slice(&seed.to_be_bytes());
let slot = keccak256(&pre);
let probe = probe_with(vec![pre], vec![(slot, U256::from(7u64))]);
let a = &probe.accesses(&[key.into_word()])[0];
assert_eq!(
a.layout,
SlotLayout::PackedSeed {
seed: U256::from(seed)
}
);
assert!(a.keyed_by(key.into_word()));
}
#[test]
fn resolves_nested_mapping() {
let owner = address!("00000000000000000000000000000000000000A1");
let spender = address!("00000000000000000000000000000000000000B2");
let mut inner_pre = vec![0u8; 64];
inner_pre[0..32].copy_from_slice(owner.into_word().as_slice());
inner_pre[63] = 4;
let inner = keccak256(&inner_pre);
let mut outer_pre = vec![0u8; 64];
outer_pre[0..32].copy_from_slice(spender.into_word().as_slice());
outer_pre[32..64].copy_from_slice(inner.as_slice());
let outer = keccak256(&outer_pre);
let probe = probe_with(vec![inner_pre, outer_pre], vec![(outer, U256::from(9u64))]);
let a = &probe.accesses(&[owner.into_word(), spender.into_word()])[0];
assert_eq!(a.layout, SlotLayout::Nested);
assert_eq!(a.base_slot, U256::from(4u64));
assert_eq!(a.depth, 2);
assert_eq!(a.keys, vec![spender.into_word(), owner.into_word()]);
}
#[test]
fn tracked_mapping_round_trips_each_layout() {
let key = address!("00000000000000000000000000000000000000A1");
let t = TrackedMapping::new(Address::ZERO, U256::from(3u64), SlotLayout::SolidityMapping);
assert_eq!(t.slot_for(key.into_word()).unwrap(), sol_slot(key, 3));
let t = TrackedMapping::new(Address::ZERO, U256::from(2u64), SlotLayout::VyperMapping);
assert_eq!(t.slot_for(key.into_word()).unwrap(), vyper_slot(2, key));
let seed = 0x87a2_11a2u32;
let t = TrackedMapping::new(
Address::ZERO,
U256::from(seed),
SlotLayout::PackedSeed {
seed: U256::from(seed),
},
);
assert_eq!(t.slot_for(key.into_word()).unwrap(), solady_slot(key, seed));
}
#[test]
fn as_tracked_rejects_nested_and_arrays() {
let access = HashSlotAccess {
slot: B256::ZERO,
value: U256::ZERO,
keys: vec![B256::ZERO, B256::ZERO],
base_slot: U256::from(4u64),
layout: SlotLayout::Nested,
depth: 2,
confidence: Confidence::High,
};
assert!(access.as_tracked(Address::ZERO).is_none());
}
}