Most DeFi exploits are not new. They are the same dozen mechanisms, re-applied to a new protocol that was written by a team who had not yet seen them. An attacker reading a freshly deployed lending market or vault does not need creativity; they need a checklist of patterns and the patience to find which one the developers forgot. This article is that checklist, written from the defender's side.
It is for engineers who build DeFi protocols and for the people who run them in production. We have organised it by mechanism rather than by incident, because the mechanism is what transfers. Names change, chains change, the money moves the same way.
Flash loans: the enabler, not the bug
A flash loan is an uncollateralised loan that must be repaid within the same transaction. It is not an exploit. It is a capital multiplier that turns a vulnerability any whale could have used into one that anyone with gas money can use.
The pre-condition for almost every pattern below is "the attacker has a lot of capital for one block". The correct threat model now assumes that every external caller has access to the entire liquid supply of every major asset for the duration of a single transaction. The consequence for design: a pool's reserves, a deposited balance, a governance token holding, a collateral ratio can all be manufactured for one block and dissolved before the next. Protocols that act on a single-block snapshot are acting on a number the attacker chose.
Our auditors trace every value a protocol reads and ask one question: could this be different in the next block at no cost to the caller? If the answer is yes, that value must not be an input to anything that moves funds.
Price and oracle manipulation
The most expensive pattern of all. The protocol derives a price from somewhere the attacker can push, the attacker pushes it, the protocol acts on the false price, the attacker unwinds.
Spot prices from a pool
A protocol that prices an asset by reading the reserves of an AMM pool is pricing it by a number that one swap can move. The attack is a sandwich with the victim in the middle: borrow, swap to move the reserves, interact with the victim protocol at the distorted price (borrow more than the collateral is worth, mint more shares than deposited, liquidate a healthy position), swap back, repay. The pool returns to its original state; the victim does not.
The only pre-condition is that the pool is shallower than what the attacker can borrow, which is true of nearly every pool.
Time-weighted prices
A TWAP is harder to move within a block, but where one actor can control consecutive blocks or the window is short, it can be walked over several blocks for a cost that is small relative to the extractable value. It also lags during a real market move, which is when liquidations need accurate prices. We treat a TWAP as a mitigation for single-block manipulation, not as an oracle on its own.
Pushed feeds and staleness
The failure we see most often with pushed feeds is not a corrupted answer but a stale one: the protocol never checks the timestamp or round, and prices against a value from hours ago. During a sharp move that is a free arbitrage against the protocol's reserves. Every oracle read needs a staleness threshold, a sanity bound against a secondary source, and a defined behaviour on failure, which should be "stop accepting this asset", not "fall back to the pool".
How we test
We build a fork test that takes the largest flash loan available on the chain, moves every price the protocol reads as far as the liquidity allows, and measures the delta in what the protocol will hand out. If that delta is positive after fees and gas, it is a finding, regardless of how unlikely the team believes the trade is.
Reentrancy, including read-only
The classic form
A function calls an external contract before it has finished updating its own state, and the callee re-enters to find the pre-update state still in place. The fix is ordering: checks, then effects, then interactions. A reentrancy guard is a belt to go with those braces, not a replacement.
Cross-function and cross-contract
A guard on withdraw does nothing if transfer or borrow reads the same balance and is unguarded. Reentrancy is a property of shared state, not of a single function. The audit question is: while this external call has control, which other entry points read state that this function has not yet updated?
Read-only reentrancy
This is the form that catches mature teams. A pool's get_virtual_price or equivalent view derives a value from reserves and token supply. During a withdrawal with a callback, the reserves have been sent out but the supply has not yet been reduced, so the view reports an inflated value. The pool is not harmed. The victim is a third protocol that uses that view to price collateral and is drained by a borrow during the callback.
View functions are part of your attack surface if anyone else reads them. If you integrate another protocol's views, you need to know whether they can be called mid-update, and if so, a guard that reverts when the source contract is in a re-entered state.
Token hooks
ERC-777, ERC-721 and ERC-1155 transfers call the recipient. A token transfer is an external call, and we treat every safeTransferFrom as a control transfer to an adversary.
Broken accounting and share inflation
Any contract that issues shares against deposited assets has an exchange rate, and that exchange rate is a place where arithmetic can be weaponised.
The first-depositor inflation attack
An empty vault calculates shares as deposit multiplied by total shares divided by total assets. The attacker deposits one wei and receives one share. They then transfer a large amount of the asset directly to the vault contract, bypassing deposit. Total assets are now large; total shares are still one. The next legitimate depositor, depositing slightly less than the attacker's donation, receives zero shares because the division rounds down. The attacker redeems their one share for the whole balance.
Mitigations: virtual shares and assets, a burned or locked minimum first deposit, or a protocol-owned seed deposit. We check that one exists on every path, including vaults created by factories after launch.
Donation attacks more generally
The inflation attack is one instance of a broader pattern: the contract uses balanceOf(address(this)) as its source of truth, and anyone can change that number without going through the contract's logic. Direct transfers distort reward calculations, trip invariant checks, and skew exchange rates. Internal accounting should track what the contract has accepted, not what it holds, and the difference should be reclaimable or ignored, never distributed.
Rounding direction
Every division has a direction. Deposits round shares down, withdrawals round assets down, fees round up in the protocol's favour. A conversion that rounds the wrong way is an extraction loop: repeat a small operation thousands of times and collect the error each time. We trace every division and write down which party benefits.
Rebasing and fee-on-transfer tokens
A token whose balances change outside transfers, or which delivers less than was sent, breaks any contract that assumes the amount sent equals the amount received. Measure the balance before and after and account only the difference, or refuse such tokens explicitly.
Access control and upgrade abuse
Missing and mis-scoped modifiers
Every state-changing function needs a documented answer to "who may call this". The failures are familiar: an internal function declared public; an emergency function added late with no modifier; a role granted to the deployer and never transferred; a callback that trusts msg.sender without checking it.
Uninitialised implementations and proxies
An upgradeable contract whose initialiser can still be called is a contract whose owner can be anyone. This has been exploited repeatedly, usually against forks deployed without the initialisation step. We check that implementations disable initialisers in their constructor and that every proxy's initialize was called on the deployed instance, not just in the tests.
Upgrade as the attack
If the upgrade key is a single externally owned account, the protocol's security is that of one private key: an upgrade can swap in an implementation that transfers everything out in one transaction. The defence is a multisig behind a timelock long enough for users to exit. We also check storage layout between versions, because a corrupted layout can lock funds as thoroughly as a malicious one.
Governance attacks
Governance is access control with extra steps, and flash loans turned it into a same-block attack.
The mechanism: a proposal to move the treasury, change a critical parameter, or upgrade a contract is created; voting power is borrowed, bought, or manufactured; the vote passes; the proposal executes. Variants include borrowing governance tokens for the snapshot block, delegation that counts tokens at the wrong moment, and proposals whose description is innocuous but whose calldata is not.
Defences that work: voting power measured at a past snapshot block the proposer cannot predict, a delay between creation and voting, a quorum that borrowable supply cannot meet, and a timelock on execution with a guardian able to cancel. Governance contracts that call arbitrary targets with arbitrary calldata are privileged functions and get audited as such.
Stale and infinite approvals
Users approve protocols to spend their tokens, usually for the maximum amount, once, and never revoke. That approval outlives the user's interest in the protocol and it outlives the protocol's security.
The pattern has two forms. In the first, a contract with open approvals is upgraded or has a bug found, and the attacker uses its existing permission to pull tokens from every wallet that ever approved it. In the second, a router has a function that calls transferFrom with a caller-supplied from, and anyone can drain any wallet that approved the router.
Never expose a path where the from of a transferFrom is not msg.sender or an address that explicitly authorised the caller. Any contract that holds approvals is a custodian of every approving wallet's balance, with the key management and upgrade controls that implies.
Composability as attack surface
Every protocol that integrates another inherits its failure modes, and adds the interaction between the two.
A lending market that accepts an LP token as collateral is exposed to the pool's pricing, reentrancy properties, and upgrade key. A yield aggregator that routes through several protocols is exposed to the weakest. Many of the largest losses were not bugs in the drained protocol's own code; they were assumptions about a dependency that the dependency never promised to keep.
For each external contract a protocol calls or reads, we ask what it assumes, and whether that assumption is documented by the dependency or merely observed to be true today. Observed assumptions are the ones that break on the dependency's next upgrade.
Why forks inherit bugs
A fork copies the code, the deployment scripts, and the known issues. It does not copy the team that understood why each line was there, the fixes made after the fork point, or the operational practices that kept the original safe.
Forks fail in predictable ways. They deploy with the initialisation step missing. They change one parameter, such as a token with different decimals or a fee-on-transfer token in the pool, without noticing the original's invariants depended on it. They carry a vulnerability that was patched upstream after the fork was taken, and attackers watch upstream disclosures specifically to find forks that have not patched.
If your protocol is a fork, your threat model includes every incident the original has suffered and every fix made since your fork point. "It is basically a fork of X" is not a security argument; it is a list of homework.
How we test for these patterns
An engagement that covers this list is not a read-through. We build a fork of mainnet state and work through the patterns as an attacker would.
- Capital assumption: every test starts from the premise that the caller holds the largest flash loan available on the chain.
- Oracle tracing: we enumerate every price and balance the protocol reads, classify each as manipulable in-block, over several blocks, or not at all, and write a proof of concept for each manipulable one.
- Reentrancy mapping: we list every external call, the state that is stale during it, and every entry point that reads that stale state, including view functions other protocols consume.
- Accounting invariants: property tests asserting that no sequence of deposits and withdrawals extracts more than was deposited, and that direct transfers to the contract do not change what any user can withdraw.
- Privilege map: every role, every holder, every function it gates, and what a compromise of that holder costs.
- Dependency review: every external contract and whether the assumptions about it survive its upgrade or manipulation.
Next steps
Before your next deployment or audit, work through the list above and write the answers down. The questions that take longest to answer are the ones that matter. Then read our 2026 smart contract audit checklist for the full set of items our auditors cover, and if your protocol touches other chains, how cross-chain bridges get drained covers the patterns that appear at the boundary.
If you want us to run these tests against your code, our audit scope is on the services page, and you can reach the team on Telegram.