Smart Contract Security
In web2, a bug means you push a hotfix. In web3, a bug means your $50M gets drained in a 15s mempool race. Security isn't a stage — it's the core of the job.
1. Reentrancy — the original sin
A contract calls out to an external address before updating its own state. The external contract (attacker) calls back in and drains.
// BAD
function withdraw() external {
uint256 bal = balances[msg.sender];
(bool ok,) = msg.sender.call{value: bal}(""); // ← hands control to attacker
require(ok);
balances[msg.sender] = 0; // ← too late
}
Fix — checks-effects-interactions:
function withdraw() external {
uint256 bal = balances[msg.sender];
balances[msg.sender] = 0; // effect first
(bool ok,) = msg.sender.call{value: bal}(""); // interaction last
require(ok);
}
Or use a ReentrancyGuard modifier (OpenZeppelin).
2. Integer overflow/underflow
Solidity ≥0.8 reverts on overflow by default. Pre-0.8 contracts used SafeMath. Still bite you in unchecked { } blocks and in assembly.
3. Access control
// BAD: anyone can withdraw
function rescue(uint256 amt) external { payable(msg.sender).transfer(amt); }
// GOOD
function rescue(uint256 amt) external onlyOwner { ... }
Every state-mutating function needs an explicit answer to "who can call this?" — default to locked-down.
4. tx.origin for auth — don't
// BAD
require(tx.origin == owner, "not owner");
If owner calls a malicious contract that calls your contract, tx.origin is still owner → attacker wins. Use msg.sender.
5. Front-running & MEV
The mempool is public. Anyone can see your pending tx and pay more gas to execute first.
- Sandwich attacks — bot buys before your swap (pumping price), sells after (dumping). You get worse slippage.
- Copy-trading — bot sees your "mint NFT" tx, submits identical tx with higher tip.
- Commit–reveal schemes, private mempools (Flashbots Protect), and slippage limits are the defenses.
6. Price-oracle manipulation
Flash-loan + manipulate a low-liquidity DEX price + call your lending contract that uses that DEX as an oracle. $100M gone in 1 tx. Don't use a single DEX spot price as an oracle. Use Chainlink or TWAPs (time-weighted average prices).
7. Delegatecall + storage collision
contract Proxy {
address implementation; // slot 0
fallback() external { implementation.delegatecall(msg.data); }
}
contract Logic {
uint256 count; // ← slot 0 collides with implementation!
function bump() external { count++; } // overwrites Proxy's impl → total hijack
}
Fix: use EIP-1967 — store implementation at keccak256("eip1967.proxy.implementation") - 1. OpenZeppelin's upgradeable contracts do this correctly.
8. Signature replay
Users sign a message; attacker replays it on another chain (same chainId not included) or after the nonce was reused. Always include: chainId, contract address, nonce, expiry in the signed payload. EIP-712 does this for you.
9. Denial of Service via unbounded loops
// Iterating a user-controlled array — attacker adds 10,000 entries
for (uint i = 0; i < holders.length; i++) pay(holders[i]);
Fix with a pull pattern: let each user claim themselves.
10. Other classics
- Unchecked low-level calls — always check the returned
bool. - Block.timestamp manipulation — validators can wiggle it ±15s.
- Insufficient deadline/slippage on swaps — user's tx sits in mempool for 3 hours and executes at a terrible price.
- Initializer not locked — someone front-runs your deploy and re-inits the contract as themselves.
11. Tooling
| Tool | What it finds |
|---|---|
| Slither (Trail of Bits) | Static analysis; top 40 patterns. Run in CI. |
| Mythril / MythX | Symbolic execution, deeper but slower |
Foundry fuzz / forge test --fuzz-runs 10000 | Property tests with random inputs |
| Echidna | Invariant fuzzing — "assert balance >= 0 always" |
| Certora | Formal verification (expensive, SaaS) |
12. A Slither run, in 30 seconds
pip install slither-analyzer
slither contracts/MyToken.sol
# or with hardhat
slither .
It will complain about a lot. Most findings are "informational" — read each one, decide, document, suppress with // slither-disable-next-line.
13. Secure-by-default habits
- Start from OpenZeppelin; don't reinvent.
- Use
SafeERC20for every token interaction. - Use checks-effects-interactions and
ReentrancyGuard. - Limit admin powers — timelock + multisig (Gnosis Safe).
- Emit events on every state change (indexable audit trail).
- Pause switch for emergencies (but don't make it a permanent kill switch).
- Ship with >90% branch coverage + fuzz + Slither clean.
14. Project
ethernaut.openzeppelin.com) and solve levels 1–10. Write up each exploit as a short postmortem. Then run Slither on your Phase 6 token and fix or justify every finding.Quiz
- Reentrancy
- Using a manipulable spot price on a low-liquidity pool as an oracle; attacker swung the price within one block
- Missing
onlyOwner - Slither wasn't run