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