From e17e955d355fb8332db0890239e85c454e805547 Mon Sep 17 00:00:00 2001 From: Zhongwei Li Date: Sun, 30 Nov 2025 09:01:14 +0800 Subject: [PATCH] Initial commit --- .claude-plugin/plugin.json | 12 + README.md | 3 + plugin.lock.json | 104 ++++ skills/skill/SKILL.md | 258 ++++++++ skills/skill/references/anvil-advanced.md | 305 ++++++++++ skills/skill/references/cast-advanced.md | 259 ++++++++ skills/skill/references/chisel.md | 127 ++++ skills/skill/references/cicd.md | 289 +++++++++ skills/skill/references/configuration.md | 450 ++++++++++++++ skills/skill/references/debugging.md | 251 ++++++++ skills/skill/references/dependencies.md | 203 +++++++ skills/skill/references/deployment.md | 554 +++++++++++++++++ skills/skill/references/forge-std-api.md | 567 +++++++++++++++++ skills/skill/references/gas-optimization.md | 333 ++++++++++ skills/skill/references/patterns.md | 581 ++++++++++++++++++ skills/skill/references/resources.md | 126 ++++ skills/skill/references/security.md | 520 ++++++++++++++++ skills/skill/references/solidity-modern.md | 641 ++++++++++++++++++++ skills/skill/references/testing.md | 635 +++++++++++++++++++ 19 files changed, 6218 insertions(+) create mode 100644 .claude-plugin/plugin.json create mode 100644 README.md create mode 100644 plugin.lock.json create mode 100644 skills/skill/SKILL.md create mode 100644 skills/skill/references/anvil-advanced.md create mode 100644 skills/skill/references/cast-advanced.md create mode 100644 skills/skill/references/chisel.md create mode 100644 skills/skill/references/cicd.md create mode 100644 skills/skill/references/configuration.md create mode 100644 skills/skill/references/debugging.md create mode 100644 skills/skill/references/dependencies.md create mode 100644 skills/skill/references/deployment.md create mode 100644 skills/skill/references/forge-std-api.md create mode 100644 skills/skill/references/gas-optimization.md create mode 100644 skills/skill/references/patterns.md create mode 100644 skills/skill/references/resources.md create mode 100644 skills/skill/references/security.md create mode 100644 skills/skill/references/solidity-modern.md create mode 100644 skills/skill/references/testing.md diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..93a11c8 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,12 @@ +{ + "name": "foundry-solidity", + "description": "Skill: Build and test Solidity smart contracts with Foundry toolkit (Forge, Cast, Anvil) - testing patterns, deployment scripts, and modern Solidity 0.8.30", + "version": "0.0.0-2025.11.28", + "author": { + "name": "Misha Kolesnik", + "email": "misha@kolesnik.io" + }, + "skills": [ + "./skills/skill" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..03ab37f --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# foundry-solidity + +Skill: Build and test Solidity smart contracts with Foundry toolkit (Forge, Cast, Anvil) - testing patterns, deployment scripts, and modern Solidity 0.8.30 diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..fd4c3f4 --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,104 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:tenequm/claude-plugins:foundry-solidity", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "88d2a999c768b6f075de83316c1f36c4f7d21606", + "treeHash": "1c51bdffe44cb7995297be05eaad9e994143c2a9c3292df7899556be860d6cbb", + "generatedAt": "2025-11-28T10:28:39.100272Z", + "toolVersion": "publish_plugins.py@0.2.0" + }, + "origin": { + "remote": "git@github.com:zhongweili/42plugin-data.git", + "branch": "master", + "commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390", + "repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data" + }, + "manifest": { + "name": "foundry-solidity", + "description": "Skill: Build and test Solidity smart contracts with Foundry toolkit (Forge, Cast, Anvil) - testing patterns, deployment scripts, and modern Solidity 0.8.30" + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "6515f2d14f41296f3fcdb1c7e3a6128c68981b896f0403356c80a75635a0e3e1" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "091894c3e995c0ca2d9bb14430721c7183c2e82cc2861ac9481e64790034fb8c" + }, + { + "path": "skills/skill/SKILL.md", + "sha256": "f235dfea65236676f2be938afa4172567f56c95d981d795123a3c44178907c81" + }, + { + "path": "skills/skill/references/resources.md", + "sha256": "f769aff1686f563fcb97a975eed5d02e91cf729b851f92edacacd2101e015cdd" + }, + { + "path": "skills/skill/references/anvil-advanced.md", + "sha256": "75e0cdff5a204218890a4b93f1992128b4a89a794006fdf69ed24e62b49ed8b2" + }, + { + "path": "skills/skill/references/dependencies.md", + "sha256": "a82a8b1b5ac8e8cbe2d6f7647fa0df0e78ae3321ce544fc6d1f7455e7a250811" + }, + { + "path": "skills/skill/references/testing.md", + "sha256": "acb8482cbc13062be64da13eda13f5bc734de69d4242a4d5d273ae45c4a930f2" + }, + { + "path": "skills/skill/references/forge-std-api.md", + "sha256": "ce4cc968aaee3e9d2af94f573f5df79c0b59172d547d3a14752ede152d6dcbf4" + }, + { + "path": "skills/skill/references/cicd.md", + "sha256": "1777bf4c25192012f1cc906a4deccfce32ec899e8e12d9a95e2ebdf4ef1c3a9e" + }, + { + "path": "skills/skill/references/patterns.md", + "sha256": "44a9cbf8b942e3f4ae0e4fe6380d861c73748400e751110b030bc3e7f5e40533" + }, + { + "path": "skills/skill/references/deployment.md", + "sha256": "1ad234698e8e47604af8b192755c19a4810b0c0fcb0b3cf7e35e6e57df433f7c" + }, + { + "path": "skills/skill/references/configuration.md", + "sha256": "4f3f7038606f1269f25198cf21e88808b14f0f2989c7ec908b7c60fcc9949c08" + }, + { + "path": "skills/skill/references/solidity-modern.md", + "sha256": "e2e858706c8f373914b316f533cfbbafb3d6d3fcd52bff20325662cbb9869fd4" + }, + { + "path": "skills/skill/references/cast-advanced.md", + "sha256": "946e29e032c526129f7004adc9afc10036d18c7d548e0d20df99bae309bc159a" + }, + { + "path": "skills/skill/references/chisel.md", + "sha256": "43e3f7ee660e64d287dd54c4bae270e27d5aee68b294cf8a0b9844a833e4444d" + }, + { + "path": "skills/skill/references/debugging.md", + "sha256": "0341c0e3ba880a1116bab55889931344570c79e03624fd8d3f56612ad0b2f973" + }, + { + "path": "skills/skill/references/security.md", + "sha256": "f18759ffff0571bdcebf193987cf880ffe3aaf6f40b81ef949875ffdbe6405cb" + }, + { + "path": "skills/skill/references/gas-optimization.md", + "sha256": "51582bbbba0ee8d59ac7419e60ae5ca205d8f101061c0ec8262bb06b2e529e05" + } + ], + "dirSha256": "1c51bdffe44cb7995297be05eaad9e994143c2a9c3292df7899556be860d6cbb" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file diff --git a/skills/skill/SKILL.md b/skills/skill/SKILL.md new file mode 100644 index 0000000..5243092 --- /dev/null +++ b/skills/skill/SKILL.md @@ -0,0 +1,258 @@ +--- +name: foundry-solidity +description: Build and test Solidity smart contracts with Foundry toolkit. Use when developing Ethereum contracts, writing Forge tests, deploying with scripts, or debugging with Cast/Anvil. Triggers on Foundry commands (forge, cast, anvil), Solidity testing, smart contract development, or files like foundry.toml, *.t.sol, *.s.sol. +--- + +# Foundry Solidity Development + +Complete guide for building secure, efficient smart contracts with **Foundry 1.5.0** and **Solidity 0.8.30**. + +## When to Use This Skill + +- Developing Ethereum/EVM smart contracts +- Writing Forge tests (unit, fuzz, invariant, fork) +- Deploying contracts with scripts +- Using Foundry tools (forge, cast, anvil, chisel) +- Working with `foundry.toml`, `*.t.sol`, `*.s.sol` files +- Debugging transactions and contract interactions + +## Quick Start + +```bash +# Create new project +forge init my-project && cd my-project + +# Build contracts +forge build + +# Run tests +forge test + +# Deploy (dry-run) +forge script script/Deploy.s.sol --rpc-url sepolia + +# Deploy (broadcast) +forge script script/Deploy.s.sol --rpc-url sepolia --broadcast --verify +``` + +## Project Structure + +``` +my-project/ +├── foundry.toml # Configuration +├── src/ # Contracts +│ └── Counter.sol +├── test/ # Tests (*.t.sol) +│ └── Counter.t.sol +├── script/ # Deploy scripts (*.s.sol) +│ └── Deploy.s.sol +└── lib/ # Dependencies + └── forge-std/ +``` + +## Core Commands + +### Build & Test + +```bash +forge build # Compile +forge test # Run all tests +forge test -vvvv # With traces +forge test --match-test testDeposit # Filter by test name +forge test --match-contract Vault # Filter by contract +forge test --fork-url $RPC_URL # Fork testing +forge test --gas-report # Gas usage report +``` + +### Deployment + +```bash +# Single contract +forge create src/Token.sol:Token --rpc-url sepolia --private-key $KEY --broadcast + +# Script deployment (recommended) +forge script script/Deploy.s.sol:Deploy --rpc-url sepolia --broadcast --verify + +# Verify existing contract +forge verify-contract $ADDRESS src/Token.sol:Token --chain sepolia +``` + +### Cast - Blockchain Interactions + +```bash +cast call $CONTRACT "balanceOf(address)" $USER --rpc-url mainnet +cast send $CONTRACT "transfer(address,uint256)" $TO $AMOUNT --private-key $KEY +cast decode-tx $TX_HASH +cast storage $CONTRACT 0 --rpc-url mainnet +``` + +### Anvil - Local Node + +```bash +anvil # Start local node +anvil --fork-url $RPC_URL # Fork mainnet +anvil --fork-block-number 18000000 +``` + +## Basic Test Contract + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.30; + +import {Test, console} from "forge-std/Test.sol"; +import {Counter} from "../src/Counter.sol"; + +contract CounterTest is Test { + Counter public counter; + address public user; + + function setUp() public { + counter = new Counter(); + user = makeAddr("user"); + deal(user, 10 ether); + } + + function test_Increment() public { + counter.increment(); + assertEq(counter.number(), 1); + } + + function test_RevertWhen_Unauthorized() public { + vm.expectRevert("Unauthorized"); + vm.prank(user); + counter.adminFunction(); + } + + function testFuzz_SetNumber(uint256 x) public { + x = bound(x, 0, 1000); + counter.setNumber(x); + assertEq(counter.number(), x); + } +} +``` + +## Essential Cheatcodes + +```solidity +// Identity & ETH +address alice = makeAddr("alice"); // Create labeled address +deal(alice, 10 ether); // Give ETH +deal(address(token), alice, 1000e18); // Give ERC20 + +// Impersonation +vm.prank(alice); // Next call as alice +vm.startPrank(alice); // All calls as alice +vm.stopPrank(); + +// Time & Block +vm.warp(block.timestamp + 1 days); // Set timestamp +vm.roll(block.number + 100); // Set block number + +// Assertions +vm.expectRevert("Error message"); // Expect revert +vm.expectRevert(CustomError.selector); // Custom error +vm.expectEmit(true, true, false, true); // Expect event +emit Transfer(from, to, amount); // Must match next emit + +// Storage +vm.store(addr, slot, value); // Write storage +vm.load(addr, slot); // Read storage +``` + +## Deploy Script + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.30; + +import {Script, console} from "forge-std/Script.sol"; +import {Counter} from "../src/Counter.sol"; + +contract Deploy is Script { + function run() external { + uint256 deployerKey = vm.envUint("PRIVATE_KEY"); + + vm.startBroadcast(deployerKey); + Counter counter = new Counter(); + counter.setNumber(42); + vm.stopBroadcast(); + + console.log("Deployed to:", address(counter)); + } +} +``` + +## Modern Solidity Patterns (0.8.30) + +```solidity +// Custom errors (gas efficient) +error InsufficientBalance(uint256 available, uint256 required); + +// Transient storage (0.8.28+) - cheap reentrancy guard +bool transient locked; +modifier nonReentrant() { + require(!locked, "Reentrancy"); + locked = true; + _; + locked = false; +} + +// Immutable variables (cheap reads) +address public immutable owner; + +// Named mapping parameters +mapping(address user => uint256 balance) public balances; + +// require with custom error (0.8.26+) +require(amount <= balance, InsufficientBalance(balance, amount)); +``` + +## Configuration (foundry.toml) + +```toml +[profile.default] +src = "src" +out = "out" +libs = ["lib"] +solc = "0.8.30" +optimizer = true +optimizer_runs = 200 +evm_version = "prague" + +fuzz.runs = 256 +invariant.runs = 256 +invariant.depth = 50 + +[rpc_endpoints] +mainnet = "${MAINNET_RPC_URL}" +sepolia = "${SEPOLIA_RPC_URL}" + +[etherscan] +mainnet = { key = "${ETHERSCAN_API_KEY}" } +sepolia = { key = "${ETHERSCAN_API_KEY}" } + +[profile.ci] +fuzz.runs = 10000 +invariant.runs = 1000 +``` + +## References + +For detailed guides, see: + +- **Testing**: See `references/testing.md` for complete testing patterns (unit, fuzz, invariant, fork), all cheatcodes, and best practices +- **forge-std API**: See `references/forge-std-api.md` for complete library reference (150+ functions) +- **Solidity 0.8.30**: See `references/solidity-modern.md` for new features and modern syntax +- **Deployment**: See `references/deployment.md` for scripting, verification, and multi-chain deployment +- **Configuration**: See `references/configuration.md` for all foundry.toml options +- **Gas Optimization**: See `references/gas-optimization.md` for storage packing, compiler settings, and profiling +- **Patterns**: See `references/patterns.md` for access control, reentrancy guards, factories, and common idioms +- **Security**: See `references/security.md` for vulnerabilities, defensive patterns, and audit preparation +- **Resources**: See `references/resources.md` for official docs, libraries, security tools, and learning paths +- **Debugging**: See `references/debugging.md` for traces, breakpoints, console.log, and the interactive debugger +- **Dependencies**: See `references/dependencies.md` for forge install, remappings, and Soldeer package manager +- **CI/CD**: See `references/cicd.md` for GitHub Actions workflows, caching, and gas tracking +- **Chisel**: See `references/chisel.md` for the interactive Solidity REPL +- **Cast Advanced**: See `references/cast-advanced.md` for decoding, encoding, wallet management, and batch operations +- **Anvil Advanced**: See `references/anvil-advanced.md` for impersonation, state manipulation, and mining modes diff --git a/skills/skill/references/anvil-advanced.md b/skills/skill/references/anvil-advanced.md new file mode 100644 index 0000000..8ca18b9 --- /dev/null +++ b/skills/skill/references/anvil-advanced.md @@ -0,0 +1,305 @@ +# Anvil Advanced Usage + +Advanced Anvil features for local development and testing. + +## Account Impersonation + +### Auto-Impersonate All Accounts + +```bash +# Start with auto-impersonation +anvil --auto-impersonate +``` + +Now any account can send transactions without private key: + +```bash +cast send $CONTRACT "transfer(address,uint256)" $TO 1000 \ + --from 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 \ + --unlocked +``` + +### Impersonate Specific Account + +```bash +# Via RPC +cast rpc anvil_impersonateAccount 0x1234... + +# Stop impersonating +cast rpc anvil_stopImpersonatingAccount 0x1234... +``` + +### In Foundry Tests + +```solidity +address whale = 0x47ac0Fb4F2D84898e4D9E7b4DaB3C24507a6D503; + +vm.startPrank(whale); +usdc.transfer(address(this), 1_000_000e6); +vm.stopPrank(); +``` + +## State Manipulation + +### Set Balance + +```bash +# Set ETH balance +cast rpc anvil_setBalance 0x1234... 0xDE0B6B3A7640000 # 1 ETH in hex + +# In tests +vm.deal(address, 100 ether); +``` + +### Set Code + +```bash +# Deploy code at address +cast rpc anvil_setCode 0x1234... 0x608060405234801... + +# In tests +vm.etch(address, code); +``` + +### Set Storage + +```bash +# Set storage slot +cast rpc anvil_setStorageAt 0x1234... 0x0 0x... + +# In tests +vm.store(address, slot, value); +``` + +### Set Nonce + +```bash +cast rpc anvil_setNonce 0x1234... 0x10 # 16 in hex +``` + +## Mining Modes + +### Auto-Mining (Default) + +```bash +anvil # Mines block on each transaction +``` + +### Interval Mining + +```bash +# Mine every 12 seconds +anvil --block-time 12 +``` + +### Manual Mining + +```bash +# Disable auto-mining +anvil --no-mining + +# Mine manually +cast rpc evm_mine + +# Mine multiple blocks +cast rpc anvil_mine 10 # Mine 10 blocks +``` + +### Mining Control + +```bash +# Enable auto-mine +cast rpc evm_setAutomine true + +# Set interval +cast rpc evm_setIntervalMining 5000 # 5 seconds in ms +``` + +## State Snapshots + +### Dump State + +```bash +# Start anvil, make changes, then dump +anvil --dump-state state.json + +# Load from previous state +anvil --load-state state.json +``` + +### Runtime Snapshots + +```bash +# Create snapshot +SNAPSHOT_ID=$(cast rpc evm_snapshot) + +# Make changes... + +# Revert to snapshot +cast rpc evm_revert $SNAPSHOT_ID +``` + +### In Tests + +```solidity +uint256 snapshot = vm.snapshot(); + +// Make changes... + +vm.revertTo(snapshot); +``` + +## Fork Configuration + +### Basic Fork + +```bash +anvil --fork-url https://eth-mainnet.g.alchemy.com/v2/KEY +``` + +### Pin Block + +```bash +anvil --fork-url $RPC_URL --fork-block-number 18000000 +``` + +### Fork with Caching + +```bash +# Cache fork data locally +anvil --fork-url $RPC_URL --fork-retry-backoff 1000 + +# Disable caching +anvil --fork-url $RPC_URL --no-storage-caching +``` + +### Multiple Forks + +```solidity +// In tests +uint256 mainnetFork = vm.createFork("mainnet"); +uint256 arbitrumFork = vm.createFork("arbitrum"); + +vm.selectFork(mainnetFork); +// Test on mainnet... + +vm.selectFork(arbitrumFork); +// Test on arbitrum... +``` + +## Time Manipulation + +```bash +# Set timestamp +cast rpc evm_setNextBlockTimestamp 1700000000 + +# Increase time +cast rpc evm_increaseTime 86400 # 1 day + +# In tests +vm.warp(block.timestamp + 1 days); +vm.roll(block.number + 100); +``` + +## RPC Methods + +### Common Anvil RPC + +| Method | Description | +|--------|-------------| +| `anvil_setBalance` | Set ETH balance | +| `anvil_setCode` | Set contract code | +| `anvil_setStorageAt` | Set storage slot | +| `anvil_setNonce` | Set account nonce | +| `anvil_impersonateAccount` | Impersonate address | +| `anvil_mine` | Mine blocks | +| `anvil_reset` | Reset fork | +| `anvil_dumpState` | Export state | +| `anvil_loadState` | Import state | + +### EVM Methods + +| Method | Description | +|--------|-------------| +| `evm_snapshot` | Create snapshot | +| `evm_revert` | Revert to snapshot | +| `evm_mine` | Mine single block | +| `evm_setAutomine` | Toggle auto-mining | +| `evm_increaseTime` | Advance time | +| `evm_setNextBlockTimestamp` | Set next timestamp | + +## Configuration + +### Startup Options + +```bash +anvil \ + --port 8545 \ + --accounts 10 \ + --balance 10000 \ + --mnemonic "test test test..." \ + --derivation-path "m/44'/60'/0'/0/" \ + --block-time 12 \ + --gas-limit 30000000 \ + --gas-price 0 \ + --chain-id 31337 \ + --hardfork prague +``` + +### Hardfork Selection + +```bash +anvil --hardfork shanghai +anvil --hardfork cancun +anvil --hardfork prague # Latest +``` + +## Testing Patterns + +### Fork Test Setup + +```solidity +function setUp() public { + vm.createSelectFork(vm.envString("MAINNET_RPC_URL"), 18000000); + + // Impersonate whale + address whale = 0x47ac0Fb4F2D84898e4D9E7b4DaB3C24507a6D503; + vm.startPrank(whale); + usdc.transfer(address(this), 1_000_000e6); + vm.stopPrank(); +} +``` + +### State Reset Between Tests + +```solidity +uint256 snapshot; + +function setUp() public { + if (snapshot == 0) { + // First run: setup and snapshot + _deployContracts(); + snapshot = vm.snapshot(); + } else { + // Subsequent runs: revert to clean state + vm.revertTo(snapshot); + } +} +``` + +### Testing Mainnet Interactions + +```solidity +function test_SwapOnUniswap() public { + vm.createSelectFork("mainnet"); + + address router = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; + address weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + + deal(address(this), 10 ether); + + IRouter(router).swapExactETHForTokens{value: 1 ether}( + 0, path, address(this), block.timestamp + ); +} +``` diff --git a/skills/skill/references/cast-advanced.md b/skills/skill/references/cast-advanced.md new file mode 100644 index 0000000..aa24a51 --- /dev/null +++ b/skills/skill/references/cast-advanced.md @@ -0,0 +1,259 @@ +# Cast Advanced Usage + +Advanced cast commands for blockchain interaction, decoding, and analysis. + +## Transaction Decoding + +### Decode Transaction + +```bash +# Decode transaction by hash +cast decode-tx 0x1234... --rpc-url mainnet + +# Output includes: +# - from, to, value +# - function selector +# - decoded calldata +# - gas used +``` + +### 4byte Signature Lookup + +```bash +# Get function name from selector +cast 4byte 0xa9059cbb +# transfer(address,uint256) + +# Get selector from signature +cast sig "transfer(address,uint256)" +# 0xa9059cbb + +# Decode calldata with known selector +cast 4byte-decode 0xa9059cbb000000000000000000000000... +``` + +### Decode Calldata + +```bash +# Decode with ABI +cast calldata-decode "transfer(address,uint256)" 0xa9059cbb... + +# Output: +# 0x1234... [address] +# 1000000 [uint256] +``` + +## ABI Encoding/Decoding + +### Encode Function Call + +```bash +# Encode calldata +cast calldata "transfer(address,uint256)" 0x1234... 1000000 +# 0xa9059cbb000000000000000000000000... + +# Encode with complex types +cast calldata "swap((address,uint256,bytes))" "(0x1234...,100,0x)" +``` + +### Encode Arguments + +```bash +# ABI encode +cast abi-encode "constructor(string,uint256)" "Token" 1000000 + +# ABI encode packed +cast abi-encode --packed "test(string)" "hello" +``` + +### Decode ABI Data + +```bash +# Decode return data +cast abi-decode "balanceOf(address)(uint256)" 0x00000000... +# 1000000 + +# Decode with multiple returns +cast abi-decode "getReserves()(uint112,uint112,uint32)" 0x... +``` + +## Wallet Management + +### Create Wallet + +```bash +# Generate new wallet +cast wallet new + +# Generate with mnemonic +cast wallet new-mnemonic + +# Derive from mnemonic +cast wallet derive-private-key "word1 word2 ... word12" +``` + +### Wallet Info + +```bash +# Get address from private key +cast wallet address --private-key 0x... + +# Get address from mnemonic +cast wallet address --mnemonic "word1 word2..." + +# Sign message +cast wallet sign "message" --private-key 0x... +``` + +### Keystore + +```bash +# Import to keystore +cast wallet import my-wallet --private-key 0x... + +# List keystores +cast wallet list + +# Use keystore +cast send ... --account my-wallet +``` + +## Contract Interaction + +### Read Functions + +```bash +# Call view function +cast call $CONTRACT "balanceOf(address)" $USER --rpc-url mainnet + +# With block number +cast call $CONTRACT "balanceOf(address)" $USER --block 18000000 + +# Decode result +cast call $CONTRACT "decimals()" | cast to-dec +``` + +### Write Functions + +```bash +# Send transaction +cast send $CONTRACT "transfer(address,uint256)" $TO $AMOUNT \ + --private-key $KEY \ + --rpc-url mainnet + +# With value +cast send $CONTRACT "deposit()" --value 1ether --private-key $KEY + +# Estimate gas +cast estimate $CONTRACT "transfer(address,uint256)" $TO $AMOUNT +``` + +## Storage Inspection + +```bash +# Read storage slot +cast storage $CONTRACT 0 --rpc-url mainnet + +# Read multiple slots +for i in {0..10}; do + echo "Slot $i: $(cast storage $CONTRACT $i)" +done + +# Find storage slot for mapping +cast index address $KEY 0 # mapping at slot 0 +``` + +## Block & Transaction Info + +```bash +# Get block +cast block latest --rpc-url mainnet +cast block 18000000 --field timestamp + +# Get transaction +cast tx 0x1234... --rpc-url mainnet + +# Get receipt +cast receipt 0x1234... --rpc-url mainnet + +# Get logs +cast logs --from-block 18000000 --to-block 18000100 \ + --address $CONTRACT \ + --topic0 0xddf252ad... # Transfer event +``` + +## Type Conversions + +```bash +# Hex to decimal +cast to-dec 0x64 +# 100 + +# Decimal to hex +cast to-hex 100 +# 0x64 + +# Wei conversions +cast to-wei 1 ether +# 1000000000000000000 + +cast from-wei 1000000000000000000 +# 1.000000000000000000 + +# ASCII/bytes conversions +cast to-ascii 0x68656c6c6f +# hello + +cast from-utf8 "hello" +# 0x68656c6c6f +``` + +## Address Utilities + +```bash +# Checksum address +cast to-checksum-address 0x1234... + +# Compute CREATE address +cast compute-address $DEPLOYER --nonce 5 + +# Compute CREATE2 address +cast create2 --starts-with 0x1234 --init-code 0x... +``` + +## ENS + +```bash +# Resolve ENS name +cast resolve-name vitalik.eth --rpc-url mainnet + +# Lookup address +cast lookup-address 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 +``` + +## Gas + +```bash +# Get gas price +cast gas-price --rpc-url mainnet + +# Get base fee +cast base-fee --rpc-url mainnet + +# Estimate gas +cast estimate $CONTRACT "transfer(address,uint256)" $TO $AMOUNT +``` + +## Batch Operations + +```bash +# Multiple calls with bash +for addr in $ADDR1 $ADDR2 $ADDR3; do + echo "$addr: $(cast call $TOKEN 'balanceOf(address)' $addr | cast to-dec)" +done + +# Using multicall3 +cast call 0xcA11bde05977b3631167028862bE2a173976CA11 \ + "aggregate((address,bytes)[])" \ + "[($TOKEN,$(cast calldata 'balanceOf(address)' $ADDR1)),...]" +``` diff --git a/skills/skill/references/chisel.md b/skills/skill/references/chisel.md new file mode 100644 index 0000000..52935ad --- /dev/null +++ b/skills/skill/references/chisel.md @@ -0,0 +1,127 @@ +# Chisel REPL + +Interactive Solidity REPL for quick experimentation. + +## Starting Chisel + +```bash +# Basic REPL +chisel + +# With fork +chisel --fork-url https://eth-mainnet.g.alchemy.com/v2/KEY + +# Specific block +chisel --fork-url $RPC_URL --fork-block-number 18000000 +``` + +## Basic Usage + +``` +➜ uint256 x = 42 +➜ x * 2 +Type: uint256 +├ Hex: 0x54 +├ Hex (full word): 0x0000000000000000000000000000000000000000000000000000000000000054 +└ Decimal: 84 + +➜ address alice = address(0x1234) +➜ alice.balance +Type: uint256 +└ Decimal: 0 +``` + +## Commands + +| Command | Description | +|---------|-------------| +| `!help` | Show all commands | +| `!clear` | Clear session state | +| `!source` | Show generated source | +| `!rawstack` | Show raw stack output | +| `!edit` | Open in editor | +| `!export` | Export session to script | + +## Session Management + +```bash +# List saved sessions +chisel list + +# Load session +chisel load my-session + +# Save current session (in REPL) +!save my-session + +# Clear cache +chisel clear-cache +``` + +## Working with Contracts + +``` +➜ interface IERC20 { + function balanceOf(address) external view returns (uint256); +} + +➜ IERC20 usdc = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) + +➜ usdc.balanceOf(0x47ac0Fb4F2D84898e4D9E7b4DaB3C24507a6D503) +Type: uint256 +└ Decimal: 1234567890 +``` + +## Math Testing + +Quick calculations without deploying: + +``` +➜ uint256 a = 1000000 +➜ uint256 b = 3 +➜ a / b +Type: uint256 +└ Decimal: 333333 + +➜ (a * 1e18) / b +Type: uint256 +└ Decimal: 333333333333333333333333 +``` + +## Hash Functions + +``` +➜ keccak256("hello") +Type: bytes32 +└ 0x1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8 + +➜ keccak256(abi.encode(uint256(1), address(0x1234))) +``` + +## ABI Encoding + +``` +➜ abi.encode(uint256(42), address(0x1234)) +➜ abi.encodePacked("hello", "world") +➜ abi.encodeWithSelector(bytes4(0x12345678), 100) +``` + +## Use Cases + +1. **Quick math**: Test calculations before implementing +2. **ABI encoding**: Debug encoding issues +3. **Hash verification**: Check keccak256 outputs +4. **Contract interaction**: Test calls on fork +5. **Solidity syntax**: Experiment with new features + +## Configuration + +Chisel inherits project settings from `foundry.toml`: + +```toml +[profile.default] +solc = "0.8.30" +evm_version = "prague" +``` + +Run chisel from project root to use these settings. diff --git a/skills/skill/references/cicd.md b/skills/skill/references/cicd.md new file mode 100644 index 0000000..4187a44 --- /dev/null +++ b/skills/skill/references/cicd.md @@ -0,0 +1,289 @@ +# CI/CD Integration + +GitHub Actions workflows for Foundry projects. + +## Basic Workflow + +```yaml +# .github/workflows/test.yml +name: Test + +on: + push: + branches: [main] + pull_request: + +env: + FOUNDRY_PROFILE: ci + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Run tests + run: forge test -vvv +``` + +## With Caching + +```yaml +name: Test + +on: [push, pull_request] + +env: + FOUNDRY_PROFILE: ci + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + cache: true + + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: | + lib + cache + out + key: ${{ runner.os }}-foundry-${{ hashFiles('**/foundry.toml') }} + restore-keys: | + ${{ runner.os }}-foundry- + + - name: Build + run: forge build + + - name: Run tests + run: forge test -vvv +``` + +## Full Pipeline + +```yaml +name: CI + +on: + push: + branches: [main] + pull_request: + +env: + FOUNDRY_PROFILE: ci + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + cache: true + + - name: Build + run: forge build --sizes + + test: + runs-on: ubuntu-latest + needs: build + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + cache: true + + - name: Run tests + run: forge test -vvv + + coverage: + runs-on: ubuntu-latest + needs: build + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + cache: true + + - name: Generate coverage + run: forge coverage --report lcov + + - name: Upload coverage + uses: codecov/codecov-action@v4 + with: + files: lcov.info + + gas-report: + runs-on: ubuntu-latest + needs: build + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + cache: true + + - name: Gas snapshot + run: forge snapshot --check --tolerance 5 +``` + +## Fork Testing in CI + +```yaml +jobs: + fork-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + cache: true + + - name: Run fork tests + run: forge test --match-test "testFork" -vvv + env: + MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }} +``` + +## Gas Snapshot Tracking + +### Check for regressions + +```yaml +- name: Gas snapshot check + run: | + forge snapshot + forge snapshot --diff .gas-snapshot +``` + +### Comment on PR + +```yaml +- name: Compare gas + run: forge snapshot --diff .gas-snapshot > gas-diff.txt + +- name: Post gas diff + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const diff = fs.readFileSync('gas-diff.txt', 'utf8'); + if (diff.trim()) { + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: '## Gas Changes\n```\n' + diff + '\n```' + }); + } +``` + +## Deployment + +```yaml +jobs: + deploy: + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' + environment: production + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Deploy + run: | + forge script script/Deploy.s.sol \ + --rpc-url ${{ secrets.RPC_URL }} \ + --broadcast \ + --verify + env: + PRIVATE_KEY: ${{ secrets.DEPLOYER_PRIVATE_KEY }} + ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }} +``` + +## CI Profile + +Configure higher fuzz runs for CI in `foundry.toml`: + +```toml +[profile.default] +fuzz.runs = 256 +invariant.runs = 256 + +[profile.ci] +fuzz.runs = 10000 +invariant.runs = 1000 +verbosity = 3 +``` + +Use with `FOUNDRY_PROFILE=ci forge test`. + +## Secrets Management + +Required secrets for CI: +- `MAINNET_RPC_URL`: For fork testing +- `DEPLOYER_PRIVATE_KEY`: For deployment +- `ETHERSCAN_API_KEY`: For verification + +**Never commit secrets to the repository.** + +## Matrix Testing + +Test across Solidity versions: + +```yaml +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + solc: ["0.8.20", "0.8.25", "0.8.30"] + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Test with Solc ${{ matrix.solc }} + run: forge test + env: + FOUNDRY_SOLC_VERSION: ${{ matrix.solc }} +``` diff --git a/skills/skill/references/configuration.md b/skills/skill/references/configuration.md new file mode 100644 index 0000000..bdb572b --- /dev/null +++ b/skills/skill/references/configuration.md @@ -0,0 +1,450 @@ +# Foundry Configuration Reference + +Complete reference for `foundry.toml` configuration options. + +## Basic Structure + +```toml +# Default profile +[profile.default] +src = "src" +out = "out" +libs = ["lib"] + +# Additional profiles +[profile.ci] +# CI-specific overrides + +[profile.production] +# Production build settings +``` + +## Project Structure + +```toml +[profile.default] +# Source directories +src = "src" # Contract sources +test = "test" # Test files +script = "script" # Deployment scripts +out = "out" # Compiled output +libs = ["lib"] # Dependency directories +cache_path = "cache" # Compilation cache + +# Remappings (alternative to remappings.txt) +remappings = [ + "@openzeppelin/=lib/openzeppelin-contracts/", + "@solmate/=lib/solmate/src/", + "forge-std/=lib/forge-std/src/" +] +``` + +## Compiler Settings + +```toml +[profile.default] +# Solidity version +solc = "0.8.30" # Exact version +# solc = "^0.8.0" # Version range +# auto_detect_solc = true # Auto-detect from pragmas + +# EVM version +evm_version = "prague" # Target EVM version +# Options: homestead, tangerineWhistle, spuriousDragon, byzantium, +# constantinople, petersburg, istanbul, berlin, london, +# paris, shanghai, cancun, prague + +# Optimizer +optimizer = true +optimizer_runs = 200 # Optimize for ~200 runs +via_ir = false # Use IR-based compilation + +# Output +extra_output = ["abi", "evm.bytecode", "storageLayout"] +extra_output_files = ["abi", "storageLayout"] + +# Bytecode hash +bytecode_hash = "ipfs" # ipfs, bzzr1, or none +cbor_metadata = true # Include CBOR metadata +``` + +## Testing Configuration + +```toml +[profile.default] +# Verbosity (0-5) +verbosity = 2 + +# Gas settings +gas_limit = 9223372036854775807 +gas_price = 0 +block_base_fee_per_gas = 0 +tx_origin = "0x0000000000000000000000000000000000000001" + +# Block settings +block_coinbase = "0x0000000000000000000000000000000000000000" +block_timestamp = 1 +block_number = 1 +block_difficulty = 0 +block_gas_limit = 30000000 +chain_id = 31337 + +# Sender +sender = "0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38" + +# Memory limit (bytes) +memory_limit = 33554432 # 32 MB + +# Show gas reports +gas_reports = ["*"] # All contracts +# gas_reports = ["MyContract", "OtherContract"] +gas_reports_ignore = [] + +# Fail test if gas exceeds this limit +# gas_report_fail_on_increase = true +``` + +## Fuzz Testing + +```toml +[profile.default] +# Number of fuzz runs +fuzz.runs = 256 + +# Seed for deterministic fuzzing +fuzz.seed = "0x1234" + +# Maximum test rejects before failing +fuzz.max_test_rejects = 65536 + +# Dictionary weight (how much to use discovered values) +fuzz.dictionary_weight = 40 + +# Include push bytes +fuzz.include_push_bytes = true + +# Include storage +fuzz.include_storage = true + +# Show logs +fuzz.show_logs = false +``` + +## Invariant Testing + +```toml +[profile.default] +# Number of runs (sequences) +invariant.runs = 256 + +# Depth (calls per run) +invariant.depth = 15 + +# Fail on revert +invariant.fail_on_revert = false + +# Call override +invariant.call_override = false + +# Dictionary weight +invariant.dictionary_weight = 80 + +# Include storage +invariant.include_storage = true + +# Include push bytes +invariant.include_push_bytes = true + +# Shrink run limit +invariant.shrink_run_limit = 5000 + +# Max fuzz dictionary addresses +invariant.max_fuzz_dictionary_addresses = 15 + +# Max fuzz dictionary values +invariant.max_fuzz_dictionary_values = 10 + +# Gas limit +invariant.gas_limit = 9223372036854775807 +``` + +## Fork Testing + +```toml +[profile.default] +# Default fork URL +# eth_rpc_url = "https://eth-mainnet.alchemyapi.io/v2/..." + +# Fork block number +# fork_block_number = 18000000 + +# Fork retry backoff +fork_retry_backoff = "0" + +# RPC storage caching +rpc_storage_caching = { + chains = "all", + endpoints = "all" +} + +# No storage caching +no_storage_caching = false +``` + +## RPC Endpoints + +```toml +[rpc_endpoints] +mainnet = "https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}" +sepolia = "https://eth-sepolia.g.alchemy.com/v2/${ALCHEMY_KEY}" +arbitrum = "https://arb-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}" +optimism = "https://opt-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}" +polygon = "https://polygon-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}" +base = "https://base-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}" + +# Local +localhost = "http://localhost:8545" +anvil = "http://127.0.0.1:8545" + +# Environment variable interpolation +custom = "${CUSTOM_RPC_URL}" +``` + +## Etherscan Configuration + +```toml +[etherscan] +mainnet = { key = "${ETHERSCAN_API_KEY}" } +sepolia = { key = "${ETHERSCAN_API_KEY}" } +arbitrum = { key = "${ARBISCAN_API_KEY}" } +optimism = { key = "${OPTIMISTIC_ETHERSCAN_API_KEY}" } +polygon = { key = "${POLYGONSCAN_API_KEY}" } +base = { key = "${BASESCAN_API_KEY}" } + +# Custom chain +custom = { key = "${CUSTOM_API_KEY}", url = "https://api.custom-explorer.com/api" } +``` + +## Formatting + +```toml +[fmt] +# Line length +line_length = 120 + +# Tab width +tab_width = 4 + +# Bracket spacing +bracket_spacing = false + +# Int types (preserve, short, long) +int_types = "long" + +# Multiline function header +multiline_func_header = "attributes_first" + +# Quote style +quote_style = "double" + +# Number underscore (preserve, thousands, none) +number_underscore = "preserve" + +# Hex underscore +hex_underscore = "remove" + +# Single line statement blocks +single_line_statement_blocks = "preserve" + +# Override spacing +override_spacing = false + +# Wrap comments +wrap_comments = false + +# Ignore files +ignore = ["src/external/**"] + +# Contract new lines +contract_new_lines = false + +# Sort imports +sort_imports = false +``` + +## Documentation + +```toml +[doc] +# Output directory +out = "docs" + +# Repository link +repository = "https://github.com/user/repo" + +# Ignore patterns +ignore = ["src/test/**"] +``` + +## Profiles + +### Default Profile + +```toml +[profile.default] +src = "src" +out = "out" +libs = ["lib"] +optimizer = true +optimizer_runs = 200 +``` + +### CI Profile + +```toml +[profile.ci] +fuzz.runs = 10000 +invariant.runs = 1000 +invariant.depth = 100 +verbosity = 3 +``` + +### Production Profile + +```toml +[profile.production] +optimizer = true +optimizer_runs = 1000000 +via_ir = true +bytecode_hash = "none" +cbor_metadata = false +``` + +### Gas Optimization Profile + +```toml +[profile.gas] +optimizer = true +optimizer_runs = 1000000 +gas_reports = ["*"] +``` + +### Fast Development Profile + +```toml +[profile.fast] +optimizer = false +fuzz.runs = 100 +invariant.runs = 50 +no_match_test = "testFork_" +``` + +## Using Profiles + +```bash +# Use default profile +forge build +forge test + +# Use CI profile +FOUNDRY_PROFILE=ci forge test + +# Use production profile +FOUNDRY_PROFILE=production forge build +``` + +## Environment Variables + +```bash +# Override any config option +FOUNDRY_SRC=contracts forge build +FOUNDRY_OPTIMIZER=false forge build +FOUNDRY_OPTIMIZER_RUNS=1000000 forge build +FOUNDRY_EVM_VERSION=shanghai forge build + +# Common overrides +FOUNDRY_PROFILE=ci # Select profile +FOUNDRY_FUZZ_RUNS=10000 # Fuzz runs +FOUNDRY_INVARIANT_RUNS=1000 # Invariant runs +FOUNDRY_VERBOSITY=3 # Test verbosity +``` + +## Complete Example + +```toml +# foundry.toml + +[profile.default] +# Project +src = "src" +out = "out" +libs = ["lib"] +test = "test" +script = "script" + +# Compiler +solc = "0.8.30" +evm_version = "prague" +optimizer = true +optimizer_runs = 200 + +# Testing +verbosity = 2 +fuzz.runs = 256 +fuzz.seed = "0x1234" +invariant.runs = 256 +invariant.depth = 50 + +# Gas +gas_reports = ["*"] + +# Output +extra_output = ["storageLayout"] + +# Remappings +remappings = [ + "@openzeppelin/=lib/openzeppelin-contracts/contracts/", + "forge-std/=lib/forge-std/src/" +] + +[profile.ci] +fuzz.runs = 10000 +fuzz.seed = "0xdeadbeef" +invariant.runs = 1000 +invariant.depth = 100 +verbosity = 3 + +[profile.production] +optimizer = true +optimizer_runs = 1000000 +via_ir = true + +[profile.local] +optimizer = false +fuzz.runs = 100 + +[rpc_endpoints] +mainnet = "${MAINNET_RPC_URL}" +sepolia = "${SEPOLIA_RPC_URL}" +arbitrum = "${ARBITRUM_RPC_URL}" +optimism = "${OPTIMISM_RPC_URL}" +base = "${BASE_RPC_URL}" +localhost = "http://127.0.0.1:8545" + +[etherscan] +mainnet = { key = "${ETHERSCAN_API_KEY}" } +sepolia = { key = "${ETHERSCAN_API_KEY}" } +arbitrum = { key = "${ARBISCAN_API_KEY}" } +optimism = { key = "${OPTIMISTIC_ETHERSCAN_API_KEY}" } +base = { key = "${BASESCAN_API_KEY}" } + +[fmt] +line_length = 120 +tab_width = 4 +bracket_spacing = false +int_types = "long" +multiline_func_header = "attributes_first" +quote_style = "double" +number_underscore = "thousands" +sort_imports = true +``` diff --git a/skills/skill/references/debugging.md b/skills/skill/references/debugging.md new file mode 100644 index 0000000..574d99f --- /dev/null +++ b/skills/skill/references/debugging.md @@ -0,0 +1,251 @@ +# Debugging Workflows + +Foundry debugging tools for finding and fixing smart contract issues. + +## Verbosity Levels + +```bash +forge test # Summary only +forge test -v # Show passing test names +forge test -vv # Print logs for all tests +forge test -vvv # Traces for failing tests +forge test -vvvv # Traces for all tests + setup +forge test -vvvvv # All traces + storage changes +``` + +**Use:** +- `-vv`: Quick check of console.log output +- `-vvv`: First step when test fails +- `-vvvv`: Full debugging with all traces + +## Understanding Traces + +### Trace Format + +``` +[24661] TestContract::testFunction() +├─ [2262] Target::read() +│ └─ ← 0 +├─ [20398] Target::write(42) +│ └─ ← () +└─ ← () +``` + +- `[gas]`: Gas consumed by call and nested calls +- **Green**: Successful calls +- **Red**: Reverting calls +- **Blue**: Cheatcode calls +- **Cyan**: Emitted logs +- **Yellow**: Contract deployments + +### Common Trace Errors + +| Error | Meaning | +|-------|---------| +| `OOG` | Out of gas | +| `Revert` | Transaction reverted | +| `InvalidFEOpcode` | Unknown opcode (0xFE) | +| `NotActivated` | EVM feature not available | + +## Console Logging + +### Basic Usage + +```solidity +import {console} from "forge-std/console.sol"; + +function test_Debug() public { + console.log("Value:", value); + console.log("Address:", msg.sender); + console.log("Balance:", address(this).balance); +} +``` + +### Format Specifiers + +```solidity +console.log("String: %s", "hello"); +console.log("Decimal: %d", 123); +console.log("Hex: %x", 255); + +// Multiple arguments (up to 4) +console.log("From %s to %s: %d", from, to, amount); + +// Type-specific +console.logBytes32(hash); +console.logAddress(token); +console.logBool(success); +``` + +### Debugging Pattern + +```solidity +function test_Transfer() public { + console.log("=== Transfer ==="); + console.log("From:", from); + console.log("To:", to); + console.log("Amount:", amount); + + token.transfer(to, amount); + + console.log("Balance after:", token.balanceOf(to)); +} +``` + +## Breakpoints + +Set breakpoints in code, jump to them in debugger: + +```solidity +function test_Complex() public { + vm.breakpoint("start"); + uint256 x = calculate(); + + vm.breakpoint("middle"); + process(x); + + vm.breakpoint("end"); +} +``` + +In debugger, press `'` + letter to jump (e.g., `'a` for first breakpoint). + +## Interactive Debugger + +### Starting + +```bash +# Debug specific test +forge test --debug --match-test "test_Function" + +# Debug script +forge script script/Deploy.s.sol --debug + +# Debug transaction from chain +cast run --debug 0x123... +``` + +### Debugger Layout + +Four quadrants: +1. **Top-left**: EVM opcodes (current instruction highlighted) +2. **Top-right**: Stack state +3. **Bottom-left**: Solidity source code +4. **Bottom-right**: Memory contents + +### Navigation Keys + +**Movement:** +- `j/k`: Step forward/backward +- `g/G`: Go to beginning/end +- `c/C`: Next/previous CALL instruction +- `'a-z`: Jump to breakpoint + +**Display:** +- `t`: Toggle stack labels +- `m`: Toggle memory as UTF8 +- `h`: Help +- `q`: Quit + +### Debugging Workflow + +```bash +# 1. Test fails +forge test --match-test "test_Deposit" + +# 2. See what failed +forge test -vvv --match-test "test_Deposit" + +# 3. Add console.log for quick debugging +# OR use interactive debugger +forge test --debug --match-test "test_Deposit" + +# 4. Step through with j/k, watch stack with t +``` + +## Gas Profiling + +### Gas Reports + +```bash +forge test --gas-report +``` + +Output: +``` +│ Function │ min │ avg │ median │ max │ calls │ +├─────────────┼───────┼───────┼────────┼───────┼───────┤ +│ transfer │ 2900 │ 5234 │ 5200 │ 8901 │ 145 │ +│ balanceOf │ 596 │ 596 │ 596 │ 596 │ 234 │ +``` + +### Gas Snapshots + +```bash +forge snapshot # Create snapshot +forge snapshot --diff # Compare to previous +forge snapshot --check # Fail if changed +``` + +### Inline Measurement + +```solidity +function test_GasUsage() public { + uint256 gasBefore = gasleft(); + + contract.operation(); + + uint256 gasUsed = gasBefore - gasleft(); + console.log("Gas used:", gasUsed); +} +``` + +## Common Error Patterns + +### Assertion Failure + +``` +AssertionError: a == b + Expected: 1000000 + Actual: 500000 +``` + +**Debug:** Run with `-vvv` to trace calculation. + +### Revert Without Message + +``` +Error: reverted +``` + +**Debug:** +1. Run with `-vvvv` for full trace +2. Find red (reverting) call in trace +3. Add console.log before suspect operations + +### Out of Gas + +``` +Error: OutOfGas +``` + +**Fix:** Reduce loop iterations or split into batches. + +### Custom Error + +```solidity +vm.expectRevert(abi.encodeWithSelector( + InsufficientBalance.selector, + 1000, // have + 2000 // need +)); +token.transfer(recipient, 2000); +``` + +## Best Practices + +1. **Start simple**: Use console.log before --debug +2. **Isolate tests**: Test one thing per test function +3. **Use descriptive logs**: Log state at each step +4. **Check assumptions**: Verify preconditions +5. **Save traces**: `forge test -vvvv > trace.txt` diff --git a/skills/skill/references/dependencies.md b/skills/skill/references/dependencies.md new file mode 100644 index 0000000..169c1b4 --- /dev/null +++ b/skills/skill/references/dependencies.md @@ -0,0 +1,203 @@ +# Dependency Management + +Managing dependencies in Foundry using git submodules and Soldeer. + +## Git Submodules (Default) + +### Installing Dependencies + +```bash +# Install latest master +forge install vectorized/solady + +# Install specific tag +forge install vectorized/solady@v0.0.265 + +# Install specific commit +forge install vectorized/solady@a5bb996e91aae5b0c068087af7594d92068b12f1 + +# No automatic commit (for CI) +forge install OpenZeppelin/openzeppelin-contracts --no-commit +``` + +Dependencies are cloned to `lib/[name]`. + +### Remappings + +Forge auto-generates remappings: + +```bash +$ forge remappings +forge-std/=lib/forge-std/src/ +solady/=lib/solady/src/ +``` + +Use in imports: + +```solidity +import {Test} from "forge-std/Test.sol"; +import {ERC20} from "solady/tokens/ERC20.sol"; +``` + +### Custom Remappings + +Create `remappings.txt`: + +``` +@openzeppelin/=lib/openzeppelin-contracts/contracts/ +@solmate/=lib/solmate/src/ +forge-std/=lib/forge-std/src/ +``` + +Or in `foundry.toml`: + +```toml +[profile.default] +remappings = [ + "@openzeppelin/=lib/openzeppelin-contracts/contracts/", + "@solmate/=lib/solmate/src/" +] +``` + +### Updating Dependencies + +```bash +# Update specific dependency +forge update lib/solady + +# Update all +forge update +``` + +### Removing Dependencies + +```bash +forge remove solady +# or +forge remove lib/solady +``` + +## Soldeer (Modern Package Manager) + +Soldeer provides npm-style dependency management with a central registry. + +### Initialize + +```bash +forge soldeer init +``` + +Creates `dependencies/` folder and configures `foundry.toml`. + +### Installing Packages + +```bash +# From registry (soldeer.xyz) +forge soldeer install @openzeppelin-contracts~5.0.2 +forge soldeer install forge-std~1.8.1 + +# From URL +forge soldeer install @custom~1.0.0 --url https://example.com/lib.zip + +# From git +forge soldeer install lib~1.0 --git https://github.com/org/lib.git --tag v1.0 +``` + +Configuration in `foundry.toml`: + +```toml +[profile.default] +libs = ["dependencies"] + +[dependencies] +"@openzeppelin-contracts" = { version = "5.0.2" } +forge-std = { version = "1.8.1" } +``` + +### Updating + +```bash +forge soldeer update +forge soldeer update --regenerate-remappings +``` + +### Removing + +```bash +forge soldeer uninstall @openzeppelin-contracts +``` + +### Soldeer Config + +```toml +[soldeer] +remappings_generate = true +remappings_version = true # @lib-5.0.2 suffix +remappings_prefix = "@" # @lib instead of lib +remappings_location = "txt" # or "config" +recursive_deps = true # Install sub-dependencies +``` + +## Comparison + +| Feature | Git Submodules | Soldeer | +|---------|----------------|---------| +| Setup | Simple | Requires config | +| Version pinning | Commit hash | Semantic version | +| Central registry | No | Yes (soldeer.xyz) | +| Team sharing | .gitmodules | foundry.toml | +| Private repos | Full support | URL only | + +## Best Practices + +### Version Pinning + +```bash +# Production: Use specific tag +forge install openzeppelin/openzeppelin-contracts@v5.0.0 + +# Development: Can use master +forge install vectorized/solady +``` + +### Handling Conflicts + +When dependencies have conflicting versions, use remapping contexts: + +``` +# remappings.txt +lib/lib_1/:@openzeppelin/=lib/lib_1/node_modules/@openzeppelin/ +lib/lib_2/:@openzeppelin/=lib/lib_2/node_modules/@openzeppelin/ +``` + +### CI Configuration + +```bash +# After clone, init submodules +git submodule update --init --recursive + +# Or use --no-commit during install +forge install OpenZeppelin/openzeppelin-contracts --no-commit +``` + +### Hardhat Compatibility + +```bash +# Enable node_modules support +forge install --hh +``` + +## Troubleshooting + +**Submodule not found after clone:** +```bash +git submodule update --init --recursive +``` + +**Remapping not working:** +```bash +forge remappings > remappings.txt +``` + +**Soldeer package missing:** +Check [soldeer.xyz](https://soldeer.xyz) or publish your own. diff --git a/skills/skill/references/deployment.md b/skills/skill/references/deployment.md new file mode 100644 index 0000000..b023671 --- /dev/null +++ b/skills/skill/references/deployment.md @@ -0,0 +1,554 @@ +# Foundry Deployment Guide + +Complete guide to deploying and verifying smart contracts with Foundry. + +## forge create (Single Contract) + +Quick deployment for single contracts. + +### Basic Usage + +```bash +# Deploy with constructor args +forge create src/Token.sol:Token \ + --rpc-url sepolia \ + --private-key $PRIVATE_KEY \ + --constructor-args "MyToken" "MTK" 18 + +# Deploy and verify +forge create src/Token.sol:Token \ + --rpc-url sepolia \ + --private-key $PRIVATE_KEY \ + --broadcast \ + --verify \ + --etherscan-api-key $ETHERSCAN_API_KEY \ + --constructor-args "MyToken" "MTK" 18 + +# Deploy with value (payable constructor) +forge create src/Vault.sol:Vault \ + --rpc-url sepolia \ + --private-key $PRIVATE_KEY \ + --value 1ether +``` + +### Using Ledger + +```bash +forge create src/Token.sol:Token \ + --rpc-url mainnet \ + --ledger \ + --mnemonic-derivation-path "m/44'/60'/0'/0/0" +``` + +## Solidity Scripts (Recommended) + +More powerful and flexible deployment method. + +### Basic Deploy Script + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.30; + +import {Script, console} from "forge-std/Script.sol"; +import {Token} from "../src/Token.sol"; + +contract DeployToken is Script { + function run() external { + // Load private key from environment + uint256 deployerKey = vm.envUint("PRIVATE_KEY"); + address deployer = vm.addr(deployerKey); + + console.log("Deploying from:", deployer); + console.log("Balance:", deployer.balance); + + vm.startBroadcast(deployerKey); + + Token token = new Token("MyToken", "MTK", 18); + console.log("Token deployed to:", address(token)); + + // Initial setup + token.mint(deployer, 1_000_000e18); + + vm.stopBroadcast(); + } +} +``` + +### Running Scripts + +```bash +# Dry run (simulation only) +forge script script/Deploy.s.sol:DeployToken --rpc-url sepolia + +# Broadcast transactions +forge script script/Deploy.s.sol:DeployToken \ + --rpc-url sepolia \ + --broadcast + +# Broadcast and verify +forge script script/Deploy.s.sol:DeployToken \ + --rpc-url sepolia \ + --broadcast \ + --verify + +# Resume failed deployment +forge script script/Deploy.s.sol:DeployToken \ + --rpc-url sepolia \ + --broadcast \ + --resume +``` + +### Script Execution Phases + +1. **Local Simulation**: Run script, collect `vm.broadcast()` transactions +2. **On-chain Simulation**: (with `--rpc-url`) Simulate against chain state +3. **Broadcasting**: (with `--broadcast`) Send transactions to network +4. **Verification**: (with `--verify`) Verify contracts on Etherscan + +### Complex Deployment + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.30; + +import {Script, console} from "forge-std/Script.sol"; +import {Token} from "../src/Token.sol"; +import {Staking} from "../src/Staking.sol"; +import {Governance} from "../src/Governance.sol"; + +contract DeployProtocol is Script { + function run() external { + uint256 deployerKey = vm.envUint("PRIVATE_KEY"); + address admin = vm.envAddress("ADMIN_ADDRESS"); + + vm.startBroadcast(deployerKey); + + // Deploy token + Token token = new Token("Protocol Token", "PTK", 18); + console.log("Token:", address(token)); + + // Deploy staking with token reference + Staking staking = new Staking(address(token)); + console.log("Staking:", address(staking)); + + // Deploy governance with token and staking + Governance gov = new Governance( + address(token), + address(staking), + admin + ); + console.log("Governance:", address(gov)); + + // Setup permissions + token.grantRole(token.MINTER_ROLE(), address(staking)); + staking.setGovernance(address(gov)); + + vm.stopBroadcast(); + } +} +``` + +### Configuration-Based Deployment + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.30; + +import {Script, console} from "forge-std/Script.sol"; +import {Token} from "../src/Token.sol"; + +contract DeployConfigured is Script { + struct Config { + string name; + string symbol; + uint256 initialSupply; + address admin; + } + + function run() external { + Config memory config = getConfig(); + + uint256 deployerKey = vm.envUint("PRIVATE_KEY"); + + vm.startBroadcast(deployerKey); + + Token token = new Token(config.name, config.symbol, 18); + token.mint(config.admin, config.initialSupply); + + if (config.admin != vm.addr(deployerKey)) { + token.transferOwnership(config.admin); + } + + vm.stopBroadcast(); + + console.log("Deployed:", address(token)); + } + + function getConfig() internal view returns (Config memory) { + uint256 chainId = block.chainid; + + if (chainId == 1) { + return Config({ + name: "Production Token", + symbol: "PROD", + initialSupply: 100_000_000e18, + admin: 0x1234567890123456789012345678901234567890 + }); + } else if (chainId == 11155111) { + return Config({ + name: "Test Token", + symbol: "TEST", + initialSupply: 1_000_000e18, + admin: vm.envAddress("TEST_ADMIN") + }); + } else { + revert("Unsupported chain"); + } + } +} +``` + +### Deterministic Deployment (CREATE2) + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.30; + +import {Script, console} from "forge-std/Script.sol"; +import {Token} from "../src/Token.sol"; + +contract DeployDeterministic is Script { + // Deterministic deployment factory (present on most chains) + address constant CREATE2_FACTORY = 0x4e59b44847b379578588920cA78FbF26c0B4956C; + + function run() external { + uint256 deployerKey = vm.envUint("PRIVATE_KEY"); + bytes32 salt = keccak256("my-token-v1"); + + // Predict address + bytes memory bytecode = abi.encodePacked( + type(Token).creationCode, + abi.encode("MyToken", "MTK", 18) + ); + address predicted = computeCreate2Address(salt, keccak256(bytecode)); + console.log("Predicted address:", predicted); + + // Check if already deployed + if (predicted.code.length > 0) { + console.log("Already deployed!"); + return; + } + + vm.startBroadcast(deployerKey); + + Token token = new Token{salt: salt}("MyToken", "MTK", 18); + require(address(token) == predicted, "Address mismatch"); + + vm.stopBroadcast(); + + console.log("Deployed to:", address(token)); + } + + function computeCreate2Address(bytes32 salt, bytes32 initCodeHash) + internal + view + returns (address) + { + return address(uint160(uint256(keccak256(abi.encodePacked( + bytes1(0xff), + address(this), + salt, + initCodeHash + ))))); + } +} +``` + +## Multi-Chain Deployment + +### Sequential Deployment + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.30; + +import {Script, console} from "forge-std/Script.sol"; +import {Token} from "../src/Token.sol"; + +contract DeployMultiChain is Script { + function run() external { + uint256 deployerKey = vm.envUint("PRIVATE_KEY"); + + // Deploy to Ethereum + vm.createSelectFork("mainnet"); + vm.startBroadcast(deployerKey); + Token mainnetToken = new Token("Token", "TKN", 18); + vm.stopBroadcast(); + console.log("Mainnet:", address(mainnetToken)); + + // Deploy to Arbitrum + vm.createSelectFork("arbitrum"); + vm.startBroadcast(deployerKey); + Token arbitrumToken = new Token("Token", "TKN", 18); + vm.stopBroadcast(); + console.log("Arbitrum:", address(arbitrumToken)); + + // Deploy to Optimism + vm.createSelectFork("optimism"); + vm.startBroadcast(deployerKey); + Token optimismToken = new Token("Token", "TKN", 18); + vm.stopBroadcast(); + console.log("Optimism:", address(optimismToken)); + } +} +``` + +Run with: + +```bash +forge script script/DeployMultiChain.s.sol \ + --broadcast \ + --multi \ + --slow \ + --verify +``` + +## Contract Verification + +### Auto-Verification + +```bash +# During deployment +forge create src/Token.sol:Token \ + --rpc-url sepolia \ + --private-key $KEY \ + --verify \ + --etherscan-api-key $ETHERSCAN_KEY \ + --constructor-args "Name" "SYM" 18 + +# With script +forge script script/Deploy.s.sol --broadcast --verify +``` + +### Manual Verification + +```bash +# Verify existing contract +forge verify-contract \ + --chain sepolia \ + --compiler-version 0.8.30 \ + --num-of-optimizations 200 \ + --constructor-args $(cast abi-encode "constructor(string,string,uint8)" "Name" "SYM" 18) \ + 0xYourContractAddress \ + src/Token.sol:Token + +# Check verification status +forge verify-check \ + --chain sepolia \ + $GUID +``` + +### Verification with Libraries + +```bash +forge verify-contract \ + --chain mainnet \ + --libraries src/lib/Math.sol:Math:0xLibraryAddress \ + --libraries src/lib/Utils.sol:Utils:0xUtilsAddress \ + 0xContractAddress \ + src/MyContract.sol:MyContract +``` + +### Configuration for Verification + +```toml +# foundry.toml +[etherscan] +mainnet = { key = "${ETHERSCAN_API_KEY}" } +sepolia = { key = "${ETHERSCAN_API_KEY}" } +arbitrum = { key = "${ARBISCAN_API_KEY}" } +optimism = { key = "${OPTIMISTIC_ETHERSCAN_API_KEY}" } +base = { key = "${BASESCAN_API_KEY}" } +polygon = { key = "${POLYGONSCAN_API_KEY}" } +``` + +## Broadcast Artifacts + +Scripts save transaction data to `broadcast/` directory: + +``` +broadcast/ +└── Deploy.s.sol/ + └── 11155111/ # Chain ID + ├── run-latest.json # Latest run + ├── run-1699999999.json # Timestamped runs + └── receipts/ + └── ... +``` + +### Reading Artifacts in Scripts + +```solidity +function readDeployment() internal view returns (address) { + string memory root = vm.projectRoot(); + string memory path = string.concat( + root, + "/broadcast/Deploy.s.sol/11155111/run-latest.json" + ); + + string memory json = vm.readFile(path); + bytes memory contractAddr = json.parseRaw(".transactions[0].contractAddress"); + return abi.decode(contractAddr, (address)); +} +``` + +## Best Practices + +### 1. Use Environment Variables + +```solidity +uint256 deployerKey = vm.envUint("PRIVATE_KEY"); +address admin = vm.envAddress("ADMIN_ADDRESS"); +string memory rpcUrl = vm.envString("RPC_URL"); +``` + +### 2. Validate Before Broadcasting + +```solidity +function run() external { + uint256 deployerKey = vm.envUint("PRIVATE_KEY"); + address deployer = vm.addr(deployerKey); + + // Pre-flight checks + require(deployer.balance > 0.1 ether, "Insufficient balance"); + require(block.chainid == 11155111, "Wrong network"); + + vm.startBroadcast(deployerKey); + // ... +} +``` + +### 3. Log Everything + +```solidity +vm.startBroadcast(deployerKey); + +Token token = new Token("Name", "SYM", 18); +console.log("Token deployed to:", address(token)); +console.log(" - Name:", token.name()); +console.log(" - Symbol:", token.symbol()); +console.log(" - Owner:", token.owner()); + +vm.stopBroadcast(); +``` + +### 4. Use Numbered Scripts + +``` +script/ +├── 01_DeployToken.s.sol +├── 02_DeployStaking.s.sol +├── 03_ConfigurePermissions.s.sol +└── 04_TransferOwnership.s.sol +``` + +### 5. Test Scripts + +```solidity +contract DeployTokenTest is Test { + DeployToken deployer; + + function setUp() public { + deployer = new DeployToken(); + // Set required env vars + vm.setEnv("PRIVATE_KEY", vm.toString(uint256(1))); + } + + function testDeploy() public { + deployer.run(); + // Verify deployment + } +} +``` + +### 6. Handle Failures Gracefully + +```solidity +function run() external { + uint256 deployerKey = vm.envUint("PRIVATE_KEY"); + + vm.startBroadcast(deployerKey); + + try new Token("Name", "SYM", 18) returns (Token token) { + console.log("Success:", address(token)); + } catch Error(string memory reason) { + console.log("Failed:", reason); + } + + vm.stopBroadcast(); +} +``` + +## Upgrade Patterns + +### Transparent Proxy + +```solidity +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; + +contract DeployUpgradeable is Script { + function run() external { + uint256 deployerKey = vm.envUint("PRIVATE_KEY"); + + vm.startBroadcast(deployerKey); + + // Deploy implementation + TokenV1 impl = new TokenV1(); + + // Deploy proxy admin + ProxyAdmin admin = new ProxyAdmin(msg.sender); + + // Deploy proxy + bytes memory initData = abi.encodeCall(TokenV1.initialize, ("Name", "SYM")); + TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy( + address(impl), + address(admin), + initData + ); + + vm.stopBroadcast(); + + console.log("Implementation:", address(impl)); + console.log("ProxyAdmin:", address(admin)); + console.log("Proxy:", address(proxy)); + } +} +``` + +### UUPS Proxy + +```solidity +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +contract DeployUUPS is Script { + function run() external { + uint256 deployerKey = vm.envUint("PRIVATE_KEY"); + + vm.startBroadcast(deployerKey); + + // Deploy implementation + TokenV1 impl = new TokenV1(); + + // Deploy proxy + bytes memory initData = abi.encodeCall(TokenV1.initialize, ("Name", "SYM")); + ERC1967Proxy proxy = new ERC1967Proxy(address(impl), initData); + + vm.stopBroadcast(); + + console.log("Implementation:", address(impl)); + console.log("Proxy:", address(proxy)); + } +} +``` diff --git a/skills/skill/references/forge-std-api.md b/skills/skill/references/forge-std-api.md new file mode 100644 index 0000000..452bd5d --- /dev/null +++ b/skills/skill/references/forge-std-api.md @@ -0,0 +1,567 @@ +# forge-std API Reference + +Complete reference for the Forge Standard Library. + +## Overview + +```solidity +import {Test, console} from "forge-std/Test.sol"; +import {Script} from "forge-std/Script.sol"; +``` + +`Test` inherits: `StdAssertions`, `StdChains`, `StdCheats`, `StdInvariant`, `StdUtils` + +## StdAssertions + +### Boolean + +```solidity +assertTrue(bool condition); +assertTrue(bool condition, string memory err); + +assertFalse(bool condition); +assertFalse(bool condition, string memory err); +``` + +### Equality + +```solidity +// Works with: bool, uint256, int256, address, bytes32, string, bytes +assertEq(T left, T right); +assertEq(T left, T right, string memory err); + +assertNotEq(T left, T right); +assertNotEq(T left, T right, string memory err); + +// Arrays +assertEq(T[] memory left, T[] memory right); +``` + +### Comparison + +```solidity +// Works with: uint256, int256 +assertLt(T left, T right); // < +assertLt(T left, T right, string memory err); + +assertGt(T left, T right); // > +assertGt(T left, T right, string memory err); + +assertLe(T left, T right); // <= +assertLe(T left, T right, string memory err); + +assertGe(T left, T right); // >= +assertGe(T left, T right, string memory err); +``` + +### Decimal Formatting + +```solidity +// Shows values with decimal places in error messages +assertEqDecimal(uint256 left, uint256 right, uint256 decimals); +assertNotEqDecimal(uint256 left, uint256 right, uint256 decimals); +assertLtDecimal(uint256 left, uint256 right, uint256 decimals); +assertGtDecimal(uint256 left, uint256 right, uint256 decimals); +assertLeDecimal(uint256 left, uint256 right, uint256 decimals); +assertGeDecimal(uint256 left, uint256 right, uint256 decimals); + +// Example +assertEqDecimal(1e18, 1e18, 18); // Shows "1.0" not "1000000000000000000" +``` + +### Approximation + +```solidity +// Absolute difference +assertApproxEqAbs(uint256 left, uint256 right, uint256 maxDelta); +assertApproxEqAbs(uint256 left, uint256 right, uint256 maxDelta, string memory err); + +// Relative difference (maxPercentDelta: 1e18 = 100%) +assertApproxEqRel(uint256 left, uint256 right, uint256 maxPercentDelta); +assertApproxEqRel(uint256 left, uint256 right, uint256 maxPercentDelta, string memory err); + +// Examples +assertApproxEqAbs(1000, 1005, 10); // Pass: |1000-1005| <= 10 +assertApproxEqRel(100, 101, 0.02e18); // Pass: 1% diff <= 2% +``` + +### Call Comparison + +```solidity +assertEqCall(address target, bytes memory callDataA, bytes memory callDataB); +assertEqCall(address targetA, bytes memory callDataA, address targetB, bytes memory callDataB); +``` + +### Failure + +```solidity +fail(); +fail(string memory err); +bool hasFailed = failed(); +``` + +## StdCheats + +### Address Creation + +```solidity +// Create labeled address +address alice = makeAddr("alice"); + +// Create address with private key +(address bob, uint256 bobKey) = makeAddrAndKey("bob"); + +// Create account struct +Account memory account = makeAccount("charlie"); +// account.addr, account.key +``` + +### Account Setup + +```solidity +// ETH +deal(address to, uint256 amount); + +// ERC20 +deal(address token, address to, uint256 amount); +deal(address token, address to, uint256 amount, bool adjustTotalSupply); + +// ERC721 +dealERC721(address token, address to, uint256 tokenId); + +// ERC1155 +dealERC1155(address token, address to, uint256 id, uint256 amount); +dealERC1155(address token, address to, uint256 id, uint256 amount, bool adjustTotalSupply); +``` + +### Time Manipulation + +```solidity +skip(uint256 seconds); // Move forward +rewind(uint256 seconds); // Move backward + +// Examples +skip(1 days); +skip(1 hours); +rewind(30 minutes); +``` + +### Prank with ETH (hoax) + +```solidity +// Single call as sender with ETH +hoax(address sender); +hoax(address sender, uint256 give); +hoax(address sender, address origin); +hoax(address sender, address origin, uint256 give); + +// Multiple calls +startHoax(address sender); +startHoax(address sender, uint256 give); +// ... calls ... +vm.stopPrank(); + +// Example +hoax(alice, 10 ether); +vault.deposit{value: 1 ether}(); +``` + +### Code Deployment + +```solidity +// Deploy from artifacts +address deployed = deployCode("ContractName.sol"); +address deployed = deployCode("ContractName.sol:ContractName"); +address deployed = deployCode("ContractName.sol", constructorArgs); +address deployed = deployCode("ContractName.sol", constructorArgs, value); + +// Deploy to specific address +deployCodeTo("ContractName.sol", targetAddress); +deployCodeTo("ContractName.sol", constructorArgs, targetAddress); +``` + +### Assumptions + +```solidity +// Address type checks +assumeNotZeroAddress(address addr); +assumeNotPrecompile(address addr); +assumeNotPrecompile(address addr, uint256 chainId); +assumeNotForgeAddress(address addr); +assumePayable(address addr); +assumeNotPayable(address addr); + +// Token blacklists +assumeNotBlacklisted(address token, address addr); + +// Combined checks +assumeAddressIsNot(address addr, AddressType t); +assumeAddressIsNot(address addr, AddressType t1, AddressType t2); + +// AddressType enum: ZeroAddress, Precompile, ForgeAddress +``` + +### Fork Detection + +```solidity +bool forking = isFork(); + +// Modifiers +function testOnlyLocal() public skipWhenForking { } +function testOnlyForked() public skipWhenNotForking { } +``` + +### Gas Metering + +```solidity +// Disable gas metering for expensive setup +modifier noGasMetering; + +function testExpensiveSetup() public noGasMetering { + // Gas not counted +} +``` + +### Account Destruction + +```solidity +destroyAccount(address target, address beneficiary); +``` + +## StdStorage + +Dynamic storage slot finding and manipulation. + +### Setup + +```solidity +using stdStorage for StdStorage; +``` + +### Finding Slots + +```solidity +// Simple variable +uint256 slot = stdstore + .target(address(contract)) + .sig("variableName()") + .find(); + +// Mapping +uint256 slot = stdstore + .target(address(contract)) + .sig("balances(address)") + .with_key(user) + .find(); + +// Nested mapping +uint256 slot = stdstore + .target(address(contract)) + .sig("allowances(address,address)") + .with_key(owner) + .with_key(spender) + .find(); + +// Struct field +uint256 slot = stdstore + .target(address(contract)) + .sig("structVar()") + .depth(0) // Field index + .find(); +``` + +### Writing Values + +```solidity +stdstore + .target(address(contract)) + .sig("balances(address)") + .with_key(user) + .checked_write(1000e18); + +// For int256 +stdstore + .target(address(contract)) + .sig("delta()") + .checked_write_int(-100); +``` + +### Example + +```solidity +function testSetBalance() public { + // Set alice's balance to 1000 tokens + stdstore + .target(address(token)) + .sig("balanceOf(address)") + .with_key(alice) + .checked_write(1000e18); + + assertEq(token.balanceOf(alice), 1000e18); +} +``` + +## StdUtils + +### Bounded Randomness + +```solidity +// Constrain fuzz input to range +uint256 bounded = bound(uint256 x, uint256 min, uint256 max); +int256 bounded = bound(int256 x, int256 min, int256 max); + +// Constrain to valid private key range +uint256 key = boundPrivateKey(uint256 pk); + +// Example +function testFuzz(uint256 amount) public { + amount = bound(amount, 1, 1000); + // amount is now in [1, 1000] +} +``` + +### Address Computation + +```solidity +// CREATE address +address addr = computeCreateAddress(address deployer, uint256 nonce); + +// CREATE2 address +address addr = computeCreate2Address(bytes32 salt, bytes32 initCodeHash, address deployer); +address addr = computeCreate2Address(bytes32 salt, bytes32 initCodeHash); // Uses CREATE2_FACTORY + +// Init code hash +bytes32 hash = hashInitCode(bytes memory creationCode); +bytes32 hash = hashInitCode(bytes memory creationCode, bytes memory args); + +// Example +bytes32 initHash = hashInitCode(type(MyContract).creationCode); +address predicted = computeCreate2Address(salt, initHash, factory); +``` + +### Token Utilities + +```solidity +// Batch balance query (uses Multicall3) +uint256[] memory balances = getTokenBalances(address token, address[] memory addresses); +``` + +### Byte Conversion + +```solidity +uint256 value = bytesToUint(bytes memory b); +``` + +## StdJson + +### Reading + +```solidity +using stdJson for string; + +string memory json = vm.readFile("data.json"); + +// Single values +uint256 amount = json.readUint(".amount"); +int256 balance = json.readInt(".balance"); +address addr = json.readAddress(".recipient"); +bytes32 hash = json.readBytes32(".hash"); +string memory name = json.readString(".name"); +bytes memory data = json.readBytes(".data"); +bool flag = json.readBool(".enabled"); + +// Arrays +uint256[] memory amounts = json.readUintArray(".amounts"); +address[] memory addrs = json.readAddressArray(".addresses"); +string[] memory names = json.readStringArray(".names"); + +// With defaults +uint256 amount = json.readUintOr(".amount", 100); +address addr = json.readAddressOr(".recipient", address(0)); + +// Check existence +bool exists = json.keyExists(".key"); + +// Raw bytes +bytes memory raw = json.parseRaw(".data"); +``` + +### Writing + +```solidity +using stdJson for string; + +string memory json = "obj"; +json = json.serialize("amount", uint256(100)); +json = json.serialize("recipient", address(0x123)); +json = json.serialize("enabled", true); +json = json.serialize("amounts", amounts); + +json.write("output.json"); +json.write("output.json", ".config"); +``` + +## StdToml + +Identical API to StdJson: + +```solidity +using stdToml for string; + +string memory toml = vm.readFile("config.toml"); +uint256 runs = toml.readUint(".profile.default.fuzz_runs"); +``` + +## StdChains + +Access chain configuration: + +```solidity +Chain memory chain = getChain("mainnet"); +// chain.name, chain.chainId, chain.rpcUrl + +Chain memory chain = getChain(1); // By chain ID + +// Set custom RPC +setChain("custom", ChainData({ + name: "Custom Chain", + chainId: 12345, + rpcUrl: "https://..." +})); +``` + +## StdInvariant + +For invariant testing targets: + +```solidity +// Target contracts for fuzzing +targetContract(address); +targetContracts(); // Returns address[] + +// Exclude from fuzzing +excludeContract(address); +excludeContracts(); // Returns address[] + +// Target senders +targetSender(address); +targetSenders(); // Returns address[] + +// Exclude senders +excludeSender(address); +excludeSenders(); // Returns address[] + +// Target specific selectors +targetSelector(FuzzSelector memory); +targetSelectors(); // Returns FuzzSelector[] + +// Target artifacts (deploy and fuzz) +targetArtifact(string memory); +targetArtifacts(); // Returns string[] + +// Target artifact selectors +targetArtifactSelector(FuzzArtifactSelector memory); +targetArtifactSelectors(); // Returns FuzzArtifactSelector[] +``` + +## StdError + +Common error selectors: + +```solidity +import {stdError} from "forge-std/StdError.sol"; + +vm.expectRevert(stdError.arithmeticError); // Overflow/underflow +vm.expectRevert(stdError.assertionError); // assert() failed +vm.expectRevert(stdError.divisionError); // Division by zero +vm.expectRevert(stdError.encodeStorageError); // Storage encoding +vm.expectRevert(stdError.enumConversionError); // Invalid enum +vm.expectRevert(stdError.indexOOBError); // Array index out of bounds +vm.expectRevert(stdError.memOverflowError); // Memory overflow +vm.expectRevert(stdError.popEmptyArrayError); // Pop empty array +vm.expectRevert(stdError.zeroVarError); // Zero-initialized function pointer +``` + +## StdMath + +```solidity +import {stdMath} from "forge-std/StdMath.sol"; + +uint256 absolute = stdMath.abs(int256 x); +uint256 delta = stdMath.delta(uint256 a, uint256 b); +uint256 delta = stdMath.delta(int256 a, int256 b); +uint256 percent = stdMath.percentDelta(uint256 a, uint256 b); +``` + +## Console Logging + +```solidity +import {console} from "forge-std/console.sol"; +// or +import {console2} from "forge-std/console2.sol"; // Smaller bytecode + +console.log("message"); +console.log("value:", value); +console.log("a:", a, "b:", b); + +// Type-specific +console.log(uint256 x); +console.log(int256 x); +console.log(address x); +console.log(bool x); +console.log(string memory x); +console.log(bytes memory x); +console.log(bytes32 x); + +// Formatted +console.logBytes(bytes memory); +console.logBytes1(bytes1); +// ... up to logBytes32 +``` + +## Script.sol + +Base for deployment scripts: + +```solidity +import {Script, console} from "forge-std/Script.sol"; + +contract DeployScript is Script { + function setUp() public {} + + function run() public { + uint256 deployerKey = vm.envUint("PRIVATE_KEY"); + + vm.startBroadcast(deployerKey); + + MyContract c = new MyContract(); + c.initialize(); + + vm.stopBroadcast(); + + console.log("Deployed:", address(c)); + } +} +``` + +### Script vs Test + +| Feature | Test | Script | +|---------|------|--------| +| Base | `Test` | `Script` | +| Cheats | Full `StdCheats` | `StdCheatsSafe` | +| Purpose | Testing | Deployment | +| Broadcast | No | Yes | +| State changes | Local | On-chain | + +## Constants + +```solidity +// From CommonBase +address constant VM_ADDRESS = 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D; +address constant CONSOLE = 0x000000000000000000636F6e736F6c652e6c6f67; +address constant CREATE2_FACTORY = 0x4e59b44847b379578588920cA78FbF26c0B4956C; +address constant DEFAULT_SENDER = 0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38; +address constant DEFAULT_TEST_CONTRACT = 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f; +address constant MULTICALL3_ADDRESS = 0xcA11bde05977b3631167028862bE2a173976CA11; +``` diff --git a/skills/skill/references/gas-optimization.md b/skills/skill/references/gas-optimization.md new file mode 100644 index 0000000..72e3bfa --- /dev/null +++ b/skills/skill/references/gas-optimization.md @@ -0,0 +1,333 @@ +# Solidity Gas Optimization Guide + +Comprehensive gas optimization techniques for Solidity 0.8.30 with Foundry profiling. + +## Storage Optimization + +### Variable Packing + +Pack multiple variables into single 32-byte storage slots: + +```solidity +// BAD: 3 storage slots (3 SSTORE operations) +contract Unoptimized { + uint256 amount; // slot 0 + uint8 status; // slot 1 + address owner; // slot 2 +} + +// GOOD: 2 storage slots +contract Optimized { + uint128 amount; // slot 0 (16 bytes) + uint96 lockTime; // slot 0 (12 bytes) + uint8 status; // slot 0 (1 byte) + address owner; // slot 1 (20 bytes) +} +``` + +**Savings:** 15,000+ gas per packed write + +### Struct Packing + +Pack struct fields in declaration order: + +```solidity +// BAD: 3 slots +struct BadPacking { + uint256 a; // slot 0 + uint8 b; // slot 1 + uint256 c; // slot 2 +} + +// GOOD: 2 slots +struct GoodPacking { + uint256 a; // slot 0 + uint128 c; // slot 1 (16 bytes) + uint8 b; // slot 1 (1 byte) +} +``` + +### Storage vs Memory vs Calldata + +**Gas Costs:** +| Operation | Gas | +|-----------|-----| +| SLOAD (cold) | 2,100 | +| SLOAD (warm) | 100 | +| SSTORE (0→non-0) | 20,000 | +| SSTORE (non-0→non-0) | 5,000 | +| MLOAD/MSTORE | 3 per word | +| Calldata read | 3 per byte | + +**Cache storage reads:** + +```solidity +// BAD: 3 SLOAD operations +function process() external returns (uint256) { + return value * 2 + value * 3 + value * 4; +} + +// GOOD: 1 SLOAD + memory operations +function process() external returns (uint256) { + uint256 v = value; + return v * 2 + v * 3 + v * 4; +} +``` + +**Use calldata for external function arrays:** + +```solidity +// BAD: Copies to memory +function batchProcess(uint256[] memory values) external { } + +// GOOD: Reads directly from calldata +function batchProcess(uint256[] calldata values) external { + for (uint256 i; i < values.length; ) { + // Process values[i] + unchecked { i++; } + } +} +``` + +## Arithmetic Optimization + +### Unchecked Blocks + +Solidity 0.8+ adds overflow checks by default (~50-100 gas per operation): + +```solidity +// BAD: Overflow check on every increment +for (uint256 i = 0; i < 100; i++) { + result += i; +} + +// GOOD: Unchecked increment (safe when bounded) +for (uint256 i = 0; i < 100; ) { + result += i; + unchecked { i++; } +} +``` + +**Savings:** 50-100 gas per iteration + +### Short-Circuit Logic + +Order conditions by likelihood to fail: + +```solidity +// Expensive check last (short-circuits if first fails) +if (amount > 0 && expensiveValidation(amount)) { + // ... +} +``` + +### Bitwise Flag Packing + +Combine booleans into single uint256: + +```solidity +// BAD: 4 storage slots +bool isActive; +bool isVerified; +bool isPaused; +bool isBlocked; + +// GOOD: 1 storage slot, bit manipulation +uint256 flags; + +function isActive() view returns (bool) { + return (flags & (1 << 0)) != 0; +} + +function setActive(bool _active) { + if (_active) flags |= (1 << 0); + else flags &= ~(1 << 0); +} +``` + +## Variable Declaration + +### Immutable vs Constant vs Storage + +```solidity +// Storage variable: 2,100 gas (cold read) +uint256 public maxSupply = 1_000_000; + +// Constant: ~3 gas (embedded in bytecode) +uint256 public constant MAX_SUPPLY = 1_000_000; + +// Immutable: ~100 gas (set once in constructor) +uint256 public immutable maxSupply; +constructor(uint256 _max) { maxSupply = _max; } +``` + +**Use constant for compile-time values, immutable for constructor-set values.** + +### Function Visibility + +```solidity +// Public creates getter (extra dispatch) +uint256 public value; + +// External is cheaper for functions not called internally +function getValue() external view returns (uint256) { + return _value; +} +``` + +## Error Handling + +### Custom Errors vs Require Strings + +```solidity +// BAD: String stored in bytecode (~50+ gas) +require(msg.sender == owner, "Unauthorized access"); + +// GOOD: Custom error (~24 gas) +error Unauthorized(); +if (msg.sender != owner) revert Unauthorized(); +``` + +**Savings:** 20-50 gas per revert + +## Events vs Storage + +Store transient data in events instead of contract storage: + +```solidity +// BAD: 20,000+ gas per write +uint256[] public transfers; +function recordTransfer(uint256 amount) external { + transfers.push(amount); +} + +// GOOD: ~375 gas base + 8 gas per byte +event Transfer(address indexed to, uint256 amount); +function recordTransfer(address to, uint256 amount) external { + emit Transfer(to, amount); +} +``` + +## Compiler Settings + +```toml +# foundry.toml +[profile.default] +optimizer = true +optimizer_runs = 200 # Balance size/runtime + +[profile.production] +optimizer = true +optimizer_runs = 1000000 # Optimize for runtime +via_ir = true # IR pipeline (slower compile, better optimization) +``` + +**Guidance:** +- `200` runs: Smaller deployment, higher runtime cost +- `10000+` runs: Larger deployment, lower runtime cost +- `via_ir = true`: Best optimization, slowest compilation + +## EVM Opcode Reference + +| Operation | Gas | Notes | +|-----------|-----|-------| +| ADD/SUB/MUL | 3 | Basic arithmetic | +| DIV/MOD | 5 | Division operations | +| SLOAD (cold) | 2,100 | First storage read | +| SLOAD (warm) | 100 | Subsequent reads | +| SSTORE (0→non-0) | 20,000 | New storage slot | +| SSTORE (non-0→non-0) | 5,000 | Update existing | +| SSTORE (→0) | 5,000 | +15,000 refund | +| CALL | 700+ | External call base | +| KECCAK256 | 30 | +6 per word | +| LOG0-LOG4 | 375-1,875 | Events | + +## Foundry Gas Profiling + +### Gas Reports + +```bash +# Generate gas report +forge test --gas-report + +# Filter by contract +forge test --match-contract MyContract --gas-report +``` + +### Gas Snapshots + +```bash +# Create snapshot +forge snapshot + +# Compare against previous +forge snapshot --diff + +# Check threshold +forge snapshot --check --tolerance 5 +``` + +### Inline Gas Measurement + +```solidity +function testGasUsage() public { + uint256 gasBefore = gasleft(); + + // Code to measure + contract.doSomething(); + + uint256 gasUsed = gasBefore - gasleft(); + console.log("Gas used:", gasUsed); +} +``` + +### Section Snapshots + +```solidity +function testOptimization() public { + vm.startSnapshotGas("operation"); + + // Code to profile + value = 1; + + uint256 gasUsed = vm.stopSnapshotGas(); +} +``` + +## Optimization Checklist + +**Storage:** +- [ ] Pack related variables together +- [ ] Use smaller integer types when possible +- [ ] Cache frequently accessed storage variables +- [ ] Use mappings instead of arrays for lookups +- [ ] Use immutable/constant for fixed values + +**Functions:** +- [ ] Mark external (not public) when not called internally +- [ ] Use calldata for array parameters +- [ ] Use unchecked blocks for safe arithmetic +- [ ] Short-circuit expensive conditions +- [ ] Use custom errors instead of require strings + +**Compilation:** +- [ ] Enable optimizer +- [ ] Set appropriate optimizer_runs +- [ ] Consider via_ir for production + +**Testing:** +- [ ] Use `forge test --gas-report` regularly +- [ ] Create gas snapshots for regression detection +- [ ] Profile critical functions + +## Quick Wins + +| Pattern | Gas Saved | Effort | +|---------|-----------|--------| +| Custom errors | 20-50/revert | Low | +| Unchecked loops | 50-100/iter | Low | +| Calldata vs memory | 5,000+ | Low | +| Variable packing | 15,000/write | Low | +| Immutable vars | 2,000/read | Low | +| Bitwise flags | 15,000 | Medium | +| Events vs storage | 19,000+ | Medium | diff --git a/skills/skill/references/patterns.md b/skills/skill/references/patterns.md new file mode 100644 index 0000000..08a17fe --- /dev/null +++ b/skills/skill/references/patterns.md @@ -0,0 +1,581 @@ +# Solidity Patterns and Idioms + +Common patterns for modern Solidity 0.8.30 smart contract development. + +## Access Control Patterns + +### Ownable Pattern + +Single owner - simplest but centralized: + +```solidity +contract Ownable { + address private _owner; + + error Unauthorized(); + + modifier onlyOwner() { + if (msg.sender != _owner) revert Unauthorized(); + _; + } + + constructor() { + _owner = msg.sender; + } + + function owner() public view returns (address) { + return _owner; + } + + function transferOwnership(address newOwner) public onlyOwner { + require(newOwner != address(0), "Invalid owner"); + _owner = newOwner; + } +} +``` + +**Use:** Simple contracts, single admin +**Pitfall:** Single point of failure + +### Role-Based Access Control + +Fine-grained permissions: + +```solidity +contract AccessControl { + mapping(bytes32 => mapping(address => bool)) private _roles; + + bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); + + error AccessDenied(address account, bytes32 role); + + modifier onlyRole(bytes32 role) { + if (!_roles[role][msg.sender]) + revert AccessDenied(msg.sender, role); + _; + } + + function grantRole(bytes32 role, address account) public onlyRole(ADMIN_ROLE) { + _roles[role][account] = true; + } + + function hasRole(bytes32 role, address account) public view returns (bool) { + return _roles[role][account]; + } +} +``` + +**Use:** DeFi protocols, complex permissions +**Recommendation:** Use OpenZeppelin's AccessControl + +### Multi-Signature Pattern + +Require multiple approvals for critical operations: + +```solidity +contract MultiSig { + address[] public owners; + uint256 public requiredSignatures; + + mapping(bytes32 => mapping(address => bool)) public confirmations; + mapping(bytes32 => bool) public executed; + + function submitTransaction( + address target, + uint256 value, + bytes calldata data + ) external returns (bytes32) { + bytes32 txHash = keccak256(abi.encode(target, value, data, block.timestamp)); + confirmations[txHash][msg.sender] = true; + return txHash; + } + + function executeTransaction( + address target, + uint256 value, + bytes calldata data, + bytes32 txHash + ) external { + require(!executed[txHash], "Already executed"); + + uint256 count = 0; + for (uint256 i = 0; i < owners.length; i++) { + if (confirmations[txHash][owners[i]]) count++; + } + require(count >= requiredSignatures, "Not enough signatures"); + + executed[txHash] = true; + (bool success,) = target.call{value: value}(data); + require(success, "Execution failed"); + } +} +``` + +**Use:** Treasury, critical upgrades, governance + +## Reentrancy Protection + +### Checks-Effects-Interactions (CEI) + +Most important pattern - prevent state inconsistencies: + +```solidity +// BAD: State update AFTER external call +function withdrawBad() external { + uint256 amount = balances[msg.sender]; + (bool success,) = msg.sender.call{value: amount}(""); + require(success); + balances[msg.sender] = 0; // Vulnerable! +} + +// GOOD: CEI pattern +function withdrawGood() external { + uint256 amount = balances[msg.sender]; + balances[msg.sender] = 0; // Effects first + (bool success,) = msg.sender.call{value: amount}(""); + require(success); // Interactions last +} +``` + +### Mutex Lock (ReentrancyGuard) + +Traditional approach using storage: + +```solidity +contract ReentrancyGuard { + uint256 private constant NOT_ENTERED = 1; + uint256 private constant ENTERED = 2; + uint256 private _status = NOT_ENTERED; + + modifier nonReentrant() { + require(_status != ENTERED, "ReentrancyGuard: reentrant call"); + _status = ENTERED; + _; + _status = NOT_ENTERED; + } +} +``` + +**Cost:** ~5,000 gas (2 SSTOREs) + +### Transient Storage Lock (0.8.28+) + +Gas-efficient using EIP-1153: + +```solidity +contract TransientReentrancyGuard { + modifier nonReentrant() { + assembly { + if tload(0) { revert(0, 0) } + tstore(0, 1) + } + _; + assembly { + tstore(0, 0) + } + } +} +``` + +**Cost:** ~200 gas (TSTORE/TLOAD) - 25x cheaper + +## Factory Patterns + +### Basic Factory (CREATE) + +```solidity +contract TokenFactory { + address[] public deployedTokens; + + event TokenCreated(address indexed token, address indexed creator); + + function createToken(string memory name, string memory symbol) + external + returns (address) + { + Token token = new Token(name, symbol, msg.sender); + deployedTokens.push(address(token)); + emit TokenCreated(address(token), msg.sender); + return address(token); + } +} +``` + +**Use:** Simple deployments where address doesn't matter + +### Deterministic Factory (CREATE2) + +Deploy to predictable addresses across chains: + +```solidity +contract DeterministicFactory { + event Deployed(address indexed addr, bytes32 salt); + + function deploy(bytes32 salt, bytes memory bytecode) + external + returns (address addr) + { + assembly { + addr := create2(0, add(bytecode, 0x20), mload(bytecode), salt) + if iszero(extcodesize(addr)) { revert(0, 0) } + } + emit Deployed(addr, salt); + } + + function computeAddress(bytes32 salt, bytes32 bytecodeHash) + external + view + returns (address) + { + return address(uint160(uint256(keccak256(abi.encodePacked( + bytes1(0xff), + address(this), + salt, + bytecodeHash + ))))); + } +} +``` + +**Use:** Counterfactual deployments, cross-chain consistency + +### Minimal Proxy (ERC-1167 Clones) + +Deploy cheap proxies (~45 bytes): + +```solidity +contract CloneFactory { + function clone(address implementation) internal returns (address instance) { + assembly { + mstore(0x00, or( + shr(0xe8, shl(0x60, implementation)), + 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000 + )) + mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3)) + instance := create(0, 0x09, 0x37) + } + } +} +``` + +**Cost:** ~10K gas vs ~170K for full contract +**Use:** Token factories, vault templates + +## Payment Patterns + +### Pull Over Push + +**Push (Anti-pattern):** +```solidity +// BAD: Can fail if recipient reverts +function sendRewards() external { + for (uint256 i = 0; i < recipients.length; i++) { + payable(recipients[i]).transfer(amounts[i]); + } +} +``` + +**Pull (Recommended):** +```solidity +contract PullPayments { + mapping(address => uint256) public pendingWithdrawals; + + function claimReward() external { + uint256 amount = pendingWithdrawals[msg.sender]; + require(amount > 0, "Nothing to claim"); + + pendingWithdrawals[msg.sender] = 0; + (bool success,) = msg.sender.call{value: amount}(""); + require(success, "Transfer failed"); + } +} +``` + +**Why pull is safer:** +- User controls when to withdraw +- Failing transfers don't block protocol +- Prevents DoS via malicious fallbacks + +## Emergency Controls + +### Pausable Pattern + +```solidity +contract Pausable { + bool public paused; + address public owner; + + error ContractPaused(); + + modifier whenNotPaused() { + if (paused) revert ContractPaused(); + _; + } + + function pause() external onlyOwner { + paused = true; + } + + function unpause() external onlyOwner { + paused = false; + } + + function transfer(address to, uint256 amount) external whenNotPaused { + // Transfer logic + } +} +``` + +**Use:** Security response, gradual rollouts + +## Proxy Patterns + +### UUPS (Recommended) + +Implementation controls upgrades: + +```solidity +import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; + +contract MyContractV1 is UUPSUpgradeable { + uint256 public value; + + function initialize(uint256 _value) public initializer { + value = _value; + } + + function _authorizeUpgrade(address newImplementation) + internal + override + onlyOwner + {} +} +``` + +### Transparent Proxy + +Admin calls go to proxy, user calls go to implementation: + +```solidity +// Use OpenZeppelin's TransparentUpgradeableProxy +import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +``` + +**Trade-offs:** +- UUPS: Smaller proxy, upgrade logic in implementation +- Transparent: Larger proxy, clearer admin separation + +## State Machine Pattern + +```solidity +contract Auction { + enum Phase { Bidding, Reveal, Finished } + + Phase public currentPhase; + uint256 public phaseDeadline; + + error WrongPhase(Phase expected, Phase actual); + + modifier atPhase(Phase expected) { + _checkPhaseTransition(); + if (currentPhase != expected) + revert WrongPhase(expected, currentPhase); + _; + } + + function _checkPhaseTransition() internal { + if (block.timestamp >= phaseDeadline) { + currentPhase = Phase(uint256(currentPhase) + 1); + phaseDeadline = block.timestamp + 1 days; + } + } + + function placeBid(bytes32 hashedBid) external atPhase(Phase.Bidding) { + // Bidding logic + } + + function revealBid(uint256 value, bytes32 secret) external atPhase(Phase.Reveal) { + // Reveal logic + } +} +``` + +## Commit-Reveal Scheme + +Prevent front-running: + +```solidity +contract CommitReveal { + mapping(address => bytes32) public commits; + mapping(address => uint256) public commitTimes; + + uint256 public constant REVEAL_DELAY = 1 hours; + + function commit(bytes32 hash) external { + commits[msg.sender] = hash; + commitTimes[msg.sender] = block.timestamp; + } + + function reveal(uint256 value, bytes32 salt) external { + require(block.timestamp >= commitTimes[msg.sender] + REVEAL_DELAY, "Too early"); + require(commits[msg.sender] == keccak256(abi.encode(value, salt, msg.sender)), "Invalid reveal"); + + delete commits[msg.sender]; + // Process revealed value + } +} +``` + +**Use:** Auctions, voting, sealed-bid protocols + +## Timelock Pattern + +```solidity +contract Timelock { + uint256 public constant DELAY = 2 days; + + mapping(bytes32 => uint256) public queuedTransactions; + + function queue(address target, bytes calldata data) external returns (bytes32) { + bytes32 txHash = keccak256(abi.encode(target, data, block.timestamp)); + queuedTransactions[txHash] = block.timestamp + DELAY; + return txHash; + } + + function execute(address target, bytes calldata data, bytes32 txHash) external { + uint256 executeTime = queuedTransactions[txHash]; + require(executeTime != 0, "Not queued"); + require(block.timestamp >= executeTime, "Too early"); + + delete queuedTransactions[txHash]; + (bool success,) = target.call(data); + require(success, "Execution failed"); + } +} +``` + +**Use:** Governance, critical upgrades, parameter changes + +## Error Handling + +### Custom Errors (Preferred) + +```solidity +error InsufficientBalance(uint256 available, uint256 required); +error Unauthorized(address caller); +error ZeroAddress(); + +function withdraw(uint256 amount) external { + if (balances[msg.sender] < amount) + revert InsufficientBalance(balances[msg.sender], amount); + // ... +} +``` + +**Advantages:** +- ~90% gas savings vs require strings +- Structured error data for debugging +- Type-safe parameters + +## Initialization Patterns + +### Constructor (Non-upgradeable) + +```solidity +contract Permanent { + address public immutable owner; + uint256 public immutable maxSupply; + + constructor(address _owner, uint256 _maxSupply) { + owner = _owner; + maxSupply = _maxSupply; + } +} +``` + +### Initializer (Upgradeable) + +```solidity +contract Upgradeable { + bool private _initialized; + address public owner; + + modifier initializer() { + require(!_initialized, "Already initialized"); + _initialized = true; + _; + } + + function initialize(address _owner) external initializer { + owner = _owner; + } +} +``` + +**Critical:** Constructors don't run on proxies - use initializers + +## Common Pitfalls + +### tx.origin vs msg.sender + +```solidity +// WRONG: Vulnerable to phishing +if (tx.origin == owner) { /* ... */ } + +// CORRECT +if (msg.sender == owner) { /* ... */ } +``` + +### Storage Layout with Upgrades + +```solidity +// V1 +contract V1 { + uint256 public value; // slot 0 +} + +// V2 - CORRECT: Append only +contract V2 is V1 { + uint256 public newValue; // slot 1 +} + +// V2 - WRONG: Inserting breaks layout +contract V2Bad { + uint256 public newValue; // slot 0 - collision! + uint256 public value; // slot 1 +} +``` + +### Unbounded Loops + +```solidity +// DANGEROUS: Can run out of gas +function distributeAll() external { + for (uint256 i = 0; i < recipients.length; i++) { + payable(recipients[i]).transfer(amounts[i]); + } +} + +// SAFE: Batch processing +function distributeBatch(uint256 start, uint256 end) external { + require(end <= recipients.length && end - start <= 100); + for (uint256 i = start; i < end; i++) { + payable(recipients[i]).transfer(amounts[i]); + } +} +``` + +### Unchecked External Calls + +```solidity +// WRONG: Return value ignored +token.transfer(user, amount); + +// CORRECT: Check return +bool success = token.transfer(user, amount); +require(success, "Transfer failed"); + +// BEST: Use SafeERC20 +IERC20(token).safeTransfer(user, amount); +``` diff --git a/skills/skill/references/resources.md b/skills/skill/references/resources.md new file mode 100644 index 0000000..087d5fa --- /dev/null +++ b/skills/skill/references/resources.md @@ -0,0 +1,126 @@ +# Foundry & Solidity Resources + +Authoritative resources for Foundry and Solidity development. + +## Official Documentation + +### Foundry +- **Foundry Book**: https://book.getfoundry.sh - Comprehensive guide (forge, anvil, cast, chisel) +- **Foundry GitHub**: https://github.com/foundry-rs/foundry - Source code and issues +- **forge-std**: https://github.com/foundry-rs/forge-std - Standard library reference + +### Solidity +- **Solidity Docs**: https://docs.soliditylang.org - Language reference +- **Solidity GitHub**: https://github.com/ethereum/solidity - Compiler source +- **Solidity Blog**: https://soliditylang.org/blog - Release notes and features + +### Ethereum +- **Ethereum.org Developers**: https://ethereum.org/developers - Developer portal +- **EIPs**: https://eips.ethereum.org - Ethereum Improvement Proposals +- **Ethereum Stack Exchange**: https://ethereum.stackexchange.com - Q&A community + +## Smart Contract Libraries + +### Production-Ready +- **OpenZeppelin Contracts**: https://docs.openzeppelin.com/contracts - Audited implementations + - ERC-20, ERC-721, ERC-1155 tokens + - Access control, pausable, upgradeable + - Security utilities + +- **Solady**: https://github.com/Vectorized/solady - Gas-optimized utilities + - Used by Coinbase, Optimism, Uniswap + - Highly optimized ERC implementations + +- **Solmate**: https://github.com/transmissions11/solmate - Minimalist implementations + - Gas-efficient ERC tokens + - Auth and utility contracts + +## Security Resources + +### Audit Firms +- **OpenZeppelin Security**: https://www.openzeppelin.com/security-audits +- **Trail of Bits**: https://blog.trailofbits.com +- **Consensys Diligence**: https://consensys.io/diligence + +### Crowdsourced Audits +- **Code4rena**: https://code4rena.com - Competitive audits +- **Sherlock**: https://sherlock.xyz - Audit competitions +- **Immunefi**: https://immunefi.com - Bug bounties + +### Vulnerability Resources +- **SWC Registry**: https://swcregistry.io - Smart Contract Weaknesses +- **Rekt News**: https://rekt.news - Exploit post-mortems +- **DeFiHackLabs**: https://github.com/SunWeb3Sec/DeFiHackLabs - Exploit reproductions + +### Security Tools +- **Slither**: https://github.com/crytic/slither - Static analysis +- **Mythril**: https://mythril.ai - Symbolic execution +- **Echidna**: https://github.com/crytic/echidna - Fuzzing +- **Certora**: https://www.certora.com - Formal verification + +## Learning Resources + +### Interactive +- **Solidity by Example**: https://solidity-by-example.org - Runnable code examples +- **CryptoZombies**: https://cryptozombies.io - Gamified learning +- **SpeedRun Ethereum**: https://speedrunethereum.com - Hands-on challenges + +### Courses +- **Cyfrin Updraft**: https://updraft.cyfrin.io - Patrick Collins courses +- **Alchemy University**: https://university.alchemy.com - Web3 development + +### Patterns & Best Practices +- **Solidity Patterns**: https://docs.soliditylang.org/en/latest/common-patterns.html +- **OpenZeppelin Docs**: https://docs.openzeppelin.com/contracts - Implementation guides + +## Development Tools + +### IDEs & Editors +- **VS Code + Solidity Extension**: https://marketplace.visualstudio.com/items?itemName=JuanBlanco.solidity +- **Remix IDE**: https://remix.ethereum.org - Browser-based + +### Testing & Debugging +- **Tenderly**: https://tenderly.co - Transaction debugging +- **Phalcon**: https://explorer.phalcon.xyz - Transaction analysis + +### Infrastructure +- **Alchemy**: https://www.alchemy.com - RPC provider +- **Infura**: https://www.infura.io - RPC provider +- **QuickNode**: https://www.quicknode.com - RPC provider + +## Community + +### Discussion +- **Foundry Telegram**: https://t.me/foundry_rs +- **Ethereum R&D Discord**: https://discord.gg/ethereum-r-d +- **OpenZeppelin Forum**: https://forum.openzeppelin.com + +### Research +- **Paradigm Research**: https://www.paradigm.xyz/writing +- **a]16z Crypto Research**: https://a16zcrypto.com/research + +## Quick Reference + +### Getting Started Path +1. **Solidity by Example** - Learn syntax +2. **Foundry Book** - Learn tooling +3. **OpenZeppelin Docs** - Learn patterns +4. **Code4rena Reports** - Learn security + +### Testing Best Practices +1. **forge-std** - Test utilities +2. **Foundry Book Testing** - Fuzz and invariant testing +3. **DeFiHackLabs** - Real exploit patterns + +### Security Path +1. **OpenZeppelin Audit Readiness** - Preparation +2. **Trail of Bits Blog** - Deep dives +3. **Code4rena** - Real audit reports + +## Version Context + +Current versions (November 2025): +- **Foundry**: v1.5.0 +- **Solidity**: 0.8.30 +- **OpenZeppelin Contracts**: v5.x +- **forge-std**: v1.9.x diff --git a/skills/skill/references/security.md b/skills/skill/references/security.md new file mode 100644 index 0000000..5882c66 --- /dev/null +++ b/skills/skill/references/security.md @@ -0,0 +1,520 @@ +# Solidity Security & Audit Patterns + +Security vulnerabilities, defensive patterns, and Foundry testing strategies for smart contract audits. + +## Critical Vulnerabilities + +### Reentrancy + +**Risk:** External calls allow recursive entry before state updates, draining contracts. + +```solidity +// VULNERABLE: State update after external call +function withdraw(uint256 amount) public { + require(balances[msg.sender] >= amount); + (bool sent,) = msg.sender.call{value: amount}(""); + require(sent); + balances[msg.sender] -= amount; // Too late! +} + +// SECURE: CEI pattern + reentrancy guard +function withdraw(uint256 amount) public nonReentrant { + require(balances[msg.sender] >= amount); + balances[msg.sender] -= amount; // Effects first + (bool sent,) = msg.sender.call{value: amount}(""); + require(sent); // Interactions last +} +``` + +**Foundry Test:** +```solidity +contract ReentrancyAttacker { + Vault vault; + uint256 count; + + receive() external payable { + if (count++ < 5 && address(vault).balance > 0) { + vault.withdraw(1 ether); + } + } + + function attack() external { + vault.withdraw(1 ether); + } +} + +function test_reentrancy_protected() public { + ReentrancyAttacker attacker = new ReentrancyAttacker(vault); + deal(address(attacker), 2 ether); + attacker.deposit{value: 1 ether}(); + + vm.expectRevert("ReentrancyGuard"); + attacker.attack(); +} +``` + +### Access Control + +**Risk:** Missing permission checks allow unauthorized privileged operations. + +```solidity +// VULNERABLE: No access control +function drain() public { + payable(owner).transfer(address(this).balance); +} + +// VULNERABLE: Using tx.origin +function authorize(address user) public { + require(tx.origin == admin); // Phishing vulnerable! +} + +// SECURE: Role-based access +function drain() external onlyRole(ADMIN_ROLE) { + payable(msg.sender).transfer(address(this).balance); +} +``` + +**Foundry Test:** +```solidity +function test_accessControl_unauthorized() public { + vm.prank(attacker); + vm.expectRevert( + abi.encodeWithSelector( + AccessControl.AccessControlUnauthorizedAccount.selector, + attacker, + vault.ADMIN_ROLE() + ) + ); + vault.drain(); +} +``` + +### Oracle Manipulation + +**Risk:** Manipulated price feeds cause unfair liquidations or loan exploits. + +```solidity +// VULNERABLE: Spot price (flash-loanable) +function getPrice() public view returns (uint256) { + return dex.spotPrice(token); +} + +// SECURE: Chainlink with staleness check +function getPrice() public view returns (int256) { + ( + uint80 roundId, + int256 price, + , + uint256 updatedAt, + uint80 answeredInRound + ) = priceFeed.latestRoundData(); + + require(price > 0, "Invalid price"); + require(answeredInRound >= roundId, "Stale round"); + require(block.timestamp - updatedAt < 3600, "Price too old"); + + return price; +} +``` + +**Foundry Test:** +```solidity +function test_oracle_staleness() public { + vm.warp(block.timestamp + 4000); // Past staleness threshold + + vm.expectRevert("Price too old"); + oracle.getPrice(); +} +``` + +### Integer Overflow/Underflow + +Solidity 0.8+ has built-in checks, but watch for: + +```solidity +// VULNERABLE: Unchecked block disables protection +unchecked { + balance -= amount; // Can underflow! +} + +// VULNERABLE: Type casting +uint8 small = uint8(largeNumber); // Truncates silently + +// SECURE: Explicit checks +require(balance >= amount, "Insufficient balance"); +unchecked { balance -= amount; } +``` + +### Front-Running / MEV + +**Risk:** Attackers see pending transactions and extract value. + +**Mitigations:** +- Commit-reveal schemes for sensitive operations +- Flashbots for transaction privacy +- Slippage protection in DEX trades +- Batch auctions instead of first-come-first-served + +```solidity +// Commit-reveal pattern +function commit(bytes32 hash) external { + commits[msg.sender] = hash; + commitTime[msg.sender] = block.timestamp; +} + +function reveal(uint256 value, bytes32 salt) external { + require(block.timestamp >= commitTime[msg.sender] + 1 hours); + require(commits[msg.sender] == keccak256(abi.encode(value, salt))); + // Process value +} +``` + +### Flash Loan Attacks + +**Risk:** Attackers borrow large amounts to manipulate protocol state within single transaction. + +**Mitigations:** +- Use TWAP instead of spot prices +- Require multi-block operations +- Over-collateralization requirements +- Block flash loans if not needed + +### Signature Malleability + +**Risk:** Attackers modify signatures to replay transactions. + +```solidity +// VULNERABLE: No nonce or deadline +function permit(address owner, uint256 value, bytes calldata sig) external { + // Can be replayed! +} + +// SECURE: EIP-712 with nonce and deadline +function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, bytes32 r, bytes32 s +) external { + require(block.timestamp <= deadline, "Expired"); + // Verify EIP-712 signature with nonce + nonces[owner]++; +} +``` + +### Storage Collision (Proxies) + +**Risk:** Proxy and implementation have different storage layouts. + +```solidity +// WRONG: Storage mismatch +contract Proxy { + address implementation; // slot 0 + address owner; // slot 1 +} +contract Implementation { + address owner; // slot 0 - COLLISION! +} + +// CORRECT: Use EIP-1967 storage slots +bytes32 constant IMPL_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; +``` + +### Denial of Service + +**Patterns:** +- Unbounded loops exhausting gas +- External calls that always revert +- Block gas limit exceeded + +```solidity +// VULNERABLE: Unbounded loop +function distributeAll() external { + for (uint256 i = 0; i < users.length; i++) { + payable(users[i]).transfer(rewards[i]); + } +} + +// SECURE: Paginated distribution +function distributeBatch(uint256 start, uint256 count) external { + require(count <= 100, "Batch too large"); + uint256 end = start + count; + if (end > users.length) end = users.length; + + for (uint256 i = start; i < end; i++) { + payable(users[i]).transfer(rewards[i]); + } +} +``` + +## Foundry Security Testing + +### Fuzz Testing + +Generate random inputs to find edge cases: + +```solidity +function testFuzz_transfer(address to, uint256 amount) public { + vm.assume(to != address(0)); + vm.assume(to != address(token)); + amount = bound(amount, 0, token.balanceOf(address(this))); + + uint256 balanceBefore = token.balanceOf(to); + token.transfer(to, amount); + + assertEq(token.balanceOf(to), balanceBefore + amount); +} +``` + +**Configuration:** +```toml +[fuzz] +runs = 10000 +seed = "0x1234" +max_test_rejects = 65536 +``` + +### Invariant Testing + +Test properties that must always hold: + +```solidity +contract VaultInvariant is Test { + Vault vault; + VaultHandler handler; + + function setUp() public { + vault = new Vault(); + handler = new VaultHandler(vault); + targetContract(address(handler)); + } + + // Total deposits must equal total shares value + function invariant_solvency() public view { + assertGe( + vault.totalAssets(), + vault.totalSupply(), + "Vault insolvent" + ); + } + + // Sum of balances equals total supply + function invariant_balanceSum() public view { + uint256 sum = 0; + address[] memory users = handler.getUsers(); + for (uint256 i = 0; i < users.length; i++) { + sum += vault.balanceOf(users[i]); + } + assertEq(sum, vault.totalSupply()); + } +} +``` + +### Handler Pattern + +Control fuzz inputs and track state: + +```solidity +contract VaultHandler is Test { + Vault vault; + address[] public users; + uint256 public ghost_totalDeposited; + + constructor(Vault _vault) { + vault = _vault; + users.push(makeAddr("alice")); + users.push(makeAddr("bob")); + } + + function deposit(uint256 userSeed, uint256 amount) external { + address user = users[bound(userSeed, 0, users.length - 1)]; + amount = bound(amount, 1, 1e24); + + deal(address(vault.asset()), user, amount); + + vm.startPrank(user); + vault.asset().approve(address(vault), amount); + vault.deposit(amount, user); + vm.stopPrank(); + + ghost_totalDeposited += amount; + } + + function withdraw(uint256 userSeed, uint256 shares) external { + address user = users[bound(userSeed, 0, users.length - 1)]; + shares = bound(shares, 0, vault.balanceOf(user)); + if (shares == 0) return; + + vm.prank(user); + uint256 assets = vault.redeem(shares, user, user); + + ghost_totalDeposited -= assets; + } +} +``` + +**Configuration:** +```toml +[invariant] +runs = 256 +depth = 100 +fail_on_revert = false +``` + +### Fork Testing + +Test against real mainnet state: + +```solidity +function setUp() public { + vm.createSelectFork(vm.envString("MAINNET_RPC_URL"), 18000000); +} + +function testFork_usdcIntegration() public { + IERC20 usdc = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + address whale = 0x47ac0Fb4F2D84898e4D9E7b4DaB3C24507a6D503; + + uint256 balanceBefore = usdc.balanceOf(address(this)); + + vm.prank(whale); + usdc.transfer(address(this), 1_000_000e6); + + assertEq(usdc.balanceOf(address(this)), balanceBefore + 1_000_000e6); +} +``` + +### Symbolic Execution (Halmos) + +Prove properties mathematically: + +```solidity +function check_transferPreservesSupply(address from, address to, uint256 amount) public { + uint256 supplyBefore = token.totalSupply(); + + token.transfer(from, to, amount); + + assert(token.totalSupply() == supplyBefore); +} +``` + +Run with: `halmos --contract MyTest` + +## Pre-Audit Checklist + +### Code Quality +- [ ] NatSpec documentation on all public functions +- [ ] No console.log or debug statements +- [ ] All TODO/FIXME resolved +- [ ] Consistent naming conventions + +### Security Patterns +- [ ] Reentrancy guards on external-calling functions +- [ ] CEI pattern followed +- [ ] Access control on privileged functions +- [ ] No tx.origin for authentication +- [ ] SafeERC20 for token transfers +- [ ] Input validation on all parameters + +### Testing +- [ ] >95% code coverage +- [ ] Fuzz tests with 10,000+ runs +- [ ] Invariant tests for core properties +- [ ] Edge case tests (zero, max values) +- [ ] Fork tests for integrations + +### Static Analysis +- [ ] Slither: no HIGH/CRITICAL issues +- [ ] Manual review of MEDIUM issues + +### Documentation +- [ ] README with security assumptions +- [ ] Threat model documented +- [ ] Known limitations listed +- [ ] Admin functions documented + +## Common Audit Findings + +### Missing Return Value Check + +```solidity +// BAD +token.transfer(user, amount); + +// GOOD +require(token.transfer(user, amount), "Transfer failed"); + +// BEST +IERC20(token).safeTransfer(user, amount); +``` + +### Uninitialized Proxy + +```solidity +// BAD: Anyone can initialize +function initialize(address _owner) external { + owner = _owner; +} + +// GOOD: Use initializer modifier +function initialize(address _owner) external initializer { + owner = _owner; +} +``` + +### Precision Loss + +```solidity +// BAD: Division before multiplication +uint256 share = (amount / total) * balance; // Rounds to 0 + +// GOOD: Multiplication before division +uint256 share = (amount * balance) / total; +``` + +### Unchecked Array Access + +```solidity +// BAD +return users[index]; // Can revert + +// GOOD +require(index < users.length, "Out of bounds"); +return users[index]; +``` + +## Security Tools + +**Static Analysis:** +- Slither: `slither . --json report.json` +- Mythril: `myth analyze contracts/Vault.sol` + +**Fuzzing:** +- Forge fuzz: Built-in +- Echidna: Property-based fuzzing + +**Formal Verification:** +- Certora Prover +- Halmos + +## Foundry Security Commands + +```bash +# Run all tests with coverage +forge coverage --report lcov + +# Fuzz with high iterations +forge test --fuzz-runs 50000 + +# Invariant testing +forge test --match-contract Invariant -vv + +# Fork testing +forge test --fork-url $RPC_URL + +# Gas analysis +forge test --gas-report + +# Debug failing test +forge test --match-test testName -vvvv +``` diff --git a/skills/skill/references/solidity-modern.md b/skills/skill/references/solidity-modern.md new file mode 100644 index 0000000..eb43cfe --- /dev/null +++ b/skills/skill/references/solidity-modern.md @@ -0,0 +1,641 @@ +# Modern Solidity (0.8.30) + +Complete guide to Solidity 0.8.20-0.8.30 features, gas optimization, and security patterns. + +## Version Feature Summary + +| Version | Key Features | +|---------|-------------| +| 0.8.30 | Prague EVM default, NatSpec for enums | +| 0.8.29 | Custom storage layout (`layout at`) | +| 0.8.28 | Transient storage for value types | +| 0.8.27 | Transient storage parser support | +| 0.8.26 | `require(bool, Error)` custom errors | +| 0.8.25 | Cancun EVM default, MCOPY opcode | +| 0.8.24 | `blobbasefee`, `blobhash()`, tload/tstore in Yul | +| 0.8.22 | File-level events | +| 0.8.21 | Relaxed immutable initialization | +| 0.8.20 | Shanghai EVM default, PUSH0 | +| 0.8.19 | Custom operators for user-defined types | +| 0.8.18 | Named mapping parameters | + +## Custom Errors (0.8.4+) + +Gas-efficient error handling. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.30; + +error Unauthorized(address caller, address required); +error InsufficientBalance(uint256 available, uint256 required); +error InvalidAmount(); +error Expired(uint256 deadline, uint256 current); + +contract Token { + mapping(address => uint256) public balanceOf; + address public owner; + + constructor() { + owner = msg.sender; + } + + function transfer(address to, uint256 amount) external { + if (amount == 0) revert InvalidAmount(); + if (amount > balanceOf[msg.sender]) { + revert InsufficientBalance(balanceOf[msg.sender], amount); + } + + balanceOf[msg.sender] -= amount; + balanceOf[to] += amount; + } + + function adminFunction() external { + if (msg.sender != owner) { + revert Unauthorized(msg.sender, owner); + } + // ... + } +} +``` + +### require with Custom Errors (0.8.26+) + +```solidity +function transfer(address to, uint256 amount) external { + require(amount > 0, InvalidAmount()); + require( + amount <= balanceOf[msg.sender], + InsufficientBalance(balanceOf[msg.sender], amount) + ); + + balanceOf[msg.sender] -= amount; + balanceOf[to] += amount; +} +``` + +### Gas Comparison + +```solidity +// String error: ~4 bytes selector + string data +require(x > 0, "Amount must be greater than zero"); + +// Custom error: ~4 bytes selector only +if (x == 0) revert InvalidAmount(); + +// Custom error with params: ~4 bytes + encoded params +if (x > balance) revert InsufficientBalance(balance, x); + +// Savings: 50-200+ gas per error +``` + +## Transient Storage (0.8.28+) + +Cheap temporary storage cleared after transaction. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +contract ReentrancyGuard { + bool transient locked; + + modifier nonReentrant() { + require(!locked, "Reentrancy"); + locked = true; + _; + locked = false; // CRITICAL: Reset for composability + } + + function withdraw() external nonReentrant { + // Safe from reentrancy + } +} +``` + +### Flash Loan Example + +```solidity +contract FlashLender { + IERC20 public token; + bool transient flashLoanActive; + + function flashLoan(uint256 amount, address receiver) external { + require(!flashLoanActive, "Flash loan active"); + flashLoanActive = true; + + uint256 balanceBefore = token.balanceOf(address(this)); + token.transfer(receiver, amount); + + IFlashBorrower(receiver).onFlashLoan(amount); + + require( + token.balanceOf(address(this)) >= balanceBefore, + "Flash loan not repaid" + ); + + flashLoanActive = false; + } +} +``` + +### Gas Comparison + +| Operation | Persistent Storage | Transient Storage | +|-----------|-------------------|-------------------| +| First write | 20,000+ gas | 100 gas | +| Subsequent write | 2,900 gas | 100 gas | +| Read | 100 gas | 100 gas | + +**Important**: Always reset transient storage at function exit for composability. + +## Immutable Variables (Relaxed in 0.8.21+) + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +contract Config { + address public immutable owner; + uint256 public immutable deployTime; + bytes32 public immutable configHash; + uint256 public immutable maxSupply; + + constructor(uint256 _maxSupply, bytes32 _configHash) { + owner = msg.sender; + deployTime = block.timestamp; + + // Relaxed: can read/write immutables anywhere in constructor + if (_maxSupply == 0) { + maxSupply = 1_000_000e18; + } else { + maxSupply = _maxSupply; + } + + // Can use other immutables + configHash = _configHash != bytes32(0) + ? _configHash + : keccak256(abi.encode(owner, maxSupply)); + } +} +``` + +### Gas Benefits + +- Immutable: **3 gas** (value inlined) +- Storage: **100 gas** (SLOAD) +- Constant: **0 gas** (compile-time constant) + +## User-Defined Types with Operators (0.8.19+) + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +type Amount is uint256; +type Price is uint256; + +using {add as +, sub as -, mul, eq as ==, lt as <} for Amount global; + +function add(Amount a, Amount b) pure returns (Amount) { + return Amount.wrap(Amount.unwrap(a) + Amount.unwrap(b)); +} + +function sub(Amount a, Amount b) pure returns (Amount) { + return Amount.wrap(Amount.unwrap(a) - Amount.unwrap(b)); +} + +function mul(Amount a, Price p) pure returns (uint256) { + return Amount.unwrap(a) * Price.unwrap(p); +} + +function eq(Amount a, Amount b) pure returns (bool) { + return Amount.unwrap(a) == Amount.unwrap(b); +} + +function lt(Amount a, Amount b) pure returns (bool) { + return Amount.unwrap(a) < Amount.unwrap(b); +} + +contract TypeSafe { + function calculate(Amount a, Amount b, Price p) external pure returns (uint256) { + Amount total = a + b; + if (total < Amount.wrap(100)) { + return 0; + } + return total.mul(p); + } +} +``` + +## Named Mapping Parameters (0.8.18+) + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +contract Token { + // Highly readable + mapping(address account => uint256 balance) public balanceOf; + mapping(address owner => mapping(address spender => uint256 amount)) public allowance; + + // Complex mappings + mapping(uint256 tokenId => mapping(address operator => bool approved)) public operatorApproval; +} +``` + +## File-Level Events (0.8.22+) + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +// File-level event +event Transfer(address indexed from, address indexed to, uint256 value); +event Approval(address indexed owner, address indexed spender, uint256 value); + +interface IERC20 { + function transfer(address to, uint256 amount) external returns (bool); +} + +contract Token is IERC20 { + mapping(address => uint256) public balanceOf; + + function transfer(address to, uint256 amount) external returns (bool) { + balanceOf[msg.sender] -= amount; + balanceOf[to] += amount; + emit Transfer(msg.sender, to, amount); + return true; + } +} +``` + +## Custom Storage Layout (0.8.29+) + +For EIP-7702 delegate implementations: + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.29; + +// Delegate 1: Storage starts at 0x1000 +contract DelegateA layout at 0x1000 { + uint256 public valueA; // Slot 0x1000 + + function setA(uint256 v) external { + valueA = v; + } +} + +// Delegate 2: Storage starts at 0x2000 +contract DelegateB layout at 0x2000 { + uint256 public valueB; // Slot 0x2000 + + function setB(uint256 v) external { + valueB = v; + } +} + +// Both can be delegated to from same account without storage collision +``` + +## Checked vs Unchecked Arithmetic + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.30; + +contract Arithmetic { + // Default: checked (reverts on overflow) + function checkedAdd(uint256 a, uint256 b) public pure returns (uint256) { + return a + b; // Reverts if overflow + } + + // Explicit: unchecked (wraps on overflow) + function uncheckedIncrement(uint256 i) public pure returns (uint256) { + unchecked { + return i + 1; // Does not revert + } + } + + // Common pattern: loop counter + function sum(uint256[] calldata arr) public pure returns (uint256 total) { + for (uint256 i = 0; i < arr.length;) { + total += arr[i]; + unchecked { ++i; } // Safe: i < arr.length guarantees no overflow + } + } +} +``` + +### When to Use Unchecked + +Safe scenarios: +- Loop counters bounded by array length +- Incrementing from known safe values +- Subtracting after explicit bounds check +- Timestamp/block number arithmetic + +Never use unchecked: +- User-provided inputs without validation +- Token amounts in DeFi +- Any value that could overflow + +## Gas Optimization Techniques + +### 1. Storage Packing + +```solidity +// Bad: 3 slots +contract Unpacked { + uint256 a; // Slot 0 + uint128 b; // Slot 1 + uint128 c; // Slot 2 +} + +// Good: 2 slots +contract Packed { + uint256 a; // Slot 0 + uint128 b; // Slot 1 (first half) + uint128 c; // Slot 1 (second half) +} +``` + +### 2. Caching Storage in Memory + +```solidity +// Bad: Multiple SLOADs +function bad(uint256 amount) external { + require(balances[msg.sender] >= amount); + balances[msg.sender] -= amount; + emit Transfer(msg.sender, amount, balances[msg.sender]); +} + +// Good: Cache storage +function good(uint256 amount) external { + uint256 balance = balances[msg.sender]; + require(balance >= amount); + uint256 newBalance = balance - amount; + balances[msg.sender] = newBalance; + emit Transfer(msg.sender, amount, newBalance); +} +``` + +### 3. Use calldata for Read-Only Arrays + +```solidity +// Bad: Copies to memory +function bad(uint256[] memory data) external { } + +// Good: Direct calldata access +function good(uint256[] calldata data) external { } +``` + +### 4. Short-Circuit Conditionals + +```solidity +// Cheaper checks first +function check(uint256 amount, address user) external view { + // Cheap: comparison + // Expensive: storage read + if (amount > 0 && balances[user] >= amount) { + // ... + } +} +``` + +### 5. Use Constants and Immutables + +```solidity +// Constant: 0 gas (inlined at compile time) +uint256 constant MAX_SUPPLY = 1_000_000e18; + +// Immutable: 3 gas (inlined in bytecode) +address immutable owner; + +// Storage: 100 gas (SLOAD) +address _owner; +``` + +### 6. Increment Patterns + +```solidity +// Most efficient +unchecked { ++i; } + +// Less efficient +unchecked { i++; } + +// Least efficient (checked) +i++; +``` + +### Gas Costs Reference + +| Operation | Gas Cost | +|-----------|----------| +| SLOAD (warm) | 100 | +| SLOAD (cold) | 2,100 | +| SSTORE (zero to non-zero) | 20,000 | +| SSTORE (non-zero to non-zero) | 2,900 | +| SSTORE (non-zero to zero) | Refund 4,800 | +| TLOAD | 100 | +| TSTORE | 100 | +| Memory read/write | 3 | +| Calldata read | 3 | + +## Security Best Practices + +### 1. Checks-Effects-Interactions + +```solidity +function withdraw(uint256 amount) external { + // CHECKS + require(amount <= balances[msg.sender], "Insufficient"); + + // EFFECTS + balances[msg.sender] -= amount; + + // INTERACTIONS + (bool success,) = msg.sender.call{value: amount}(""); + require(success, "Transfer failed"); +} +``` + +### 2. Reentrancy Protection + +```solidity +// Using transient storage (0.8.28+) +bool transient locked; + +modifier nonReentrant() { + require(!locked); + locked = true; + _; + locked = false; +} + +// Or OpenZeppelin ReentrancyGuard +``` + +### 3. Access Control + +```solidity +error Unauthorized(); + +modifier onlyOwner() { + if (msg.sender != owner) revert Unauthorized(); + _; +} + +// Use OpenZeppelin AccessControl for complex permissions +``` + +### 4. Safe External Calls + +```solidity +// Check return value +(bool success, bytes memory data) = target.call(payload); +require(success, "Call failed"); + +// Or use SafeCall pattern +function safeTransferETH(address to, uint256 amount) internal { + (bool success,) = to.call{value: amount}(""); + if (!success) revert TransferFailed(); +} +``` + +### 5. Input Validation + +```solidity +function deposit(uint256 amount) external { + if (amount == 0) revert InvalidAmount(); + if (amount > MAX_DEPOSIT) revert ExceedsMax(amount, MAX_DEPOSIT); + + // Proceed +} +``` + +### 6. Integer Overflow Protection + +```solidity +// Built-in since 0.8.0, but be careful with unchecked +function safeAdd(uint256 a, uint256 b) internal pure returns (uint256) { + // This reverts on overflow (default behavior) + return a + b; +} +``` + +### 7. Timestamp Dependence + +```solidity +// Don't use for randomness +// Small manipulations possible by miners + +// OK for time-based logic with tolerance +require(block.timestamp >= deadline, "Not yet"); + +// Bad: exact timing +require(block.timestamp == exactTime, "Wrong time"); +``` + +## EVM Version Features + +### Prague (0.8.30 default) + +- All Cancun features +- EIP-7702 preparation + +### Cancun (0.8.25 default) + +- Transient storage (TLOAD, TSTORE) +- MCOPY for memory copying +- BLOBHASH, BLOBBASEFEE + +### Shanghai (0.8.20 default) + +- PUSH0 opcode + +### Block Properties by Version + +```solidity +// Always available +block.number +block.timestamp +block.chainid +block.coinbase +block.gaslimit + +// Paris+ +block.prevrandao // Replaces block.difficulty + +// Cancun+ +block.blobbasefee +blobhash(index) +``` + +## Common Patterns + +### Factory Pattern + +```solidity +contract Factory { + event Created(address indexed instance); + + function create(bytes32 salt, bytes calldata initData) external returns (address) { + address instance = address(new Contract{salt: salt}(initData)); + emit Created(instance); + return instance; + } + + function predictAddress(bytes32 salt, bytes calldata initData) external view returns (address) { + bytes32 hash = keccak256(abi.encodePacked( + bytes1(0xff), + address(this), + salt, + keccak256(abi.encodePacked( + type(Contract).creationCode, + abi.encode(initData) + )) + )); + return address(uint160(uint256(hash))); + } +} +``` + +### Minimal Proxy (EIP-1167) + +```solidity +function clone(address implementation) internal returns (address instance) { + assembly { + let ptr := mload(0x40) + mstore(ptr, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000) + mstore(add(ptr, 0x14), shl(0x60, implementation)) + mstore(add(ptr, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000) + instance := create(0, ptr, 0x37) + } + require(instance != address(0), "Clone failed"); +} +``` + +### Merkle Proof Verification + +```solidity +function verify( + bytes32[] calldata proof, + bytes32 root, + bytes32 leaf +) public pure returns (bool) { + bytes32 computedHash = leaf; + for (uint256 i = 0; i < proof.length;) { + bytes32 proofElement = proof[i]; + if (computedHash <= proofElement) { + computedHash = keccak256(abi.encodePacked(computedHash, proofElement)); + } else { + computedHash = keccak256(abi.encodePacked(proofElement, computedHash)); + } + unchecked { ++i; } + } + return computedHash == root; +} +``` diff --git a/skills/skill/references/testing.md b/skills/skill/references/testing.md new file mode 100644 index 0000000..ec190f1 --- /dev/null +++ b/skills/skill/references/testing.md @@ -0,0 +1,635 @@ +# Foundry Testing Guide + +Complete guide to testing smart contracts with Forge. + +## Test Structure + +### File Conventions + +- Test files: `ContractName.t.sol` +- Script files: `ScriptName.s.sol` +- Test contracts inherit from `Test` + +### Function Prefixes + +| Prefix | Purpose | +|--------|---------| +| `test_` | Unit test | +| `testFuzz_` | Fuzz test | +| `testFork_` | Fork test | +| `invariant_` | Invariant test | +| `test_RevertIf_` | Expected revert | +| `test_RevertWhen_` | Expected revert | + +### Basic Test Contract + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.30; + +import {Test, console} from "forge-std/Test.sol"; +import {Vault} from "../src/Vault.sol"; + +contract VaultTest is Test { + Vault public vault; + address public alice; + address public bob; + + function setUp() public { + vault = new Vault(); + alice = makeAddr("alice"); + bob = makeAddr("bob"); + deal(alice, 100 ether); + deal(bob, 100 ether); + } + + function test_Deposit() public { + vm.prank(alice); + vault.deposit{value: 1 ether}(); + assertEq(vault.balanceOf(alice), 1 ether); + } +} +``` + +## Unit Testing + +### Assertions + +```solidity +// Equality +assertEq(actual, expected); +assertEq(actual, expected, "custom message"); +assertNotEq(actual, expected); + +// Comparisons +assertGt(a, b); // a > b +assertGe(a, b); // a >= b +assertLt(a, b); // a < b +assertLe(a, b); // a <= b + +// Boolean +assertTrue(condition); +assertFalse(condition); + +// Approximation (for rounding) +assertApproxEqAbs(actual, expected, maxDelta); +assertApproxEqRel(actual, expected, maxPercentDelta); // 1e18 = 100% + +// Arrays +assertEq(arr1, arr2); +``` + +### Testing Reverts + +```solidity +// Expect any revert +vm.expectRevert(); +vault.withdraw(1000 ether); + +// Expect specific message +vm.expectRevert("Insufficient balance"); +vault.withdraw(1000 ether); + +// Expect custom error (selector only) +vm.expectRevert(InsufficientBalance.selector); +vault.withdraw(1000 ether); + +// Expect custom error with parameters +vm.expectRevert( + abi.encodeWithSelector( + InsufficientBalance.selector, + 0, // available + 1000 // required + ) +); +vault.withdraw(1000 ether); + +// Partial revert (match selector only) +vm.expectPartialRevert(InsufficientBalance.selector); +vault.withdraw(1000 ether); +``` + +### Testing Events + +```solidity +// Expect event emission +vm.expectEmit(true, true, false, true); +// topic1, topic2, topic3, data +emit Transfer(alice, bob, 100); + +// Call that should emit the event +token.transfer(bob, 100); + +// Record and inspect logs +vm.recordLogs(); +token.transfer(bob, 100); +Vm.Log[] memory logs = vm.getRecordedLogs(); + +assertEq(logs[0].topics[0], keccak256("Transfer(address,address,uint256)")); +``` + +### Testing Access Control + +```solidity +function test_OnlyOwner() public { + // Should succeed as owner + vault.adminFunction(); + + // Should fail as non-owner + vm.expectRevert("Ownable: caller is not the owner"); + vm.prank(alice); + vault.adminFunction(); +} +``` + +## Fuzz Testing + +Forge automatically generates random inputs for test parameters. + +### Basic Fuzz Test + +```solidity +function testFuzz_Deposit(uint256 amount) public { + // Bound input to valid range + amount = bound(amount, 0.01 ether, 100 ether); + + deal(alice, amount); + vm.prank(alice); + vault.deposit{value: amount}(); + + assertEq(vault.balanceOf(alice), amount); +} +``` + +### Fuzz Configuration + +```toml +# foundry.toml +[fuzz] +runs = 256 # Number of fuzz runs +seed = 42 # Deterministic seed (optional) +max_test_rejects = 65536 +``` + +### Fixtures (Pre-defined Values) + +```solidity +// Array fixture (must match parameter name) +uint256[] public fixtureAmount = [0, 1, 100, type(uint256).max]; + +// Function fixture +function fixtureUser() public returns (address[] memory) { + address[] memory users = new address[](3); + users[0] = makeAddr("alice"); + users[1] = makeAddr("bob"); + users[2] = address(0); + return users; +} + +function testFuzz_Transfer(uint256 amount, address user) public { + // amount will be one of: 0, 1, 100, max + // user will be one of: alice, bob, address(0) +} +``` + +### Using `vm.assume()` + +```solidity +function testFuzz_Transfer(address recipient) public { + // Skip invalid inputs + vm.assume(recipient != address(0)); + vm.assume(recipient != address(token)); + + // Or use helper + assumeNotZeroAddress(recipient); + assumeNotPrecompile(recipient); +} +``` + +## Invariant Testing + +Test that properties hold across random function call sequences. + +### Basic Invariant Test + +```solidity +contract VaultInvariantTest is Test { + Vault public vault; + VaultHandler public handler; + + function setUp() public { + vault = new Vault(); + handler = new VaultHandler(vault); + + // Only fuzz the handler + targetContract(address(handler)); + } + + function invariant_SolvencyCheck() public view { + assertGe( + address(vault).balance, + vault.totalDeposits(), + "Vault is insolvent" + ); + } + + function invariant_TotalSupplyConsistent() public view { + assertEq( + vault.totalSupply(), + sumAllBalances(), + "Supply mismatch" + ); + } +} +``` + +### Handler Pattern + +Wrap target contract to bound inputs and track state: + +```solidity +contract VaultHandler is Test { + Vault public vault; + uint256 public ghost_totalDeposited; + address[] public actors; + + constructor(Vault _vault) { + vault = _vault; + actors.push(makeAddr("alice")); + actors.push(makeAddr("bob")); + actors.push(makeAddr("charlie")); + } + + function deposit(uint256 actorIndex, uint256 amount) external { + // Bound inputs + actorIndex = bound(actorIndex, 0, actors.length - 1); + amount = bound(amount, 0.01 ether, 10 ether); + + address actor = actors[actorIndex]; + deal(actor, amount); + + vm.prank(actor); + vault.deposit{value: amount}(); + + // Track ghost variable + ghost_totalDeposited += amount; + } + + function withdraw(uint256 actorIndex, uint256 amount) external { + actorIndex = bound(actorIndex, 0, actors.length - 1); + address actor = actors[actorIndex]; + + uint256 balance = vault.balanceOf(actor); + amount = bound(amount, 0, balance); + + vm.prank(actor); + vault.withdraw(amount); + + ghost_totalDeposited -= amount; + } +} +``` + +### Target Configuration + +```solidity +function setUp() public { + // Only fuzz specific contracts + targetContract(address(handler)); + + // Exclude contracts from fuzzing + excludeContract(address(vault)); + excludeContract(address(token)); + + // Only use specific senders + targetSender(alice); + targetSender(bob); + + // Exclude specific senders + excludeSender(address(0)); + + // Target specific functions + bytes4[] memory selectors = new bytes4[](2); + selectors[0] = handler.deposit.selector; + selectors[1] = handler.withdraw.selector; + targetSelector(FuzzSelector({ + addr: address(handler), + selectors: selectors + })); +} +``` + +### Invariant Configuration + +```toml +# foundry.toml +[invariant] +runs = 256 # Number of sequences +depth = 50 # Calls per sequence +fail_on_revert = false # Continue on reverts +shrink_run_limit = 5000 # Shrinking iterations +``` + +## Fork Testing + +Test against live blockchain state. + +### Basic Fork Test + +```solidity +contract ForkTest is Test { + uint256 mainnetFork; + address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + address constant WHALE = 0x47ac0Fb4F2D84898e4D9E7b4DaB3C24507a6D503; + + function setUp() public { + mainnetFork = vm.createFork(vm.envString("MAINNET_RPC_URL")); + vm.selectFork(mainnetFork); + } + + function testFork_USDCTransfer() public { + vm.prank(WHALE); + IERC20(USDC).transfer(address(this), 1000e6); + assertEq(IERC20(USDC).balanceOf(address(this)), 1000e6); + } +} +``` + +### Fork Cheatcodes + +```solidity +// Create fork +uint256 forkId = vm.createFork("https://eth-mainnet.alchemyapi.io/v2/..."); +uint256 forkId = vm.createFork("mainnet"); // Uses foundry.toml rpc_endpoints +uint256 forkId = vm.createFork("mainnet", 18000000); // Pin to block + +// Create and select in one call +vm.createSelectFork("mainnet"); + +// Switch between forks +vm.selectFork(mainnetFork); +vm.selectFork(arbitrumFork); + +// Get active fork +uint256 active = vm.activeFork(); + +// Roll fork to different block +vm.rollFork(18500000); +vm.rollFork(arbitrumFork, 150000000); + +// Make account persist across forks +vm.makePersistent(address(myContract)); +vm.makePersistent(alice, bob, charlie); + +// Check persistence +bool isPersistent = vm.isPersistent(address(myContract)); + +// Revoke persistence +vm.revokePersistent(address(myContract)); +``` + +### Multi-Fork Testing + +```solidity +function testFork_CrossChain() public { + // Deploy on mainnet + vm.selectFork(mainnetFork); + address mainnetToken = address(new Token()); + + // Deploy on Arbitrum + vm.selectFork(arbitrumFork); + address arbitrumToken = address(new Token()); + + // Verify different deployments + vm.selectFork(mainnetFork); + assertEq(Token(mainnetToken).name(), "Token"); + + vm.selectFork(arbitrumFork); + assertEq(Token(arbitrumToken).name(), "Token"); +} +``` + +### Fork Configuration + +```toml +# foundry.toml +[rpc_endpoints] +mainnet = "${MAINNET_RPC_URL}" +sepolia = "${SEPOLIA_RPC_URL}" +arbitrum = "${ARBITRUM_RPC_URL}" +optimism = "${OPTIMISM_RPC_URL}" +``` + +## Differential Testing + +Compare implementations against reference. + +```solidity +function testDifferential_MerkleRoot(bytes32[] memory leaves) public { + vm.assume(leaves.length > 0 && leaves.length < 100); + + // Solidity implementation + bytes32 solidityRoot = merkle.getRoot(leaves); + + // Reference implementation (via FFI) + string[] memory cmd = new string[](3); + cmd[0] = "node"; + cmd[1] = "scripts/merkle.js"; + cmd[2] = vm.toString(abi.encode(leaves)); + + bytes memory result = vm.ffi(cmd); + bytes32 jsRoot = abi.decode(result, (bytes32)); + + assertEq(solidityRoot, jsRoot, "Merkle root mismatch"); +} +``` + +## Complete Cheatcode Reference + +### State Manipulation + +```solidity +// ETH balance +vm.deal(address, uint256); + +// Code at address +vm.etch(address, bytes memory code); + +// Storage +vm.store(address, bytes32 slot, bytes32 value); +bytes32 value = vm.load(address, bytes32 slot); + +// Nonce +vm.setNonce(address, uint64 nonce); +vm.resetNonce(address); +uint64 nonce = vm.getNonce(address); + +// Copy storage +vm.copyStorage(address from, address to); +``` + +### Caller Context + +```solidity +// Single call +vm.prank(address sender); +vm.prank(address sender, address origin); + +// Multiple calls +vm.startPrank(address sender); +vm.startPrank(address sender, address origin); +vm.stopPrank(); +``` + +### Time & Block + +```solidity +vm.warp(uint256 timestamp); // block.timestamp +vm.roll(uint256 blockNumber); // block.number +vm.fee(uint256 gasPrice); // tx.gasprice +vm.difficulty(uint256 difficulty); // block.difficulty +vm.coinbase(address miner); // block.coinbase +vm.chainId(uint256 chainId); // block.chainid +vm.prevrandao(bytes32 prevrandao); // block.prevrandao +``` + +### Expectations + +```solidity +// Reverts +vm.expectRevert(); +vm.expectRevert(bytes memory message); +vm.expectRevert(bytes4 selector); +vm.expectPartialRevert(bytes4 selector); + +// Events +vm.expectEmit(bool topic1, bool topic2, bool topic3, bool data); +vm.expectEmit(bool topic1, bool topic2, bool topic3, bool data, address emitter); + +// Calls +vm.expectCall(address target, bytes memory data); +vm.expectCall(address target, uint256 value, bytes memory data); +vm.expectCall(address target, bytes memory data, uint64 count); +``` + +### Snapshots + +```solidity +uint256 snapshot = vm.snapshot(); +vm.revertTo(snapshot); +vm.revertToAndDelete(snapshot); +``` + +### Environment + +```solidity +// Read .env +string memory value = vm.envString("KEY"); +uint256 value = vm.envUint("KEY"); +address value = vm.envAddress("KEY"); +bool value = vm.envBool("KEY"); +bytes32 value = vm.envBytes32("KEY"); + +// With default +uint256 value = vm.envOr("KEY", uint256(100)); + +// Check existence +bool exists = vm.envExists("KEY"); +``` + +### Cryptography + +```solidity +// Sign message +(uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); +(bytes32 r, bytes32 vs) = vm.signCompact(privateKey, digest); + +// Get address from private key +address addr = vm.addr(privateKey); + +// Derive key from mnemonic +uint256 key = vm.deriveKey(mnemonic, index); +uint256 key = vm.deriveKey(mnemonic, path, index); + +// Create wallet +Vm.Wallet memory wallet = vm.createWallet("label"); +Vm.Wallet memory wallet = vm.createWallet(privateKey); +``` + +### Labels & Debugging + +```solidity +vm.label(address, "name"); +string memory name = vm.getLabel(address); + +vm.breakpoint(); // Debugger breakpoint +vm.breakpoint("name"); // Named breakpoint +``` + +### Gas Metering + +```solidity +vm.pauseGasMetering(); +// ... expensive operations ... +vm.resumeGasMetering(); + +Vm.Gas memory gas = vm.lastCallGas(); +``` + +### File I/O + +```solidity +string memory content = vm.readFile("path/to/file"); +vm.writeFile("path/to/file", "content"); +bool exists = vm.exists("path/to/file"); +vm.removeFile("path/to/file"); +``` + +## Test Verbosity Levels + +```bash +forge test # Summary only +forge test -v # + Logs +forge test -vv # + Assertion errors +forge test -vvv # + Stack traces (failures) +forge test -vvvv # + Stack traces (all) + setup +forge test -vvvvv # + All traces always +``` + +## Best Practices + +### Test Organization + +1. One test file per contract +2. Group related tests in same contract +3. Use descriptive function names +4. Include failure reason in test name + +### Fuzz Testing + +1. Always `bound()` numeric inputs +2. Use `vm.assume()` sparingly (reduces coverage) +3. Use fixtures for edge cases +4. Set deterministic seed in CI + +### Invariant Testing + +1. Use handler pattern for complex protocols +2. Track ghost variables for assertions +3. Start with simple invariants +4. Increase depth/runs gradually + +### Fork Testing + +1. Pin to specific block for reproducibility +2. Configure RPC endpoints in foundry.toml +3. Use `vm.makePersistent()` for deployed contracts +4. Cache responses with deterministic seed + +### General + +1. Test public interface, not internal state +2. Use `makeAddr()` for labeled addresses +3. Use `deal()` instead of minting +4. Label important addresses for traces +5. Test both success and failure paths