rea-reverse

Unverified Contract? Tell a Proxy from a Token by Its Bytecode

Written for REA 6.4.0Updated 8 min read
On this page

Short answer: even without verified source, a contract’s runtime bytecode usually shows what kind of contract it is. A proxy is short, has almost no function selectors of its own and contains a DELEGATECALL. An OpenZeppelin-style proxy also reads the EIP-1967 implementation slot 0x3608…2bbc, and that slot shows up in the storage layout of this site’s EVM function selector extractor. A token exposes the ERC-20 selectors. Extra selectors such as mint, pause or upgradeToAndCall tell you which privileged operations the code contains.

This is a technical guide to reading an interface. It doesn’t judge whether a token is a good idea, whether you should hold or trade it, or whether its operators are trustworthy. The code can tell you what functions exist. It can’t tell you how anyone will use them.

Who this is for: developers, auditors and students who meet a contract with no verified source and want to know what they’re looking at before integrating with it or writing about it.

What you will learn:

  • What bytecode reveals about an unverified contract, and what it hides
  • The bytecode signatures of an EIP-1967 proxy, a Transparent proxy and an EIP-1167 clone
  • How to find the implementation address and analyze that contract instead
  • How to read a token’s selector list as a list of capabilities

What I ran: on 2026-10-14 I compiled real OpenZeppelin Contracts 5.6.1 (ERC1967Proxy and TransparentUpgradeableProxy) and my own upgradeable token built on OpenZeppelin Contracts Upgradeable 5.6.1, all with solc 0.8.37, optimizer on, 200 runs. I also built an EIP-1167 clone from the byte template in OpenZeppelin’s Clones.sol, with a placeholder implementation address. Each runtime bytecode went through the site’s EVM tool analyzer (src/tools/evm/analyze.ts, EVMole 0.9.3). I checked the results against solc’s opcode listing and method identifiers. Nothing was deployed, and no real token was examined.

What bytecode can and can’t tell you

From runtime bytecode alone you can recover:

  • the 4-byte function selectors the dispatcher accepts, with inferred argument types and mutability
  • the storage slots the code reads or writes, and which selectors touch them
  • the compiler metadata tail (solc version, metadata hash); see reading the solc version
  • the presence of opcodes such as DELEGATECALL, SELFDESTRUCT or CALL, which solc lists for you when you have it, or any disassembler shows

You can’t recover function names (a selector is a hash, so names come only from a lookup table), variable names, comments, or the business logic in readable form. You also can’t learn who holds the owner or admin role from bytecode. That’s state, not code.

Step 1: Get the runtime bytecode

cast code 0xContract --rpc-url "$RPC_URL" > target.hex

eth_getCode over plain JSON-RPC returns the same thing. Paste the hex into the EVM tool. It runs locally and has no RPC or explorer access of its own, so fetching the code is up to you.

Step 2: Does it look like a proxy?

Here is what the tool reported for the three proxy shapes I built:

Sample Size Functions found Storage layout Metadata
OpenZeppelin ERC1967Proxy 163 bytes 0 0x360894a1…382bbc, address solc 0.8.37
OpenZeppelin TransparentUpgradeableProxy 1,059 bytes 0 0x360894a1…382bbc, address solc 0.8.37
EIP-1167 minimal clone 45 bytes 0 none none
My token implementation (for contrast) 5,301 bytes 20 10 entries in 9 slots solc 0.8.37

Three things stand out.

No selectors. A proxy forwards every call to another contract, so it has no ABI dispatcher of its own. EVMole found zero functions in all three. That matches the tool’s documented limitation: “Proxies show only the proxy’s own selectors”. The Transparent proxy does check one selector when the admin calls it, but that check sits inside its fallback logic and isn’t a normal dispatcher entry.

The EIP-1967 slot appears in the storage layout. This was the open question before writing, and the answer is yes. For both OpenZeppelin proxies, the storage table listed slot 360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc, typed address. Its reads and writes columns were empty, because no selector leads to that SLOAD. The fallback does. That value is the implementation slot defined in EIP-1967, keccak256('eip1967.proxy.implementation') - 1. I recomputed it with js-sha3 and got the same 32 bytes. solc’s opcode listing for ERC1967Proxy shows the whole mechanism in a few instructions:

PUSH32 0x360894A13BA1A3210667C828492DB98DCA3E2076CC3735A920A3CA505D382BBC SLOAD ...
CALLDATASIZE PUSH0 PUSH0 CALLDATACOPY PUSH0 PUSH0 CALLDATASIZE PUSH0 DUP5 GAS DELEGATECALL

It loads the implementation address from the slot, copies the call data and delegates. The other two EIP-1967 slots, admin (0xb531…6103) and beacon (0xa3f0…3d50), didn’t appear. OpenZeppelin 5’s Transparent proxy keeps its admin in an immutable variable “to avoid unnecessary SLOADs” (from its source). The admin slot is written once at deployment, but the runtime code never reads it.

The clone has nothing to analyze, and that’s the tell. EIP-1167 code is 45 bytes: 363d3d373d3d3d363d73, then a 20-byte implementation address, then 5af43d82803e903d91602b57fd5bf3. There’s no storage, no metadata and no selectors. If you see that exact prefix and suffix, the implementation address is the 20 bytes between them. No storage read needed.

Etherscan also flags proxies. Its proxy contracts article says it looks for “identifiable bytecode patterns associated with well-known proxy contract implementations”. It also warns that a detected delegatecall “may not even be reachable”, and that contracts using libraries with delegatecall can be flagged by mistake. The same caution applies to your own reading. A DELEGATECALL is a reason to look closer, not a verdict.

Step 3: Find the implementation and analyze it

For an EIP-1967 proxy the implementation address lives in storage, not code. Read the slot:

cast storage 0xProxy 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc --rpc-url "$RPC_URL"

cast storage (reference) returns the raw 32-byte word. The address is its last 20 bytes. The JSON-RPC equivalent is eth_getStorageAt. I didn’t run these against a chain for this guide. Then fetch the implementation’s code with cast code and analyze that. That’s where the real interface is.

For the implementation I built (UUPS, ERC-20, pausable, ownable), the tool found 20 functions. Here they are with the names it matched from its offline table, rearranged into two columns:

0x06fdde03 name()                         0x5c975abb paused()
0x095ea7b3 approve(address,uint256)       0x70a08231 balanceOf(address)
0x18160ddd totalSupply()                  0x715018a6 renounceOwnership()
0x23b872dd transferFrom(address,address,uint256)
0x313ce567 decimals()                     0x8456cb59 pause()
0x3f4ba83a unpause()                      0x8da5cb5b owner()
0x40c10f19 mint(address,uint256)          0x95d89b41 symbol()
0x4f1ef286 upgradeToAndCall(address,bytes) 0xa9059cbb transfer(address,uint256)
0x52d1902d proxiableUUID()                0xad3cb1cc UPGRADE_INTERFACE_VERSION()
0xc4d66de8 (unknown)                      0xdd62ed3e allowance(address,address)
                                          0xf2fde38b transferOwnership(address)

0xc4d66de8 isn’t in the page’s table. solc’s method identifiers for my source say it is initialize(address), which is exactly the “unknown” a reader without source would have to research. The storage table showed the implementation slot again, this time written by 0x4f1ef286 (upgradeToAndCall). That’s direct evidence that this function changes the code behind the proxy. The other slots are OpenZeppelin’s ERC-7201 namespaced storage locations. I matched 0x52c63247…ace00 (ERC-20 balances, allowances, supply, name, symbol), 0x9016d09d…9300 (Ownable), 0xcd5ed15c…3300 (Pausable) and 0xf0c57e16…a00 (Initializable) against the constants in the OpenZeppelin source.

Step 4: Read a token’s selectors as capabilities

An ERC-20 token exposes six required functions: totalSupply, balanceOf, transfer, transferFrom, approve and allowance. Most also have the optional name, symbol and decimals. If all six required selectors appear, you’re very likely looking at an ERC-20 or something built to look like one. ERC-721 and ERC-1155 have their own well-known selectors, and the tool’s table includes them.

The interesting part is what else is there. Treat each extra selector as a line in a capability list, checked against which slots it writes:

Selector seen What the code can do Evidence in the storage table
mint(address,uint256) Create new tokens writes balances and total supply
pause() / unpause() Stop and restart transfers writes the Pausable flag, which transfer and transferFrom read
upgradeToAndCall(address,bytes) Replace the contract logic behind the proxy writes the EIP-1967 slot
transferOwnership / renounceOwnership Move or drop the privileged role writes the Ownable slot

In my contract each of these is onlyOwner, and the bytecode shows mint and pause reading the Ownable slot before acting. Bytecode can’t tell you who the owner is. A renounceOwnership selector also doesn’t mean the role was renounced. To learn that, read the current state (for example the owner() value) from a node, and read any verified source that exists. Functions with names like blacklist or setFee are not in my sample. If you meet unknown selectors, look them up in a public signature database and label the match as a hypothesis, because four-byte selectors collide.

Where REA 6.4.0 fits

REA, the open-source project by morluto, has rea inspect-evm-interface <file> hex --json (MCP inspect_evm_interface), built on the same EVMole 0.9.3. Two limits matter here:

  • Linux x64 only. On my Apple Silicon Mac the CLI returned capability_unavailable: “Initial real-verified offline EVM interface profile supports Linux x64 only.”
  • No storage layout. The 6.4.0 adapter requests selectors, arguments and mutability. Its reply schema requires storage to be absent. So REA returns the function list (zero rows for a proxy) and the metadata tail, but not the EIP-1967 slot that the browser tool shows.

I ran the bundled 6.4.0 worker module directly on the ERC1967Proxy and clone samples. It confirmed 0 functions for both, CBOR metadata for the OpenZeppelin proxy and none for the clone. REA has no “is this a proxy” rule. The conclusion is yours to draw from the evidence, and the Evidence record keeps the inputs it was based on. The selector recovery guide covers REA’s output fields.

Limits to keep in mind

  • Selector collisions. A name from a table is a match on four bytes, not proof. The tool warns when a table entry’s parameters disagree with the inferred ones.
  • Inference, not ABI. Mutability and argument types are inferred. In my implementation, decimals() came out view, while in a non-upgradeable build of a similar token it came out pure.
  • Unusual proxies. Diamond (EIP-2535), beacon and hand-written proxies use other slots or lookup logic. An empty storage table or a missing EIP-1967 slot doesn’t prove a contract isn’t a proxy.
  • Code isn’t intent. Capabilities in code say nothing about whether anyone will use them, or how.

FAQ

Is a “decompiler” better than this?

A decompiler tries to rebuild logic, and its output is a guess that needs checking. Selectors, storage slots and opcodes are closer to the facts in the bytecode. Start with them, and use a decompiler for the parts you still can’t explain.

The proxy shows zero functions. Is the tool broken?

No. That’s what a proxy looks like. Analyze the implementation instead.

Can I see the implementation address without an RPC?

For an EIP-1167 clone, yes, it’s inside the 45 bytes. For EIP-1967 proxies, no. It’s in storage, so you need a node, or an explorer that reads storage for you.

Next steps

Analyze contracts you deployed, contracts you’re authorized to review, or public contracts for learning. This guide is technical documentation, not financial or investment advice. Follow the terms of the RPC providers and explorers you use, and see REA’s DISCLAIMER.md.