Security Stories 2025: Top Exploit Patterns & How to Audit Against Them
If you’ve scanned enough incident write-ups, you’ll notice the same bugs on repeat: price oracles that can be nudged, reentrancy that evades a single guard, upgradeable proxy foot-guns, signature replay across chains, and missing allowlist or pause controls. This guide collects the exploit patterns you’re most likely to face in 2025, shows you what they look like in code, and gives you plain-English checklists you can drop into your review or audit workflow.
We lean on reputable sources and docs throughout, Uniswap and Chainlink on oracles, OpenZeppelin on proxies and reentrancy, the SWC Registry, and incident trackers. See cited sources inline and the full reference list at the end.
- Oracle misuse & manipulations: On-chain DEX TWAP/SWAP-based oracles misconfigured or made too “short,” or mixing spot reads with small-liquidity pools. See Uniswap oracle design notes and hardening tips. :contentReference[oaicite:0]{index=0}
- Reentrancy (and “reentrancy-adjacent” patterns): Still prevalent when external calls happen before effects or when a single
nonReentrantguard doesn’t cover all paths. See OpenZeppelin guidance and Solidity CEI. :contentReference[oaicite:1]{index=1} - Upgradeable proxy pitfalls: UUPS/transparent proxy initializers left uncalled, unsafe upgrade paths, or storage collisions. See OpenZeppelin Upgrades docs and EIP-1967 storage slot guidance. :contentReference[oaicite:2]{index=2}
- Signature replay: Cross-chain replays, expired domain separators, or custom
permitwithout nonces. Use EIP-712/2612 patterns and modern libraries (e.g., Permit2) with explicit domains and nonces. :contentReference[oaicite:3]{index=3} - Access control gaps: Orphaned owners, unsafely exposed
onlyOwnerflows, missing timelocks, or overly permissive “admin” contracts. - Economic/MEV issues: Sandwichable flows, insufficient slippage checks, auctions vulnerable to last-minute griefing, or lack of PBS/MEV-aware deploy configs. See wallet-side mitigations discussed in prior MEV primers. (Background oracles & MEV: Uniswap oracle notes.) :contentReference[oaicite:4]{index=4}
- Bridges & cross-chain assumptions: Not the focus here, but many 2023–2024 losses came from validator/multisig or light-client issues. (Use mature designs; keep blast radius small with caps and alerting.)
Why these patterns persist (and how to beat them)
Teams ship fast. Tooling improves. But economics and statefulness haven’t changed: your contracts maintain balances, rely on external prices, and sometimes let strangers call back into you. If your mental model is off by even one step or you import a proxy/permit/oracle pattern without reading the fine print you reopen vulnerabilities we’ve known for years.
The fix isn’t “audit once.” It’s a repeatable review loop: design checklists, unit/invariant tests, fuzzing, testnet fire-drills, staged limits, and on-chain monitoring. The rest of this guide turns those into checkable items with code.
Pattern #1 — Price Oracles: Short windows, thin liquidity, wrong source
Many exploits boil down to “the contract trusted a price that could be moved.” The classic mistakes: (1) reading a spot price from a tiny pool, (2) using TWAPs with windows too short to amortize manipulation costs, or (3) mixing on-chain AMM quotes with off-chain collateral without guardrails. Uniswap’s oracle design notes explain why TWAPs were secure under certain assumptions and what PoS-era dynamics changed. :contentReference[oaicite:5]{index=5}
Safer patterns
- Use robust feeds when appropriate (e.g., Chainlink Data Feeds) with circuit breakers and stale checks. :contentReference[oaicite:6]{index=6}
- If you must read AMMs, use long enough TWAP windows, sufficient liquidity pairs, and combine with sanity bounds (max drift per block/epoch; clamp unusual moves).
- Never trust a single source for liquidation/issuance. Blend feeds or require governance/judicial review for extreme moves.
// Pseudocode: validating a price with a robust oracle + sanity checks
uint256 price = chainlink.latestAnswer(); // has heartbeat & deviation controls
require(block.timestamp - chainlink.latestTimestamp() < MAX_STALE, "stale price"); // see oracle docs
require(price >= MIN_PRICE && price <= MAX_PRICE, "out of bounds"); // clamp extremes
// Optional: compare against a long-window TWAP (Uniswap v3) before critical actions
- Identify every place a price affects balances (mint/burn, collateralization, liquidation, auction).
- Verify the data source: Chainlink/Pyth feed? AMM TWAP? Off-chain signed message? Ensure freshness and thresholds. :contentReference[oaicite:7]{index=7}
- Ensure long enough TWAP windows and sufficient pool depth if using AMMs; compare to reference feeds. :contentReference[oaicite:8]{index=8}
- Add circuit breakers: pause/guarded functions on extreme deviation, and a manual restore path.
Pattern #2 — Reentrancy (classic, cross-function, and “callback surprises”)
Reentrancy happens when a contract makes an external call (to a token, receiver hook, or another protocol) before it updates its own state. The old cure is still the best: the Checks–Effects–Interactions (CEI) pattern and, where applicable, a guard. The Solidity docs emphasize CEI, and OpenZeppelin provides ReentrancyGuard — but you must apply it consistently. :contentReference[oaicite:9]{index=9}
// Bad (effect after external call):
function withdraw(uint256 amt) external {
require(bal[msg.sender] >= amt, "insufficient");
(bool ok,) = msg.sender.call{value: amt}(""); // external call first
require(ok, "xfer failed");
bal[msg.sender] -= amt; // EFFECTS after INTERACTION - vulnerable
}
// Good (CEI) + guard:
function withdraw(uint256 amt) external nonReentrant { // OpenZeppelin ReentrancyGuard
require(bal[msg.sender] >= amt, "insufficient");
bal[msg.sender] -= amt; // EFFECTS first
(bool ok,) = msg.sender.call{value: amt}("");
require(ok, "xfer failed");
}
- Cross-function reentrancy: even if
withdrawis guarded, can an ERC-777/ERC-1155/Uniswap callback calldeposit()orborrow()mid-flight? Protect all state-mutating paths that assume invariants pre/post external call. - Guard scope: don’t sprinkle
nonReentrantrandomly. Diagram your flows; guard the “entry points” that lead into external calls.
- List every external call (token
transfer, low-levelcall, hooks) and what state it assumes before/after. - Apply CEI systematically — run a search for
.call,transfer, ERC-777 hooks (tokensReceived), and AMM callbacks. - Use
ReentrancyGuardand/or pull-payment patterns (withdrawby the recipient) for untrusted receivers. :contentReference[oaicite:10]{index=10}
Pattern #3 — Upgradeable Proxies: Initializers, auth, and storage collisions
Upgradeability lets you patch bugs, but it also introduces a new class of failure. Common issues: (1) uninitialized implementations that an attacker can initialize and seize, (2) unsafe upgrade functions that anyone can call, (3) storage layout collisions that corrupt state after an upgrade. OpenZeppelin’s Upgrades documentation covers UUPS/transparent patterns and safe initializers; EIP-1967 standardizes storage slots to avoid collisions. :contentReference[oaicite:11]{index=11}
// UUPS pattern (OpenZeppelin) - core points to verify
contract MyToken is Initializable, UUPSUpgradeable, ERC20Upgradeable {
function initialize(string memory n, string memory s) public initializer {
__ERC20_init(n, s);
__UUPSUpgradeable_init();
// set owner/roles here
}
function _authorizeUpgrade(address newImpl) internal override onlyOwner {}
}
// Review:
// - Implementation contract must be initialized exactly once (initializer modifier).
// - _authorizeUpgrade must be strict (onlyOwner/timelock).
// - Storage layout unchanged between versions, or carefully appended.
// - Proxy admin processes & timelocks documented and tested.
- Confirm implementation initialization risks: is the implementation contract left uninitialized on mainnet? Lock it or initialize with a burn admin.
- Verify upgrade auth:
_authorizeUpgradeuses a timelock/multisig; no public upgrade entry points. :contentReference[oaicite:12]{index=12} - Check storage layout: follow EIP-1967 slot rules; append state vars; never reorder. Run storage layout diff tools pre-deploy. :contentReference[oaicite:13]{index=13}
- Operational: document exact upgrade runbook (testnet simulation, pause, upgrade, post-checks, unpause).
Pattern #4 — Signature Replay: Domain separation, nonces, expiries
Users love gasless approvals and meta-tx. Attackers love signatures they can reuse elsewhere. The defense is mature:
use EIP-712 typed data with a specific domain (name, version, chainId, verifying contract), include per-owner nonces and deadlines, and stick to audited implementations (EIP-2612 permit or Uniswap’s Permit2). :contentReference[oaicite:14]{index=14}
// Sketch: EIP-2612-style permit essentials
// - DOMAIN_SEPARATOR includes chainId & verifying contract
// - mapping(address => uint256) nonces;
// - signature includes owner, spender, value, nonce, deadline
function permit(
address owner, address spender, uint256 value,
uint256 deadline, uint8 v, bytes32 r, bytes32 s
) external {
require(block.timestamp <= deadline, "expired");
bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(
_PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline
)));
address signer = ECDSA.recover(digest, v, r, s);
require(signer == owner, "bad sig");
_approve(owner, spender, value);
}
- Confirm DOMAIN_SEPARATOR binds
chainIdandverifying contract(EIP-712). :contentReference[oaicite:15]{index=15} - Ensure per-owner nonces and deadlines are enforced (EIP-2612). :contentReference[oaicite:16]{index=16}
- Use audited libraries (OpenZeppelin ECDSA, OZ ERC20Permit, or Permit2) and test cross-chain assumptions explicitly.
Pattern #5 — Access Control: Owners, roles, and timelocks
A surprising number of incidents reduce to “anyone could call the dangerous function,” or “the proxy admin was an EOA without a timelock,” or “we forgot to renounce a minting role.” The fix is straight-ahead: explicit roles (RBAC), timelocked governance, cap risk with rate limits, and a tested emergency pause.
// Sketch: role-based access with timelocked upgrades
// - Admin is a multisig
// - Timelock controls upgrade/parameter changes
// - Pause for critical paths
bytes32 public constant RISK_MANAGER = keccak256("RISK_MANAGER");
function setParam(uint256 x) external onlyRole(RISK_MANAGER) {
// will be called via timelock in production
param = x;
}
function pause() external onlyRole(DEFAULT_ADMIN_ROLE) { _pause(); }
function unpause() external onlyRole(DEFAULT_ADMIN_ROLE) { _unpause(); }
- Inventory every
onlyOwner/onlyRolefunction and document who/what can call it in prod. - Put dangerous ops behind a timelock + multisig. Dry-run the time-delayed path on testnet.
- Confirm pause/unpause is wired for the right functions (and cannot be griefed by external reverts).
Pattern #6 — Economic & MEV: Sandwiches, auctions, griefing
Not every loss is a “bug” some are structural. If your protocol lets users post orders that can be sandwiched, or relies on a last-price auction without protection, you’ll get griefed. Uniswap’s oracle notes (and broad AMM research) highlight why cost of manipulation matters and how PoS-era assumptions differ from PoW. :contentReference[oaicite:17]{index=17}
- Sandwich prevention: enforce max slippage & minimum out, consider RFQ/commit-reveal for large trades.
- MEV-aware deployments: support private orderflow / trusted RPCs for retail; design keeper flows that don’t leak profitable info in mempools.
- Auctions: set anti-sniping windows, commit-reveal, or Dutch mechanisms to avoid last-block griefing.
// Example: enforce minOut to reduce sandwich risk on swaps
function swapExactIn(address tokenIn, address tokenOut, uint256 amtIn, uint256 minOut) external {
uint256 out = _quote(tokenIn, tokenOut, amtIn);
require(out >= minOut, "slippage");
_pullFromUser(tokenIn, amtIn);
_pushToUser(tokenOut, out);
}
Pattern #7 — Liquidations: Borrowed assumptions, bad thresholds
Liquidation code blends price feeds, health factors, and incentive math. A small arithmetic bug can print money for liquidators or trap undercollateralized positions. Cross-check all thresholds with a spreadsheet and simulation tests; use the same rounding rules everywhere. Sanity-check against external feeds and clamp extreme moves.
- Single source of truth for health factor math (library), unit-tested across ranges.
- Oracle freshness/variance checks; circuit breaker on extreme drift. :contentReference[oaicite:18]{index=18}
- Cap maximum liquidation size per tx; avoid total wipes based on stale data.
Pattern #8 — ERC-20 Allowances: Infinite approvals & “approve front-runs”
Infinite approvals simplify UX and concentrate risk. Users forget DApps they approved. Attackers love stale allowances. For protocols, prefer permit-style signed approvals with deadlines (EIP-2612/Permit2) and spend-per-tx flows. For UIs, surface allowance reviews and provide one-click revoke links. :contentReference[oaicite:19]{index=19}
Pattern #9 — Math & Rounding: Basis points, fee splits, and off-by-one
When fees or shares are split in different places, rounding choices diverge. This can create slow drains or liquidation edge-cases. Centralize math into a library and write invariants: “sum of shares == total supply,” “can’t mint dust that can’t be burned,” etc.
Pattern #10 — Emergency Response: Pause, partial pause, and withdrawal mode
Mature protocols design for failure. A good pause strategy limits damage without bricking safe user exits. Consider “withdrawal-only” mode, rate-limited redemptions, and explicit kill-switches for known-bad modules.
Your 2025 audit workflow (practical & repeatable)
1) Threat modeling and invariants
- Write down what must never change (e.g., “total assets ≥ claims + fees”). Turn those into invariant tests.
- List trust edges: prices, cross-chain messages, privileged actors, keeper networks.
2) Static review & patterns
- Search/grep for red-flag tokens:
.call,delegatecall,selfdestruct, ERC-777 hooks,_authorizeUpgrade,initialize. - Map every external call and verify CEI & guards. Use OpenZeppelin’s
ReentrancyGuardproperly. :contentReference[oaicite:20]{index=20} - Check proxy layout and initializers; follow UUPS/transparent guidance. :contentReference[oaicite:21]{index=21}
- Verify oracle sources and windows; prefer Chainlink/Pyth with staleness checks; use AMM TWAPs carefully. :contentReference[oaicite:22]{index=22}
3) Testing & fuzzing
- Unit tests for each path; fuzz tests for critical math; invariant tests for system-wide properties.
- Fork-tests against live pools/feeds; simulate MEV/sandwich by injecting adversarial mempool/ordering.
- Simulate upgrades on a fork: deploy new impl, run initializer, verify storage diffs & behavior.
4) Operational safety
- Timelocks + multisigs on upgrades and parameter changes.
- Deploy caps and rate limits (per-block mint/burn caps, per-epoch redemption caps).
- On-chain monitors: price deviation alerts, oracle staleness, anomalous slippage, large single-tx share of liquidity.
Mini Case Files (composite patterns)
Case A — AMM-based price + short window = manipulation risk
Protocols that used spot price or very short TWAPs from thin pools were repeatedly exploited. Hardening guidance from Uniswap’s oracle posts: require longer windows, ensure sufficient pool depth, and design systems so the cost of moving price outweighs potential profit. :contentReference[oaicite:23]{index=23}
Case B — Reentrancy via token callbacks
Even “safe” ERC-20 transfers can trigger external code if you accidentally integrate ERC-777 hooks or AMM callbacks in the wrong order.
CEI + nonReentrant across entry points is still your best defense. See OpenZeppelin ReentrancyGuard and Solidity CEI. :contentReference[oaicite:24]{index=24}
Case C — Uninitialized UUPS impl seized; unsafe upgrade
Leaving the implementation uninitialized lets an attacker call initialize and set themselves as owner,
then authorize malicious upgrades. Follow OZ’s UUPS guidance and initialize implementations (or protect with constructor-like locks). :contentReference[oaicite:25]{index=25}
Case D — Replay of “permit” across domains
Custom permit functions that skipped EIP-712 domain separation or per-owner nonces enabled replay on forks or L2s.
Use EIP-2612/EIP-712 patterns, or Permit2, and always bind chainId + verifying contract. :contentReference[oaicite:26]{index=26}
Clip-and-Save: Review Checklists
Oracle & Pricing
- Identify all price-dependent functions (mint/burn/liquidate/auction).
- Primary source is Chainlink/Pyth with staleness & deviation checks. :contentReference[oaicite:27]{index=27}
- If AMM TWAP used, ensure ≥ 30-min (context-dependent), sufficient depth, and compare to reference feed. :contentReference[oaicite:28]{index=28}
- Implement circuit breakers; test “halt and recover” runbooks.
Reentrancy & External Calls
- CEI everywhere. Guards on entry points that lead to external calls. :contentReference[oaicite:29]{index=29}
- Map callbacks (AMMs, ERC-777, ERC-1155) and ensure invariants survive reentry.
- Adopt pull-payments for untrusted receivers; avoid low-level
.callwhen possible.
Upgradeable Proxies
- Verify initializers are called exactly once and implementation isn’t left open. :contentReference[oaicite:30]{index=30}
- Restrict
_authorizeUpgradevia timelock/multisig; document upgrade runbook. - Storage layout diff run before upgrade; adhere to EIP-1967 slot rules. :contentReference[oaicite:31]{index=31}
Signatures & Approvals
- EIP-712 domain bound to chainId + verifying contract; per-owner nonces + deadlines. :contentReference[oaicite:32]{index=32}
- Prefer audited libs (OZ ERC20Permit, Permit2). Negative tests for replay on forks/L2s.
Access Control & Ops
- Timelock + multisig on parameter changes and upgrades.
- Pause modes pre-wired; withdrawal-only fallback tested.
- Caps/rate-limits in place; anomaly monitoring live.
Developer Toolbox (docs you’ll actually use)
- OpenZeppelin Contracts & Upgrades — ReentrancyGuard, ERC20Permit, UUPS/transparent proxy docs and patterns. :contentReference[oaicite:33]{index=33}
- Chainlink Data Feeds — Price feed docs, heartbeats, deviation thresholds, and best practices. :contentReference[oaicite:34]{index=34}
- Uniswap v3 Oracles — TWAP notes and manipulation cost discussions. :contentReference[oaicite:35]{index=35}
- EIPs 712/2612 — Typed data signing and token permits (via reputable summaries/implementations where canonical pages are blocked). :contentReference[oaicite:36]{index=36}
- EIP-1967 — Proxy storage slots (overview & community explanations). :contentReference[oaicite:37]{index=37}
- Aggregate incident trackers — Rekt News leaderboard (broad trends), recap posts from security firms and data providers for 2024–2025. :contentReference[oaicite:38]{index=38}
Appendix — Test Patterns You Can Reuse
Invariant: total assets conserve
// Foundry-style sketch:
// Invariants hold for arbitrary sequences of deposits/withdrawals/redeems.
function invariant_assetsConserve() public {
uint256 assets = vault.totalAssets();
uint256 sumClaims = token.totalSupply() * vault.pricePerShare() / 1e18;
assert(assets + feesReserve >= sumClaims); // tweak for fee model
}
Oracle staleness test
// Simulate stale price feed by advancing time and ensuring actions revert
warp(block.timestamp + MAX_STALE + 1);
vm.expectRevert("stale price");
system.mintWithCollateral(...);
Upgrade rehearsal
// Fork mainnet state, deploy new impl, run initializer, compare storage & behavior
address newImpl = address(new ImplV2());
vm.prank(timelock);
proxy.upgradeToAndCall(newImpl, abi.encodeWithSelector(ImplV2.initV2.selector));
assertEq(proxy.version(), "2.0.0");
// Run a set of golden-path tests to confirm no behavior regressions.
References & further reading
- Uniswap — TWAP Oracles in PoS (oracle manipulation cost & design notes). :contentReference[oaicite:39]{index=39}
- Chainlink — Data Feeds Docs (heartbeats, deviation thresholds, best practices). :contentReference[oaicite:40]{index=40}
- OpenZeppelin — ReentrancyGuard & UUPSUpgradeable. :contentReference[oaicite:41]{index=41}
- Ethereum Improvement Proposals — EIP-712 (typed data), EIP-2612 (permit). See widely-used summaries/implementations and Uniswap Permit2 documentation. :contentReference[oaicite:42]{index=42}
- EIP-1967 — Proxy storage slots (community explanation and slot layout guidance). :contentReference[oaicite:43]{index=43}
- Incident roundups — Rekt News leaderboards and 2025 exploits recaps; Chainalysis 2025 crypto crime posts; CertiK/PeckShield monthly roundups. :contentReference[oaicite:44]{index=44}
- Background research — Panoptic (oracle-free options on v3 pools) explains broader oracle trade-offs. :contentReference[oaicite:45]{index=45}
