Smart Contract Risks Re entrancy, oracle-manipulation

Smart Contract Risks: Re-entrancy, Oracles, Access Control & More

Recognize top vulnerability classes and the standard defenses used in production.

TL;DR: Most exploits are preventable. Use CEI, ReentrancyGuard, strict access control, safe math by default (>=0.8), and robust oracle design. Test, fuzz, and audit before mainnet.

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.
Testing tips: Write a malicious test contract that re-enters. Fuzz sequences of 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 or AccessControl: 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"); _; }
Design note: Centralization isn’t automatically “bad,” but it must be transparent, delay-gated, and narrow. Publish a roles matrix and response plan.

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 add SafeCast 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...
}
User hygiene reminder: Prefer bookmarks, review approvals regularly, and verify addresses on a hardware wallet screen before signing.

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 approving type(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

  1. What does CEI stand for and why?
  2. Why is tx.origin unsafe for auth?
  3. 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.

Next: Auditing & Testing →