foundry-solidity
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 fil
Install
npx skills add https://github.com/tenequm/skills/tree/main/skills/foundry-solidity
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install tenequm-skills@llmmart
git clone https://github.com/tenequm/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole tenequm/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
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.solfiles - Debugging transactions and contract interactions
Quick Start
# 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
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
# 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
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
anvil # Start local node
anvil --fork-url $RPC_URL # Fork mainnet
anvil --fork-block-number 18000000
Basic Test Contract
// 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
// 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
// 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)
// 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)
[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.mdfor complete testing patterns (unit, fuzz, invariant, fork), all cheatcodes, and best practices - forge-std API: See
references/forge-std-api.mdfor complete library reference (150+ functions) - Solidity 0.8.30: See
references/solidity-modern.mdfor new features and modern syntax - Deployment: See
references/deployment.mdfor scripting, verification, and multi-chain deployment - Configuration: See
references/configuration.mdfor all foundry.toml options - Gas Optimization: See
references/gas-optimization.mdfor storage packing, compiler settings, and profiling - Patterns: See
references/patterns.mdfor access control, reentrancy guards, factories, and common idioms - Security: See
references/security.mdfor vulnerabilities, defensive patterns, and audit preparation - Resources: See
references/resources.mdfor official docs, libraries, security tools, and learning paths - Debugging: See
references/debugging.mdfor traces, breakpoints, console.log, and the interactive debugger - Dependencies: See
references/dependencies.mdfor forge install, remappings, and Soldeer package manager - CI/CD: See
references/cicd.mdfor GitHub Actions workflows, caching, and gas tracking - Chisel: See
references/chisel.mdfor the interactive Solidity REPL - Cast Advanced: See
references/cast-advanced.mdfor decoding, encoding, wallet management, and batch operations - Anvil Advanced: See
references/anvil-advanced.mdfor impersonation, state manipulation, and mining modes
Files (skills)
-
references
-
anvil-advanced.md 5.2 KB
# 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 ); } ``` -
cast-advanced.md 4.6 KB
# 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)),...]" ``` -
chisel.md 2.3 KB
# 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. -
cicd.md 5.6 KB
# 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: <secret-MAINNET_RPC_URL> ``` In your real workflow, replace `<secret-MAINNET_RPC_URL>` with the GitHub Actions secrets expression for the same name (see [Using encrypted secrets in a workflow](https://docs.github.com/en/actions/security-guides/encrypted-secrets#using-encrypted-secrets-in-a-workflow)). The same convention applies to the deploy block below. ## 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 <secret-RPC_URL> \ --broadcast \ --verify env: PRIVATE_KEY: <secret-DEPLOYER_PRIVATE_KEY> ETHERSCAN_API_KEY: <secret-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> ``` -
configuration.md 8.4 KB
# 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 ``` -
debugging.md 5.1 KB
# 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` -
dependencies.md 3.8 KB
# 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. -
deployment.md 13.6 KB
# 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)); } } ``` -
forge-std-api.md 13 KB
# 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; ``` -
gas-optimization.md 7.2 KB
# 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 | -
patterns.md 13.3 KB
# 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); ``` -
resources.md 4.5 KB
# 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 -
security.md 11.8 KB
# 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 ``` -
solidity-modern.md 14.7 KB
# 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; } ``` -
testing.md 14.3 KB
# 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
-
-
LICENSE.txt 8.9 KB
Apache License Version 2.0, January 2004 https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS -
SKILL.md 9.2 KB
--- name: foundry-solidity description: Build and test Solidity contracts with Foundry. Use when developing Ethereum contracts, writing Forge tests, deploying with scripts, or debugging with Cast/Anvil. Triggers on forge, cast, anvil, foundry.toml, *.t.sol, or *.s.sol. metadata: version: "0.2.5" categories: "development" topics: "solidity, foundry, forge, smart-contracts, ethereum" openclaw: homepage: https://github.com/tenequm/skills/tree/main/skills/foundry-solidity emoji: "⚒️" primaryEnv: PRIVATE_KEY requires: anyBins: - forge - cast install: - kind: brew formula: foundry bins: - forge - cast - anvil envVars: - name: PRIVATE_KEY required: false description: Deployer signing key. Use throwaway/testnet keys. - name: ETHERSCAN_API_KEY required: false description: Etherscan verification API key. - name: MAINNET_RPC_URL required: false description: Ethereum mainnet RPC endpoint. - name: SEPOLIA_RPC_URL required: false description: Sepolia testnet RPC endpoint. - name: ARBITRUM_RPC_URL required: false description: Arbitrum RPC endpoint. - name: OPTIMISM_RPC_URL required: false description: Optimism RPC endpoint. - name: ARBISCAN_API_KEY required: false description: Arbiscan verification key. - name: OPTIMISTIC_ETHERSCAN_API_KEY required: false description: Optimistic Etherscan verification key. - name: POLYGONSCAN_API_KEY required: false description: Polygonscan verification key. - name: BASESCAN_API_KEY required: false description: Basescan verification key. - name: DEPLOYER_PRIVATE_KEY required: false description: Alternative deployer key alias. - name: TEST_ADMIN required: false description: Test admin address used in fork tests. --- # 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
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.