Web3 & Smart Contract Security
Smart contracts are immutable programs that control billions of dollars in digital assets. Unlike traditional software where you can patch a vulnerability after deployment, a vulnerable smart contract is permanently exploitable unless explicitly designed with upgrade mechanisms. The stakes are absolute: a single bug can drain an entire protocol's treasury in a single transaction.
Since 2016, over $10 billion has been lost to smart contract exploits, bridge hacks, and DeFi protocol attacks. This page covers the vulnerability classes, auditing methodology, real-world case studies, and tools used by smart contract security researchers and auditors.
Related: Cybersecurity Overview | Web App Pentesting | Practical Cryptography | Bug Bounty
Immutable Code, Permanent Risk
Smart contracts cannot be patched after deployment (unless they use a proxy pattern). A vulnerability discovered post-deployment may result in total loss of funds. Security audits before deployment are not optional — they are existential.
Smart Contract Attack Surface
Solidity Vulnerabilities
Reentrancy
The most famous smart contract vulnerability. An external call to an untrusted contract allows that contract to call back into the vulnerable function before the first execution completes.
// VULNERABLE — state updated after external call
contract VulnerableVault {
mapping(address => uint256) public balances;
function withdraw() external {
uint256 amount = balances[msg.sender];
require(amount > 0, "No balance");
// External call BEFORE state update — reentrancy vulnerability
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
// State updated AFTER external call — attacker re-enters before this line
balances[msg.sender] = 0;
}
}
// ATTACKER contract
contract Attacker {
VulnerableVault public vault;
constructor(address _vault) {
vault = VulnerableVault(_vault);
}
// Fallback function — called when ETH is received
receive() external payable {
if (address(vault).balance >= 1 ether) {
vault.withdraw(); // Re-enter withdraw() before balance is zeroed
}
}
function attack() external payable {
vault.deposit{value: 1 ether}();
vault.withdraw(); // First call — triggers reentrancy loop
}
}// SECURE — Checks-Effects-Interactions pattern
contract SecureVault {
mapping(address => uint256) public balances;
function withdraw() external {
uint256 amount = balances[msg.sender];
require(amount > 0, "No balance");
// Effect: update state BEFORE external call
balances[msg.sender] = 0;
// Interaction: external call AFTER state update
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
}
// Even more secure — use ReentrancyGuard from OpenZeppelin
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract SecureVaultV2 is ReentrancyGuard {
mapping(address => uint256) public balances;
function withdraw() external nonReentrant {
uint256 amount = balances[msg.sender];
require(amount > 0, "No balance");
balances[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
}Checks-Effects-Interactions Pattern
The fundamental defense against reentrancy:
- Checks — Validate all conditions (require statements)
- Effects — Update all state variables
- Interactions — Make external calls last
Integer Overflow / Underflow
Before Solidity 0.8, arithmetic operations silently wrapped around on overflow/underflow. Since 0.8, overflow reverts by default.
// Pre-Solidity 0.8 — VULNERABLE
contract VulnerableToken {
mapping(address => uint256) public balances;
function transfer(address to, uint256 amount) external {
// If balances[msg.sender] = 0 and amount = 1:
// 0 - 1 = 2^256 - 1 (max uint256) — underflow!
balances[msg.sender] -= amount;
balances[to] += amount;
}
}
// Post-Solidity 0.8 — safe by default (reverts on overflow)
// But 'unchecked' blocks bypass this protection
contract ModernToken {
mapping(address => uint256) public balances;
function unsafeTransfer(address to, uint256 amount) external {
unchecked {
// DANGEROUS — overflow/underflow not checked
balances[msg.sender] -= amount;
balances[to] += amount;
}
}
}Access Control Flaws
Missing or incorrect access control is the simplest but most common vulnerability class.
// VULNERABLE — anyone can call administrative functions
contract VulnerableAdmin {
address public owner;
bool public paused;
// Missing access control — anyone can change owner
function setOwner(address newOwner) external {
owner = newOwner;
}
// Missing access control — anyone can drain funds
function withdrawAll() external {
payable(msg.sender).transfer(address(this).balance);
}
}
// SECURE — proper access control
contract SecureAdmin {
address public owner;
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
function setOwner(address newOwner) external onlyOwner {
require(newOwner != address(0), "Zero address");
owner = newOwner;
}
function withdrawAll() external onlyOwner {
payable(owner).transfer(address(this).balance);
}
}Front-Running / MEV
Transactions in the mempool are visible before execution. Miners/validators and MEV bots can reorder, insert, or censor transactions for profit.
| MEV Type | Description | Profit Source |
|---|---|---|
| Sandwich Attack | Front-run + back-run a swap | User pays higher price |
| Arbitrage | Cross-DEX price differences | Market inefficiency |
| Liquidation | Front-run liquidation calls | Liquidation bonus |
| JIT Liquidity | Add liquidity just-in-time for a large swap | Swap fees |
| Time-bandit | Reorg blocks to steal MEV | Historical transactions |
DeFi Exploit Case Studies
Major Exploits Timeline
| Year | Exploit | Loss | Vulnerability | ATT&CK |
|---|---|---|---|---|
| 2016 | The DAO | $60M | Reentrancy | Smart contract flaw |
| 2020 | bZx | $8M | Flash loan + oracle manipulation | Price oracle |
| 2021 | Poly Network | $611M | Access control (recovered) | Cross-chain |
| 2022 | Wormhole Bridge | $326M | Signature verification bypass | Bridge |
| 2022 | Ronin Bridge | $625M | Private key compromise (5/9 validators) | Key management |
| 2022 | Nomad Bridge | $190M | Merkle root initialization flaw | Bridge |
| 2023 | Euler Finance | $197M | Donation attack + liquidation logic | Flash loan |
| 2023 | Multichain | $126M | Centralized key management | Infrastructure |
Case Study: The DAO (2016)
The attack that led to the Ethereum/Ethereum Classic hard fork.
Case Study: Wormhole Bridge (2022)
// Simplified vulnerability — signature verification bypass
// The guardian set verification could be bypassed because
// the 'verify_signatures' instruction accepted a fabricated
// SignatureSet account
// Attacker created a fake SignatureSet with valid-looking data
// bypassing the verification that messages came from guardians
// Impact: Attacker minted 120,000 wETH ($326M) on Solana
// without depositing any ETH on EthereumOracle Manipulation
Price oracles feed external data (prices, randomness) to smart contracts. Manipulating an oracle lets attackers trick protocols into using incorrect prices.
| Oracle Type | Risk Level | Example |
|---|---|---|
| Spot price (single DEX) | Critical | Uniswap getReserves() — trivially manipulable |
| TWAP (Time-Weighted Average) | Medium | Uniswap V3 TWAP — harder but not impossible |
| Chainlink feeds | Low | Decentralized oracle network — multiple data sources |
| Band Protocol | Low | Cross-chain oracle with economic incentives |
Never Use Spot Prices as Oracles
Using getReserves() or similar single-block price sources is the most common oracle vulnerability. Always use time-weighted averages (TWAP) or decentralized oracles like Chainlink.
Bridge Security
Cross-chain bridges are the highest-value targets in Web3. They hold locked assets on one chain while minting wrapped assets on another.
| Bridge Architecture | How It Works | Risk |
|---|---|---|
| Lock-and-Mint | Lock on Chain A, mint on Chain B | If mint verification fails, unlimited minting |
| Burn-and-Mint | Burn on Chain A, mint on Chain B | If burn is faked, unlimited minting |
| Liquidity Pool | Swap between pools on each chain | If pool is drained, insolvency |
| Validator Set | N-of-M validators attest to transactions | If M/2+1 validators compromised, game over |
Smart Contract Auditing Methodology
Audit Process
Audit Checklist
| Category | Check | Severity if Missing |
|---|---|---|
| Reentrancy | All external calls follow CEI pattern | Critical |
| Access Control | All admin functions have proper modifiers | Critical |
| Input Validation | All inputs validated (zero address, bounds) | High |
| Oracle Security | Price feeds use TWAP or Chainlink, not spot | Critical |
| Flash Loan | Protocol logic safe against atomic composability | Critical |
| Integer Safety | Solidity 0.8+ or SafeMath used | High |
| Front-Running | Commit-reveal or private mempools for sensitive ops | Medium |
| Centralization | Multi-sig for admin, timelock for upgrades | High |
| Upgrade Safety | Storage layout preserved across upgrades | Critical |
| Gas Griefing | No unbounded loops, no external call in loops | Medium |
Security Tools
Static Analysis
# Slither — static analysis framework by Trail of Bits
pip install slither-analyzer
slither contracts/ --print human-summary
slither contracts/ --detect reentrancy-eth,reentrancy-no-eth
slither contracts/ --print contract-summary
# Mythril — symbolic execution
pip install mythril
myth analyze contracts/Vault.sol --solv 0.8.20
myth analyze contracts/Vault.sol --execution-timeout 300
# Aderyn — Rust-based Solidity analyzer
aderyn contracts/Fuzzing
# Foundry fuzzing — property-based testing
# Write invariant tests in Solidity
# Example fuzz test (Foundry)
# forge test --match-test testFuzz// Foundry fuzz test example
contract VaultFuzzTest is Test {
Vault vault;
function setUp() public {
vault = new Vault();
}
// Foundry automatically fuzzes the 'amount' parameter
function testFuzz_DepositWithdraw(uint256 amount) public {
vm.assume(amount > 0 && amount < 100 ether);
vault.deposit{value: amount}();
assertEq(vault.balances(address(this)), amount);
vault.withdraw();
assertEq(vault.balances(address(this)), 0);
}
// Invariant test — total balance should always match contract ETH
function invariant_solvency() public {
assertEq(
address(vault).balance,
vault.totalDeposits()
);
}
}# Run Foundry tests
forge test -vvv
forge test --match-test testFuzz -vvv
# Run invariant tests
forge test --match-test invariant -vvv
# Echidna — Haskell-based smart contract fuzzer
echidna contracts/Vault.sol --contract VaultEchidnaTest --config echidna.yamlFormal Verification
| Tool | Approach | Best For |
|---|---|---|
| Certora Prover | SMT-based formal verification | DeFi protocols, critical invariants |
| Halmos | Symbolic testing for Foundry | Property verification |
| KEVM | K Framework semantics for EVM | Low-level bytecode verification |
Web3 Security Best Practices
| Practice | Description |
|---|---|
| Use OpenZeppelin | Battle-tested, audited contracts for common patterns |
| Multiple audits | No single auditor catches everything; get 2-3 audits |
| Bug bounty program | Immunefi, HackerOne — let whitehats find bugs post-deploy |
| Timelock on upgrades | 48-72h delay on admin actions gives users time to exit |
| Multi-sig admin | Never a single EOA as admin; use Gnosis Safe (3/5 minimum) |
| Monitoring | Forta, OpenZeppelin Defender for real-time alerting |
| Circuit breakers | Pause functionality for emergency response |
| Gradual rollout | Cap TVL initially, increase limits over time |
Web3 Bug Bounty Platforms
| Platform | Focus | Max Bounty | Programs |
|---|---|---|---|
| Immunefi | DeFi, smart contracts | $10M+ (some protocols) | 300+ |
| HackerOne | General + Web3 | Varies | Growing |
| Code4rena | Competitive audits | $100K+ per contest | Regular contests |
| Sherlock | Audit contests + coverage | $50K+ per contest | Growing |
Getting Into Web3 Security
- Learn Solidity fundamentals (CryptoZombies, Solidity by Example)
- Study past exploits (Rekt.news, DeFi Hack Labs)
- Practice: Ethernaut, Damn Vulnerable DeFi, Capture the Ether
- Use Foundry for testing and exploitation
- Start with Code4rena or Sherlock contests
Further Reading
- Web App Pentesting — Traditional web security overlaps
- Practical Cryptography — Cryptographic primitives underlying blockchain
- Bug Bounty Hunting — Bug bounty methodology applicable to Web3
- API Security Testing — RPC and API security for dApps
- Security Certifications — Emerging blockchain security certifications
Key Takeaway
- Smart contracts are immutable — a vulnerability discovered post-deployment is permanently exploitable unless an upgrade mechanism was built in
- Reentrancy remains the most devastating vulnerability: always follow the Checks-Effects-Interactions pattern and use OpenZeppelin's ReentrancyGuard
- Flash loans enable atomic composability attacks — any protocol using spot prices as oracles is vulnerable to price manipulation in a single transaction
Hands-On Lab
Lab: Exploit Vulnerable Smart Contracts
- Install Foundry (
curl -L https://foundry.paradigm.xyz | bash && foundryup) - Complete the first 5 levels of Ethernaut (OpenZeppelin's Solidity CTF)
- Set up a local Ethereum environment with Anvil (
anvil) - Write a reentrancy exploit: deploy the VulnerableVault contract from this page, then deploy the Attacker contract and drain it
- Fix the vulnerability using the Checks-Effects-Interactions pattern
- Run Slither static analysis on both the vulnerable and fixed versions — verify Slither catches the reentrancy
- Write a Foundry fuzz test that verifies the solvency invariant (contract balance always equals total deposits)
CTF Challenge
Challenge: The Broken Vault
A DeFi vault contract allows users to deposit and withdraw ETH. The contract has a withdraw() function that sends ETH before updating the user's balance. There is also a flashLoan() function that lends ETH without collateral. The vault holds 100 ETH. Drain it.
Hints:
- The
withdraw()function is vulnerable to reentrancy - You need to deposit some ETH first to have a non-zero balance
- Your attacker contract's
receive()function should re-enterwithdraw()
Answer
Deploy an attacker contract that deposits 1 ETH, then calls withdraw(). In the attacker's receive() function, check if the vault balance is greater than 1 ETH and re-enter withdraw(). The loop drains 100 ETH because the balance is only set to zero after the external call. Fix: move balances[msg.sender] = 0 before the external call. Flag: CTF{checks_effects_interactions_saves_billions}.
:::
Common Misconceptions
- "Solidity 0.8 prevents all math bugs" — Solidity 0.8 adds overflow checks by default, but
uncheckedblocks bypass them. Rounding errors, precision loss, and logic errors in calculations remain common. - "An audit means the contract is safe" — No single audit catches everything. Major exploits (Wormhole, Euler) occurred in audited contracts. Multiple independent audits, formal verification, and bug bounties provide layered assurance.
- "Decentralized means no single point of failure" — Many DeFi protocols have admin keys, upgradeability, or oracle dependencies that are centralized. A compromised admin key is game over.
- "Using OpenZeppelin means your contract is secure" — OpenZeppelin provides secure building blocks, but your custom logic that connects them can still be vulnerable. The most exploited code is almost always the custom parts.
Quiz
1. What is the Checks-Effects-Interactions pattern?
a) A design pattern for UI components b) Validate conditions first, update state second, make external calls last c) A method of testing smart contracts d) An encryption algorithm
Answer
b) Checks-Effects-Interactions ensures all state changes (Effects) happen before any external calls (Interactions), preventing reentrancy attacks where external calls re-enter the function before state is updated.
2. Why are flash loans dangerous for DeFi protocols?
a) They charge high interest rates b) They allow atomic manipulation of prices and state within a single transaction c) They require collateral d) They are illegal
Answer
b) Flash loans provide uncollateralized capital that must be repaid within the same transaction. This enables attackers to manipulate oracle prices, exploit arbitrage, and drain protocols — all atomically with zero risk.
3. What makes bridge protocols the highest-value targets in Web3?
a) They are the oldest protocols b) They hold locked assets from multiple chains, making them high-value honeypots c) They use the simplest code d) They have no security audits
Answer
b) Bridges lock assets on one chain and mint wrapped assets on another. They aggregate value from multiple chains, making them the most lucrative targets — several bridges have lost hundreds of millions.
4. What tool performs static analysis on Solidity contracts?
a) Nmap b) Slither c) Burp Suite d) Wireshark
Answer
b) Slither (by Trail of Bits) is the primary static analysis tool for Solidity. It detects reentrancy, access control issues, and other vulnerability patterns without executing the code.
5. Why should you never use a single DEX spot price as a price oracle?
a) DEX prices are always wrong b) Spot prices can be manipulated within a single transaction using flash loans c) Spot prices are too slow d) DEX prices require API keys
Answer
b) A flash loan can temporarily manipulate a DEX's spot price by executing a large swap, interacting with the victim protocol at the manipulated price, then reversing the swap — all in one transaction.
:::
One-Liner Summary: In Web3, code is law and bugs are permanent — a single smart contract vulnerability can drain billions in a single transaction.