When to use / when not to use
- Use: when you have contract bytecode (on-chain address or .bin/.hex file) and need to recover the logic—ABI recovery, decompilation, storage layout reconstruction
- Use: contract vulnerability analysis (reentrancy/integer overflow/permissions) or audit scoping before exploit development
- Use: forensics on malicious contracts/token scam contracts (on-chain data acquisition, read-only pulls)
- Don't use: when Solidity source code is available and unobfuscated—reading the source directly is more accurate (audit approach same as [[re-vuln]]); this skill targets no-source/bytecode-only
- Don't use: non-EVM chains not covered in this section (e.g., WASM-based chains, different instruction set); for WASM go to [[re-wasm]]
- Note: on-chain interaction is primarily read-only (
cast call/cast code/cast storage); any chain writes/deployment/dynamic verification default to local anvil or an isolated network ([[re-analyze/platform-tips]] sandbox highest principle); contract analysis is basically static, follow "static first"
Tool preparation
python3 — the foundation for scripted analysis
- Linux:
apt install python3/dnf install python3/pacman -S python - macOS:
brew install python3;Windows:choco install python - Verify:
python3 --version(all scripts in this skill are Python 3)
cast (foundry) — the workhorse for on-chain interaction/bytecode/storage
- Official installer (all platforms, installs to
~/.foundry/bin, includes forge/cast/anvil/chisel):curl -L https://foundry.paradigm.xyz | bash && foundryup - macOS:
brew install foundry(official Homebrew formula, includes all four tools) - Arch: official reponone foundry—use AUR
foundry-bin(yay -S foundry-binetc.);Notepacman -S foundryis an unrelated GNOME Builder toolkit, don't install the wrong one; Debian/Ubuntu/Fedora have no official package—use the official installer - Verify:
cast --version && anvil --version
web3.py (pip, Python >=3.10) — scripted on-chain analysis
pip install web3(official PyPI; current 8.x requires Python >=3.10,PyPIrequires-pythonto be<4,>=3.10—only 7.x supports 3.8. Python 3.8/3.9 environments need to pin 7.x)- Verify:
python3 -c "import web3; print(web3.__version__)"
panoramix (pip, Python 3.9–3.11) — EVM decompiler (disasm→control flow)
pip install panoramix-decompiler(official PyPI 0.6.x, requires Python 3.9–3.11); the more actively maintained fork ispanoramix-decompiler-abi, both provide thepanoramixcommand- Verify:
panoramix --help(orpython3 -m panoramix --help) - Usage: pass raw bytecode directly
panoramix <hex> --abito decompile; to pass a contract address you need theWEB3_PROVIDER_URIenvironment variable (pulls bytecode from the chain); outputs pseudocode.pan/disassembly.asmwritten to the cache directory
pyevmasm (pip, Python 3) — EVM disassembly
pip install pyevmasm(official PyPI); verify:pyevmasm -d 0x6001600101outputs instruction lines- Alternative:
cast disassemble <hex>(bundled with foundry, same purpose)
hevm / echidna (optional, formal/fuzz verification)
- echidna: GitHub releases precompiled binaries (crytic/echidna) or
cargo install --locked echidna(no distro packages in official repos; Arch has an AUR package); verify:echidna --version - hevm: GitHub releases (ethereum/hevm) or cargo build; verify:
hevm --version - Both optional—finish the static analysis in steps 1-5 first, then bring these in if behavior verification is needed
solc / solc-select (optional) — storage layout cross-reference
pip install solc-select(Python 3)+solc-select install 0.8.28(official PyPI/compiled artifact download); or official solc-bin binaries (Linux x86 static package)- Verify:
solc --version - Not needed for source-less contracts; used when "obtaining the compiled artifact of the same source" to compare against the authoritative storage layout (Pitfall 2)
Procedure
Execute in order; archive each step's artifacts (bytecode/selector table/pseudocode/asm) with sha256 + path ([[re-ioc]] evidence chain).
-
Contract bytecode acquisition (on-chain/file):
# 链上(RPC 节点;公开 RPC 或自有节点) cast code 0xDEAD... --rpc-url https://eth.llamarpc.com > code.hex # 文件(.bin/.hex 文本或二进制) xxd -r -p code.hex > code.bin && file code.bin && xxd code.bin | head -2 sha256sum code.bin > code.sha256- Distinguish creation code(includes initcode + runtime, the front part is deployment logic) from runtime code(pure business logic, first instruction is often
0x6080...PUSH1 0x80 to allocate memory)——Etherscan's Bytecode page is creation, Deployed Bytecode page is runtime; use runtime to analyze business logic - On-chain data acquisition discipline:
cast call/cast code/cast storageread-only is safe; write-chain/transaction-type operations default to starting a localanviltest chain
- Distinguish creation code(includes initcode + runtime, the front part is deployment logic) from runtime code(pure business logic, first instruction is often
-
ABI recovery (function selectors/signatures):
# 从字节码提取 PUSH4 选择器常量(4 字节 = keccak256(signature)[:4]) cast disassemble "$(cat code.hex)" | grep -E 'PUSH4' | awk '{print $NF}' | sort -u > selectors.txt # 对照签名库(4byte.directory / etherface 在线服务) while read s; do echo -n "$s "; cast 4byte "$s"; done < selectors.txt # 已知签名本地计算(签名必须规范化,见坑 1) cast sig "transfer(address,uint256)" cast sig "balanceOf(address)"- Event topics are analogous: log topic0 (first 32 bytes) = keccak256(event signature), query etherface/4byte
- When there are many candidate signatures, disambiguate using parameter length (CALLDATASIZE comparison), fallback logic, storage access patterns (Pitfall 1)
-
Decompilation (panoramix approach: disasm→flow):
panoramix "$(cat code.hex)" --abi > contract.pan # 伪码(可读性优先) cast disassemble "$(cat code.hex)" > contract.asm # 精确指令流(交叉验证真值) # 按地址反编译(需节点): WEB3_PROVIDER_URI=<rpc> panoramix 0xDEAD...- Workflow: disasm (instruction-level ground truth) → function boundary location (dispatch table:
PUSH4 sel EQ PUSH2 off JUMPIpattern, located in the ~0x00-0x1f region at the start of the bytecode) → rough pseudocode read → return to asm for close reading of key functions - Rough output is the norm (Pitfall 4); all decompilation conclusions must be cross-verified with asm
- Workflow: disasm (instruction-level ground truth) → function boundary location (dispatch table:
-
Logic recovery (storage layout/state variables):
# 链上存储读取(状态变量从 slot 0 起、每槽 32 字节) cast storage 0xDEAD... 0 --rpc-url <rpc> cast storage 0xDEAD... 1 --rpc-url <rpc> # 动态数组/映射槽位 = keccak256 派生(见坑 2 公式) cast call 0xDEAD... "owner()(address)" --rpc-url <rpc> # 公开变量 getter # 无源码:从反编译的 SSTORE/SLOAD 常量与 getter 推断语义 grep -E 'SSTORE|SLOAD' contract.asm | head- Layout rules: inheritance is merged according to C3 linearization without changing the layout, each variable is 32 bytes (
bool/uint8/addressetc. can be packed in the same slot), dynamic arraysslot = keccak256(slot_index)、mappingslot = keccak256(key ++ slot_index); when source is available,solc --storage-layoutoutputs authoritative JSON for comparison - Cross-verify with ABI getters: public state variables automatically generate getters,
cast callinfer variable types and slots from the result
- Layout rules: inheritance is merged according to C3 linearization without changing the layout, each variable is 32 bytes (
-
Vulnerability analysis (reentrancy/integer overflow/permissions):
grep -cE 'CALL|DELEGATECALL|CALLCODE' contract.asm # 外部调用面(重入/交互边界) grep -E 'ADD|MUL|SUB' contract.asm | head # 算术运算(<0.8 无内置溢出检查) grep -E 'CALLER' contract.asm | head # 调用者检查(onlyOwner 权限)- Reentrancy: state is modified only after an external call (CALL to a user-controllable address) (SSTORE order is after CALL)——check whether effects precede interactions
- Overflow: Solidity <0.8 has no built-in checks——look at whether there is an overflow branch after ADD/MUL; for 0.8+ look for
uncheckedblock - Permissions: CALLER compared against owner slot pattern; owner slot writable/unchecked means arbitrary calls (only initializing owner in the constructor is a common missing permission)
- Optional behavior verification: echidna property testing (invariant/reentrancy/overflow patterns), hevm symbolic execution,
anvillocal reproduction of attack sequence - Conclusion discipline: vulnerability determination requires "path reachable" evidence (call chain + state conditions); instruction patterns alone are weak signals (Pitfall 4)
Non-EVM chains (Solana / Move)
The process is isomorphic to EVM (bytecode decompilation → vulnerability analysis), but the instruction set and resource model differ——first identify the runtime, then follow the corresponding path.
- Solana (BPF bytecode): identifying features——ELF file + .text section is BPF instructions (eBPF-like instruction set); decompilation path——
llvm-objdump -d(BPF disassembly) or a dedicated decompiler; analysis focus—program accounts and instruction scheduling (CPI cross-program invocation chains), account data layout (struct offsets) - Sui / Aptos (Move bytecode): identification features—module/resource/function table structure; decompilation—move disassembler (
move disassemble); analysis focus—the impact of the resource model (objects/abilities) on the vulnerability surface (transfer/ownership logic), module dependency graph - DeFi/NFT scenarios: pool contracts (AMM constant product formula), token standard variants (SPL, etc.), locating authorization and withdrawal logic
- Vulnerability analysis continuation: for logic vulnerabilities (reentrancy/permissions) follow the EVM approach; make separate judgments where the resource/account model differs
Cross-domain federation
- [[re-managed]]: after this gateway's "identify runtime" identifies EVM bytecode (.bin/hex), it fixedly invokes this skill (contract bytecode is the "managed bytecode" branch)
- [[re-vuln]]: interface with the contract vulnerability and vulnerability discovery gateway—echidna's coverage-guided/corpus ideas are the same as [[re-fuzzing]], and assertion crash analysis ideas are the same as [[re-crash-triage]]; the vulnerability report structure is consistent (entry/conditions/impact/fix)
- On-chain data retrieval: cast/web3.py read-only via RPC (public RPC, self-owned node, Etherscan API); writing to chain and dynamic verification default to local anvil (isolated network)
- [[re-wasm]]: non-EVM Wasm contracts/on-chain Wasm modules (same domain, different instruction set)
- Reference [[re-analyze/platform-tips]] for static-first (large samples) and sandbox isolation branch (writing to chain/dynamic verification isolation)
Common pitfalls and traps
- Selector collision: symptom—
cast 4bytereturns multiple candidate signatures, or parsing by a guessed signaturecast callfails/is misaligned; cause—the selector is the first 4 bytes of keccak256(signature), collisions exist (malicious variants/known collision cases), and a large number of signatures are not in the database; countermeasures—cast sigholds only for canonical signatures (no spaces between parameters,uint256do not writeuint, addresses lowercase and complete), disambiguate candidate signatures using CALLDATASIZE comparison (parameter-length dispatch), fallback logic (fallback handling), storage access patterns; finally confirm by keccak256 comparison of the full signature - Storage layout vs source mapping is difficult: symptom—for contracts without source code, inference by "declaration order = slot order" is wrong, and the types of values read do not match/mix slots; cause—the layout is affected by compiler version, optimization flags, inheritance order (C3), and struct/array packing rules, and without source code there is no authoritative mapping; countermeasures—with source code:
solc --storage-layoutoutput JSON for comparison (compiler version must match); without source code: infer types from the SSTORE constants in the decompilation + getter behavior + value size, mark confidence and do not guess hard; compute dynamic array/mapping slots by the keccak256 formula (step 4), do not add one linearly - Proxy contract (delegatecall logic is elsewhere): symptom—the main contract's decompilation has only fallback + delegatecall forwarding, and no business logic can be found; cause—the upgradeable proxy pattern (EIP-1967, etc.) puts the logic in the implementation contract: the main contract only stores the implementation address and delegatecalls, state variable layout is in the main contract, code is in the implementation contract (layout conflicts are themselves a classic vulnerability); countermeasures—first check the EIP-1967 implementation slot:
cast storage <addr> 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc --rpc-url <rpc>(admin slot0xb53127684a568b3173ae13bae8d486943e0793e1d5dccd29778a66c42e1c39d), for non-standard proxies locate the SLOAD constant slot from the decompilation to read the implementation address; thencast code <实现地址>Analyze the implementation; implementation address is mutable = upgradeable, annotate the conclusion with the point in time at which it was read - Decompiler tool output is rough and requires manual work: Phenomenon — panoramix pseudocode lacks variable names/types/storage semantics, function boundaries are misaligned, leading to the mistaken belief that the contract logic is chaotic or that key branches are missing; Cause — the EVM has no types and no symbol table, and symbolic execution/heuristic decompilation loses information, while modern Solidity optimized outputs (ABIEncoderV2, optimizer) have complex structures; Countermeasure — treat pseudocode only as a "rough reading map", and for key functions return to
cast disassembleinstruction-level close reading (dispatch table location → instruction-by-instruction semantics → storage/call patterns); cross-check with multiple tools (panoramix + cast disasm + echidna behavioral verification); annotate conclusions with confidence level and verification method