Defensive programming for smart contracts
Smart contract code is unforgiving: it runs in a public, adversarial environment, and once deployed
an exploit "unfolds in a single transaction," long before you can intervene. This skill is the
baseline mindset from Mastering Ethereum ch. 9, plus a checklist of the common vulnerability
classes. The other skills in this repo go deep on individual topics; start here.
The five priorities (in order)
- Minimalism / simplicity. More code means more bugs, not more value. Before writing a
component, ask whether it's needed; after, cut edge cases and nonessential features. Simpler
contracts are easier to reason about, test, and audit.
- Code reuse. Don't reinvent the wheel: reuse battle-tested libraries (OpenZeppelin, Solady).
Reused code beats freshly written code "no matter how confident you feel.".
- Code quality. Treat it like aerospace, not a web widget. Rigorous methodology; assume no
second chance to fix.
- Readability / auditability. Public bytecode; write for the auditor and other devs. Clear names, documented
invariants.
- Test coverage. "Never assume that input is well formed or properly bounded or that it has a
benign purpose." Test all arguments against expected ranges and formats.
The antipattern catalogue
| Class |
The bug |
Fix (see also) |
| Reentrancy |
External call re-enters before state settles |
Checks-effects-interactions;nonReentrant; pull payments → reentrancy-guards |
| DELEGATECALL |
Executing foreign code against your storage layout |
Don't delegatecall untrusted code;NoDelegateCall on singletons |
| Entropy illusion |
On-chain "randomness" (blockhash, timestamp) is validator-influenced |
Use a VRF/commit-reveal; never gamble on block values |
| Unchecked CALL returns |
Ignoring afalse/no-return from a low-level call |
Check return; useSafeERC20 → safe-erc20-transfers |
| Race / front-running |
Outcome depends on tx ordering in the mempool |
minAmountOut-style bounds, commit-reveal, oracle price → oracle-safety |
| Denial of service |
One reverting/unbounded element blocks everyone |
Pull over push; bound every loop; no external call in a loop that can revert |
| Floating point / precision |
Truncation and wrong rounding leak value |
Full-precisionmulDiv, round against the extractor → fixed-point-rounding |
| Price manipulation |
Single spot reading moved in one block |
TWAP / vetted feed, staleness + bounds →oracle-safety |
| Improper input validation |
Trusting caller-supplied addresses/amounts/lengths |
Validate every argument: non-zero, in-range, array-length-matched, bounded |
| Signature replay |
A signature reused across time/chains/contracts |
EIP-712 domain + nonce + expiry + malleability check →eip712-signature-verification |
| Misconfiguration |
Wrong owner, no emergency stop, over-broad authority |
Two-step ownership, least privilege, pause →two-step-access-control, pausable-circuit-breaker |
Input validation
Because "anyone can execute your contract with whatever input they want":
- Reject
address(0) where a real address is required.
- Bound every array length that drives a loop (
require(arr.length <= MAX)), and check parallel
arrays are equal length.
- Enforce numeric ranges and monotonicity where the domain requires it (e.g. cumulative totals must
not decrease).
- Prefer custom errors (
error TooMany(uint256 got, uint256 max);) over string reverts: cheaper
and machine-readable. Keep the codebase consistent; don't mix revert("1") string codes with
custom errors.
Pre-ship self-review
References
- Mastering Ethereum, ch. 9 "Smart Contract Security" (full antipattern catalogue)
- The other skills in this repository, referenced per row above
1---2name: defensive-programming3description: The baseline mindset and antipattern catalogue for smart-contract security — minimalism, reuse, input validation, and the common vulnerability classes with their fixes. Use at the start of any contract work and as a pre-audit self-review.4---5# Defensive programming for smart contracts67Smart contract code is unforgiving: it runs in a public, adversarial environment, and once deployed8an exploit "unfolds in a single transaction," long before you can intervene. This skill is the9baseline mindset from *Mastering Ethereum* ch. 9, plus a checklist of the common vulnerability10classes. The other skills in this repo go deep on individual topics; start here.1112## The five priorities (in order)13141. **Minimalism / simplicity.** More code means more bugs, not more value. Before writing a15 component, ask whether it's needed; after, cut edge cases and nonessential features. Simpler16 contracts are easier to reason about, test, and audit.172. **Code reuse.** Don't reinvent the wheel: reuse battle-tested libraries (OpenZeppelin, Solady).18 Reused code beats freshly written code "no matter how confident you feel.".193. **Code quality.** Treat it like aerospace, not a web widget. Rigorous methodology; assume no20 second chance to fix.214. **Readability / auditability.** Public bytecode; write for the auditor and other devs. Clear names, documented22 invariants.235. **Test coverage.** "Never assume that input is well formed or properly bounded or that it has a24 benign purpose." Test all arguments against expected ranges and formats.2526## The antipattern catalogue2728| Class | The bug | Fix (see also) |29| ------------------------------------ | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |30| **Reentrancy** | External call re-enters before state settles | Checks-effects-interactions;`nonReentrant`; pull payments → `reentrancy-guards` |31| **DELEGATECALL** | Executing foreign code against your storage layout | Don't delegatecall untrusted code;`NoDelegateCall` on singletons |32| **Entropy illusion** | On-chain "randomness" (blockhash, timestamp) is validator-influenced | Use a VRF/commit-reveal; never gamble on block values |33| **Unchecked CALL returns** | Ignoring a`false`/no-return from a low-level call | Check return; use`SafeERC20` → `safe-erc20-transfers` |34| **Race / front-running** | Outcome depends on tx ordering in the mempool | `minAmountOut`-style bounds, commit-reveal, oracle price → `oracle-safety` |35| **Denial of service** | One reverting/unbounded element blocks everyone | Pull over push; bound every loop; no external call in a loop that can revert |36| **Floating point / precision** | Truncation and wrong rounding leak value | Full-precision`mulDiv`, round against the extractor → `fixed-point-rounding` |37| **Price manipulation** | Single spot reading moved in one block | TWAP / vetted feed, staleness + bounds →`oracle-safety` |38| **Improper input validation** | Trusting caller-supplied addresses/amounts/lengths | Validate every argument: non-zero, in-range, array-length-matched, bounded |39| **Signature replay** | A signature reused across time/chains/contracts | EIP-712 domain + nonce + expiry + malleability check →`eip712-signature-verification` |40| **Misconfiguration** | Wrong owner, no emergency stop, over-broad authority | Two-step ownership, least privilege, pause →`two-step-access-control`, `pausable-circuit-breaker` |4142## Input validation4344Because "anyone can execute your contract with whatever input they want":4546- Reject `address(0)` where a real address is required.47- Bound every array length that drives a loop (`require(arr.length <= MAX)`), and check parallel48 arrays are equal length.49- Enforce numeric ranges and monotonicity where the domain requires it (e.g. cumulative totals must50 not decrease).51- Prefer **custom errors** (`error TooMany(uint256 got, uint256 max);`) over string reverts: cheaper52 and machine-readable. Keep the codebase consistent; don't mix `revert("1")` string codes with53 custom errors.5455## Pre-ship self-review5657- [ ] Every external input validated (zero-address, ranges, array lengths).58- [ ] Every loop is bounded; no revert-prone external call inside a loop.59- [ ] CEI + guards on all value-moving functions.60- [ ] Reused audited libraries instead of hand-rolled crypto/token/math.61- [ ] Emergency stop on entry paths; ownership is two-step; authority is least-privilege.62- [ ] Invariants written down and covered by tests, including adversarial inputs.63- [ ] No on-chain randomness for anything valuable; no `tx.origin` auth.6465## References6667- Mastering Ethereum, ch. 9 "Smart Contract Security" (full antipattern catalogue)68- The other skills in this repository, referenced per row above