Smart Contract Risks: Re-entrancy, Oracles, Upgrades

Smart Contract Risks: Re-entrancy, Oracles, Upgrades

Intermediate
Security
• ~12 min read
• Updated: 08/08/2025

Most DeFi incidents trace back to a small set of mistakes. If you can recognize these patterns in code reviews and audits, you eliminate a huge fraction of tail risk before users ever interact with your app. This guide expands each risk class with concrete examples, subtle variants that trip up experienced teams, and pragmatic defenses. Treat it as a pre-launch playbook: design with least privilege, measure what matters, and practice your incident response before mainnet value arrives.


1) Re-entrancy

Re-entrancy happens when your contract makes an external call (to an EOA or another contract) before finishing its own state updates. The callee can run code that calls you back (“re-enters”) and observe or modify state you haven’t finalized yet. The classic shape is a withdraw function that sends funds first and decreases balances after the DAO (2016) pattern. but modern exploits use subtler paths:

  • Cross-function re-entrancy: An attacker re-enters into a different function that assumes invariants (e.g., accounting) already hold.
  • Read-only re-entrancy: A view function reads state during re-entry and emits or returns information later trusted by off-chain indexers or oracles (TWAP manipulation / accounting desync).
  • ERC-777/ERC-1155 hooks: Token standards with callbacks (tokensReceived, onERC1155Received) let the recipient run arbitrary code mid-transfer. If your transfer/mint/burn triggers a callback before internal accounting settles, risk rises.
  • receive()/fallback() traps: Sending ETH to a contract calls its receive or fallback; if your logic is not CEI-safe, this is an entry point.
// ❌ vulnerable
function withdraw(uint amount) external {
    require(bal[msg.sender] >= amount);
    (bool ok,) = msg.sender.call{value: amount}(""); // external call first
    require(ok);
    bal[msg.sender] -= amount;                       // state update last
}

// ✅ CEI + guard + pull pattern
bool locked;
modifier noReenter(){ require(!locked,"reenter"); locked=true; _; locked=false; }

function withdraw(uint amount) external noReenter {
    require(bal[msg.sender] >= amount);
    bal[msg.sender] -= amount;                       // effects first
    _safeTransferETH(msg.sender, amount);            // interactions last
}

// Even safer: accrual into a claimable balance & user pulls later.
Defenses: (1) Use Checks-Effects-Interactions consistently. (2) Add ReentrancyGuard for write functions with external calls. (3) Prefer pull payments and claim flows over pushing funds. (4) Be cautious with ERC-777/1155 hooks; consider using “non-receivable” wrappers or re-entrancy-safe sequences. (5) For view logic that influences prices/limits, design it to be re-entrancy agnostic or snapshot state before external calls.

2) Oracle & Price-Manipulation Risk

If liquidations, mints, or redemptions rely on a price an attacker can swing within one transaction, you’ve handed them an ATM. Thin AMM pools, self-quoted oracles, or stale data are the common culprits. Flash loans make manipulation cheap and instantaneous.

// ❌ single-pool spot price (manipulable)
uint price = tokenA.balanceOf(pair) * 1e18 / tokenB.balanceOf(pair);

// ✅ robust sources + sanity + staleness
(int256 ans,, uint256 startedAt, uint256 updatedAt,) = feed.latestRoundData();
require(ans > 0, "bad");
require(block.timestamp - updatedAt < MAX_AGE, "stale");
require(ans <= MAX_REASONABLE, "deviation");
  • Prefer off-chain aggregated oracles (e.g., Chainlink) for blue-chip pairs; verify freshness (heartbeat) and bound deviations.
  • If you must use DEX data, use time-weighted averages (TWAP) across sufficiently liquid pools; anchor to liquidity thresholds; ignore single-block spikes.
  • Cross-checks: Compare two independent feeds; pause or circuit-break on large disagreement.
  • Design liquidation math with buffers (e.g., liquidation thresholds < LTV) to tolerate feed jitter; avoid mid-tx spot queries for safety-critical logic.

3) Upgradeable Proxies & Initialization

Proxy patterns (Transparent, UUPS, Beacon) separate storage (proxy) from logic (implementation). This unlocks iteration but adds new footguns:

  • Uninitialized implementation: If the logic contract is left initializable, anyone can seize “owner” on the implementation and potentially grief or confuse tooling.
  • Storage layout drift: Changing variable order/types breaks storage; you’ll corrupt balances/roles. Follow upgradeable patterns with reserved gaps.
  • Auth on upgrade: Missing onlyOwner/onlyProxy equiv lets anyone replace logic (UUPS), or lets owners upgrade from the wrong context.
  • Delegatecall hazards: Avoid delegatecalling into untrusted addresses; an attacker can write into your storage slots.
// ❌ forgot to lock implementation
contract Impl is Initializable, UUPSUpgradeable, OwnableUpgradeable {
    function initialize(address _o) public initializer { __Ownable_init(_o); }
    // ...
}
// ✅ lock impl & authorize upgrades
contract Impl is Initializable, UUPSUpgradeable, OwnableUpgradeable {
    constructor(){ _disableInitializers(); }               // <— lock
    function initialize(address _o) public initializer { __Ownable_init(_o); }
    function _authorizeUpgrade(address) internal override onlyOwner {}
    uint256[50] private __gap;                             // storage gap
}
Governance hardening: Put upgrade/admin behind a multisig; add a timelock so the community can review proposed upgrades; emit rich events; publish diffs & rationale; document a rollback plan.

4) Auth & Access Control (Ownership, Roles)

Many “exploits” are configuration errors: anyone can mint, set fees, upgrade, pause, or drain because an onlyOwner was missing—or an initializer ran twice after a proxy deploy.

// ✅ explicit roles over a grab-bag owner
bytes32 constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 constant PAUSER_ROLE = keccak256("PAUSER_ROLE");

function mint(address to, uint a) external onlyRole(MINTER_ROLE) { _mint(to, a); }
function pause() external onlyRole(PAUSER_ROLE) { _pause(); }
  • Never use tx.origin for auth. It’s phishable via intermediate contracts.
  • Role separation: Distinct keys for minting, pausing, upgrading, and treasury ops. Limit EOAs; prefer hardware-backed multisigs.
  • Permit & signatures: Validate nonces and expiries; guard against replay across chains; prefer EIP-712 typed data; beware malleability in custom schemes.
  • Initializer safety: Use initializer/onlyInitializing modifiers; ensure initializers cannot be re-entered via delegates.

5) Math Bugs & Rounding

Solidity ≥0.8 reverts on overflow/underflow, but precision loss, integer division, and ordering cause silent value leakage. Share systems, fee accrual, and AMM math are common failure points.

// ✅ scale first, document rounding
uint fee = amount * feeBps / 10_000; // truncates toward zero
require(fee <= amount, "fee>amount");

// Fixed-point helpers (wad = 1e18)
function wmul(uint x, uint y) internal pure returns (uint) {
    return (x * y + 5e17) / 1e18; // round half-up
}
  • Define rounding direction in docs (“fees round down to protocol”).
  • Use fixed-point libraries for WAD/RAY math; avoid hand-rolled scaling everywhere.
  • Handle fee-on-transfer tokens: Don’t assume amountIn == amountReceived. Measure before/after balances around transfers.
  • Fuzz invariants: “sum of shares ≤ total underlying”, “no free mint/burn”, “k increases on fees.”

6) MEV / Front-running & Sandwiching

Public mempools allow third parties to reorder, insert, or censor transactions for profit. Swaps, auctions, liquidations, and oracle updates are prime targets.

  • For protocols: Use batch auctions or commit-reveal for price discovery; validate minOut/maxIn parameters; consider uniform clearing price mechanisms.
  • For users & UIs: Offer private order flow (RPCs with MEV protection), set sane default slippage, and surface simulations so users see final states before signing.
  • For liquidations: Prefer Dutch or sealed-bid auctions to reduce toxic priority gas bidding that leaks value from protocol to searchers.

7) DoS Patterns & Griefing

Not every failure is a theft; some attackers just make your app unusable or unprofitable.

  • Unbounded iteration: Loops over user arrays or on-chain registries can revert once gas costs exceed block limits. Structure storage as paged or let users iterate their own records.
  • Griefable external calls: If a single failing external call reverts the whole operation (e.g., distributing to N recipients), one malicious recipient can block progress. Use pull payments or per-recipient try/catch with accounting.
  • Timestamp/Block-number reliance: Miners/validators can nudge timestamps. For vesting/auctions, allow small drift; prefer block numbers on networks where block time is stable and the number cannot be manipulated significantly.
  • Single-point dependencies: If your core logic depends on one third-party contract, add a fallback or circuit breaker mode that freezes risky features but allows withdrawals.

8) Pre-deploy checklist

  • Threat model written: assumptions (oracles, bridged assets, L2 finality), roles, trust boundaries.
  • Re-entrancy reviewed; CEI everywhere; ReentrancyGuard on write paths with external calls; ERC-777/1155 hooks considered.
  • Oracle design: aggregated feeds or TWAP; freshness & deviation bounds; circuit breaker on large moves; no spot-mid-tx for safety-critical paths.
  • Upgrades & admins: implementation locked (_disableInitializers()); storage gaps kept; upgrades authorized; multisig + timelock; events emitted.
  • Access control: explicit roles; least privilege; emergency pause defined (and scoped); no tx.origin anywhere.
  • Math & accounting: fixed-point helpers; rounding documented; fee-on-transfer handled; share invariants fuzzed.
  • DoS & failure modes: no unbounded loops; pull payments for distributions; fallback paths for dependencies.
  • Testing: unit + integration + fuzz + invariant tests; mainnet-fork sims for oracle feeds, MEV scenarios, and liquidation flows.
  • Security process: static analysis (Slither), linters, coverage reports; external review/audit or contest; signed artifacts & reproducible builds.
  • Runbooks: incident response, pause/unpause protocol, upgrade rollback, comms plan; dry-run these before TVL.

9) Further resources

← Staking & Restaking: Risks and Rewards
On-chain Privacy: Mixers, Stealth Addresses, and Compliance →