Skip to main content
Sigvex

Red Team / Blue Team EVM · Part 2: Mapping the Attack Surface

How an attacker enumerates every way value and control enter and leave a contract — and how to draw that same map of your own code before anyone else does.

Red Team / Blue Team EVM · Part 2: Mapping the Attack Surface

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

In Part 1 you turned a stranger’s deployed bytecode back into something you could read. Now comes the step every attacker does before touching a single exploit: building a map. Not “where’s the bug” — that’s later. First, “where could a bug even matter.” Value and control only enter and leave a contract through a finite set of doors. Enumerate the doors and you’ve bounded the problem. Skip that step and you’re guessing.

This post is about drawing that map for your own contract, the way someone hunting it would.

Start with the doors: entry points

Every external interaction goes through the selector dispatch table you recovered in Part 1 — the switch on the first four bytes of calldata that routes a call to a function body. That table is the public surface. Nothing else is reachable from outside. So the first column of your map is simple: list every external and public function.

Then annotate each one along two axes.

State-changing vs. view. A view/pure function can’t move money or corrupt storage on its own, but don’t dismiss it — attackers read view functions to understand internal accounting and to find the math a state-changing function trusts. The functions that actually matter for an exploit are the ones that write storage.

Does it move value? For each state-changing function, mark whether it:

  • is payable (accepts ETH in),
  • sends ETH or tokens out (call{value:}, transfer, transferFrom),
  • or changes an allowance or accounting entry that later lets value move (approve, minting a balance, updating a debt).

That last category is the one people under-count. A function that sets an allowance to type(uint256).max doesn’t transfer anything in that transaction, but it hands someone a key. Value-movement is not just the transfer line; it’s anything that changes who is entitled to value.

Who can call what: the trust and authority map

Now the second column, and the one attackers care about most: for each entry point, who is allowed to call it?

Walk the guards. In readable Solidity these are onlyOwner-style modifiers, require(msg.sender == owner) checks, or a role system like OpenZeppelin’s AccessControl (onlyRole(MINTER_ROLE)). In decompiled output the same thing shows up as a load of an owner/role slot compared against CALLER, with a branch to revert when it fails. Part 1 taught you to spot exactly that shape.

Sort every function into three buckets:

  1. Guarded — a check gates the call. Note which authority: owner, a specific role, a timelock.
  2. Open by design — anyone can call it and that’s intended (a swap, a deposit, mint in a fair-launch token).
  3. Open by accident — a function that should be guarded and isn’t.

Bucket 3 is where fortunes are lost, and the only way to find it is to know what each function does and compare that against what protects it. A public initialize() that sets the owner, a public upgradeTo(), a public function that sweeps the contract’s balance — these are catastrophic precisely because the guard is missing, not present-but-weak. Access control failures are consistently among the most damaging bug classes on-chain, and most of them are a missing modifier, not a clever bypass.

The mental discipline here is to separate “protected” from “assumed safe.” A function nobody happened to call yet is not protected. Your map should have a guard named in every row, or a deliberate note that says “open — and here’s why that’s fine.” Blank means unaudited, not safe.

Follow the money: value flow

Third column: trace where value enters and where it leaves, and who is on the other end.

  • In: payable functions, transferFrom pulls, deposits.
  • Out: ETH sends, token transfers, and every external call.

External calls are the interesting ones, because they hand control to code you don’t control. Note the three flavors and what each means:

  • call — invokes another contract; if it happens before your state is finalized, that’s the classic reentrancy setup.
  • staticcall — a read that can’t change state, but can still return attacker-influenced data (think an oracle price).
  • delegatecall — runs another contract’s code in your storage context. This one deserves its own paragraph.

delegatecall is a surface amplifier. When contract A delegatecalls contract B, B’s code executes but reads and writes A’s storage, with A’s balance and A’s msg.sender. That’s the entire basis of the proxy pattern: the address users interact with holds the state, and the real logic lives in a separate implementation contract that gets delegatecalled. Which means the code that governs your funds is not in the contract you’re looking at. Map only the proxy and you’ve mapped an empty shell. You have to resolve the implementation address (Part 1 covered reading the EIP-1967 slot) and map that too — and it can change under an upgrade, so the proxy’s own upgrade path is part of the surface. Proxy and upgrade vulnerabilities get their own treatment for exactly this reason.

State an attacker wants to touch

Behind the functions sits storage, and some slots matter far more than others. On your map, flag:

  • Ownership and roles — the slots that decide who’s privileged.
  • Upgrade and pause switches — implementation address, paused flag, timelock parameters. These are single points of total control.
  • Accounting invariants — the properties that must always hold: the sum of user balances equals the recorded total, collateral ratio stays above the liquidation threshold, totalSupply tracks minted minus burned. An attacker’s real goal is usually to break one of these invariants while every individual function looks like it behaved. Write the invariants down explicitly; you’ll test them in Parts 3 and 5.

Dependencies are surface too

The last column reaches outside the contract entirely. Your attack surface includes everything your code trusts:

  • Oracles. If a function reads a price and acts on it, that price is an input an attacker may be able to move. Oracle manipulation — often funded by a flash loan — turns a “read” into an injection point.
  • Other protocols you call into, whose behavior and whose own upgrades you inherit.
  • Tokens with surprising behavior. Fee-on-transfer tokens deliver less than the amount you asked for. Rebasing tokens change balances out from under you. ERC-777 tokens fire a hook on transfer that hands control to the recipient — a reentrancy vector hiding inside an ordinary-looking transfer. If your contract assumes ERC-20 tokens are inert and honest, every token that isn’t becomes part of your surface.

The map, as a checklist

Everything above collapses into something you can run on your own contract in an afternoon. Make one row per external function and fill four cells:

  1. Function — name and signature from the dispatch table.
  2. Who can call it — the named guard, or “open” with a reason.
  3. What value it touches — ETH in/out, token moves, allowance or accounting writes, or “none.”
  4. What it depends on — external calls, oracles, other contracts, token assumptions.

When you’re done, three things jump off the page: functions that move value with no guard, external calls that run before your state settles, and invariants that several functions can each nudge. That’s not a list of bugs — it’s a list of places a bug would be fatal, which is exactly where to spend your attention.

Part 3 takes this map and starts pulling on it. We’ll walk the specific bug classes that turn an unguarded door or an ill-timed external call into someone else’s payday — reentrancy, arithmetic, and the value-moving logic errors that show up again and again in the same spots your map just flagged.