Skip to main content
Sigvex

Red Team / Blue Team EVM · Part 1: Read a Deployed Contract Like an Attacker

The deployed bytecode is the contract, not your GitHub repo. Here is how an attacker gets from a bare address to understanding, and why your private variables were never secret.

Red Team / Blue Team EVM · Part 1: Read a Deployed Contract Like an Attacker

EVM Red Team / Blue Team — a six-part path from reading a stranger’s deployed bytecode to shipping a contract that survives contact with an attacker.

  1. Read a Deployed Contract Like an Attacker
  2. Mapping the Attack Surface
  3. The Bugs That Move Money
  4. Owning the Contract: Access Control and Upgrades
  5. Blue Team: From Bytecode to Triaged Findings
  6. Blue Team: Hardening and Holding the Line

You write Solidity. You ship contracts. You have probably never sat on the other side of the table and tried to break one. That is what this series is for — four posts building an attacker’s mental model, then two turning it into defense. We start where the attacker starts: with an address and nothing else.

Your repo is not the contract

When you think about your contract, you picture the .sol files — the comments, the variable names, the modifiers you were careful to get right. An attacker never sees any of that. They see a 20-byte address and whatever the EVM will hand them when they ask.

Source code is a courtesy. You publish it so people trust you, so integrators can build against you, so a block explorer shows a green “verified” checkmark. None of that is the contract. The contract is the runtime bytecode sitting at that address, and the bytecode is the only thing the EVM actually executes.

Two facts follow. First, verified source can differ from what runs. Verification checks that some source compiles to the deployed bytecode under a declared compiler and settings — but proxies, metadata quirks, and plain mismatches mean the pretty code on the explorer is not always a faithful map of what executes. Second, most contracts on-chain are not verified at all. The green checkmark is the exception, not the rule. An attacker who only knew how to read verified source would be blind to the majority of the chain — and to the exact contracts where nobody was watching.

So the attacker starts from the address and treats the bytecode as ground truth. This is the premise behind bytecode-native security: the deployed artifact is the only thing worth reading, because it is the only thing that actually runs.

What an attacker pulls before writing a single line of exploit

Everything they need is a public JSON-RPC call away. No permissions, no source, no cooperation from you.

  • Runtime bytecode via eth_getCode — the actual instructions at the address.
  • Storage slots via eth_getStorageAt — every value the contract has ever written, one 32-byte word at a time, readable by anyone.
  • Constructor arguments, recoverable from the deployment transaction’s input data — the initial owner, the token address, the fee, whatever you passed at birth.
  • Event history via log queries — a timestamped record of what the contract did and, often, who it did it with.

Together this reconstructs state and behavior owing nothing to your documentation. The attacker learns who the admin is by reading a storage slot, not by trusting a comment that says // owner.

Why raw bytecode is hard — and how it becomes readable again

Pull the bytecode and you get a wall of hex. Disassemble it and you get a flat stream of stack-machine opcodes: PUSH, DUP, SWAP, SLOAD, JUMPI. There are no functions. No variables. No types. No named storage. The EVM is a stack machine, so values are shuffled through a stack rather than held in named locals, and control flow is bare jumps to byte offsets rather than if/for/while.

That is the raw form. Decompilation is the work of lifting it back toward something a human reads, and it leans on structure the compiler could not help leaving behind:

  • Function dispatch by selector. Solidity routes calls through a dispatcher that compares the first four bytes of calldata — the keccak256 hash of the function signature — against a table of known selectors. That table is a gift. It hands the attacker a list of every externally callable entry point, whether or not you documented it.
  • Storage-slot patterns. Sequential simple variables land in slots 0, 1, 2, and so on; mappings and dynamic arrays hash their keys into pseudo-random slots by a fixed, public rule. Reading and writing patterns let the layout be reconstructed.
  • Control-flow recovery. Jumps and jump destinations get stitched back into a control-flow graph — basic blocks, branches, loops — so the tangle of offsets becomes something shaped like a program again.

From there the pipeline can raise the code through intermediate forms toward readable pseudo-Solidity. The fundamentals of EVM bytecode analysis walk through the disassembly and dispatch mechanics; if you want the theory of the ladder from opcodes to structured output, intermediate representations for smart contracts covers why analysis tools work on those middle layers rather than raw hex. The point for now: “unverified” does not mean “unreadable.” It means the attacker spends an afternoon with a decompiler instead of reading your repo.

The mindset shift: everything you didn’t hide is public

Here is the reframe. On the EVM there is no such thing as “hidden on the server.” There is no server. Every byte of code and every byte of storage lives on a public ledger that anyone can query. The only things truly secret are the ones that were never put on-chain — a private key, an off-chain password, a value you kept in your own database.

Two habits from ordinary programming get people hurt:

  • private is not secret. A private state variable is private to other contracts’ Solidity code. Its storage slot is right there for eth_getStorageAt. Storing a password, a seed, or a “hidden” whitelist in a private variable puts it in a public place with a sign that says “please don’t look.”
  • internal is not a security boundary. Visibility controls which Solidity can call what at compile time. It says nothing about what an attacker can reach through crafted calldata, delegatecalls, or a selector you forgot was still routed. The dispatcher decides what is callable, and the dispatcher is in the bytecode.

Names and comments are gone. Structure is not. The attacker cannot read your intent, but they can read your logic exactly, because the machine reads the same thing.

What survives, concretely

Take a small snippet:

contract Vault {
    address private owner;          // slot 0
    uint256 private totalDeposits;  // slot 1
    mapping(address => uint256) private balances; // slot 2

    function withdraw(uint256 amount) external {
        require(balances[msg.sender] >= amount, "insufficient");
        balances[msg.sender] -= amount;
        (bool ok, ) = msg.sender.call{value: amount}("");
        require(ok);
    }
}

The names owner, totalDeposits, balances, withdraw, the comments, the string "insufficient" — the string survives as raw bytes, the rest is gone. What the attacker reconstructs:

  • A selector for withdraw(uint256) in the dispatch table, marking it as a public entry point taking one 256-bit argument.
  • Slot 0 holds an address that gates nothing here but is clearly the admin elsewhere; slot 1 is a running counter; slot 2 is the base of a mapping whose per-user slots are keccak256(userAddress . 2). Anyone can read anyone’s balance directly.
  • A control-flow shape: check a stored balance, decrement it, then make an external call that hands control to the caller before the function returns. That ordering is the classic reentrancy setup — and an attacker reads it straight out of the block structure, no source required.

The private keyword bought nothing. The logic that matters came through fully intact.

That is the uncomfortable half of the lesson: your contract is already an open book to anyone willing to read it at the level the EVM does. The useful half is that you can read it the same way — and should, before someone less friendly does.

Knowing the entry points and the storage layout is only the start. Next the attacker turns that raw map into a ranked list of what is worth attacking — which functions move value, which paths cross trust boundaries, where the assumptions live. That is Part 2: Mapping the Attack Surface.