Smart Contract Risks: Re-entrancy, Oracles, Upgrades
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 bareonlyOwner
. - 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
- Cyfrin Updraft top-tier smart contract & security curriculum (deep dives + labs).
- OpenZeppelin Contracts/Upgrades Docs secure patterns & libraries.
- Trail of Bits Blog real-world postmortems & research.
- Damn Vulnerable DeFi hands-on security puzzles.
- Chainlink Education oracle design patterns & pitfalls.
- Ethereum.org Security general best practices.
On-chain Privacy: Mixers, Stealth Addresses, and Compliance →