Skip to content

Hack My Contract™

Adversarial Smart Contract Testing

Find out how your contract could be attacked — before attackers do.

A scanner answers what is wrong with this code. This answers the different question you actually have before launch: what can an attacker actually exploit? It takes the findings the scan produced and tries to build them into a working path — and for every path it builds, it tells you how much of it is proven, how much is inference, and whether a proof of concept was ever executed. It will not call something confirmed because it looks bad.

21 attack chains
10 attack families
9 critical
69 detectors behind them
13 with a documented incident

What it covers

Ten families of attack. The counts below are measured from the simulator's own pattern table when this page is built — if a family ever emptied out, this page would fail to build rather than keep advertising it.

Access ControlA function that grants power and forgets to check who is calling.4 chains
ReentrancyGetting back inside before the contract finishes writing down what you took.1 chain
Flash-Loan Attack PathsAttacks that need no capital, because the capital is borrowed and repaid in one transaction.2 chains
Oracle ManipulationThe contract asks something else what a thing is worth, and the answer can be chosen.1 chain
Price ManipulationMoving the market the contract prices against, or transacting around somebody who is about to.2 chains
Signature / ReplayA signature that works more than once, on more than one chain, or for more than one thing.1 chain
UpgradeabilityThe code behind the address is not the code that was audited.1 chain
Privilege EscalationA path from ordinary caller to owner, or from owner to more than owner.3 chains
Business LogicEvery line is correct and the rules they add up to are not.4 chains
Economic / DeFiNothing is broken. The incentives pay an attacker more than they pay you.2 chains

What the verdicts mean

Every path this tool reports carries an exploitability verdict, and the strongest one it can reach from reading your source is Potential. That is a deliberate ceiling, not a limitation we forgot to remove.

  • Confirmeda proof of concept was executed against an isolated copy of the contract and it worked.Never produced by this lane — it executes nothing.
  • Potentialevery required precondition is satisfied by something the scan can point at in your code, and most of the supporting evidence is present. No proof of concept was executed — this says the path is open, not that somebody walked it.
  • Unverifiedthe required preconditions hold, but the chain rests largely on structural inference rather than direct detector hits. Treat it as a lead for a human reviewer.

Run it on your contract

The scan runs in your browser — the source never leaves it. Only the findings it produced (detector ids, the function table, the access-control flags) are sent to compose the chains, and you can watch that in the network tab.

Reading the contract is free. Composing the chains is on the AI Agents plan and costs 5 credits.

The playbook — 21 ways a contract loses its money

This is the complete list the simulator draws from. Nothing here was written by a language model: each chain exists because the scanner has detectors that can prove its preconditions, and each one says which conditions must hold before it will ever be reported against your code. Read them in the order they are ranked — the ones at the top are the ones that end projects.

critical

Run your own code against this contract's storage

Can I choose the code that this contract executes as itself?

Delegatecall

What it costs you

Delegatecall executes someone else's code with this contract's storage, balance and identity. Overwriting the owner slot is the usual first move; after that every other power is available legitimately.

What has to be true first

  • must be trueThe contract delegatecalls
  • must be trueThe target address is influenced by the caller or by a settable variable
  • adds weightThe contract holds state worth corrupting
  • adds weightStorage layout is not protected against collision

The simulator will not report this chain against your contract unless all 2 of those conditions are satisfied by something it can point at in the code.

How the attack runs

  1. Deploy a contract whose storage layout lines up with the target's.
  2. Get the target to delegatecall it, either directly or by setting the stored address.
  3. Overwrite the ownership slot with your own address.
  4. Use the powers that ownership now grants.

How you close it

Never delegatecall an address a caller can influence. Restrict targets to an allowlist fixed at deployment, and keep proxy storage in standardised slots so an implementation cannot collide with it.

It has happened: Parity Multisig, 2017 — ~$150M An initialisation path was left callable by anyone, so a stranger took ownership of the library every wallet delegated to.
critical

Move a thin market with borrowed capital, inside one transaction

Does this read a price that I can move with money I do not have to own?

Oracle / price manipulation

What it costs you

The attack needs no capital of its own. Borrow, distort the price the contract trusts, transact against the distortion, unwind and repay — all before any arbitrageur can react, because it is one atomic transaction.

What has to be true first

  • must be trueA price is read from a source that can move within a block
  • must be trueThat price decides how much value leaves the contract
  • adds weightNo time-weighted or multi-source price defence
  • adds weightThe answer is used without freshness or bounds checks

The simulator will not report this chain against your contract unless all 2 of those conditions are satisfied by something it can point at in the code.

How the attack runs

  1. Borrow a large amount from a flash-loan provider.
  2. Trade into the pool this contract prices against, pushing the quote far from its true value.
  3. Call the function that uses that quote — deposit, borrow, redeem or liquidate at the distorted rate.
  4. Reverse the trade, repay the loan in the same transaction, and keep the difference.

How you close it

Never price off a spot reading that a single transaction can move. Use a time-weighted average or an independent feed, require a fresh round, and bound the deviation you will accept between two sources.

It has happened: Cream Finance, 2021 — $130M A price read from a manipulable source, moved with borrowed capital inside one transaction.
critical

Borrow a majority, pass your own proposal, repay

Is voting power counted from a balance I can rent for one block?

Governance

What it costs you

Governance stops being a decision by the community and becomes a purchase — for the length of one transaction. The proposal that passes can be 'send the treasury to this address'.

What has to be true first

  • must be trueVoting power is read from a current balance
  • must be trueNo snapshot or checkpoint fixes voting power to a past block
  • adds weightA passed proposal can execute without a meaningful delay
  • adds weightGovernance controls something worth taking

The simulator will not report this chain against your contract unless all 2 of those conditions are satisfied by something it can point at in the code.

How the attack runs

  1. Submit the proposal in advance so it is already pending.
  2. Flash-loan enough of the governance token to hold a majority.
  3. Vote with the borrowed balance.
  4. Execute immediately, because nothing forces a delay between passing and executing.
  5. Repay the loan in the same transaction.

How you close it

Count voting power from a snapshot taken before the proposal existed, and require a timelock between a proposal passing and executing so token holders can react or exit.

It has happened: Beanstalk Farms, 2022 — $181M Voting power was counted from the current balance, so a flash loan bought a supermajority and executed a proposal in the same block.
critical

Repoint the price source, then borrow against a price you chose

Is there anything I can set, with no permission check, that a money function later trusts?

Privilege → Oracle → Value

What it costs you

The attacker does not need to move a market at all. They point the contract at a source they wrote, quote whatever price makes their position solvent, and take out far more than they put in — in one transaction, with no capital.

What has to be true first

  • must be trueA setter for a price/oracle/router dependency has no access control
  • must be trueThe contract moves value on paths that can consume that dependency
  • adds weightA price is actually read and used in a decision
  • adds weightNo timelock or delay between changing the source and using it

The simulator will not report this chain against your contract unless all 2 of those conditions are satisfied by something it can point at in the code.

How the attack runs

  1. Deploy a contract that satisfies the expected price interface and returns a value you choose.
  2. Call the unprotected setter to point the target at it.
  3. Enter the position the contract now misprices — deposit a little, borrow or redeem a lot.
  4. Optionally restore the original source so the state looks untouched afterwards.

How you close it

Put the setter behind access control AND a timelock, so a change is visible before it takes effect. Validate the new address is a contract, sanity-bound the returned price against a second source, and emit an event on every change.

It has happened: Mango Markets, 2022 — $114M A thin spot market was pushed with a large position, and the inflated mark was borrowed against.
critical

Take ownership through an unprotected privileged function

Is there any path that grants control and forgets to check who is calling?

Access control

What it costs you

Every owner-only power transfers with the role in one call — minting, pausing, fee changes, upgrades, treasury access. The contract keeps working perfectly; it just answers to somebody else.

What has to be true first

  • must be trueA privileged function is externally callable with no permission check
  • must be trueThe contract has an owner or role concept worth taking
  • adds weightAn ungated function can also grant or move that authority
  • adds weightOwnership transfer is single-step, so it lands immediately
  • adds weightThere are real powers behind that authority

The simulator will not report this chain against your contract unless all 2 of those conditions are satisfied by something it can point at in the code.

How the attack runs

  1. Call the unprotected privileged function and name yourself.
  2. Confirm the authority moved by reading the owner or role back on-chain.
  3. Exercise whichever power is worth the most: mint, sweep, change fees, or swap the implementation.

How you close it

Every state-changing external function needs an explicit permission check — the default for a new function must be 'refuse', not 'allow'. Use two-step ownership transfer, and put the authority itself behind a multisig.

It has happened: Parity Multisig, 2017 — ~$150M An initialisation path was left callable by anyone, so a stranger took ownership of the library every wallet delegated to.
critical

Drain the balance by re-entering a withdrawal

Can I get back inside this contract before it finishes writing down what I took?

Reentrancy

What it costs you

Every attempt repeats the payout against a balance that has not been reduced yet, so the loop continues until the contract's whole ether balance is gone — not just the attacker's own deposit.

What has to be true first

  • must be trueA function makes an external call before it updates state
  • must be trueThe contract sends value out
  • adds weightNo reentrancy guard anywhere in the contract
  • adds weightThe contract also takes deposits, so there is a pooled balance to drain

The simulator will not report this chain against your contract unless all 2 of those conditions are satisfied by something it can point at in the code.

How the attack runs

  1. Deposit a small amount from a contract you control, so you hold a legitimate balance.
  2. Call the withdrawal for that amount.
  3. The contract sends the value BEFORE reducing your recorded balance, which hands execution to your fallback while the old balance is still on record.
  4. From the fallback, call the withdrawal again — the check still passes.
  5. Repeat until the contract is empty, then let the stack unwind.

How you close it

Apply checks-effects-interactions: write the balance down BEFORE the external call. Add a `nonReentrant` guard as defence in depth, and prefer pull-payments over pushing value inside the accounting function.

It has happened: The DAO, 2016 — 3.6M ETH A withdrawal sent ether before it updated the balance, so the recipient's fallback re-entered and withdrew again.
critical

Delete the contract with the money inside

Can anybody remove this code from the chain?

Access control

What it costs you

This is not a theft, it is a deletion. The balance goes to whatever address the call names, and the code at that address is gone — every integration pointing at it now calls into nothing, and no upgrade can undo it because there is nothing left to upgrade.

What has to be true first

  • must be trueA selfdestruct path exists and is not properly guarded
  • must be trueThere is value or accounting to lose with it
  • adds weightOther contracts depend on this code being there
  • adds weightThe address could later be reoccupied by different code

The simulator will not report this chain against your contract unless all 2 of those conditions are satisfied by something it can point at in the code.

How the attack runs

  1. Call the unguarded destruction path.
  2. Name your own address as the recipient of the remaining balance.
  3. Everything that delegated into or called this address now executes empty code.

How you close it

Delete the selfdestruct. If an emergency shutdown is genuinely needed, use a pause flag that stops new activity while leaving withdrawals open — a pause is reversible and a deletion is not. Note that SELFDESTRUCT no longer frees storage after EIP-6780, so the funds move but the code stays: verify what your compiler emits.

It has happened: Parity Multisig (November), 2017 — 513,774 ETH frozen A stranger took ownership of the shared library and called kill() — every wallet delegating into it has been unreachable since.
critical

Mint supply to yourself

Can I create balances without paying for them?

Access control

What it costs you

Supply becomes meaningless. The attacker mints an arbitrary balance and sells or redeems it against real liquidity — every existing holder's share is diluted to nothing and the pooled value leaves.

What has to be true first

  • must be trueA minting entry point exists
  • must be true…and it has no access-control guard
  • adds weightNo maximum supply bounds the damage
  • adds weightThe minted balance is redeemable or tradeable for value

The simulator will not report this chain against your contract unless all 2 of those conditions are satisfied by something it can point at in the code.

How the attack runs

  1. Call the mint entry point directly, naming yourself as the recipient.
  2. Choose an amount large enough to matter but small enough not to trip any monitoring.
  3. Redeem or sell the minted balance against whatever real liquidity exists.
  4. Repeat while the path stays open.

How you close it

Gate the mint behind access control, and prefer a role held by a multisig or timelock over a single owner key. Add a maximum supply that the mint checks, and emit an event that a monitor can watch.

It has happened: Qubit Finance, 2022 — $80M A deposit that never happened was credited on the other side, minting assets backed by nothing.
critical

Replace the code behind the address

Who can change what this contract does after everyone has already deposited?

Upgradeability

What it costs you

Nothing in the deployed code has to be exploitable. Whoever holds the upgrade key replaces it with code that does whatever they like — and every audit of the previous implementation stops describing what users are exposed to.

What has to be true first

  • must be trueThe contract is upgradeable
  • must be trueNo timelock stands between deciding to upgrade and it taking effect
  • adds weightAuthority looks like a single key rather than a multisig
  • adds weightThe upgrade authorisation itself was flagged

The simulator will not report this chain against your contract unless all 2 of those conditions are satisfied by something it can point at in the code.

How the attack runs

  1. Obtain or compromise the single key that authorises upgrades.
  2. Deploy an implementation with the same storage layout and an added withdrawal path.
  3. Point the proxy at it.
  4. Use the new path, with users' funds still sitting at the same address they trusted.

How you close it

Put upgrades behind a timelock long enough for users to exit, and behind a multisig rather than one key. Publish the pending implementation before it lands, and emit an event a monitor can alert on.

It has happened: Poly Network, 2021 — $611M A crafted call reached a privileged path and changed the keeper set — privilege escalation, not a maths bug.
high

Raise the tax after everybody is in

Is there a ceiling on the fee, or just a setter?

Owner power / token

What it costs you

A sell tax that was 3% at launch can be 100% by the time you try to leave. No funds are stolen in any sense the contract would recognise — the transfer succeeds, and the whole amount goes to the fee wallet.

What has to be true first

  • must be trueA fee or tax can be changed after deployment
  • must be trueNothing bounds what the new value may be
  • adds weightThe fee is taken out of holders' transfers
  • adds weightThe change takes effect on the next block

The simulator will not report this chain against your contract unless all 2 of those conditions are satisfied by something it can point at in the code.

How the attack runs

  1. Launch with a fee small enough that nobody objects.
  2. Wait for holders and liquidity.
  3. Call the setter with a value the code never bounded.
  4. Every sale from that point forward pays out to the fee recipient instead of the seller.

How you close it

Put a hard maximum in the code — `require(newFee <= MAX_FEE)` with MAX_FEE a constant — so the ceiling is verifiable by anyone reading the source rather than promised in a document. Emit an event on every change, and put the setter behind a timelock.

No incident in our Hall of Hacks is a close enough match to cite here. We would rather say that than attach a famous name to a different bug.

high

Let them buy, then stop them selling

Is there a switch that decides whether MY transfer is allowed?

Owner power / token

What it costs you

Buying works. Selling is what stops working. The contract does not have to be broken for this — it is behaving exactly as written, and the write-up will read 'the owner disabled transfers', which is not a bug anyone can refund.

What has to be true first

  • must be trueTransfers are gated by a state the owner controls
  • must be trueThe contract is a token, so the gate applies to holders
  • adds weightThe switch answers to one key, with no delay and no committee
  • adds weightThe same authority holds other powers over the same balance

The simulator will not report this chain against your contract unless all 2 of those conditions are satisfied by something it can point at in the code.

How the attack runs

  1. Launch with the gate open, so early buys and sells both succeed and the chart looks normal.
  2. Let liquidity and holders accumulate.
  3. Flip the gate — blacklist the sellers, or switch trading off — from the owner key.
  4. Exit through whatever path stays open to the owner.

How you close it

If the freeze exists for a legal reason, say so on the site and put it behind a timelock and a multisig so a holder can see it coming and leave. If it does not, delete the mapping and the check — a transfer function that consults an owner-controlled list is indistinguishable from a honeypot to everyone reading it from outside.

Read this before you accuse anybody

A freeze switch is a capability, not an intent. Regulated tokens carry one on purpose. What this says is that somebody other than you decides whether your sale goes through.

No incident in our Hall of Hacks is a close enough match to cite here. We would rather say that than attach a famous name to a different bug.

high

Extract value from a rounding error, repeatedly

Does the maths lose a fraction, and can I make it lose that fraction to me?

Arithmetic

What it costs you

Each call is individually negligible and entirely legitimate. Automated and repeated thousands of times, the residue adds up — and because every transaction is valid, nothing looks like an attack until the accounting no longer balances.

What has to be true first

  • must be trueDivision before multiplication, or a rounding direction that favours the caller
  • must be trueThe path can be called repeatedly with no per-call cost that exceeds the gain
  • adds weightThe result decides an amount of value
  • adds weightArithmetic is unchecked somewhere

The simulator will not report this chain against your contract unless all 2 of those conditions are satisfied by something it can point at in the code.

How the attack runs

  1. Find the input size where the rounding favours you most — usually the smallest amount that still produces a non-zero result.
  2. Call the path with that amount.
  3. Repeat automatically, batching to keep gas below the extracted value.

How you close it

Multiply before dividing. Always round in the protocol's favour, never the caller's. Reject amounts that compute to zero, and assert the invariant that should hold after the operation.

It has happened: Uranium Finance, 2021 — $50M A single wrong digit in a constant broke the invariant that kept the pool solvent.
high

Reuse a signature that was only meant to work once

Does anything stop me submitting the same signed message twice?

Signature handling

What it costs you

One legitimate authorisation becomes unlimited authorisations. The same signed message is submitted repeatedly — or on another chain, if the message never committed to a chain id.

What has to be true first

  • must be trueThe contract authorises actions with signatures
  • must be trueNo nonce, or no domain separator, binds the message to one use
  • adds weightThe verification itself was flagged as weak
  • adds weightA signed action moves value or grants rights

The simulator will not report this chain against your contract unless all 2 of those conditions are satisfied by something it can point at in the code.

How the attack runs

  1. Observe a valid signed message being submitted.
  2. Resubmit the identical message and signature.
  3. Nothing marks it as consumed, so it is honoured again.
  4. Where the message omits a chain id or contract address, replay it against a deployment on another chain.

How you close it

Bind every signed message to a nonce that is consumed on use, and to an EIP-712 domain carrying the chain id and the contract address. Reject high-s signatures, and check the recovered address is not zero.

It has happened: Wormhole, 2022 — $326M Signature verification was sidestepped, letting forged approval authorise a mint.
high

Withdraw user funds through a rescue function

Is there a function that lets an admin move balances that are not theirs?

Owner power

What it costs you

This is the shape of most rug pulls, and it needs no bug at all — it is the contract working as written. The admin calls a function labelled 'rescue' and the pooled deposits leave.

What has to be true first

  • must be trueA rescue, sweep or withdraw-all path exists
  • must be trueThe contract custodies balances that belong to users
  • adds weightNothing restricts the rescue to non-user assets
  • adds weightThe power is immediate

The simulator will not report this chain against your contract unless all 2 of those conditions are satisfied by something it can point at in the code.

How the attack runs

  1. Wait until deposits have accumulated.
  2. Call the rescue or sweep path as the privileged account.
  3. Move the balance out — the transaction is entirely legitimate as far as the contract is concerned.

How you close it

A rescue function must exclude the assets users deposited — restrict it to tokens the protocol does not account for. Better, put it behind a timelock so a withdrawal is visible before it can complete.

No incident in our Hall of Hacks is a close enough match to cite here. We would rather say that than attach a famous name to a different bug.

high

Make the owner sign it for you

Does this check who STARTED the transaction rather than who is calling?

Access control

What it costs you

The attacker never needs a key. They need the owner to touch one contract — a mint page, an airdrop claim, anything. The owner's wallet starts the transaction, so tx.origin is the owner all the way down the call stack, and the privileged function agrees.

What has to be true first

  • must be trueAuthorisation compares against tx.origin
  • must be trueThe permission it grants reaches value or a privileged action
  • adds weightA named owner or role system is present
  • adds weightThe same path does not also check msg.sender

The simulator will not report this chain against your contract unless all 2 of those conditions are satisfied by something it can point at in the code.

How the attack runs

  1. Deploy a contract with an attractive reason for the owner to call it.
  2. Inside its function, call the target's privileged path.
  3. Get the owner to make that one call — the transaction they started is now the transaction that satisfies the check.
  4. The target sees tx.origin == owner and executes.

How you close it

Authenticate with `msg.sender`, always. `tx.origin` identifies whoever paid for the transaction, not whoever is calling you, and there is no case where it is the right check for permission.

No incident in our Hall of Hacks is a close enough match to cite here. We would rather say that than attach a famous name to a different bug.

high

Inflate the share price so the next depositor's money rounds to you

Can I make one share so expensive that a real deposit buys zero of them?

Accounting / rounding

What it costs you

A depositor receives zero shares for a real deposit, and the value stays in the pool backing the attacker's single share. The victim's money is not stolen by a transfer — it is absorbed by the arithmetic.

What has to be true first

  • must be trueDeposits are converted into shares against a total
  • must be trueThe backing balance can be increased without minting shares
  • adds weightShare issuance rounds in the protocol's favour, to zero
  • adds weightNo minimum initial deposit or dead-share defence

The simulator will not report this chain against your contract unless all 2 of those conditions are satisfied by something it can point at in the code.

How the attack runs

  1. Be the first depositor and mint exactly one share.
  2. Send a large amount directly to the contract, so the backing balance rises without new shares.
  3. Wait for a genuine deposit smaller than the new per-share price.
  4. Their share calculation rounds down to zero; redeem your one share for the whole pool.

How you close it

Mint a fixed number of dead shares at deployment, or track the backing internally instead of reading the raw balance. Round share issuance in the depositor's favour, and reject deposits that would mint zero shares.

It has happened: Euler Finance, 2023 — $197M A donation moved the accounting a position was valued against, turning a self-liquidation into a profit.
high

Know the outcome before the draw

Is the 'random' number something I can read, or influence, first?

Randomness

What it costs you

The lottery is not random for anyone who reads the same chain state you compute from. An attacker calls only when they have already computed that they win, and reverts otherwise — so from the outside the draw looks fair, and they never lose.

What has to be true first

  • must be trueRandomness comes from a value on the chain
  • must be trueThat number decides who receives something
  • adds weightThe draw can be read in the same transaction it is used
  • adds weightA validator could nudge the input

The simulator will not report this chain against your contract unless all 2 of those conditions are satisfied by something it can point at in the code.

How the attack runs

  1. Compute the outcome from the same block values the contract uses.
  2. Enter only when that computation says you win.
  3. Wrap the entry in a contract that reverts if the precondition fails, so a losing attempt costs gas and nothing else.
  4. Repeat every block.

How you close it

Nothing on-chain and readable can be a random number. Use a commit-reveal with an economic penalty, or a verifiable randomness service (Chainlink VRF) where the answer arrives in a later transaction than the request.

No incident in our Hall of Hacks is a close enough match to cite here. We would rather say that than attach a famous name to a different bug.

medium

Wedge the contract so nobody can withdraw

Can I make one step fail forever, so the whole queue stops?

Denial of service

What it costs you

Nothing is stolen and nothing can be recovered either. Funds stay in the contract, permanently, because the only path out reverts on an entry the attacker controls.

What has to be true first

  • must be trueA path can be made to revert or run out of gas for everyone
  • must be trueThe affected path is shared, so one failure blocks all users
  • adds weightThere is no independent per-user withdrawal to fall back on
  • adds weightNo emergency pause to contain it

The simulator will not report this chain against your contract unless all 2 of those conditions are satisfied by something it can point at in the code.

How the attack runs

  1. Enter the shared structure — a recipient list, a queue, a loop over holders.
  2. Make your own entry fail: a contract that rejects payment, or an entry that pushes the loop past the block gas limit.
  3. Every subsequent call to the shared path reverts before reaching anyone else.

How you close it

Use pull payments so each user withdraws independently and one failure is isolated. Bound every loop, and never let a transfer to one address decide whether everyone else gets paid.

No incident in our Hall of Hacks is a close enough match to cite here. We would rather say that than attach a famous name to a different bug.

medium

Read the pending transaction and get there first

Does seeing a transaction before it lands let me profit from it?

Transaction ordering

What it costs you

The person who was supposed to benefit gets a worse result, or none. Where the pending transaction reveals a secret — an answer, a bid, a claim — the observer simply uses it first.

What has to be true first

  • must be trueAn outcome depends on transaction order or on a revealed value
  • must be trueThe action is submitted publicly before it takes effect
  • adds weightGetting there first is worth something
  • adds weightNo commit-reveal or deadline protects the action

The simulator will not report this chain against your contract unless all 2 of those conditions are satisfied by something it can point at in the code.

How the attack runs

  1. Watch the public mempool for the relevant call.
  2. Read the parameters, which are visible before the transaction is mined.
  3. Submit the same or an opposing action with a higher fee so it is ordered first.

How you close it

Use commit-reveal for anything whose value depends on secrecy. Let callers set a minimum-received bound and a deadline, so an unfavourable ordering reverts rather than executing at any price.

It has happened: Compound Finance, 2021 — $147M An upgrade shipped with a comparison the wrong way round and paid out rewards nobody had earned.
medium

Trade in front of the swap and behind it

Does this swap accept whatever price it gets?

Transaction ordering

What it costs you

The swap always succeeds, so nothing looks wrong — it just returns less every time. A bot buys before it, sells after it, and the difference is paid by whoever sent the transaction.

What has to be true first

  • must be trueA swap or liquidity operation sets no minimum output
  • must be trueThe unbounded path is reachable by an ordinary user
  • adds weightThe transaction has no useful deadline
  • adds weightOrdering dependence was flagged elsewhere too

The simulator will not report this chain against your contract unless all 2 of those conditions are satisfied by something it can point at in the code.

How the attack runs

  1. Watch the mempool for a call into this path with no minimum-output bound.
  2. Buy the same asset first, at a higher gas price, moving the price against the pending swap.
  3. Let the victim's swap execute at the price you just set.
  4. Sell back in the same block and keep the spread.

How you close it

Pass a minimum-received amount computed by the caller, and let them set it. A deadline of `block.timestamp` is not a deadline — it is satisfied in every block, including one mined an hour later.

No incident in our Hall of Hacks is a close enough match to cite here. We would rather say that than attach a famous name to a different bug.

medium

Use the door beside the timelock

Is the delay the only way in, or just the advertised one?

Governance

What it costs you

The delay is what the documentation points at, and it is real — for the path it covers. The second path takes effect immediately, so the protection that made holders comfortable never applied to the action that matters.

What has to be true first

  • must be trueThe contract has a timelock
  • must be true…and an admin path that does not go through it
  • adds weightThere are real powers on the other side
  • adds weightThe bypass answers to one key

The simulator will not report this chain against your contract unless all 2 of those conditions are satisfied by something it can point at in the code.

How the attack runs

  1. Read which functions the timelock actually wraps.
  2. Find the same state reachable from an admin function that does not.
  3. Use that one — the change lands in the next block with no announcement.

How you close it

Make the timelock the only holder of the privileged role, so there is no second path by construction. If an emergency path must exist, restrict it to pausing — an action that can only stop things, never move value.

No incident in our Hall of Hacks is a close enough match to cite here. We would rather say that than attach a famous name to a different bug.

Where the testing happens

Everything on this page is static and offline. The source is analysed in your browser, the chains are composed from the evidence that analysis produced, and no transaction is constructed, signed or sent at any point — against mainnet or anywhere else. Where a future release executes a proof of concept, it will run against a local fork or a testnet, never against a live contract holding real money, and the verdict it produces will say so.

That is a product boundary as much as a safety one. A tool that ran real exploit attempts against arbitrary mainnet addresses on request would be an attack service with an audit page attached, and it is not what this is.

What this cannot tell you

Composed from what the scan could prove about this source: detector hits, the parsed function table, access-control flags and the call graph. A scenario appears only when every one of its preconditions is satisfied, and each one shows its evidence. Confidence is how much of the chain the scan PROVED — it is not a probability that an attack would succeed, which depends on live state (balances, liquidity, who holds the keys) that static analysis cannot see. Steps are conceptual, not runnable exploits.

Silence is not a certificate. A contract that produces no chain here has produced no chain from the evidence a static read can gather — business logic, economic design and who actually holds the keys are outside that. Those are what a full audit is for.