Smart Contract Security Engineering
Security work is a disciplined attempt to falsify the system’s claims. Tools prioritize attention; they do not establish correctness.
1. Start with a threat model
Before reading code, make four tables: assets (what can be lost), authorities (who can change what), trust boundaries (untrusted contracts/oracles/relayers), and invariants (facts that must remain true). State the attacker’s capabilities: arbitrary calldata, flash liquidity, transaction ordering, malicious token callbacks, stale dependencies, and compromised low-privilege keys.
| Claim | Concrete invariant | Test strategy |
|---|---|---|
| Users cannot withdraw others’ funds | withdrawn[user] ≤ credited[user] | handler invariant + impersonated callers |
| Shares cannot create assets | redeemable shares ≤ accounted assets | fuzz deposits, donations, losses, rounding edges |
| Admin cannot silently drain funds | admin functions are bounded or timelocked | role/time-delay/revert tests + event assertions |
| Messages execute once | messageId transitions pending → consumed exactly once | duplicate/reordered delivery tests |
2. Reentrancy is an interaction problem
function withdraw(uint256 shares, address receiver) external nonReentrant {
uint256 assets = previewRedeem(shares);
_burn(msg.sender, shares); // effects first
SafeERC20.safeTransfer(asset, receiver, assets); // interaction last
emit Withdraw(msg.sender, receiver, assets, shares);
}
Audit every external boundary: ETH transfers, ERC-777 hooks, ERC-721/1155 receiver callbacks, token adapters, routers, oracles, and delegatecalls. Test cross-function and read-only reentrancy, not only recursive calls into the same method. Use a hostile receiver/token mock that calls every public method during the callback.
3. Signatures, permissions, and governance
- For EIP-712, verify domain (name/version/chain/verifying contract), signer, nonce, expiry, and every value-bearing field. Do not accept a signature as “login” or “approval” without an exact scope.
- Use
Ownable2Stepor explicit role administration. Separate pauser, risk manager, upgrader, and treasury roles where the product warrants it. - Test proposal thresholds, quorum, vote snapshots, flash-loan resistance, timelock cancellation, and emergency-pause authority. Governance is an attack surface, not a UI feature.
- For proxies, test initializer locking, upgrade authorization, storage layout, implementation initialization, and rollback. Never delegatecall an implementation selected from untrusted input.
4. Economic and integration attacks
| Class | Adversarial test |
|---|---|
| Oracle manipulation | Move spot price; use stale/zero/negative feeds; cross the circuit-breaker boundary. |
| MEV / sandwich | Execute user trade after an adverse reserve move; prove minOut/deadline rejects it. |
| Token incompatibility | Use no-return, fee-on-transfer, rebasing, paused/blacklisted and callback tokens. |
| Precision / rounding | Fuzz 0/1/max values, decimal mismatches, donation and first-depositor paths. |
| DoS / griefing | Fill queues, revert callbacks, grow user arrays, and make a receiver fail. |
5. Review pipeline
forge fmt --check
forge test -vvv
forge test --match-path "test/invariant/*" --fuzz-runs 20000
forge snapshot --check
slither . --exclude-dependencies
# Add your project-specific static/formal tools only after tests are reliable.
Triage findings in a log: identifier, severity, exploit preconditions, affected invariant, fix, regression test, reviewer, and disposition. “Slither clean” and “90% coverage” are not a security conclusion.
5b. Echidna & mutation testing
# echidna.yaml — fuzz invariants on hardened contracts
testMode: assertion
testLimit: 50000
contracts:
- HardenedVault
- Echidna — stateful invariant fuzzing; complement Foundry handlers.
- medusa — mutation testing on exploit tests; surviving mutants indicate weak assertions.
- Halmos / Certora — optional formal path for critical invariants (time-boxed).
5c. Responsible disclosure
Before testnet launch, add SECURITY.md (template: labs/SECURITY.md): scope, contact, safe harbor, embargo policy. Practice with a mock audit in §6 — findings must include exploit path and remediation proof.
6. Practical lab — exploit then harden
- Write a deliberately vulnerable vault: external transfer before accounting, raw ERC-20 call, owner-only upgrade, and unsigned/under-specified withdrawal authorization.
- Create attacker contracts/tests for reentrancy, non-standard tokens, replay, role escalation, donation inflation, and price manipulation. Demonstrate the loss or invariant failure.
- Harden the vault. Every fix must retain its exploit test as a passing regression test.
- Produce a two-page threat model, authority diagram, invariant list, Slither triage, and Safe/timelock deployment plan.
- Run a mock audit: review a peer’s diff, submit findings with exploit path and proof, then verify the remediation.
Lab repo: labs/phase-21-security/ — starter breaks VulnerableVault; solution ships HardenedVault + CI.