Production Contract Engineering
This is the bridge between “I can write Solidity” and “I can safely change a protocol repository under review.” Every section ends in evidence that belongs in a pull request, not just a tutorial screenshot.
1. Standard toolchain: Foundry first
Hardhat remains useful for plugins and some frontend stacks. For contract development, make Foundry the default feedback loop: forge for build/test/scripts, cast for RPC inspection, and anvil for a local node.
mkdir vault-protocol && cd vault-protocol
forge init --force
forge install OpenZeppelin/openzeppelin-contracts
forge test -vvv
anvil --chain-id 31337
# Pin the fork block: reproducible tests, not a moving-mainnet test.
export ETH_RPC_URL="https://your-archive-rpc"
forge test --fork-url "$ETH_RPC_URL" --fork-block-number 22000000 -vvv
2. Contract design as an API
| Design item | What must be explicit | Evidence |
|---|---|---|
| Authority | Who can pause, upgrade, mint, change oracle, or move funds; which actions use a Safe/timelock | Authority map + tests for every role |
| Asset accounting | Units, decimals, rounding direction, accounting invariant, maximum loss | NatSpec + invariant tests |
| External calls | Trusted/untrusted callbacks, failure policy, token compatibility, reentrancy boundary | Interaction matrix + adversarial mocks |
| Events | State transitions needed by users/indexers; indexed filters; no secrets | Event assertions + ABI docs |
| Upgrade decision | Immutable or proxy; upgrade authority; migration and rollback plan | Storage report + simulation |
Use interfaces, libraries, custom errors, NatSpec, immutable, constant, and small cohesive contracts. Know receive/fallback, ABI encoding, logs/topics, call, delegatecall, and CREATE2 before reviewing a protocol that uses them.
2b. EVM execution & gas diagnosis
# Opcode-level trace for a failing tx
cast run $TX_HASH --rpc-url $ETH_RPC_URL
# Gas report per function
forge test --gas-report
# ABI calldata edge case: dynamic types offset must be word-aligned
cast calldata "swap(uint256,address,bytes)" 100 $ADDR 0x
- Precompiles (ecrecover, modexp) — know gas and failure modes.
delegatecallinherits caller storage — layout bugs are total loss.- Low-level
call: check return value; empty returndata on revert.
Lab: labs/phase-17-vault/ — starter implements cap/pause; solution includes CREATE2 factory + CI.
3. Test pyramid: examples are not enough
// test/Vault.t.sol
function testFuzz_DepositWithdrawConservesAssets(uint96 rawAssets) public {
uint256 assets = bound(uint256(rawAssets), 1, 1_000_000e6);
deal(address(usdc), alice, assets);
vm.startPrank(alice);
usdc.approve(address(vault), assets);
uint256 shares = vault.deposit(assets, alice);
uint256 received = vault.redeem(shares, alice, alice);
vm.stopPrank();
assertLe(received, assets); // fees/rounding may lower, never create assets.
}
function invariant_totalAssetsCoversRedeemableShares() public view {
assertGe(vault.totalAssets(), vault.convertToAssets(vault.totalSupply()));
}
- Unit tests: authorization, state transitions, custom errors, events, rounding boundaries.
- Fuzz tests: value ranges, malformed inputs, repeated operations; use
boundintentionally. - Invariant/handler tests: sequences of deposits, withdrawals, transfers, donations, pauses, and liquidations.
- Fork tests: real token quirks (USDT-style returns, fee/rebase behavior), oracle contracts, and protocol integrations.
- Regression tests: every bug gets a minimal failing test before the fix.
4. Secure integration patterns
- Use
SafeERC20; never assume an ERC-20 returnstrue. - Apply checks-effects-interactions and use
ReentrancyGuardat externally reachable asset-moving boundaries. Test callback and read-only reentrancy explicitly. - Use pull payments when an ETH transfer can fail or trigger arbitrary code.
- For signatures, bind
chainId, verifying contract, nonce, expiry, signer, and every economic field through EIP-712. Test cross-chain and expired replay attempts. - For ERC-4626, test donation/inflation attacks, decimal offsets, first-depositor behavior, rounding, and share/asset invariants.
- For approvals/Permit2, show the exact spender, amount, chain, expiry, and revocation path in the UI.
5. Upgradeability and deterministic deployment
Default to immutable contracts. If an upgrade is justified, choose UUPS or transparent proxies deliberately; test initialization exactly once, authorization, storage-layout compatibility, and the post-upgrade invariant. For deterministic deployments, record the CREATE2 deployer, salt, init-code hash, expected address, and constructor/config artifact.
# Fails CI when a storage-layout change is unsafe.
forge inspect src/Vault.sol:Vault storage-layout > artifacts/vault-storage-layout.json
forge snapshot
slither . --exclude-dependencies
forge test --match-path "test/invariant/*" --fuzz-runs 10000
6. Client, account abstraction, and service boundaries
- Use typed ABIs (for example TypeScript inference from ABI artifacts) and simulate writes before wallet submission. Batch reads with multicall and decode custom errors/traces in the UI.
- Support injected and WalletConnect-capable wallets through a maintained stack such as viem/wagmi; never embed privileged RPC keys in the browser.
- For ERC-4337, model the entire flow: smart account validation, EntryPoint, bundler, paymaster, nonce space, sponsorship limits, session-key scope, and UserOperation monitoring. EIP-7702-style delegation does not remove the need for explicit authorization boundaries.
- Backends must distinguish an RPC provider from a source of truth. Define archive/full-node requirements, provider failover, websocket reconnect/backfill, request budgets, and trace/debug access before operating an indexer or relayer.
7. Practical lab — release a guarded ERC-4626 vault
- Build an ERC-4626 vault around a mock USDC using OpenZeppelin. Add a deposit cap,
AccessControlroles, two-step admin transfer, pause, and a withdrawal queue/pull-payment path where appropriate. - Write NatSpec for public/external methods plus a one-page authority map and invariant list.
- Implement unit, fuzz, handler-invariant, and pinned-fork tests. Include donation, reentrancy-callback, unauthorized upgrade, non-standard-token, and EIP-712 replay cases.
- Run Slither and triage every finding. Add a gas snapshot and coverage report; do not treat coverage percentage as security proof.
- Deploy with a versioned Foundry script to Sepolia or Base Sepolia, verify source, transfer admin to a Safe, and save deploy inputs/addresses/transaction hashes in the repository.
8. Definition of done
docs/LAUNCH_DRILL.md.