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 hacks come from a handful of recurring patterns. If you can spot these early,
you’ll avoid shipping dangerous code and you’ll be able to read audits with real understanding.
This guide summarizes the big ones, how they happen, and how to mitigate them.


1) Re-entrancy

A contract makes an external call before it finishes updating its own state. The callee
re-enters the function and drains funds. Classic: DAO (2016).

// ❌ 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
}

// ✅ fix: checks-effects-interactions + reentrancy guard
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
    (bool ok,) = payable(msg.sender).call{value: amount}("");
    require(ok);
}
  • Follow Checks-Effects-Interactions.
  • Use ReentrancyGuard (OpenZeppelin) when needed.
  • Minimize arbitrary external calls; prefer pull patterns.

2) Oracle & Price-Manipulation Risk

If your protocol trusts a manipulable price (e.g., a thin AMM pair), attackers can move price within
one tx (often via a flash loan) and steal collateral or mint under-collateralized debt.

// ❌ vulnerable: spot price from single AMM
uint price = tokenA.balanceOf(pair) * 1e18 / tokenB.balanceOf(pair);

// ✅ prefer: Chainlink or TWAP with liquidity / deviation checks
int256 price = chainlink.latestAnswer();
require(price > 0 && price < MAX_REASONABLE);
  • Use robust oracles (e.g., Chainlink) or time-weighted averages (TWAP) with liquidity checks.
  • Bound prices, add sanity checks, and consider circuit breakers on extreme moves.

3) Upgradeable Proxies & Initialization

Proxy patterns (UUPS/Transparent/Beacon) are powerful but easy to misconfigure:
uninitialized implementations, delegatecall to self, storage collisions, or upgrade role hijack.

// ❌ forgot to initialize (implementation left open)
contract Impl is Initializable {
    address public owner;
    function initialize(address _o) public initializer { owner = _o; }
}
// anyone can call initialize() on the implementation and take ownership

// ✅ lock implementation & restrict upgrades
constructor() { _disableInitializers(); } // OZ helper
function _authorizeUpgrade(address) internal override onlyOwner {}
  • Call _disableInitializers() in implementation constructors (OZ).
  • Protect upgradeTo with strict auth & timelocks; emit events.
  • Use consistent storage layout; follow OZ upgradeable patterns.

4) Auth & Access Control

Many exploits are just missing or mis-scoped permissions: anyone can mint, pause, withdraw, or upgrade.

// ✅ use explicit roles
bytes32 constant MINTER_ROLE = keccak256("MINTER_ROLE");
function mint(address to, uint a) external onlyRole(MINTER_ROLE) { _mint(to, a); }
  • Prefer role-based access (OZ AccessControl) over bare onlyOwner.
  • Use multisig or a timelocked governor for powerful operations.
  • Beware of init functions that can be called twice.

5) Math Bugs & Rounding

Solidity ≥0.8 reverts on over/underflow, but precision loss and bad order of operations still bite.
Fee math and share accounting are common footguns.

// ✅ multiply before divide, check rounding
uint fee = amount * feeBps / 10_000; // beware truncation
require(fee <= amount);
  • Document rounding direction; use fixed-point helpers when needed.
  • Fuzz test invariants (sum of shares, no free-mint, etc.).

6) MEV / Front-running & Sandwiching

Public mempools let adversaries reorder your txs. Swaps and auctions are typical victims.

  • Use commit-reveal for auctions, or batch auctions (uniform clearing price).
  • For users: allow private order flow (e.g., RPCs that support MEV-protection) and slippage limits.
  • Design contracts assuming adversarial ordering.

7) DoS Patterns & Griefing

  • Unbounded loops over user arrays (tx reverts once gas too high).
  • External calls that can revert and block progress; use try/catch or pull patterns.
  • Dependencies on a single external contract with no fallback.

8) Pre-deploy checklist

  • ✅ Reentrancy reviewed; CEI pattern; guards where needed.
  • ✅ Oracle design: robust source (or TWAP), bounds, circuit breakers.
  • ✅ Upgrade path secured: initializers locked, roles/multisig, timelocks, events.
  • ✅ Access control explicit; privileged ops minimized; pausable only if necessary.
  • ✅ Math audited; invariants fuzzed; edge cases unit-tested.
  • ✅ No unbounded loops; external calls isolated; failure paths tested.
  • ✅ Run a contest or external audit before significant TVL.

9) Further resources

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