Methodology

How the red team works.

Eight specialised attackers run against every contract you submit. Each is fluent in a single class of exploit, briefed against the full history of how that class has been used to drain real protocols, and instructed to break the contract within those bounds. What follows is the briefing each one operates from.

ADVERSARY 01

Reentrancy Hunter

What it hunts

Functions that hand control to an external address before they finish updating their own state. Once an attacker holds the call, anything that depends on a state variable not yet written becomes a re-entry primitive.

Patterns it tests

  • External calls placed before state mutations (the classic checks-effects-interactions inversion).
  • Cross-function reentry via shared storage — withdraw and a different state-mutating function reading the same balance.
  • Read-only reentrancy — view functions that return stale values during an external call, exploited by integrating protocols.
  • ERC-777 tokensReceived and ERC-1155 onERC1155Received hook callbacks invoked mid-transfer.
  • Native ETH transfers using call{value:}("") without ReentrancyGuard or pull-over-push.
  • Compiler reentrancy — Vyper 0.2.15–0.3.0 emitted a flawed @nonreentrant lock that did not actually prevent re-entry.

Hack history briefed against

  • The DAO2016 · ~3.6M ETH (~$60M)

    Recursive splitDAO call drained tokens before the balance was zeroed. The vector that hard-forked Ethereum into ETH and ETC.

  • Lendf.Me2020 · $25M

    ERC-777 transferFrom callback re-entered supply() before the borrow position was settled.

  • Cream Finance2021 · $130M

    AMP token tokensReceived hook combined with a flash loan to re-enter borrow during a single block.

  • Curve Finance2023 · ~$73M

    Vyper compiler bug — the @nonreentrant lock did not enforce. Affected pools using stETH, msETH, alETH, and CRV.

Sample finding

Reentrant withdraw permits balance reuse before state updateCritical
function withdraw(uint256 amt) external { require(bal[msg.sender] >= amt); (bool ok,) = msg.sender.call{value: amt}(""); // EXTERNAL CALL require(ok, "transfer failed"); bal[msg.sender] -= amt; // STATE WRITE AFTER }

State is mutated after the external call. A malicious recipient can re-enter withdraw while bal still reflects the pre-debit balance, draining the contract. Remediation: invert to checks-effects-interactions, write bal first, or apply OpenZeppelin ReentrancyGuard.

ADVERSARY 02

Flash Loan Attacker

What it hunts

State that can be moved within a single block by anyone with access to uncollateralised liquidity. Price feeds, governance vote weight, reward accounting, and liquidation thresholds are the usual targets.

Patterns it tests

  • On-chain TVL or LP-share-based price oracles read at the same block they can be manipulated in.
  • Governance vote weight derived from spot balance instead of time-weighted snapshot.
  • Reward distributions calculated against in-block balances.
  • Liquidation eligibility computed from manipulable spot prices.
  • Single-block deposit-and-borrow loops that arbitrage protocol parameters.

Hack history briefed against

  • bZx2020 · ~$954k

    Two attacks in four days. Flash-loaned ETH, swapped through Uniswap to move sUSD price, opened an undercollateralised short.

  • Harvest Finance2020 · $34M

    Flash loan into Curve, manipulated y-pool price, deposited into and withdrew from yUSD vault for share-price arbitrage.

  • PancakeBunny2021 · ~$200M

    Flash-loaned BNB to manipulate the BUNNY/BNB pair, triggered an over-mint of BUNNY rewards, dumped into the same pool.

  • Beanstalk2022 · $182M

    Flash-loaned governance tokens to pass an emergency proposal that drained the protocol — single-block governance attack.

Sample finding

Reward accounting reads in-block LP balance, vulnerable to flash-loan inflationCritical
function harvest() external { // shares uses spot LP balance — attacker can flash-loan in uint256 shares = IERC20(LP).balanceOf(address(this)); uint256 reward = (totalReward * shares) / totalShares; _mint(msg.sender, reward); }

totalShares and shares are read against the current block. A flash loan into the LP inflates shares for one block, draining a disproportionate reward. Remediation: snapshot LP balance at deposit-time, or require a time-weighted average over n blocks.

ADVERSARY 03

Access Control Prober

What it hunts

Functions that should be privileged but are not, initializers that can be called twice, role grants that escalate, and proxy admin paths that bypass timelocks. The simplest class of bug; still the most expensive when missed.

Patterns it tests

  • Privileged functions missing onlyOwner / onlyRole / AccessControl modifiers.
  • Initializers without initializer modifier — callable a second time to reassign owner.
  • Constructor logic in upgradeable contracts — runs on the implementation, not the proxy.
  • Proxy admin functions reachable through the proxy interface (transparent vs UUPS confusion).
  • Role-mint paths where DEFAULT_ADMIN_ROLE can grant itself MINTER_ROLE post-deployment.
  • Unrenounced ownership when contracts are advertised as immutable.

Hack history briefed against

  • Poly Network2021 · $611M

    Cross-chain manager accepted unauthorised messages — attacker forged a message granting themselves keeper role across chains. Funds returned by whitehat.

  • Wormhole2022 · $325M

    Signature verification used a deprecated Solana sysvar that an attacker could supply directly, bypassing guardian checks. 120k wETH minted on Solana.

  • Nomad Bridge2022 · ~$190M

    An initialisation set the trusted root to 0x00, marking every message as proven. Anyone could replay any message body.

  • Audius2022 · ~$1.1M

    Proxy initializer left unprotected after upgrade — attacker re-initialised governance to themselves.

Sample finding

Initializer is publicly callable and lacks the initializer modifierCritical
function init(address _admin) external { // no initializer modifier require(admin == address(0), "already set"); admin = _admin; }

The require check protects against re-initialisation only if admin is never zero again. If a future upgrade re-introduces admin = address(0), the function is callable by anyone. Remediation: import OpenZeppelin Initializable and apply the initializer modifier; never gate ownership on a mutable storage check.

ADVERSARY 04

Overflow Saboteur

What it hunts

Integer math that wraps, rounds, or loses precision in a way that benefits an attacker. Solidity 0.8 protects most paths by default, but assembly, unchecked blocks, and fixed-point math reintroduce the entire pre-SafeMath surface.

Patterns it tests

  • unchecked { } blocks where the bound is not provably safe.
  • Inline assembly performing add / sub / mul without overflow guards.
  • Fixed-point arithmetic with division before multiplication — silent precision loss.
  • Casts from uint256 to smaller types (uint128, uint64) without explicit range checks.
  • Pre-0.8 contracts deployed without SafeMath — still in production for many proxies.
  • Token decimals mismatches between USDC (6) and most ERC-20s (18).

Hack history briefed against

  • BeautyChain (BEC)2018 · Token rendered worthless

    Overflow in batchTransfer multiplied amount * receivers, wrapping to zero. Sender passed amount checks while transferring near-infinite tokens.

  • PoWHC / various early ERC-20s2018 · $multiple

    Pre-SafeMath multiplications wrapped to zero, allowing minting beyond intended supply.

  • Compound Drip2021 · ~$80M COMP overpaid

    Distribution calculation used a stale exchange rate — not strictly overflow, but precision-loss class. Required a governance pause and partial recovery.

  • Hundred Finance2023 · $7.4M

    Empty-market share-price inflation via decimals mismatch — attacker donated tokens to manipulate exchangeRate.

Sample finding

Reward calculation divides before multiplying, zeroing small balancesHigh
function reward(uint256 stake, uint256 totalStake) public view returns (uint256) { // division first — small stakes round to 0 return (stake / totalStake) * rewardPool; }

Integer division truncates. Any stake < totalStake yields zero before multiplication. Remediation: reorder to (stake * rewardPool) / totalStake, with overflow protection by sizing rewardPool to fit uint256 ÷ max(totalStake).

ADVERSARY 05

Oracle Manipulator

What it hunts

Price feeds that can be moved, spoofed, or read stale. Almost every DeFi exploit since 2020 has an oracle component — the manipulator is what surfaces them before the market does.

Patterns it tests

  • Spot-price oracles derived from a single AMM pair — manipulable with a single trade.
  • TWAP windows shorter than the cost of moving the underlying pool.
  • Single-source feeds with no fallback or sanity bound.
  • Stale Chainlink rounds — missing checks on roundId, updatedAt, and answeredInRound.
  • L2 sequencer-down conditions that freeze price feeds while liquidations continue.
  • Reward-rate setters that read price at call time without staleness validation.

Hack history briefed against

  • bZx2020 · ~$954k

    Manipulated Uniswap sUSD spot price within a single transaction to borrow against an inflated collateral value.

  • Cream Finance2021 · $130M

    yUSD price oracle inflation via Curve y-pool manipulation, combined with a flash loan and reentrancy.

  • Mango Markets2022 · $116M

    Avraham Eisenberg pumped MNGO perp price on Mango itself, then borrowed against the inflated unrealised PnL.

  • Inverse Finance2022 · $15.6M

    INV/DOLA price manipulated through a thin Uniswap v2 pool used as the protocol oracle.

Sample finding

Reward rate setter accepts price without staleness checkCritical
function setRewardRate() external { (, int256 price,,,) = chainlinkFeed.latestRoundData(); require(price > 0, "bad price"); rewardRate = uint256(price) / 1e8; }

No checks on roundId / answeredInRound / updatedAt. If the feed is stale (sequencer down, round not yet posted), the call returns a price hours or days old. Remediation: require updatedAt > block.timestamp - heartbeat and answeredInRound >= roundId; on L2, gate on the sequencer uptime feed.

ADVERSARY 06

MEV Predator

What it hunts

Order-of-execution dependence. Anything where the outcome differs based on who arrives first — swaps, auctions, NFT mints, governance — is a sandwich, frontrun, or backrun primitive.

Patterns it tests

  • Swap functions without amountOutMin (slippage tolerance) — sandwich-trivial.
  • Auctions without commit-reveal — the highest visible bid is always the winning bid + 1 wei.
  • Mint functions where a public read function reveals the next price tier mid-block.
  • Liquidations distributed first-come-first-served instead of via Dutch auction.
  • Governance proposals where execution depends on the order of votes within the same block.
  • AMM rebalances that announce the target ratio before the trade lands.

Hack history briefed against

  • Generalised MEVongoing · $1B+ annually

    Per Flashbots / EigenPhi, sandwich attacks alone extract hundreds of millions per year from unsophisticated swaps on Ethereum and Base.

  • Curve sandwich incidents2023 · Multiple $100k+

    Large LP withdrawals frontrun for slippage capture, particularly on stable-to-volatile pools.

  • NFT mint frontrunning2021–2023 · Thousands of incidents

    Free-mint contracts using msg.sender == tx.origin consistently lost reservation slots to MEV bots.

Sample finding

Swap accepts no slippage parameter — sandwich-trivialHigh
function buy() external payable { uint256 out = router.swapExactETHForTokens{value: msg.value}( 0, // amountOutMin = 0 path, msg.sender, block.timestamp ); }

amountOutMin set to zero accepts any output. A sandwich bot frontruns with a buy that moves price, fills the user at the worst rate, and backruns with a sell capturing the spread. Remediation: accept amountOutMin from the caller and validate it against an off-chain quote with explicit deadline.

ADVERSARY 07

Economic Exploit

What it hunts

Edge cases in protocol math that are valid Solidity but catastrophic economics. Donation attacks, share-price inflation, fee-on-transfer mismatches, rounding asymmetries — the ones that pass formal verification because they’re features, not bugs.

Patterns it tests

  • Empty-vault share-price inflation — first depositor donates tokens to make subsequent shares cost ≥ 1 unit.
  • Fee-on-transfer tokens treated as 1:1 transfers, breaking accounting after every move.
  • Rebasing tokens whose balance changes silently between block N and block N+1.
  • Liquidation discount + dust debt combinations that strand positions.
  • Reward distributions where withdrawing before / after a rebase yields different outcomes.
  • ERC-4626 deposits that round in the user’s favour rather than the vault’s.

Hack history briefed against

  • Hundred Finance2023 · $7.4M

    Empty-market share-price inflation — attacker created a market, donated tokens to inflate exchangeRate, borrowed against a single share.

  • Euler Finance2023 · $197M

    donateToReserves let an attacker push their own debt into bad-debt territory, triggering self-liquidation at a profit.

  • Curve Finance2023 · ~$73M

    Vyper compiler reentrancy combined with stETH-ETH rebalancing math in stable pools.

  • Hopelessly long tailongoing · $100s of M

    Yearn, Cream, Inverse, Beanstalk, Saddle, Mim — economic-class bugs survive line-by-line review and only emerge under adversarial exploration.

Sample finding

ERC-4626 deposit allows first-depositor share-price inflationCritical
function deposit(uint256 assets) external returns (uint256 shares) { if (totalSupply() == 0) { shares = assets; // 1:1 only if first depositor } else { shares = assets * totalSupply() / asset.balanceOf(address(this)); } _mint(msg.sender, shares); }

A first depositor mints 1 share for 1 wei, then donates a large amount of asset directly to the contract. asset.balanceOf becomes huge while totalSupply stays at 1. The next depositor’s shares round to zero, granting nothing. Remediation: mint a baseline of 1e3 dead shares to a burn address, or require minimum first-deposit thresholds (OpenZeppelin’s ERC4626 inflation-attack mitigation).

ADVERSARY 08

Gas Griefer

What it hunts

Code that turns a denial-of-service vector into a governance lever or a profit centre. Unbounded loops, refund mechanisms that revert, storage layouts that punish callers, and gas-griefing tactics that target oracles, keepers, and crosschain messengers.

Patterns it tests

  • Unbounded for-loops over user-controlled arrays — grow the array, freeze the function.
  • Refund logic that reverts when the recipient cannot accept ETH.
  • Pull-payment patterns implemented as push-payments.
  • send / transfer with hard-coded 2300 gas — fails for any contract recipient.
  • Storage layouts that force redundant SLOADs in the hot path.
  • Cross-chain messengers without out-of-gas defence on the receiving side.

Hack history briefed against

  • King of the Ether2016 · Game soft-locked

    Refund to the previous king sent via send() — a contract recipient that reverted prevented anyone from claiming the throne.

  • GovernMental Ponzi2016 · ~1100 ETH stuck

    Looped over a creditor list that grew past the block gas limit, permanently freezing payouts.

  • Akutar Mint2022 · $34M permanently locked

    Refund mechanism reverted on contract participants. The contract had no rescue path; funds remain unrecoverable.

  • Cross-chain bridge OOM patternsongoing · Numerous

    Receiving messengers that loop over recipient call data are repeatedly griefed to drop messages.

Sample finding

Unbounded loop over withdrawals enables governance DoSHigh
function payAll() external onlyOwner { for (uint i = 0; i < beneficiaries.length; i++) { // unbounded — attacker calls register() repeatedly to grow the array beneficiaries[i].call{value: amounts[i]}(""); } }

beneficiaries can be appended without limit. Once the loop exceeds the block gas limit, payAll permanently reverts. Remediation: switch to pull-payment (each beneficiary calls claim() themselves), or paginate with a startIndex / endIndex.

Report consolidation

One report, deduplicated, graded by exploit cost.

Each attacker submits findings independently. A consolidator pass cross-references every finding against the other seven, merges duplicates, and grades severity by what an attacker would actually need to spend to exploit it on mainnet — not by an arbitrary critical / high / medium ladder.

Findings unique to a single attacker are flagged for human review. Findings confirmed by two or more are escalated. The output is the same artefact whether you reached it from Claude Desktop, Cursor, the web app, or an autonomous agent paying via x402.