evm - 1-3-7/disrobe GitHub Wiki
disrobe evm reads Ethereum Virtual Machine bytecode from a file and recovers the contract interface, the control-flow graph and pseudo-Solidity. It needs no node, RPC endpoint, block explorer or compiler, and it never executes the code.
disrobe evm inspect Token.hex # selectors, mutability, argument types, events, errors, proxy
disrobe --json evm inspect runtime.bin # the same report as JSON
disrobe evm disasm Token.hex --blocks # constructor, runtime and metadata sections, basic blocks
disrobe evm decompile Token.hex -o Token.sol- The file holds hex text, with or without
0xand with whitespace between digits allowed, or raw bytes.--encoding hexor--encoding rawforces one reading. Bytecode is accepted up to 1 MiB. - Creation bytecode is split at its runtime image. disrobe follows the constructor to the
RETURNof a memory region that aCODECOPYfilled from the code itself, and reports the constructor length, the runtime offset and length, and the bytes after the runtime (constructor arguments). Constructors that decode their own arguments and copy the runtime to a computed address split the same way.--layout runtimeor--layout creationforces one reading; a forced creation reading without a runtime image is an error. - An EOF container (magic
0xef00, EIP-3540) is refused by name.
- Disassembly of every legacy opcode through Osaka, including
PUSH0,TLOAD,TSTORE,MCOPY,BLOBHASH,BLOBBASEFEEandCLZ. An unassigned byte prints asUNDEFINED(0xNN); aPUSHcut short by the end of the code is marked. The report names the oldest fork whose opcode set covers the code. - The compiler metadata trailer: for solc, the compiler version, IPFS CID (base58),
bzzr0orbzzr1Swarm hash and the experimental flag from the end of the runtime; for Vyper, the compiler version from the end of the creation code. - Basic blocks and edges. A
0x5bbyte insidePUSHdata is not a jump destination. Jump targets resolve by constant propagation that carries return addresses through internal calls, so each internal function returns to its own caller. Each block lists its entry stack heights; unresolved jumps, jumps to non-JUMPDESTbytes and stack underflows are listed. - Dispatched functions. A selector is recognised where the selector word (
SHR 224orDIV 2**224of the first calldata word) is compared withEQ,XOR,SUBorISZERObefore aJUMPI. Linear dispatchers,GT/LTbinary-search dispatchers, selectors with leading zero bytes and the all-zero selector are covered. Vyper's sparse and dense selector tables, which reduce the selector modulo a bucket count and read the bucket from a data section withCODECOPY, are followed through every bucket. The report states whether a fallback and a receive function exist. - For each function: the entry offset and the state mutability. A function is
nonpayablewhen aCALLVALUEcheck, alone or combined with a calldata size check, sends a non-zero value to a revert,viewwhen noSSTORE,TSTORE,LOG,CALL,CALLCODE,DELEGATECALL,CREATE,CREATE2orSELFDESTRUCTis reachable, andpurewhen it also reads no storage, balance, block, transaction or caller value. ASTATICCALLto a precompile (0x01to0x11) keeps a functionpure. - Argument count and types. The count comes from the calldata size check and the highest head word read. Each type comes from the code that validates or uses the word: an
ANDmask givesaddress,uintNorbytesN,SIGNEXTENDgivesintN, a doubleISZEROgivesbool, signed arithmetic givesint256. In code that is not from solc (Vyper, or no solc prologue), a shift range check gives the same types: a word whose top bits must be zero afterSHRisaddress,booloruintN, and one whose low bits must be zero afterSHLisbytesN. A word read as an offset is dynamic: its length multiplied by 32, or a bounded copy of 32 bytes per element, givesT[]with the element type read the same way, an unscaled length givesbytes, and an element read as an offset givesbytes[]. Each argument carries the basis of its type. - Storage slots read and written, with mapping slots shown as
mapping_N[key], events with the topic0 signature where it is known, external calls with the callee selector, and reverts with theError(string)text, thePaniccode and its meaning, or the custom error selector. - Known names from a built-in table of ERC-20, ERC-2612, ERC-721, ERC-1155, ERC-4626, ERC-165, ERC-1271, ERC-2981, ERC-3156, Ownable, AccessControl, Pausable and EIP-1967/ERC-1822 proxy functions, their events and the OpenZeppelin custom errors. The selectors are Keccak-256 hashes of the signatures; the inferred argument types are reported beside the table signature.
- Proxies: EIP-1167 minimal proxies and the EIP-7511
PUSH0variant with the implementation address, and fallbacks thatDELEGATECALLan address loaded from the EIP-1967 implementation, beacon or admin slot, the ERC-1822 slot, another fixed slot or a constant.
decompile writes pseudo-Solidity for each function from its explored paths: require where one side of a branch reverts, if/else otherwise, and labels with goto where paths join or a loop repeats.
disrobe evm decompile corpus/evm/solc/0.8.37-o1/Erc20.hex prints, for transferOwnership:
function transferOwnership(address arg0) external {
require(msg.value == 0);
require(!(slt(msg.data.length - 4, 32)));
require(arg0 == address(arg0));
require(msg.sender == address(storage[5]), error_8e4a23d6(msg.sender));
require(address(arg0) != 0, "Ownable: new owner is the zero address");
emit OwnershipTransferred(address(storage[5]), address(arg0));
storage[5] = address(arg0) | bytes12(storage[5]);
return;
}The references are solc and Vyper. corpus/evm/build.sh compiles the contracts in corpus/evm/src with solc-js 0.8.37 and 0.8.19 (optimizer on and off, via-IR on and off) and with 0.6.12 and 0.4.26 (ABI coder v1). corpus/evm/build_vyper.py compiles corpus/evm/vyper-src with Vyper 0.4.3 in its gas and codesize modes, which emit the sparse and the dense selector table. corpus/evm/MANIFEST.toml records each build and the SHA-256 of every output. The contracts cover every value type width, bytes, string, dynamic arrays, payable, nonpayable, view and pure functions, events, custom errors, require strings, panics, AccessControl, ERC-20, ERC-721, ERC-1155 and ERC-4626 shapes, an EIP-1967 proxy with fallback and receive, a clone factory and selectors with one, two and three leading zero bytes.
crates/disrobe-pass-evm/tests/solc_reference.rs grades 69 compiled contracts (67 from solc, 2 from Vyper) holding 758 functions against the compilers' own output:
| Check | Result |
|---|---|
| Runtime split from creation bytecode, byte-exact against the compiler's deployed bytecode | 69 of 69 |
| Selectors against the compiler's method identifiers, with no extra selector | 758 of 758 |
| State mutability against the ABI | 758 of 758 |
| Argument types against the ABI | 876 of 947 exact, 71 in the class the code cannot separate, 0 wrong |
| Fallback and receive against the ABI | 69 of 69 |
| Metadata trailer: compiler version where the trailer holds one, hash kind, or no trailer when the build omits it | 69 of 69 |
Events, custom errors and require strings against the ABI and the source |
every one found, none extra |
The 71 class matches are bytes32 arguments the code never constrains, reported as unconstrained uint256, and string arguments, reported as bytes. Getters of constant and immutable variables are graded as pure, the mutability their code has, from solc's own AST. Every jump in the compiled code resolves. Clearing the JUMPDEST of a function entry, or changing one PUSH4 selector, turns the grader red. 400 randomly corrupted contracts and a set of adversarial shapes (50,000 JUMPDESTs, fork chains, 3,000-entry dispatchers, deep stacks) produce a report or a typed error, never a panic.
- Fixed-size arrays and static structs in arguments are reported as their separate words, because the ABI encodes them identically.
- A word that the code never masks, sign-extends or uses in signed arithmetic is reported as
uint256; the code does not distinguish it fromint256orbytes32. - A getter of a
constantorimmutablevariable is reportedpurealthough the ABI declares itview. - Path exploration is bounded per function (steps, nodes, two forks per branch site and context, loop arrivals) and over the whole contract (40 million steps). A function that reaches a bound is marked incomplete, and its mutability and types come from the explored part.
- Selectors are found from comparisons. A dispatcher that jumps through a computed table other than a selector reduced modulo at most 256 buckets yields no selectors.
- Dispatched function names come only from the built-in table; other selectors keep their hex value.