Solidity Development with Foundry
Overview
Comprehensive skill for professional Solidity smart contract development using the Foundry framework. Provides security-first development practices, testing patterns, static analysis integration (Slither, solhint), and deployment workflows for EVM-compatible blockchains.
When to Use This Skill
Automatically activate when:
- Working with
.sol(Solidity) files - User mentions smart contracts, blockchain, Web3, DeFi, or Ethereum
- Foundry project detected (
foundry.tomlpresent) - User requests contract deployment, testing, or security auditing
- Working with forge, cast, or anvil commands
- Implementing ERC standards (ERC20, ERC721, ERC1155, etc.)
Development Workflow
1. Write Secure Contracts
Follow security-first patterns from references/solidity-security.md:
Always implement:
- Checks-Effects-Interactions (CEI) pattern
- Access control (Ownable, AccessControl)
- Input validation
- Emergency pause mechanisms
- Pull over push for payments
- Custom errors for gas efficiency
Example contract structure:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Pausable} from "@openzeppelin/contracts/security/Pausable.sol";
contract MyContract is Ownable, Pausable {
/*//////////////////////////////////////////////////////////////
ERRORS
//////////////////////////////////////////////////////////////*/
error InvalidInput();
error InsufficientBalance();
/*//////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
event ActionCompleted(address indexed user, uint256 amount);
/*//////////////////////////////////////////////////////////////
STATE VARIABLES
//////////////////////////////////////////////////////////////*/
mapping(address => uint256) public balances;
/*//////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////*/
constructor() Ownable(msg.sender) {}
/*//////////////////////////////////////////////////////////////
EXTERNAL FUNCTIONS
//////////////////////////////////////////////////////////////*/
function deposit() external payable whenNotPaused {
if (msg.value == 0) revert InvalidInput();
balances[msg.sender] += msg.value;
emit ActionCompleted(msg.sender, msg.value);
}
function withdraw(uint256 amount) external whenNotPaused {
// CEI Pattern: Checks
if (amount == 0) revert InvalidInput();
if (balances[msg.sender] < amount) revert InsufficientBalance();
// Effects
balances[msg.sender] -= amount;
// Interactions
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
emit ActionCompleted(msg.sender, amount);
}
}
2. Write Comprehensive Tests
Follow testing patterns from references/foundry-testing.md:
Test structure:
import {Test, console} from "forge-std/Test.sol";
import {MyContract} from "../src/MyContract.sol";
contract MyContractTest is Test {
MyContract public myContract;
address constant OWNER = address(1);
address constant USER = address(2);
function setUp() public {
vm.prank(OWNER);
myContract = new MyContract();
}
// Unit tests
function test_Deposit() public { }
// Edge cases
function test_RevertWhen_ZeroDeposit() public { }
// Fuzz tests
function testFuzz_Deposit(uint256 amount) public { }
// Invariant tests
function invariant_TotalBalanceMatchesContract() public { }
}
Run tests:
forge test # Run all tests
forge test -vvv # Verbose output
forge test --gas-report # Include gas costs
forge coverage # Coverage report
3. Security Analysis
Run comprehensive security checks:
# Static analysis with Slither
slither . --exclude-optimization --exclude-informational
# Linting with solhint
solhint 'src/**/*.sol' 'test/**/*.sol'
# Or use the automated script
bash scripts/check_security.sh
Pre-deployment checklist (from references/solidity-security.md):
- [ ] Reentrancy protection implemented
- [ ] Access controls on privileged functions
- [ ] Input validation on all external functions
- [ ] Emergency pause mechanism
- [ ] External calls follow CEI pattern
- [ ] Events emitted for state changes
- [ ] Gas optimizations applied
- [ ] Test coverage >90%
- [ ] Slither analysis passed
- [ ] solhint checks passed
- [ ] Professional audit for mainnet
4. Build and Deploy
Build contracts:
forge build --optimize --optimizer-runs 200
Deploy using script:
// script/Deploy.s.sol
import {Script} from "forge-std/Script.sol";
import {MyContract} from "../src/MyContract.sol";
contract DeployScript is Script {
function run() external {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
MyContract myContract = new MyContract();
console.log("Deployed at:", address(myContract));
vm.stopBroadcast();
}
}
Execute deployment:
# Load environment variables
source .env
# Simulate deployment
forge script script/Deploy.s.sol --rpc-url $SEPOLIA_RPC_URL
# Deploy to testnet
forge script script/Deploy.s.sol \
--rpc-url $SEPOLIA_RPC_URL \
--broadcast \
--verify
# Deploy to mainnet (after audits!)
forge script script/Deploy.s.sol \
--rpc-url $MAINNET_RPC_URL \
--broadcast \
--verify
Advanced Testing Patterns
Mainnet Forking
Test against real deployed contracts:
contract ForkTest is Test {
IERC20 constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
function setUp() public {
vm.createSelectFork("mainnet", 18_000_000);
}
function test_InteractWithRealProtocol() public {
// Test with actual mainnet state
}
}
forge test --fork-url $MAINNET_RPC_URL
Invariant Testing
Test properties that must always hold:
function invariant_SumOfBalancesEqualsTotalSupply() public {
assertEq(token.totalSupply(), handler.sumOfBalances());
}
Fuzz Testing
Test with randomized inputs:
function testFuzz_Transfer(address to, uint256 amount) public {
vm.assume(to != address(0));
amount = bound(amount, 1, 1000 ether);
// Test with random inputs
}
Gas Optimization
Key strategies from references/solidity-security.md:
- Storage packing - Group uint8/uint16/etc in single slots
- uint256 for counters - Native EVM word size
- calldata over memory - For function parameters
- Events over storage - Log data instead of storing
- Custom errors - More gas-efficient than strings
- Immutable/constant - For unchanging values
Contract Verification
# Verify on Etherscan
forge verify-contract $CONTRACT_ADDRESS \
src/MyContract.sol:MyContract \
--chain-id 1 \
--etherscan-api-key $ETHERSCAN_KEY
# With constructor args
forge verify-contract $CONTRACT_ADDRESS \
src/MyContract.sol:MyContract \
--chain-id 1 \
--etherscan-api-key $ETHERSCAN_KEY \
--constructor-args $(cast abi-encode "constructor(uint256)" 100)
Common Commands Reference
Quick reference from references/foundry-commands.md:
Development:
forge build # Compile contracts
forge test # Run tests
forge test -vvv # Verbose test output
forge coverage # Coverage report
forge fmt # Format code
forg