Reentrancy, Flash Loans, and Oracle Manipulation: Advanced Solidity Security
Advanced security patterns for Solidity developers, with detailed attack walkthroughs and defensive code patterns for reentrancy, flash loan attacks, and price oracle manipulation.
Beyond Basic Reentrancy: The CEI Pattern Is Not Enough
Every Solidity developer learns Checks-Effects-Interactions (CEI) as the primary reentrancy defense. Update state before making external calls, and a reentrant call cannot exploit stale state. CEI is correct and necessary — but not sufficient for modern DeFi contracts.
Read-only reentrancy is a class of attack that CEI does not prevent. The attack pattern: while a contract is in the middle of a state-updating external call (which calls back into a different view function on your contract), the view function returns inconsistent state that another contract reads. The second contract makes economic decisions based on this inconsistent state.
Curve Finance experienced a read-only reentrancy incident in 2023. During a remove_liquidity call on Curve (which made an ETH transfer before updating price state), an attacker called a lending protocol that used Curve's get_virtual_price() (a view function) as a price oracle. The price returned during the reentrancy window was incorrect, allowing the attacker to borrow against inflated collateral.
Defense: use a reentrancy guard on both state-modifying and view functions in any contract that other protocols use as an oracle or price reference.
Flash Loan Architecture and Attack Patterns
Flash loans are uncollateralized loans that must be repaid within a single transaction. They are not inherently malicious — legitimate uses include arbitrage, self-liquidation, and collateral swapping. But they are the tool that makes many DeFi attacks economically viable.
Flash loan attack anatomy:
1. Borrow millions in Token A via flash loan.
2. Dump Token A into a DEX pool, distorting the price.
3. Your target contract uses the distorted price as a reference.
4. Exploit the target contract using the manipulated price (borrow against inflated collateral, liquidate artificially underwater positions, drain a treasury that uses spot price).
5. Repay the flash loan. Net profit = exploitation gain minus flash loan fee.
The critical insight: any on-chain price that can be moved with a single large transaction is potentially manipulable with a flash loan. This includes:
- Spot prices from low-liquidity DEX pools.
- Balances of tokens in contracts that count holdings as collateral.
- Any computation based on reserve ratios that can be temporarily altered.
Defense: never use a price that an adversary can move within the same transaction. Use TWAPs, Chainlink feeds, or median-based oracles. For any collateral valuation, require a minimum time delay between deposit and borrowing.
Price Oracle Manipulation: Defense in Depth
Oracle manipulation is the most common flash loan attack vector. Defense requires multiple layers:
Layer 1 — Use resistant oracle sources:
Chainlink price feeds aggregate prices from many sources and require sustained price movement to move the feed. A single flash loan cannot move a Chainlink feed. For any price-sensitive DeFi application, Chainlink is the primary recommendation.
Layer 2 — TWAP with sufficient window:
If you must use on-chain DEX prices (for assets with no Chainlink feed), use a TWAP with at minimum 15-30 minutes. A 30-minute TWAP requires sustained price manipulation for 30 minutes — the capital cost and opportunity cost make most attacks economically unviable.
Layer 3 — Sanity bounds:
Even with Chainlink, check that the returned price is within reasonable bounds. If the price has moved more than 20% from the previous value in a short time, circuit-break the protocol.
Layer 4 — Multi-oracle consensus:
For high-value operations, require agreement between two independent oracle sources. If Chainlink and a long-window TWAP differ by more than 5%, revert with an oracle discrepancy error.
Layer 5 — Economic circuit breakers:
Limit how much can be borrowed or withdrawn in a single block or transaction. Even if an oracle is briefly manipulated, a circuit breaker limits the maximum damage.
Reentrancy in Complex Systems: Cross-Function and Cross-Contract
Single-function reentrancy (reenter the same function) is the textbook case. Production attacks are more subtle.
Cross-function reentrancy: Function A calls an external contract. The external contract calls Function B on your contract (not Function A). If Function A has updated some state and Function B depends on that state, Function B may see inconsistent intermediate state.
Example: Function A withdraws ETH (before updating balance) and calls an attacker contract. The attacker calls Function B which checks the balance — the balance has not been updated yet because Function A has not finished.
Defense: apply reentrancy guard to all functions that share state or call external contracts, not just the entry point.
Cross-contract reentrancy: Contract X calls Contract Y. Contract Y calls back into Contract Z (not Contract X). Contract Z reads state from Contract X that is in an inconsistent intermediate state.
This is the read-only reentrancy pattern. Protocols that read external contract state (Curve's get_virtual_price, Compound's exchangeRate, Aave's getReserveData) during a reentrancy window may receive incorrect values.
Detection: in your threat model, identify all external contracts that call into your contract. For each, identify what state your contract exposes that they might read. Protect against reentrancy on all state-exposing views if you make external calls that trigger arbitrary code.
Formal Verification and Fuzzing for Security
Advanced security analysis goes beyond audits and standard testing.
Invariant fuzzing in Foundry: define properties that must always hold. "The sum of all user balances never exceeds totalSupply." "The protocol is always solvent: totalAssets >= totalLiabilities." Write these as Foundry invariant tests. The fuzzer generates random sequences of transactions and checks invariants after each one. This is the most effective automated technique for finding economic vulnerabilities.
Symbolic execution with Halmos: Halmos is a symbolic execution tool for Foundry tests. It proves that a test passes for all possible inputs, not just random ones. If a test can fail under any input, Halmos finds a concrete counterexample.
Formal verification with Certora Prover: write formal specifications (rules) in the Certora Verification Language. The Prover formally verifies that your contract satisfies the rules for all possible inputs and states. This is the gold standard for critical financial infrastructure.
The security investment pyramid: tests (all teams), fuzzing (all DeFi teams), formal verification (high-value protocols, financial infrastructure). Time investment scales accordingly: fuzzing requires hours of setup and seconds to minutes of execution. Formal verification requires days to weeks of specification writing and expert review.
No technique is complete alone. Combine code review, formal verification, economic analysis, and a bug bounty program for defense in depth.