一键导入
testing
Smart contract testing with Foundry — unit tests, fuzz testing, fork testing, invariant testing. What to test, what not to test, and what LLMs get wrong.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Smart contract testing with Foundry — unit tests, fuzz testing, fork testing, invariant testing. What to test, what not to test, and what LLMs get wrong.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Best practices for building performant 2D web games with Phaser 3, React integration, and TypeScript.
Verified contract addresses for major Ethereum protocols across mainnet and L2s. Use this instead of guessing or hallucinating addresses. Includes Uniswap, Aave, Compound, Aerodrome, GMX, Pendle, Velodrome, Camelot, SyncSwap, Lido, Rocket Pool, 1inch, Permit2, MakerDAO/sDAI, EigenLayer, Across, Chainlink CCIP, Yearn V3, USDC, USDT, DAI, ENS, Safe, Chainlink, and more. Always verify addresses against a block explorer before sending transactions.
Deep EVM smart contract security audit system. Use when asked to audit a contract, find vulnerabilities, review code for security issues, or file security issues on a GitHub repo. Covers 500+ non-obvious checklist items across 19 domains via parallel sub-agents. Different from the security skill (which teaches defensive coding) — this is for systematically auditing contracts you didn't write.
DeFi legos and protocol composability on Ethereum and L2s. Major protocols per chain — Aerodrome on Base, GMX/Pendle on Arbitrum, Velodrome on Optimism — plus mainnet primitives (Uniswap, Aave, Compound, Curve). How they work, how to build on them, and how to combine them. Use when building DeFi integrations, choosing protocols on a specific L2, designing yield strategies, or composing existing protocols into something new.
The essential mental models for building onchain — focused on what LLMs get wrong and what humans need explained. "Nothing is automatic" and "incentives are everything" are the core messages. Use when your human is new to onchain development, when they're designing a system, or when they ask "how does this actually work?" Also use when YOU are designing a system — the state machine + incentive framework catches design mistakes before they become dead code.
Deprecated: this skill has moved to addresses.
| name | testing |
| description | Smart contract testing with Foundry — unit tests, fuzz testing, fork testing, invariant testing. What to test, what not to test, and what LLMs get wrong. |
You test getters and trivial functions. Testing that name() returns the name is worthless. Test edge cases, failure modes, and economic invariants — the things that lose money when they break.
You don't fuzz. forge test finds the bugs you thought of. Fuzzing finds the ones you didn't. If your contract does math, fuzz it. If it handles user input, fuzz it. If it moves value, definitely fuzz it.
You don't fork-test. If your contract calls Uniswap, Aave, or any external protocol, test against their real deployed contracts on a fork. Mocking them hides integration bugs that only appear with real state.
You write tests that mirror the implementation. Testing that deposit(100) sets balance[user] = 100 is tautological — you're testing that Solidity assignments work. Test properties: "after deposit and withdraw, user gets their tokens back." Test invariants: "total deposits always equals contract balance."
You skip invariant testing for stateful protocols. If your contract has multiple interacting functions that change state over time (vaults, AMMs, lending), you need invariant tests. Unit tests check one path; invariant tests check that properties hold across thousands of random sequences.
// test/MyContract.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Test, console} from "forge-std/Test.sol";
import {MyToken} from "../src/MyToken.sol";
contract MyTokenTest is Test {
MyToken public token;
address public alice = makeAddr("alice");
address public bob = makeAddr("bob");
function setUp() public {
token = new MyToken("Test", "TST", 1_000_000e18);
// Give alice some tokens for testing
token.transfer(alice, 10_000e18);
}
function test_TransferUpdatesBalances() public {
vm.prank(alice);
token.transfer(bob, 1_000e18);
assertEq(token.balanceOf(alice), 9_000e18);
assertEq(token.balanceOf(bob), 1_000e18);
}
function test_TransferEmitsEvent() public {
vm.expectEmit(true, true, false, true);
emit Transfer(alice, bob, 500e18);
vm.prank(alice);
token.transfer(bob, 500e18);
}
function test_RevertWhen_TransferExceedsBalance() public {
vm.prank(alice);
vm.expectRevert();
token.transfer(bob, 999_999e18); // More than alice has
}
function test_RevertWhen_TransferToZeroAddress() public {
vm.prank(alice);
vm.expectRevert();
token.transfer(address(0), 100e18);
}
}
// Equality
assertEq(actual, expected);
assertEq(actual, expected, "descriptive error message");
// Comparisons
assertGt(a, b); // a > b
assertGe(a, b); // a >= b
assertLt(a, b); // a < b
assertLe(a, b); // a <= b
// Approximate equality (for math with rounding)
assertApproxEqAbs(actual, expected, maxDelta);
assertApproxEqRel(actual, expected, maxPercentDelta); // in WAD (1e18 = 100%)
// Revert expectations
vm.expectRevert(); // Any revert
vm.expectRevert("Insufficient balance"); // Specific message
vm.expectRevert(MyContract.CustomError.selector); // Custom error
// Event expectations
vm.expectEmit(true, true, false, true); // (topic1, topic2, topic3, data)
emit MyEvent(expectedArg1, expectedArg2);
// ✅ TEST: Edge cases that lose money
function test_TransferZeroAmount() public { /* ... */ }
function test_TransferEntireBalance() public { /* ... */ }
function test_TransferToSelf() public { /* ... */ }
function test_ApproveOverwrite() public { /* ... */ }
function test_TransferFromWithExactAllowance() public { /* ... */ }
// ✅ TEST: Access control
function test_RevertWhen_NonOwnerCallsAdminFunction() public { /* ... */ }
function test_OwnerCanPause() public { /* ... */ }
// ✅ TEST: Failure modes
function test_RevertWhen_DepositZero() public { /* ... */ }
function test_RevertWhen_WithdrawMoreThanDeposited() public { /* ... */ }
function test_RevertWhen_ContractPaused() public { /* ... */ }
// ❌ DON'T TEST: OpenZeppelin internals
// function test_NameReturnsName() — they already tested this
// function test_SymbolReturnsSymbol() — waste of time
// function test_DecimalsReturns18() — it does, trust it
Foundry automatically fuzzes any test function with parameters. Instead of testing one value, it tests hundreds of random values.
// Foundry calls this with random amounts
function testFuzz_DepositWithdrawRoundtrip(uint256 amount) public {
// Bound input to valid range
amount = bound(amount, 1, token.balanceOf(alice));
uint256 balanceBefore = token.balanceOf(alice);
vm.startPrank(alice);
token.approve(address(vault), amount);
vault.deposit(amount, alice);
vault.withdraw(vault.balanceOf(alice), alice, alice);
vm.stopPrank();
// Property: user gets back what they deposited (minus any fees)
assertGe(token.balanceOf(alice), balanceBefore - 1); // Allow 1 wei rounding
}
// bound() is preferred over vm.assume() — bound reshapes, assume discards
function testFuzz_Fee(uint256 amount, uint256 feeBps) public {
amount = bound(amount, 1e6, 1e30); // Reasonable token amounts
feeBps = bound(feeBps, 1, 10_000); // 0.01% to 100%
uint256 fee = (amount * feeBps) / 10_000;
uint256 afterFee = amount - fee;
// Property: fee + remainder always equals original
assertEq(fee + afterFee, amount);
}
// vm.assume() discards inputs — use sparingly
function testFuzz_Division(uint256 a, uint256 b) public {
vm.assume(b > 0); // Skip zero (would revert)
// ...
}
# Default: 256 runs
forge test
# More thorough: 10,000 runs
forge test --fuzz-runs 10000
# Set in foundry.toml for CI
# [fuzz]
# runs = 1000
Test your contract against real deployed protocols on a mainnet fork. This catches integration bugs that mocks can't.
contract SwapTest is Test {
// Real mainnet addresses
address constant UNISWAP_ROUTER = 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45;
address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
function setUp() public {
// Fork mainnet at a specific block for reproducibility
vm.createSelectFork("mainnet", 19_000_000);
}
function test_SwapETHForUSDC() public {
address user = makeAddr("user");
vm.deal(user, 1 ether);
vm.startPrank(user);
// Build swap path
ISwapRouter.ExactInputSingleParams memory params = ISwapRouter
.ExactInputSingleParams({
tokenIn: WETH,
tokenOut: USDC,
fee: 3000,
recipient: user,
amountIn: 0.1 ether,
amountOutMinimum: 0, // In production, NEVER set to 0
sqrtPriceLimitX96: 0
});
// Execute swap
uint256 amountOut = ISwapRouter(UNISWAP_ROUTER).exactInputSingle{value: 0.1 ether}(params);
vm.stopPrank();
// Verify we got USDC back
assertGt(amountOut, 0, "Should receive USDC");
assertGt(IERC20(USDC).balanceOf(user), 0);
}
}
# Fork from RPC URL
forge test --fork-url https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY
# Fork at specific block (reproducible)
forge test --fork-url https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY --fork-block-number 19000000
# Set in foundry.toml to avoid CLI flags
# [rpc_endpoints]
# mainnet = "${MAINNET_RPC_URL}"
Invariant tests verify that properties hold across thousands of random function call sequences. Essential for stateful protocols.
Invariants are properties that must ALWAYS be true, no matter what sequence of actions users take:
contract VaultInvariantTest is Test {
MyVault public vault;
IERC20 public token;
VaultHandler public handler;
function setUp() public {
token = new MockERC20("Test", "TST", 18);
vault = new MyVault(token);
handler = new VaultHandler(vault, token);
// Tell Foundry which contract to call randomly
targetContract(address(handler));
}
// This runs after every random sequence
function invariant_TotalAssetsMatchesBalance() public view {
assertEq(
vault.totalAssets(),
token.balanceOf(address(vault)),
"Total assets must equal actual balance"
);
}
function invariant_SharePriceNeverZero() public view {
if (vault.totalSupply() > 0) {
assertGt(vault.convertToAssets(1e18), 0, "Share price must never be zero");
}
}
}
// Handler: guided random actions
contract VaultHandler is Test {
MyVault public vault;
IERC20 public token;
constructor(MyVault _vault, IERC20 _token) {
vault = _vault;
token = _token;
}
function deposit(uint256 amount) public {
amount = bound(amount, 1, 1e24);
deal(address(token), msg.sender, amount);
vm.startPrank(msg.sender);
token.approve(address(vault), amount);
vault.deposit(amount, msg.sender);
vm.stopPrank();
}
function withdraw(uint256 shares) public {
uint256 maxShares = vault.balanceOf(msg.sender);
if (maxShares == 0) return;
shares = bound(shares, 1, maxShares);
vm.prank(msg.sender);
vault.redeem(shares, msg.sender, msg.sender);
}
}
# Default depth (15 calls per sequence, 256 sequences)
forge test
# Deeper exploration
forge test --fuzz-runs 1000
# Configure in foundry.toml
# [invariant]
# runs = 512
# depth = 50
ERC20.transfer works. It's been audited by dozens of firms and used by thousands of contracts. Test YOUR logic on top of it.require reverts or that mapping stores values. The compiler works.name() returns the name you passed to the constructor, that's not a test — it's a tautology.Focus your testing effort on: Custom business logic, mathematical operations, integration points with external protocols, access control boundaries, and economic edge cases.
expectEmitforge snapshot to catch regressionsslither . — no high/medium findings unaddressedforge test -vvv