UNPKG

skills-ws

Version:

85 agent skills for AI coding assistants — marketing, growth, web3, dev, design & operations. Built for OpenClaw, Claude Code, Cursor, and Codex.

835 lines (697 loc) • 37.1 kB
--- name: solidity-dev description: "Solidity smart contract development with Foundry & Hardhat — project setup, patterns, testing, deployment, security, and gas optimization. Use when writing, testing, auditing, deploying, or optimizing Solidity smart contracts, or configuring compiler/OpenZeppelin/EVM-version settings." --- # Solidity Development — Foundry & Hardhat ## 1. Project Setup ### Foundry Setup ```bash # Install Foundry curl -L https://foundry.paradigm.xyz | bash foundryup # Create new project forge init my-project cd my-project # Project structure # ├── src/ — Solidity source files # ├── test/ — Test files (.t.sol) # ├── script/ — Deployment scripts (.s.sol) # ├── lib/ — Dependencies (git submodules) # └── foundry.toml — Configuration ``` ### foundry.toml Configuration ```toml [profile.default] src = "src" out = "out" libs = ["lib"] solc_version = "0.8.30" # pin EXACTLY; see compiler version table below. Check latest at github.com/ethereum/solidity/releases evm_version = "cancun" # pin the target EVM hardfork explicitly (see EVM version table in §8) optimizer = true optimizer_runs = 200 # low = cheaper deploy; high (e.g. 1_000_000) = cheaper runtime calls via_ir = false # opt-in only; changes codegen/optimizer path — re-audit + gas-diff before flipping (see note) ffi = false # keep false unless a test genuinely needs to shell out; ffi can execute arbitrary commands fs_permissions = [{ access = "read", path = "./" }] [profile.default.fuzz] runs = 10000 max_test_rejects = 65536 seed = "0x1" [profile.ci.fuzz] runs = 50000 [profile.ci.invariant] runs = 512 depth = 50 [etherscan] mainnet = { key = "${ETHERSCAN_API_KEY}" } sepolia = { key = "${ETHERSCAN_API_KEY}" } [rpc_endpoints] mainnet = "${ETH_RPC_URL}" sepolia = "${SEPOLIA_RPC_URL}" ``` ### Choosing a compiler version (mid-2026) **Pin an exact version (no `^`) and test in lockstep** — a floating pragma lets CI and auditors compile different bytecode than production. Solidity 0.8.x is still the production line in 2026 (0.9 not released; check `github.com/ethereum/solidity/releases` for the current patch). | Version | What it actually adds / why it matters | When to use | |---------|----------------------------------------|-------------| | `0.8.24` | First release defaulting `evm_version` to `cancun`; enables `tstore`/`tload` builtins, `mcopy`, and `blobhash` *when targeting Cancun*. Note: `MCOPY` is a Cancun opcode — it is gated by `evm_version`, not just compiler version. | Conservative floor; widely audited; safe on chains that support Cancun. | | `0.8.26` | Stabilized the `transient` storage keyword for declaring transient state variables (earlier versions only had inline `tstore`/`tload` assembly). | If you want first-class `transient` variables for reentrancy locks. | | `0.8.28`–`0.8.29` | Bugfixes, `--via-ir` improvements, Prague/Pectra `evm_version` support added in this range. EOF is **experimental only** and must NOT be assumed production-ready. | General current use. | | `0.8.30` (current line, mid-2026) | Latest patch series; default `evm_version` advanced toward `prague`/`cancun` depending on release — **always set `evm_version` explicitly** rather than relying on the default. | New projects; verify the exact latest patch at the releases page above. | > **Correction vs. common myths:** `MCOPY` and `BLOBHASH` are unlocked by targeting the **Cancun** EVM (`evm_version = "cancun"`), available since 0.8.24 — not by some "≥0.8.27" rule. Transient-storage *assembly* (`tstore`/`tload`) also arrived with Cancun in 0.8.24; the high-level `transient` keyword landed in 0.8.26. **EOF is still experimental in 2026 — do not describe it as "stabilized" and do not ship EOF bytecode to mainnet.** > **`via_ir` is opt-in, not required.** No mainstream language feature *requires* the IR pipeline. Turn it on mainly to (a) escape `Stack too deep` errors or (b) chase extra optimizer wins. It selects a different codegen + optimizer path, so flipping it **changes your deployed bytecode and gas profile**. If you enable it: re-run the full test/invariant suite, regenerate `forge snapshot` and diff gas, re-verify on the explorer (verification must use the *same* `via_ir`/optimizer settings), and ideally re-audit. Never flip it between audit and deploy. ### Hardhat Setup — Hardhat 3 (current, mid-2026) **Foundry is the faster default for pure-Solidity work; reach for Hardhat when you need a TypeScript app/deploy layer, viem, or Hardhat Ignition.** Hardhat 3 is the stable line in 2026. Key changes vs Hardhat 2: it is **ESM-first**, ships native **Solidity unit tests** (run alongside Mocha/`node:test`), uses a `hardhat-toolbox-viem` (viem-based) or `hardhat-toolbox-mocha-ethers` toolbox, and configures networks under a typed `networks` block with **build profiles**. ```bash mkdir my-project && cd my-project npm init -y npm install --save-dev hardhat@^3 npx hardhat --init # scaffolds an ESM TypeScript project (Hardhat 3) # Pick the "node:test + viem" or "mocha + ethers" template when prompted. # Project structure # ├── contracts/ — Solidity sources (can also hold *.t.sol Solidity tests) # ├── test/ — TS tests (node:test or mocha) + Solidity tests # ├── scripts/ — Deployment / ops scripts (.ts) # ├── ignition/ — Hardhat Ignition deployment modules # └── hardhat.config.ts (ESM — package.json should have "type": "module") ``` ```typescript // hardhat.config.ts (Hardhat 3, ESM, viem toolbox) import type { HardhatUserConfig } from "hardhat/config"; import hardhatToolboxViem from "@nomicfoundation/hardhat-toolbox-viem"; import { configVariable } from "hardhat/config"; const config: HardhatUserConfig = { plugins: [hardhatToolboxViem], solidity: { profiles: { default: { version: "0.8.30", settings: { optimizer: { enabled: true, runs: 200 }, evmVersion: "cancun", viaIR: false }, }, production: { // higher runs for cheaper runtime in deployed code version: "0.8.30", settings: { optimizer: { enabled: true, runs: 1_000_000 }, evmVersion: "cancun" }, }, }, }, networks: { // EDR simulated chain for tests/forking hardhatMainnet: { type: "edr-simulated", chainType: "l1", forking: { url: configVariable("ETH_RPC_URL") } }, sepolia: { type: "http", url: configVariable("SEPOLIA_RPC_URL"), accounts: [configVariable("DEPLOYER_PRIVATE_KEY")], // prefer a keystore/HW wallet for mainnet (see §6) }, }, verify: { etherscan: { apiKey: configVariable("ETHERSCAN_API_KEY") } }, }; export default config; ``` > `configVariable(...)` resolves secrets from Hardhat 3's encrypted keystore / env at runtime instead of baking them into config — never paste a raw private key here. Check current plugin names and the EDR network API at `hardhat.org/docs`. ### Legacy: Hardhat 2 (only when maintaining an older repo) Hardhat 2 is CommonJS and uses the older `@nomicfoundation/hardhat-toolbox` with a flat `solidity.version`/`networks` shape. Only use this for repos that have not migrated: ```typescript // hardhat.config.ts (Hardhat 2 — legacy) import { HardhatUserConfig } from "hardhat/config"; import "@nomicfoundation/hardhat-toolbox"; import "dotenv/config"; const config: HardhatUserConfig = { solidity: { version: "0.8.30", settings: { optimizer: { enabled: true, runs: 200 }, evmVersion: "cancun" } }, networks: { hardhat: { forking: { url: process.env.ETH_RPC_URL || "", blockNumber: 22000000 } }, sepolia: { url: process.env.SEPOLIA_RPC_URL || "", accounts: process.env.DEPLOYER_PRIVATE_KEY ? [process.env.DEPLOYER_PRIVATE_KEY] : [], }, }, etherscan: { apiKey: process.env.ETHERSCAN_API_KEY }, }; export default config; ``` ### Installing Dependencies **Foundry (git submodules):** ```bash forge install OpenZeppelin/openzeppelin-contracts forge install OpenZeppelin/openzeppelin-contracts-upgradeable forge install transmissions11/solmate # Remappings in foundry.toml or remappings.txt: # @openzeppelin/=lib/openzeppelin-contracts/ # solmate/=lib/solmate/src/ ``` **Hardhat (npm):** ```bash npm install @openzeppelin/contracts @openzeppelin/contracts-upgradeable npm install --save-dev @openzeppelin/hardhat-upgrades ``` --- ## 2. Foundry Commands Reference ```bash # Build & Compile forge build # Compile all contracts forge build --sizes # Show contract sizes forge build --via-ir # Compile with Yul IR pipeline # Testing forge test # Run all tests forge test -vvvv # Verbose (show traces) forge test --match-test testDeposit # Run specific test forge test --match-contract VaultTest # Run specific contract tests forge test --fork-url $ETH_RPC_URL # Fork mainnet tests forge test --gas-report # Gas usage report forge test --fuzz-runs 50000 # Extended fuzz runs # Coverage forge coverage # Summary coverage forge coverage --report lcov # Generate lcov for HTML report # Deployment forge script script/Deploy.s.sol --rpc-url sepolia --broadcast --verify forge script script/Deploy.s.sol --rpc-url mainnet --broadcast --verify --slow # Cast (CLI interaction) cast call $ADDR "balanceOf(address)" $USER --rpc-url mainnet cast send $ADDR "transfer(address,uint256)" $TO $AMT --account deployer # use keystore/--ledger, not --private-key on mainnet cast abi-encode "constructor(address,uint256)" $ADDR 1000 cast sig "transfer(address,uint256)" # → 0xa9059cbb cast 4byte 0xa9059cbb # → transfer(address,uint256) cast storage $ADDR 0 --rpc-url mainnet # Read slot 0 cast estimate $ADDR "mint(uint256)" 5 --rpc-url mainnet cast etherscan-source $ADDR --etherscan-api-key $KEY # Anvil (local node) anvil # Start local node (port 8545) anvil --fork-url $ETH_RPC_URL # Fork mainnet anvil --fork-url $ETH_RPC_URL --fork-block-number <recent_block> # pin a recent block for determinism anvil --accounts 20 --balance 10000 # Custom accounts # Chisel (Solidity REPL) chisel # Interactive Solidity shell # !source src/MyContract.sol # Load contracts # uint256 x = 42; # x * 2 # → 84 ``` --- ## 3. Common Solidity Patterns ### Factory Pattern ```solidity contract VaultFactory { address[] public vaults; event VaultCreated(address indexed vault, address indexed owner); function createVault(address token) external returns (address) { Vault vault = new Vault(token, msg.sender); vaults.push(address(vault)); emit VaultCreated(address(vault), msg.sender); return address(vault); } } ``` ### Minimal Proxy (Clones — EIP-1167) ```solidity import "@openzeppelin/contracts/proxy/Clones.sol"; contract VaultFactory { address public immutable implementation; constructor() { implementation = address(new Vault()); } function createVault(address token, address owner) external returns (address) { address clone = Clones.clone(implementation); Vault(clone).initialize(token, owner); return clone; } } ``` Gas: ~45k to deploy clone vs ~500k+ for full contract. ### UUPS Proxy (Recommended Upgrade Pattern) — OpenZeppelin 5.x ```solidity import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; contract VaultV1 is Initializable, UUPSUpgradeable, OwnableUpgradeable { /// @custom:storage-location erc7201:myapp.storage.Vault struct VaultStorage { uint256 fee; } // keccak256(abi.encode(uint256(keccak256("myapp.storage.Vault")) - 1)) & ~bytes32(uint256(0xff)) bytes32 private constant VAULT_STORAGE = 0x.../* compute per ERC-7201 */; function _s() private pure returns (VaultStorage storage $) { assembly { $.slot := VAULT_STORAGE } } /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } function initialize(uint256 _fee) external initializer { __Ownable_init(msg.sender); // OZ v5: Ownable/OwnableUpgradeable take an explicit initialOwner __UUPSUpgradeable_init(); _s().fee = _fee; } function _authorizeUpgrade(address newImpl) internal override onlyOwner {} } ``` **OpenZeppelin 5.x upgrade-safety notes (mid-2026, Contracts v5.x / 5.4+):** - **Namespaced storage (ERC-7201)** is the v5 way to lay out upgradeable storage — OZ's own upgradeable contracts use it, which makes the old `uint256[50] __gap` arrays largely unnecessary for *new* code. Annotate your struct with `@custom:storage-location erc7201:...` so the upgrades plugin can validate layout. - Always validate layout across versions: `forge clean && forge inspect VaultV1 storageLayout` and diff vs the new impl, or use the Hardhat/Foundry **OpenZeppelin Upgrades plugin** (`upgradeProxy` runs `validateUpgrade` automatically). - v5 constructors changed: `Ownable(initialOwner)` (no longer defaults to `msg.sender`), and most lifecycle logic moved into a single **`_update`** hook (see ERC20 note in §10) instead of the old `_beforeTokenTransfer`/`_afterTokenTransfer`. - For multi-role systems prefer **`AccessManager` + `AccessManaged`** (v5's centralized, time-delayed authority) over scattering `AccessControl` roles across contracts. ### Diamond Pattern (EIP-2535) Multiple facets share one storage via delegatecall. Use for large contracts exceeding 24KB limit. ```solidity // Storage library (shared across facets) library LibDiamond { bytes32 constant DIAMOND_STORAGE_POSITION = keccak256("diamond.storage"); struct DiamondStorage { mapping(bytes4 => address) facets; address owner; } function ds() internal pure returns (DiamondStorage storage d) { bytes32 pos = DIAMOND_STORAGE_POSITION; assembly { d.slot := pos } } } ``` --- ## 4. Testing ### Unit Test (Foundry) ```solidity // test/Vault.t.sol import "forge-std/Test.sol"; import "../src/Vault.sol"; contract VaultTest is Test { Vault vault; address alice = makeAddr("alice"); address bob = makeAddr("bob"); function setUp() public { vault = new Vault(); deal(alice, 10 ether); } function test_deposit() public { vm.prank(alice); vault.deposit{value: 1 ether}(); assertEq(vault.balances(alice), 1 ether); } function test_withdraw_reverts_insufficient() public { vm.prank(alice); vm.expectRevert("Insufficient balance"); vault.withdraw(1 ether); } function test_event_emitted() public { vm.prank(alice); vm.expectEmit(true, false, false, true); emit Vault.Deposited(alice, 1 ether); vault.deposit{value: 1 ether}(); } } ``` ### Fuzz Testing ```solidity function testFuzz_deposit(uint256 amount) public { amount = bound(amount, 0.01 ether, 100 ether); deal(alice, amount); vm.prank(alice); vault.deposit{value: amount}(); assertEq(vault.balances(alice), amount); } ``` ### Invariant Testing ```solidity // test/VaultInvariant.t.sol contract VaultInvariant is Test { Vault vault; VaultHandler handler; function setUp() public { vault = new Vault(); handler = new VaultHandler(vault); targetContract(address(handler)); } function invariant_solvency() public view { assertGe(address(vault).balance, vault.totalDeposited()); } } contract VaultHandler is Test { Vault vault; constructor(Vault _v) { vault = _v; } function deposit(uint256 amount) public { amount = bound(amount, 0, 10 ether); deal(address(this), amount); vault.deposit{value: amount}(); } } ``` ### Fork Testing ```solidity function testFork_uniswapSwap() public { // Pin a RECENT block for deterministic, cacheable forks (omit to use chain head). uint256 forkId = vm.createFork(vm.envString("ETH_RPC_URL"), vm.envUint("FORK_BLOCK")); vm.selectFork(forkId); address WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; // mainnet WETH9 address USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; // mainnet USDC // Test real protocol interactions (verify addresses for your target chain)... } ``` ### Hardhat Testing (TypeScript) ```typescript import { expect } from "chai"; import { ethers } from "hardhat"; import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers"; describe("Vault", () => { async function deployFixture() { const [owner, alice] = await ethers.getSigners(); const Vault = await ethers.getContractFactory("Vault"); const vault = await Vault.deploy(); return { vault, owner, alice }; } it("accepts deposits", async () => { const { vault, alice } = await loadFixture(deployFixture); await vault.connect(alice).deposit({ value: ethers.parseEther("1") }); expect(await vault.balances(alice.address)).to.equal(ethers.parseEther("1")); }); }); ``` --- ## 5. Deployment Scripts ### Foundry Script ```solidity // script/Deploy.s.sol import "forge-std/Script.sol"; import "../src/Vault.sol"; contract DeployScript is Script { function run() external { // No raw private key in source. `--account <keystore>` / `--ledger` supplies the signer; // vm.startBroadcast() (no arg) uses the sender configured on the CLI. address treasury = vm.envAddress("TREASURY"); require(treasury != address(0), "TREASURY unset"); vm.startBroadcast(); Vault vault = new Vault(treasury); console.log("Vault deployed at:", address(vault)); console.log("chainid:", block.chainid); vm.stopBroadcast(); } } ``` ```bash # 1. DRY RUN first — simulate WITHOUT --broadcast. Inspect the printed txns, gas, and addresses. forge script script/Deploy.s.sol:DeployScript --rpc-url mainnet --account deployer --sender 0xYourDeployerAddress -vvvv # 2. Testnet broadcast (verify as you go) forge script script/Deploy.s.sol:DeployScript \ --rpc-url sepolia --account deployer --sender 0xYourDeployerAddress --broadcast --verify -vvvv # 3. Mainnet: hardware wallet or keystore, --slow (wait for each receipt), explicit chain forge script script/Deploy.s.sol:DeployScript \ --rpc-url mainnet --ledger --sender 0xYourDeployerAddress \ --broadcast --slow --verify --chain 1 ``` ### Production deployment checklist (run top-to-bottom) Money-moving deploys are irreversible. Do **not** broadcast to mainnet from a hot `PRIVATE_KEY` env var. 1. **Code frozen & audited** — tag the exact commit; bytecode you deploy must match the audited commit (same `solc_version`, `evm_version`, `optimizer_runs`, `via_ir`). 2. **Fork-simulate the full deploy** against a recent mainnet block: `forge script ... --fork-url $ETH_RPC_URL` (no `--broadcast`) and assert post-state. Optionally push the bundle through **Tenderly** for a visual simulation. 3. **Dry run on the real network without `--broadcast`** and read every queued tx: target, value, calldata, gas, and the deterministic deployment address. 4. **Verify constructor args** by decoding what you pass: `cast abi-decode "constructor(address)" <calldata>` — wrong args are the most common irreversible mistake. 5. **Confirm chain ID** (`--chain 1` for mainnet) and the **RPC endpoint** points where you think (`cast chain-id --rpc-url $ETH_RPC_URL`). 6. **Sign with a hardware wallet (`--ledger`/`--trezor`) or encrypted keystore (`--account`)** — never `--private-key`/`PRIVATE_KEY` on mainnet. 7. **Broadcast with `--slow`** so each tx is mined before the next (avoids nonce gaps / reordering); set sane gas via `--with-gas-price` or EIP-1559 flags if the mempool is hot. 8. **Verify on the explorer** with the *identical* compiler settings (`--verify`, or `forge verify-contract` after). Verification failing usually means a settings mismatch. 9. **Transfer ownership/admin to a multisig** (e.g. a Safe) immediately — `transferOwnership(safe)` / `AccessManager` admin, and for UUPS confirm `_authorizeUpgrade` is gated by it. Never leave a fresh EOA as owner. 10. **Post-deploy smoke tests** against the live address with `cast call`/a script: read invariants, do one tiny guarded write, confirm events. Record addresses + the broadcast file (`broadcast/<chainid>/run-latest.json`) in your deployment log. ### Hardhat Ignition ```typescript // ignition/modules/Vault.ts import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; const VaultModule = buildModule("VaultModule", (m) => { const vault = m.contract("Vault"); return { vault }; }); export default VaultModule; ``` ```bash npx hardhat ignition deploy ignition/modules/Vault.ts --network sepolia ``` --- ## 6. Environment & Key Management ### .env (NEVER commit this — add to `.gitignore`) ```bash # RPC + API keys are fine in .env. Do NOT keep a mainnet PRIVATE_KEY here. ETH_RPC_URL=https://eth-mainnet.example.com/v2/<YOUR_KEY> SEPOLIA_RPC_URL=https://eth-sepolia.example.com/v2/<YOUR_KEY> ETHERSCAN_API_KEY=<YOUR_ETHERSCAN_KEY> TREASURY=0xYourTreasuryAddress # DEPLOYER_PRIVATE_KEY only for throwaway testnet keys — prefer a keystore/HW wallet (below). ``` > **Key handling order of preference:** hardware wallet (mainnet) > encrypted keystore > env-var private key (testnet/throwaway only). A leaked deployer key drains everything it controls; treat it like cash. ### Foundry Keystore (encrypted, recommended for everyday signing) ```bash cast wallet import deployer --interactive # paste key once; it's stored encrypted under ~/.foundry/keystores cast wallet list # confirm the account exists # Use it (you'll be prompted for the password, key never touches disk in plaintext): forge script script/Deploy.s.sol --account deployer --sender 0xYourDeployerAddress --broadcast ``` ### Hardware Wallet (recommended for mainnet) ```bash # Ledger / Trezor — the private key never leaves the device forge script script/Deploy.s.sol --ledger --sender 0xYourDeployerAddress --broadcast --slow forge script script/Deploy.s.sol --trezor --sender 0xYourDeployerAddress --broadcast --slow cast send 0xContract "pause()" --ledger --from 0xYourDeployerAddress # cast supports --ledger too ``` --- ## 7. Verification ### Etherscan ```bash # Foundry (auto with --verify during deployment) forge verify-contract $ADDR src/Vault.sol:Vault \ --etherscan-api-key $KEY --chain sepolia # With constructor args forge verify-contract $ADDR src/Vault.sol:Vault \ --constructor-args $(cast abi-encode "constructor(address,uint256)" $TOKEN 100) \ --etherscan-api-key $KEY --chain mainnet ``` ### Sourcify ```bash forge verify-contract $ADDR src/Vault.sol:Vault \ --verifier sourcify --chain-id 1 ``` ### Hardhat ```bash npx hardhat verify --network sepolia $ADDR "constructor_arg_1" "constructor_arg_2" ``` --- ## 8. Gas Optimization Cheat Sheet ### Storage Packing ```solidity // BAD: 3 slots (96 bytes) uint256 amount; // slot 0 uint128 timestamp; // slot 1 bool active; // slot 2 // GOOD: 2 slots (64 bytes) uint128 timestamp; // slot 0 (16 bytes) bool active; // slot 0 (packed — 1 byte) uint256 amount; // slot 1 ``` ### calldata vs memory ```solidity // ~600 gas cheaper per call for read-only arrays function process(uint256[] calldata ids) external { ... } // GOOD function process(uint256[] memory ids) external { ... } // BAD for external ``` ### Unchecked Math (safe loops) ```solidity for (uint256 i; i < len; ) { // ... loop body unchecked { ++i; } // saves ~80 gas per iteration } ``` ### Custom Errors ```solidity error InsufficientBalance(uint256 available, uint256 required); if (balance < amount) revert InsufficientBalance(balance, amount); // Saves ~200+ gas vs require("Insufficient balance") ``` ### Cache Storage Reads ```solidity uint256 _totalSupply = totalSupply; // 1 SLOAD (~2100 gas) // Use _totalSupply multiple times instead of re-reading storage ``` ### Immutable & Constant ```solidity uint256 public constant FEE_BPS = 30; // Inlined at compile time — free address public immutable FACTORY; // Set once in constructor — cheap read constructor() { FACTORY = msg.sender; } ``` ### Short-Circuit Evaluation ```solidity require(amount > 0 && balances[msg.sender] >= amount); // If amount == 0, SLOAD for balances is skipped ``` ### Batch Operations ```solidity // Instead of N separate transactions, batch into one function batchTransfer(address[] calldata to, uint256[] calldata amounts) external { require(to.length == amounts.length, "len"); for (uint256 i; i < to.length; ) { _transfer(msg.sender, to[i], amounts[i]); unchecked { ++i; } } } ``` ### Transient storage for reentrancy locks (Cancun+, use with care) ```solidity // EVM must target cancun or later. Transient storage is auto-cleared at end of tx — // cheaper than an SSTORE/SSTORE lock, but ONLY safe for data that must reset per-tx. contract ReentrancyGuardTransient { // bytes32(uint256(keccak256("reentrancy.guard")) - 1) bytes32 private constant LOCK = 0x...; modifier nonReentrant() { require(_tload(LOCK) == 0, "reentrant"); _tstore(LOCK, 1); _; _tstore(LOCK, 0); // cleared anyway at tx end, but explicit is clearer } function _tload(bytes32 s) private view returns (uint256 v) { assembly { v := tload(s) } } function _tstore(bytes32 s, uint256 v) private { assembly { tstore(s, v) } } } ``` > OZ ships `ReentrancyGuardTransient` in v5 — prefer it over rolling your own. **Footgun:** never use transient storage for state that must persist across calls within different transactions, and remember a value set in one external call is still visible to re-entrant calls *within the same tx* — that's exactly what makes it a valid lock, but a bug if you treat it as permanent state. ### EVM version targets (set `evm_version` explicitly — it changes available opcodes AND gas) | `evm_version` | Notable for gas/features | When to target | |---------------|--------------------------|----------------| | `paris` | Pre-PUSH0. Needed for chains/L2s that haven't shipped Shanghai. | Only legacy/non-Shanghai chains. | | `shanghai` | Adds `PUSH0` (cheaper constant pushes, smaller bytecode). | Safe broad default if a chain lacks Cancun. | | `cancun` | `TSTORE`/`TLOAD` (transient storage), `MCOPY` (cheap memory copy), `BLOBHASH`, blob (EIP-4844) data. **Default since solc 0.8.24.** | Ethereum mainnet + most major L2s in 2026. | | `prague` / pectra | Pectra-era opcodes/precompiles; supported by recent 0.8.28+ compilers. | Only once your target chain is on the Prague hardfork — verify per-chain. | | L2-specific | Many L2s lag mainnet hardforks and price calldata/state differently. | **Check the chain's docs before targeting cancun/prague on an L2.** | ### Optimizer & viaIR tradeoffs (measure, don't guess) - `optimizer_runs` is a **deploy-vs-runtime knob**, not a quality dial. Low runs (≈200) → smaller, cheaper-to-deploy bytecode; high runs (e.g. `1_000_000`) → bigger bytecode but cheaper repeated calls. Pick based on whether the contract is called millions of times or deployed many times. - `via_ir = true` can unlock extra optimizations and fixes `Stack too deep`, but changes codegen — **always gas-diff and re-test** (see §1 note). Don't assume it's always cheaper. - Watch the **24,576-byte runtime size limit** (EIP-170): `forge build --sizes`. If you're over, split logic, use libraries, or a proxy/diamond — not a higher optimizer setting. ### Gas measurement workflow (numbers beat intuition) ```bash # 1. Per-function gas report from your tests forge test --gas-report # 2. Snapshot total gas of every test, commit it, and diff after a change forge snapshot # writes .gas-snapshot forge snapshot --diff .gas-snapshot # shows +/- per test vs the committed baseline forge snapshot --check # CI gate: fail if gas regressed # 3. Compare two configs cleanly (e.g. via_ir on vs off, or different runs) FOUNDRY_PROFILE=default forge snapshot --snap .gas-default forge build --via-ir && forge snapshot --snap .gas-ir # diff .gas-default .gas-ir # 4. Inspect deployed/runtime size against the EIP-170 limit forge build --sizes ``` > Optimize against **measured** report/snapshot deltas on realistic inputs, not micro-rules. A change that saves 80 gas in a loop but adds 5k to deploy can be a net loss depending on usage. --- ## 9. Solidity Style Guide ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.30; // pin EXACTLY (matches foundry.toml/§1) — no floating caret in production import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; /// @title MyToken — A custom ERC20 token /// @author <Your Name> /// @notice Use this for demonstration purposes contract MyToken is ERC20, Ownable { // Type declarations struct UserInfo { uint128 balance; uint128 lastClaim; } enum Status { Active, Paused, Deprecated } // State variables (constants → immutables → storage) uint256 public constant MAX_SUPPLY = 1_000_000e18; address public immutable treasury; mapping(address => UserInfo) public users; uint256 public totalClaimed; // Events event Claimed(address indexed user, uint256 amount); // Errors error ExceedsMaxSupply(); error AlreadyClaimed(); // State Status public status; // Modifiers modifier whenActive() { require(status == Status.Active, "Not active"); _; } constructor(address _treasury) ERC20("MyToken", "MTK") Ownable(msg.sender) { treasury = _treasury; } // External functions // Public functions // Internal functions // Private functions // View/pure functions last } ``` --- ## 10. Common Gotchas | Gotcha | Description | Fix | |--------|------------|-----| | Re-entrancy | External call before state update | CEI pattern + ReentrancyGuard | | tx.origin auth | Phishable via malicious contract | Always use msg.sender | | Unchecked return | ERC20 transfer may return false silently | Use SafeERC20 | | Storage vs memory | Modifying memory struct doesn't update storage | Be explicit about data location | | Uninitialized proxy | Implementation not initialized | Call _disableInitializers() in constructor | | Floating pragma | `^0.8.0` allows untested compiler versions | Pin the EXACT version you audit/deploy, e.g. `pragma solidity 0.8.30;` | | Front-running | Pending tx visible in mempool | Commit-reveal, private mempools/MEV-protect RPC, slippage limits | | Block.timestamp | Proposers can nudge it (seconds) | Don't use for precise timing/randomness | | Selector collision | Proxy + impl share selector space | Check with `forge selectors collision` | | ERC20 approve race | approve(0)→approve(new) needed for some tokens | OZ v5 **removed `increaseAllowance`/`decreaseAllowance` from base ERC20**. Use `SafeERC20.safeIncreaseAllowance`/`forceApprove`, or `permit` (EIP-2612). Don't call the removed base methods. | | `_update` override (OZ v5) | v5 replaced `_beforeTokenTransfer`/`_afterTokenTransfer` with one `_update(from,to,value)` hook | Override `_update` for mint/burn/transfer hooks; call `super._update(...)` | | `transfer`/`send` 2300 gas | Hard-coded stipend breaks with proxies/multisigs (gas repricing) | Use `call{value:...}("")` + check return, with CEI/reentrancy guard | ### Foundry Cheat Codes Quick Reference ```solidity vm.prank(alice); // Next call from alice vm.startPrank(alice); // All calls from alice until stopPrank vm.deal(alice, 1 ether); // Set ETH balance deal(address(token), alice, 1000e18); // Set ERC20 balance vm.warp(block.timestamp + 1 days); // Time travel vm.roll(block.number + 100); // Block number travel vm.expectRevert(MyError.selector); // Expect next call reverts (prefer selector over string) vm.expectEmit(true, true, false, true); // Expect event (indexed1, indexed2, indexed3, data) vm.record(); // Start recording storage vm.accesses(addr); // Get storage reads/writes vm.expectPartialRevert(MyError.selector); // Match custom-error selector ignoring args skip(1 hours); // Skip time forward rewind(1 hours); // Rewind time makeAddr("name"); // Deterministic address from label ``` --- ## 11. Security & Audit Toolchain (expected for paid work) A production/audited Solidity codebase in 2026 is expected to run static analysis, fuzz/invariant tests, and storage-layout checks in CI — not just unit tests. ### Static analysis ```bash pipx install slither-analyzer # Slither (Trail of Bits) — fast, high-signal slither . --exclude-dependencies # triage findings; suppress with // slither-disable-next-line slither . --print human-summary # contract summary, modifiers, ext calls slither-check-upgradeability . VaultV1 --new-contract-name VaultV2 # upgrade/storage safety # Mythril (symbolic execution) — slower, good for deep arithmetic/path bugs: pipx install mythril && myth analyze src/Vault.sol --solv 0.8.30 ``` ### Fuzzing & invariants (Foundry + Echidna/Medusa) - **Foundry invariant tests** (built in): write a *handler* that performs bounded random actions, then assert system-wide invariants. Use `targetContract`/`targetSelector` to focus the fuzzer, and `bound()` (not raw `%`) to constrain inputs. See §4 — design invariants like "sum of balances == totalSupply", "vault solvency", "no user can withdraw more than deposited". - **Echidna** (property/assertion fuzzer) and **Medusa** (parallel, coverage-guided, Go-based — Trail of Bits) run the *same* Solidity property contracts: ```bash echidna . --contract VaultInvariant --config echidna.yaml # property/assertion modes medusa fuzz --target . --deployment-order VaultInvariant # faster, multi-worker ``` ```solidity // Echidna/Medusa property: must always hold function echidna_solvency() public view returns (bool) { return address(vault).balance >= vault.totalDeposited(); } ``` ### Vulnerability patterns to test explicitly | Class | What to write a test/invariant for | |-------|-----------------------------------| | Reentrancy (classic / cross-function / cross-contract / read-only) | Malicious receiver re-enters in `receive()`/ERC777 hooks; assert CEI holds and view functions can't be read mid-update. Use `ReentrancyGuard`/`...Transient`. | | Oracle manipulation | Fork-test against a real oracle; assert behavior under a flash-loan-skewed spot price. Prefer TWAP/Chainlink with staleness + min-answer checks; never trust a single-block spot price. | | Signature replay / EIP-712 | Test that a signature is rejected on a second use, after a nonce bump, on a different `chainid`, and after `deadline`. Build the digest with a domain separator (`name`, `version`, `chainid`, `verifyingContract`); use OZ `ECDSA.recover` (it rejects malleable `s`). | | Access control | A test per privileged function asserting non-owner/non-role reverts; verify `_authorizeUpgrade`, pausers, and minters are gated. | | Upgrade storage layout | `slither-check-upgradeability` or OZ Upgrades `validateUpgrade`; diff `forge inspect <C> storageLayout` across versions before every upgrade. | | Integer / unit bugs | Fuzz arithmetic; check rounding direction (round *against* the user for protocol safety), decimals mismatches, and `unchecked` blocks. | ### CI example (GitHub Actions) ```yaml # .github/workflows/ci.yml name: ci on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: { submodules: recursive } - uses: foundry-rs/foundry-toolchain@v1 - run: forge fmt --check - run: forge build --sizes # catches EIP-170 size regressions - run: forge test -vvv - run: FOUNDRY_PROFILE=ci forge test # heavier fuzz/invariant runs - run: forge snapshot --check # fail on gas regression (commit .gas-snapshot) slither: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: { submodules: recursive } - uses: crytic/slither-action@v0 # Slither in CI; fails on new high/medium findings ``` > For larger audits, lean on the sibling Trail of Bits skills in this catalog (e.g. `audit-context-building`, `entry-point-analyzer`, `building-secure-contracts`, `property-based-testing`) for methodology; this section covers the day-to-day developer toolchain.