Smart Contract Risks: Re-entrancy, Oracles, Access Control & More
Recognize top vulnerability classes and the standard defenses used in production.
1) Re-entrancy
What it is: Your contract calls an external address (EOA or contract). That callee calls back into you before you finish updating state, exploiting intermediate assumptions (e.g., balances not yet set to zero). This is not limited to ETH transfers; ERC-777 hooks, token callbacks, and onERC721Received
can also re-enter.
// ❌ Vulnerable (illustrative) function withdraw() external { uint256 bal = balances[msg.sender]; require(bal > 0, "Zero"); (bool ok,) = msg.sender.call{value: bal}(""); // external call first require(ok, "Fail"); balances[msg.sender] = 0; // state updated after → re-entrancy risk }
// ✅ Fix: CEI or ReentrancyGuard function withdraw() external nonReentrant { uint256 bal = balances[msg.sender]; require(bal > 0, "Zero"); balances[msg.sender] = 0; // Effects first (bool ok,) = msg.sender.call{value: bal}(""); // Interactions last require(ok, "Fail"); }
Cross-function re-entrancy. Guard the entire class of functions that mutate shared state, not just a single one. Example: re-entering from withdraw()
into transfer()
if they affect the same balances
mapping. Use a single nonReentrant
modifier across all relevant functions or refactor shared logic into internal functions called from one external entry point.
Read-only re-entrancy. Even “view” functions can be dangerous if they read state that another external call can influence mid-execution (e.g., via callbacks updating storage). Don’t assume view
= safe; keep pricing and accounting deterministic and insulated from external side effects.
- Pull over push payments: Avoid sending ETH/tokens unprompted. Let users call
withdraw()
to pull funds, reducing the surface where you call unknown addresses. - Token pitfalls: ERC-777 hooks and ERC-721/1155 receiver callbacks can re-enter. If you must support them, isolate state updates and finalize before transfers.
- Gas assumptions: Don’t rely on “send 2300 gas” patterns; they became unreliable. Explicitly structure CEI and use guards.
deposit/withdraw/transfer
. Add an invariant that total liabilities == assets at all times.2) Oracle Manipulation
What it is: Your contract reads a price or external value that can be moved by an attacker within a single block/tx (e.g., a thin AMM pool). If you use that spot price to set collateral, liquidations, or mint amounts, an attacker can swing the price, trigger favorable logic, then unwind.
- Don’t use spot AMM price mid-tx for core logic. Thin-liquidity pools are easy to push with flash liquidity.
- Use TWAP/medianization: Time-weighted averages (AMM cumulative prices) or median across multiple exchanges smooths manipulation.
- Externally secured feeds: Chainlink-style oracles provide off-chain aggregation and update thresholds. Add staleness checks (last updated < X) and deviation checks (abs(new-old)/old <= maxDelta).
- Sanity layers: For critical ops, require price to be within a band vs. a secondary feed or a conservative circuit breaker.
// Pseudocode: sanity checks around an oracle read Price p = oracle.latest(); require(block.timestamp - p.timestamp <= MAX_STALE, "stale"); require(abs(p.value - last.value) * 1e18 / last.value <= MAX_CHANGE, "jump");
Validium/L2 caveat: If your price comes from another domain (bridge message, L2 sequencer feed), consider the liveness and censorship model there. Document the trust assumption (“who can push prices?”) and cap the blast radius (lower LTVs, slower reaction functions) until decentralization improves.
3) Access Control
What it is: Who can do what and how those rights are changed. Most catastrophic incidents stem from misconfigured roles, upgrade keys, or forgotten backdoors.
- Never use
tx.origin
for auth. It breaks with proxies and can be phished by intermediate contracts. - Prefer
Ownable2Step
orAccessControl
: Two-step ownership transfer prevents accidental loss; role-based access makes privileges explicit. - Events & timelocks: Emit events for every privileged action. Route sensitive ops through a multisig + timelock; emergency pause may bypass timelock but should be narrow and auditable.
- Upgrade safety: For UUPS/transparent proxies, protect
upgradeTo
behind strict roles; add versioned initializers and storage gaps; rehearse upgrade runbooks. - Least privilege: Separate roles for pauser, rateSetter, upgrader, treasurer. The owner should not be able to drain funds without governance process.
// Example modifiers modifier onlyOwner() { require(msg.sender == owner, "!owner"); _; } modifier onlyRole(bytes32 r) { require(hasRole(r, msg.sender), "!role"); _; }
4) Math/Overflow & Underflow
Solidity ≥0.8 reverts on overflow/underflow by default, removing many classic bugs. But math still bites via rounding, scale mismatches, and unchecked blocks.
- Rounding awareness: Integer division floors. For pro-rata payouts, decide rounding direction (favor protocol or user?) and stick to it. For precise fractions, use
mulDiv
-style helpers. - Decimals & scales: Tokens have 6/8/18 decimals. Normalize before computing rates. Document units in comments (
1e18 = 1.0
), and addSafeCast
where narrowing casts occur. - No implicit divide-by-zero: Validate denominators and empty sets (e.g., when
totalShares == 0
). - Unchecked blocks: Use only after proving bounds. Guard with assertions and tests; keep scope minimal.
- Price math: For fixed-point math (e.g., Q64.96 / wad-ray), centralize helpers; don’t duplicate ad-hoc scaling across files.
// Example: rounding up division function divUp(uint256 a, uint256 b) internal pure returns (uint256) { require(b != 0, "DIV0"); return (a + b - 1) / b; }
5) MEV & Front-running
What it is: Miners/validators/searchers reorder/insert/withhold transactions to extract value (sandwiches, backruns, liquidations). Your contract should make harmful ordering less profitable and user actions explicitly bounded.
- Slippage & deadlines: Require users to specify minOut/maxIn and
deadline
. Reject stale txs. Fail closed on excessive slippage. - Commit-reveal for sensitive values: For sealed bids, whitelists, or mints, commit a hash first; reveal later to prevent copycats.
- Private tx submission: For high-value ops (liquidations, arb), let users opt into private relays. Contracts can’t enforce this, but your UI/docs should recommend it.
- Deterministic accounting: Avoid state that depends on
tx.gasprice
,block.timestamp
alone, or other easily gamed values. Add minimums/maximums around oracle updates and fee paths. - Approvals & permits: Don’t require unlimited allowances. If supporting EIP-2612 permit, verify domain separators/nonces and show human-readable prompts in the UI.
// Pattern: user-bounded swap function swap(address tokenIn, address tokenOut, uint256 amountIn, uint256 minOut, uint256 deadline) external { require(block.timestamp <= deadline, "expired"); // compute amountOut... require(amountOut >= minOut, "slippage"); // transfer & settle... }
6) Defense Checklist
Use this as a pre-mainnet gate. If any item is red, slow down.
- Re-entrancy hardening: CEI everywhere,
nonReentrant
on external mutators, pull-payment pattern; dedicated tests with malicious callbacks. - Oracle robustness: Use TWAP/median or trusted feeds; implement stale/deviation checks; document trust assumptions; cap LTVs until feeds are mature.
- Access control: Least privilege roles; two-step ownership; multisig + timelock for upgrades; privileged events emitted; emergency pause scoped.
- Math sanity: Units documented; SafeCast for narrowing; rounding chosen and tested; no divide-by-zero; minimal
unchecked
. - Token safety: Use
SafeERC20
; handle non-standard ERC-20s; avoid approvingtype(uint256).max
by default. - Upgrades: Storage layout snapshotted; initializer versioning; upgrade tests proving invariants; runbook rehearsed.
- Testing depth: Unit + integration + fork + fuzz + invariants; negative tests for failure paths; gas/coverage tracked in CI.
- Docs & disclosure: Public risk docs; clear admin powers; bug bounty live; audit reports linked and issues addressed.
Quick check
- What does CEI stand for and why?
- Why is
tx.origin
unsafe for auth? - Name two ways to harden price oracles.
Show answers
- Checks-Effects-Interactions; prevents re-entrancy/inconsistent state by updating internal state before external calls.
- It can be phished via intermediate contracts and breaks with proxies; use
msg.sender
+ roles instead. - Use Chainlink/secured feeds; use AMM TWAP/medianization with staleness and deviation checks.
Go deeper
Now, let’s build confidence with testing & auditing workflow.