Logo
New RPC users get 35% off their first monthView the offer
OnFinality Learn
Integration & Development11 min read

Foundry Solidity Development: A Reproducible Workflow for Build, Test, and Deploy

A practical, reproducible guide to using the Foundry toolkit for Solidity smart contract development, from project layout and Forge tests to Anvil forking and RPC-backed deployment.

TL;DR

Foundry is a Rust-based toolkit for Ethereum application development that compiles Solidity, runs unit and fuzz tests written in Solidity, deploys contracts, and interacts with chains from the command line. Its four core tools are Forge for build/test/deploy, Anvil for a local node with forking, Cast for chain interaction, and Chisel for a Solidity REPL. This article gives a reproducible workflow: scaffold a project, write a Forge test, run a fork against an RPC endpoint, deploy with a script, and verify the result. It also covers common failure modes such as remappings, fork RPC errors, and nonce or gas issues, and explains where a reliable RPC provider matters.

What Foundry Adds to a Solidity Workflow

Foundry is a fast, portable, modular toolkit for Ethereum application development written in Rust. It manages dependencies, compiles Solidity, runs unit and fuzz tests, deploys contracts, and drives scripted chain interaction from the terminal or from Solidity scripts. The core tools are Forge (build, test, debug, deploy, verify), Anvil (a local Ethereum node with forking), Cast (a CLI for chain interaction), and Chisel (a Solidity REPL).

The practical difference from JavaScript-first frameworks is that tests and deployment scripts can be written in Solidity. That removes context switching between Solidity and JavaScript for contract-level work, and it makes fuzz testing and trace-based debugging part of the default loop. Foundry does not replace the need for an RPC endpoint when you fork a live network or broadcast a transaction; it consumes one. The quality and consistency of that endpoint directly affect fork tests and deployments.

  • Forge: compile, test, fuzz, debug, deploy, verify.
  • Anvil: local node, forking, deterministic accounts.
  • Cast: call, send, decode, inspect chain state.
  • Chisel: rapid Solidity prototyping and debugging.

Prerequisites and Assumptions

This workflow assumes a Unix-like shell, Git, and a working Foundry installation via foundryup. It assumes you have an RPC_URL for the network you want to fork or deploy to, and a funded PRIVATE_KEY for any broadcast step. Never commit a private key; use environment variables or an encrypted keystore.

The examples use placeholders such as RPC_URL and PRIVATE_KEY rather than public endpoints. For choosing a concrete endpoint, the RPC Assistant covers provider and network recommendations; this article focuses on the development methodology and the checks you should run yourself.

curl -L https://foundry.paradigm.xyz | bash
foundryup
forge --version
anvil --version
cast --version

Project Layout and Dependency Mental Model

A Foundry project separates contracts, tests, scripts, and dependencies. The src/ directory holds Solidity sources, test/ holds Forge tests, script/ holds deployment scripts, and lib/ holds dependencies installed with forge install. Remappings map import paths to those dependencies, which is the part most new users trip over.

Dependencies are typically Git repositories or submodules. That is lightweight, but it means import paths must match the installed repository structure. If an import fails, inspect remappings.txt and run forge remappings before changing code. For teams that prefer package-manager semantics, dependencies can also be managed through a package.json and remappings, but the underlying rule is the same: the compiler must resolve every import to a real file.

forge init my-foundry-app
cd my-foundry-app
forge install foundry-rs/forge-std
forge remappings
forge build

Writing a Forge Test in Solidity

Forge tests are Solidity contracts that inherit from forge-std's Test contract. Test functions are prefixed with test, and fuzz tests accept parameters. The example below tests a minimal counter contract, including a fuzz case that checks an invariant after incrementing.

Run the suite with forge test. Add -vvvv to see execution traces when a test fails. The expected verification result is a summary line showing passing tests and zero failures; if a fuzz test fails, Forge prints the counterexample input so you can reproduce it.

// src/Counter.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract Counter {
    uint256 public number;
    function increment() external { number += 1; }
}

// test/Counter.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {Test} from "forge-std/Test.sol";
import {Counter} from "../src/Counter.sol";

contract CounterTest is Test {
    Counter counter;

    function setUp() public { counter = new Counter(); }

    function test_Increment() public {
        counter.increment();
        assertEq(counter.number(), 1);
    }

    function testFuzz_Increment(uint8 times) public {
        for (uint256 i = 0; i < times; i++) counter.increment();
        assertEq(counter.number(), times);
    }
}

Fork Testing with Anvil and an RPC Endpoint

Fork testing runs your contracts against a snapshot of live chain state. Anvil starts a local node that fetches state from an RPC endpoint on demand. This is how you test integrations with deployed protocols without spending real funds.

Start Anvil with a fork URL, then point Forge at the local node. The expected verification result is that Anvil reports the fork block number and Forge tests pass against forked state. If the fork fails, the error usually names the RPC request that failed, which is your first diagnostic signal.

anvil --fork-url $RPC_URL --fork-block-number 19000000

# In another terminal
forge test --fork-url http://127.0.0.1:8545 -vvv

Deployment Script and Broadcast Verification

Foundry deployment scripts are Solidity contracts that inherit from Script. The run function deploys and returns the contract. Broadcasting is a separate step: forge script with --broadcast sends the transactions to the RPC endpoint.

After broadcasting, verify the result independently with Cast. The expected verification result is that the returned address has code and that the transaction receipt status is 1. Do not treat a script's console output as proof of deployment; query the chain.

// script/Deploy.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {Script} from "forge-std/Script.sol";
import {Counter} from "../src/Counter.sol";

contract Deploy is Script {
    function run() external returns (Counter) {
        vm.startBroadcast();
        Counter counter = new Counter();
        vm.stopBroadcast();
        return counter;
    }
}

# Broadcast
forge script script/Deploy.s.sol:Deploy \
  --rpc-url $RPC_URL \
  --private-key $PRIVATE_KEY \
  --broadcast

# Verify independently
cast code $CONTRACT_ADDRESS --rpc-url $RPC_URL
cast receipt $TX_HASH --rpc-url $RPC_URL

A Diagnostic Procedure for Fork and RPC Failures

When a fork test or deployment fails, isolate whether the problem is in your code, your configuration, or the endpoint. Run the checks below in order and record the outcome. This produces a results table you can compare across endpoints or runs.

The procedure: (1) confirm the RPC endpoint responds to a basic chain ID request; (2) confirm the fork block is available; (3) run the failing test with -vvvv; (4) repeat against a second endpoint if available. Record chain ID, fork block, test outcome, and error message. Do not invent numbers; fill the table with your actual observations.

  • Chain ID mismatch: the endpoint is for a different network than your config.
  • Missing block: the fork block is not available on that endpoint.
  • Rate limit or timeout: the endpoint is throttling or unreachable.
  • Nonce error: a previous transaction is pending or the key was reused.
cast chain-id --rpc-url $RPC_URL
cast block-number --rpc-url $RPC_URL
cast block 19000000 --rpc-url $RPC_URL
forge test --fork-url $RPC_URL --fork-block-number 19000000 -vvvv

Results Table Template

Use a Markdown table like the one below to record your own measurements. The columns are suggestions; the point is to separate documented behavior from what you observed. Replace the placeholder rows with your actual runs.

CheckCommandExpectedObserved
Chain IDcast chain-idMatches target network(fill in)
Fork blockcast block 19000000Block exists(fill in)
Unit testsforge testAll pass(fill in)
Fork testsforge test --fork-urlAll pass(fill in)
Deploy receiptcast receiptstatus 1(fill in)

Troubleshooting Common Foundry Errors

Most early Foundry errors fall into a few categories. Import resolution failures usually mean a missing dependency or a wrong remapping. Fork failures usually mean the RPC endpoint cannot serve the requested block or is rate-limited. Broadcast failures usually mean a nonce, gas, or key configuration problem.

For import errors, run forge remappings and compare the output to your import statements. For fork errors, test the same block with Cast. For broadcast errors, check the sender balance and pending nonce with cast balance and cast nonce. If the endpoint is the variable, compare behavior across providers; the RPC Assistant can help you choose one, while the Learn guides on RPC endpoints explain the underlying request patterns.

  • Import not found: verify lib/ contents and remappings.txt.
  • Fork RPC error: confirm block availability and endpoint rate limits.
  • Nonce too low: check pending transactions for the same key.
  • Gas estimation failed: simulate with cast call before sending.

Limitations and Tradeoffs

Foundry's Solidity-first testing is excellent for contract logic, fuzzing, and traces, but it is not a full replacement for every integration need. Teams that test JavaScript frontend interactions or complex multi-chain deployment plugins sometimes keep a JavaScript-based tool alongside Foundry. Dependency management via Git submodules can feel lightweight compared with package managers, and remappings require discipline.

Fork testing depends on the RPC endpoint's ability to serve historical state. A slow or rate-limited endpoint will make fork tests flaky, which can look like a code bug. Treat endpoint reliability as part of your test infrastructure, not an afterthought. For production deployments, use a dedicated endpoint and verify transactions independently.

Next Steps

Once the basic loop works, extend it: add invariant tests, integrate coverage reporting, and run the suite in CI with a pinned fork block. For endpoint selection, see the RPC Assistant's guidance on Ethereum RPC APIs and compare pricing and network coverage before committing to a provider.

Useful related reading: the Learn guide to RPC endpoints, the multi-chain RPC endpoints guide, and the overview of dedicated nodes for production workloads. If you are deploying to a specific network, check the supported networks list and the pricing page for RPC plans.

  • Add invariant and fuzz tests for critical invariants.
  • Pin fork blocks in CI for reproducibility.
  • Verify every deployment with Cast, not just script output.
  • Choose an endpoint that matches your fork and deployment needs.

Never Worry about Infrastructure Again

OnFinality takes away the heavy lifting of DevOps so you can build smarter and faster.

Get Started