Skip to main content
Sigvex

Red Team / Blue Team EVM · Part 6: Blue Team: Hardening and Holding the Line

Detection tells you where you're exposed. This closing post is what you do about it — the defensive patterns and process that make the attacks from Parts 3 and 4 not work.

Red Team / Blue Team EVM · Part 6: Blue Team: Hardening and Holding the Line

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

Part 5 turned raw bytecode into a triaged list of findings. That list tells you where you’re exposed. This post is what you do about it — the patterns that make the attacks from Parts 3 and 4 stop working, and the process that catches the ones no pattern covers. It’s the payoff of the whole series.

Fix the bug class, not the instance

A finding names one spot. The bug behind it is almost always a category, and the durable fix targets the category. Walk back through the attacks you learned to run in Parts 3 and 4, and each one has a defense that isn’t a patch — it’s a discipline.

Reentrancy. The setup from Part 3 was a function that made an external call before it finished updating its own state. The defense is to never do that. Order every state-changing function as checks, then effects, then interactions: validate inputs, write all your storage, and only then call out. A reentrancy guard on top is cheap insurance, but ordering is the actual fix — a guard on a function that still calls out mid-update just narrows the window. Where you can, invert the flow entirely: don’t push funds to users, let them pull. A withdrawal the user triggers against a balance you already zeroed has no reentrant path worth the name.

External calls. Once you’ve made a call, you’ve handed control to code you don’t own. Treat every callee as hostile. Check return values — a call that fails returns false, it doesn’t revert for you, and ignoring that boolean is how “successful” transfers silently do nothing. Minimize what you trust: the fewer external contracts your critical path depends on, the smaller your surface. And be deliberate about delegatecall — it runs someone else’s code against your storage. If you don’t need it, don’t reach for it.

Arithmetic and precision. The 0.8 compiler reverts on overflow by default, which retired a whole family of the bugs in integer overflow and underflow. What it didn’t retire is rounding. Every division loses a remainder, and the direction you drop it decides who eats the loss — always round in the protocol’s favor, never the caller’s. Multiply before you divide so you don’t throw away precision you needed. And if you run a share-based vault, seed it against the inflation attack from Part 3: virtual shares or a dead-shares deposit at initialization make the first-depositor manipulation uneconomical instead of trusting nobody tries it.

Oracle and price. Spot price is a number an attacker with a flash loan can set. Part 3’s manipulation worked because the victim read that number and believed it. Don’t. Use a manipulation-resistant source — a time-weighted average that a single block can’t move, or several independent feeds you cross-check — and sanity-bound whatever you read so a value ten times outside normal range halts instead of executing. Oracle manipulation detection and the flash loan anatomy post cover why the loan is never the bug; the price assumption is.

Access control and upgrades. Part 4 was about who owns the contract. The defense is least privilege: every privileged function guarded, every guard checked, and no single key holding the keys to the kingdom. Put admin actions behind a timelock so a compromised or coerced signer can’t drain in one block — the delay is your window to notice and respond. Put the keys behind a multisig or governance rather than one hot wallet. For proxies, the discipline from proxy upgrade vulnerabilities and the Parity wallet freeze: initialize on deploy so nobody can claim the uninitialized logic contract, keep storage-gap discipline so an upgrade doesn’t shift every slot underneath your data, and restrict upgradeTo to the same guarded, timelocked path as everything else. An upgrade mechanism is an admin function that can replace the entire contract — treat it like the most dangerous function you have, because it is.

Process is the part patterns can’t cover

Patterns close the bugs you know the shape of. The ones that hurt are the bugs nobody drew a pattern for yet — an interaction between two features that are each fine alone. Code review won’t reliably find those. Two things do.

The first is testing that hunts instead of confirms. Unit tests check the cases you thought of; an attacker’s whole job is the case you didn’t. State an invariant — total shares always back total assets, no user withdraws more than they deposited, the sum of balances equals the contract’s holdings — and let a fuzzer throw thousands of random sequences at it looking for one that breaks it. That’s a different activity from example-based testing, and it’s where the interaction bugs surface. Smart contract fuzzing techniques covers the mechanics; formal verification versus fuzzing covers when you want a proof that no such sequence exists rather than high confidence none was found.

The second is that shipping is the start of the exposure, not the end of it. A deployed contract is a standing target, so watch it: monitor for the anomalous activity that precedes or accompanies an exploit — a function that’s never called suddenly firing, a balance moving in a way your model didn’t predict — while there’s still time to act. And decide your incident response before the incident. A pause switch you can trigger buys time, but only if you built it, guarded it against becoming its own single point of failure, and agreed in advance on who pulls it and when. Give researchers a way to reach you, too. A contract with no disclosure channel doesn’t get fewer bugs reported — it gets them reported on-chain, by the person exploiting them.

The through-line: your defense has to survive decompilation

Here’s what ties the whole series together. The attacker in Parts 1 through 4 never read your source. They read your deployed bytecode, and treated it as the only ground truth — because it is. Every defense above has to hold at that level too.

A checks-effects-interactions ordering that’s correct in your .sol file but compiles to a shape where the external call still lands before the storage write is not a defense — it’s a comment. A guard the optimizer folded away, an initializer that didn’t actually run on the proxy, a timelock that the deployed bytecode routes around: source that looks safe and bytecode that isn’t is exactly the gap an attacker lives in. So close the loop. Read what you deployed, the way an attacker will, and confirm the safe pattern actually made it into the artifact that runs. That’s the bytecode-native habit turned inward — the same reading you learned to do to strangers’ contracts in Part 1, done to your own before someone else does it for you.

That’s the mindset the series was for. Think like the person who will read your bytecode. Build so that when they do, it’s boring — no ordering to exploit, no price to move, no key to steal, no slot to collide. The best outcome of an audit isn’t a clean report. It’s an attacker who spends an afternoon in your bytecode, finds nothing worth the effort, and moves on to an easier target.