Initial commit
This commit is contained in:
12
.claude-plugin/plugin.json
Normal file
12
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "foundry-solidity",
|
||||||
|
"description": "Skill: Build and test Solidity smart contracts with Foundry toolkit (Forge, Cast, Anvil) - testing patterns, deployment scripts, and modern Solidity 0.8.30",
|
||||||
|
"version": "0.0.0-2025.11.28",
|
||||||
|
"author": {
|
||||||
|
"name": "Misha Kolesnik",
|
||||||
|
"email": "misha@kolesnik.io"
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
"./skills/skill"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# foundry-solidity
|
||||||
|
|
||||||
|
Skill: Build and test Solidity smart contracts with Foundry toolkit (Forge, Cast, Anvil) - testing patterns, deployment scripts, and modern Solidity 0.8.30
|
||||||
104
plugin.lock.json
Normal file
104
plugin.lock.json
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
{
|
||||||
|
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||||
|
"pluginId": "gh:tenequm/claude-plugins:foundry-solidity",
|
||||||
|
"normalized": {
|
||||||
|
"repo": null,
|
||||||
|
"ref": "refs/tags/v20251128.0",
|
||||||
|
"commit": "88d2a999c768b6f075de83316c1f36c4f7d21606",
|
||||||
|
"treeHash": "1c51bdffe44cb7995297be05eaad9e994143c2a9c3292df7899556be860d6cbb",
|
||||||
|
"generatedAt": "2025-11-28T10:28:39.100272Z",
|
||||||
|
"toolVersion": "publish_plugins.py@0.2.0"
|
||||||
|
},
|
||||||
|
"origin": {
|
||||||
|
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||||
|
"branch": "master",
|
||||||
|
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||||
|
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"name": "foundry-solidity",
|
||||||
|
"description": "Skill: Build and test Solidity smart contracts with Foundry toolkit (Forge, Cast, Anvil) - testing patterns, deployment scripts, and modern Solidity 0.8.30"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "README.md",
|
||||||
|
"sha256": "6515f2d14f41296f3fcdb1c7e3a6128c68981b896f0403356c80a75635a0e3e1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".claude-plugin/plugin.json",
|
||||||
|
"sha256": "091894c3e995c0ca2d9bb14430721c7183c2e82cc2861ac9481e64790034fb8c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/skill/SKILL.md",
|
||||||
|
"sha256": "f235dfea65236676f2be938afa4172567f56c95d981d795123a3c44178907c81"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/skill/references/resources.md",
|
||||||
|
"sha256": "f769aff1686f563fcb97a975eed5d02e91cf729b851f92edacacd2101e015cdd"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/skill/references/anvil-advanced.md",
|
||||||
|
"sha256": "75e0cdff5a204218890a4b93f1992128b4a89a794006fdf69ed24e62b49ed8b2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/skill/references/dependencies.md",
|
||||||
|
"sha256": "a82a8b1b5ac8e8cbe2d6f7647fa0df0e78ae3321ce544fc6d1f7455e7a250811"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/skill/references/testing.md",
|
||||||
|
"sha256": "acb8482cbc13062be64da13eda13f5bc734de69d4242a4d5d273ae45c4a930f2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/skill/references/forge-std-api.md",
|
||||||
|
"sha256": "ce4cc968aaee3e9d2af94f573f5df79c0b59172d547d3a14752ede152d6dcbf4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/skill/references/cicd.md",
|
||||||
|
"sha256": "1777bf4c25192012f1cc906a4deccfce32ec899e8e12d9a95e2ebdf4ef1c3a9e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/skill/references/patterns.md",
|
||||||
|
"sha256": "44a9cbf8b942e3f4ae0e4fe6380d861c73748400e751110b030bc3e7f5e40533"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/skill/references/deployment.md",
|
||||||
|
"sha256": "1ad234698e8e47604af8b192755c19a4810b0c0fcb0b3cf7e35e6e57df433f7c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/skill/references/configuration.md",
|
||||||
|
"sha256": "4f3f7038606f1269f25198cf21e88808b14f0f2989c7ec908b7c60fcc9949c08"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/skill/references/solidity-modern.md",
|
||||||
|
"sha256": "e2e858706c8f373914b316f533cfbbafb3d6d3fcd52bff20325662cbb9869fd4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/skill/references/cast-advanced.md",
|
||||||
|
"sha256": "946e29e032c526129f7004adc9afc10036d18c7d548e0d20df99bae309bc159a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/skill/references/chisel.md",
|
||||||
|
"sha256": "43e3f7ee660e64d287dd54c4bae270e27d5aee68b294cf8a0b9844a833e4444d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/skill/references/debugging.md",
|
||||||
|
"sha256": "0341c0e3ba880a1116bab55889931344570c79e03624fd8d3f56612ad0b2f973"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/skill/references/security.md",
|
||||||
|
"sha256": "f18759ffff0571bdcebf193987cf880ffe3aaf6f40b81ef949875ffdbe6405cb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/skill/references/gas-optimization.md",
|
||||||
|
"sha256": "51582bbbba0ee8d59ac7419e60ae5ca205d8f101061c0ec8262bb06b2e529e05"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dirSha256": "1c51bdffe44cb7995297be05eaad9e994143c2a9c3292df7899556be860d6cbb"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"scannedAt": null,
|
||||||
|
"scannerVersion": null,
|
||||||
|
"flags": []
|
||||||
|
}
|
||||||
|
}
|
||||||
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