Building with Foundry: The Complete Smart Contract Development Guide
Everything you need to master Foundry — from forge test and fuzzing to deployment scripts, cast commands, and CI/CD integration.
Why Foundry Has Become the Standard
Foundry has displaced Hardhat as the default smart contract development framework for most serious Ethereum developers. Written in Rust, it is dramatically faster than JavaScript-based alternatives — test suites that took minutes in Hardhat run in seconds in Foundry. More importantly, Foundry's design philosophy aligns with how security researchers think: tests are Solidity, not JavaScript, which means the same language as your contracts.
The Foundry toolkit has four main components: forge (build, test, deploy), cast (blockchain interaction CLI), anvil (local testnet), and chisel (Solidity REPL). Together they cover the entire development lifecycle without requiring a JavaScript ecosystem.
This guide assumes you have Foundry installed via foundryup. If not, run: curl -L https://foundry.paradigm.xyz | bash, then foundryup.
Forge: Testing and Building
Foundry tests are written in Solidity contracts that inherit from forge-std/Test.sol. Each public function starting with test is a test case. The Test base contract provides assertion helpers (assertEq, assertTrue, assertApproxEqAbs), cheatcodes, and event testing utilities.
Key cheatcodes for testing:
vm.prank(address): the next call is made from the specified address.
vm.startPrank(address) / vm.stopPrank(): all calls between start and stop come from the specified address.
vm.deal(address, uint256): set an address's ETH balance.
vm.warp(uint256): set block.timestamp.
vm.roll(uint256): set block.number.
vm.expectRevert(bytes): assert the next call reverts with a specific message.
vm.expectEmit(bool, bool, bool, bool): assert a specific event is emitted.
Fuzzing: prefix a test function with testFuzz_ and add typed parameters. Foundry generates random inputs and runs the test hundreds of times. This catches edge cases that hand-written tests miss.
Invariant testing: define invariant_ functions that must always hold. Foundry runs random sequences of transactions against your contract and checks invariants after each one. This is the most powerful testing technique available in any smart contract framework.
Forge Scripts: Deployment and Automation
Forge scripts replace Hardhat deploy scripts. They are Solidity contracts that inherit from forge-std/Script.sol and run in a special scripting context where vm.broadcast() marks transactions for on-chain submission.
A typical deployment script:
Extend Script, override run(). Inside run(), call vm.startBroadcast(privateKey) or vm.startBroadcast() (uses the --private-key flag). Deploy contracts with new MyContract(). Call vm.stopBroadcast(). Everything between start and stop becomes a real transaction.
Running a script:
forge script script/Deploy.s.sol --rpc-url $RPC_URL --private-key $PRIVATE_KEY --broadcast --verify
The --verify flag automatically submits the contract bytecode to Etherscan for verification after deployment.
Simulation first: run without --broadcast to simulate and check for errors. Foundry prints a gas estimate and a trace of all calls without submitting anything.
Deterministic deployments: use vm.computeCreateAddress() or create2 factories to pre-compute deployment addresses. This is essential for multi-chain deployments where you want the same address everywhere.
Cast: Blockchain Interaction CLI
Cast is a command-line tool for interacting with Ethereum nodes, encoding data, and analyzing transactions. It replaces the need for custom scripts for many common tasks.
Essential cast commands:
cast call <address> <signature> <args> — read a contract state (no gas required).
cast send <address> <signature> <args> — send a transaction.
cast balance <address> — get ETH balance.
cast block <block> — get block information.
cast tx <txhash> — get transaction details.
cast trace <txhash> — replay and trace a transaction (requires archive node).
cast decode-calldata <sig> <data> — decode encoded calldata.
cast abi-encode <sig> <args> — ABI encode function arguments.
cast keccak <string> — compute keccak256 hash.
cast sig <function-signature> — get the 4-byte function selector.
The trace command is particularly valuable for debugging failed transactions in production. Paste a mainnet txhash and an archive RPC URL, and cast trace prints the full call tree with return values and revert reasons.
Anvil: Local Testnet and Forking
Anvil is Foundry's local Ethereum node, equivalent to Hardhat Network or Ganache but faster and more feature-complete.
Starting a local node: anvil runs on port 8545 with 10 pre-funded test accounts.
Forking mainnet: anvil --fork-url $MAINNET_RPC_URL --fork-block-number 21000000 starts a local node forked from a specific mainnet block. Your tests can interact with real deployed contracts (Uniswap, Aave, Chainlink) without deploying them.
This is the killer feature for DeFi testing. Instead of mocking Uniswap in your tests, fork mainnet and use the real Uniswap. Your tests are far more realistic.
Useful anvil flags:
--block-time N: auto-mine a block every N seconds.
--chain-id N: override the chain ID.
--no-mining: disable auto-mining (manually mine with cast rpc evm_mine).
--state <path>: save and restore chain state between runs.
CI/CD integration: start anvil in the background, run your test suite against it, and tear it down. Foundry's forge test automatically handles this — you do not need to start anvil manually for unit tests. For integration tests using forking, add --fork-url to your forge test command.