Foundry + Solidity: Smart Contract Development and Deployment to Goerli
Deploying smart contracts to production-grade networks like the Ethereum mainnet requires moving beyond local testing. This guide walks through the complete end-to-end lifecycle of building a decentralized crowdfunding smart contract in Solidity using Foundry, testing it locally on Anvil, interacting with it via Cast, and finally deploying and verifying it on the Goerli testnet using Alchemy and Etherscan.
1. Why Do We Need Testnets & Supernodes?
When building decentralized applications (dApps), deploying directly to the Ethereum mainnet is expensive and risky.
- The Status Quo Problem: In the early days of Ethereum, developers had to run their own local Ethereum nodes (using clients like Geth) to interact with the blockchain, which required massive disk space, time, and synchronization overhead.
- The Modern Solution (Supernodes): Services like Alchemy and Infura act as "supernodes." They maintain nodes connected not just to Ethereum, but to multiple blockchains, exposing an
httpsJSON-RPC endpoint so developers can deploy without running infrastructure locally. - Testnets (Goerli): Testnets mirror mainnet behavior and cryptography, but use worthless test ether distributed via faucets, allowing safe experimentation before risking real funds.
2. Core Mental Model & Tooling Architecture
Foundry is a blazing-fast, Rust-based toolkit for Ethereum application development. It consists of three primary command-line binaries:
Real-Life Analogy: The Local Workshop vs. Public Postal Service
Analogy: Think of Anvil as a private sandbox workshop in your garage where you build and test prototypes using scrap wood (free local test tokens). Goerli testnet is like building a life-sized model in a neighborhood park under municipal rules (testnet constraints, real gas mechanics on a shared ledger). Alchemy acts as the high-speed transit highway connecting your local workshop to the city infrastructure, while Cast is your delivery truck moving boxes back and forth.
3. Prerequisites & External API Credentials
Before initiating a Foundry project, you must procure specific configuration credentials and fund your wallet:
4. Project Initialization & Structure Setup
To start a new Foundry project from your terminal, execute the initialization command:
forge init foundry-crowdfunding
cd foundry-crowdfunding
code .
This generates the standard Foundry layout:
src/: Where your Solidity smart contracts (.sol) reside.test/: Where your testing scripts reside.script/: Where deployment automation scripts reside.foundry.toml: Global configuration file.
5. Writing the Crowdfunding Smart Contract
Inside the src/ directory, replace the default contract with a robust crowdfunding contract (Crowdfunding.sol). This contract allows creators to initialize campaigns, contributors to donate ETH, and creators to withdraw funds after meeting targets and deadlines.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
contract Crowdfunding {
struct Campaign {
address owner;
string title;
string description;
uint256 target;
uint256 deadline;
uint256 amountCollected;
string image;
address[] donators;
uint256[] donations;
}
mapping(uint256 => Campaign) public campaigns;
uint256 public numberOfCampaigns = 0;
function createCampaign(
address _owner,
string memory _title,
string memory _description,
uint256 _target,
uint256 _deadline,
string memory _image
) public returns (uint256) {
Campaign storage campaign = campaigns[numberOfCampaigns];
require(_deadline >= block.timestamp, "The deadline should be a date in the future.");
campaign.owner = _owner;
campaign.title = _title;
campaign.description = _description;
campaign.target = _target;
campaign.deadline = _deadline;
campaign.amountCollected = 0;
campaign.image = _image;
numberOfCampaigns++;
return numberOfCampaigns - 1;
}
function donateToCampaign(uint256 _id) public payable {
uint256 amount = msg.value;
Campaign storage campaign = campaigns[_id];
campaign.donators.push(msg.sender);
campaign.donations.push(amount);
campaign.amountCollected = campaign.amountCollected + amount;
}
function withdraw(uint256 _id) public {
Campaign storage campaign = campaigns[_id];
require(msg.sender == campaign.owner, "Only the owner of the campaign can withdraw funds.");
require(campaign.amountCollected >= campaign.target, "Target not reached.");
require(block.timestamp >= campaign.deadline, "Deadline has not passed yet.");
(bool sent, ) = payable(campaign.owner).call{value: campaign.amountCollected}("");
if (sent) {
campaign.amountCollected = 0;
}
}
function getCampaign(uint256 _id) public view returns (
address, string memory, string memory, uint256, uint256, uint256, string memory
) {
Campaign memory campaign = campaigns[_id];
return (
campaign.owner,
campaign.title,
campaign.description,
campaign.target,
campaign.deadline,
campaign.amountCollected,
campaign.image
);
}
}
6. Compiling, Remappings, and Testing
After pasting test suites into your test/ directory, resolve path dependency issues and compile the contracts.
Step-by-Step Build & Test Workflow
- Generate Re-mappings: If import resolution errors occur across libraries, generate your re-mappings file:
BASH
forge remappings > remappings.txt - Compile the Project:
BASH
forge build - Execute Test Suite:
BASH
forge test
Verbosity Flags for Debugging
When tests fail or require deeper inspection, use verbosity flags:
forge test: Runs tests quietly.forge test -vv: Shows execution logs for failing tests.forge test -vvvv: Shows complete trace logs for all calls (crucial for debugging revert reasons).forge test --gas-report: Outputs detailed gas consumption metrics per function.
7. Local Deployment & Interaction via Anvil and Cast
Step 1: Spin up Anvil (Local Blockchain)
Open a separate terminal window and run:
anvil
Anvil starts a local JSON-RPC node on http://127.0.0.1:8545 and pre-funds multiple accounts with test accounts and private keys.
Step 2: Export Environment Variables in Your CLI
In your main project terminal, configure your deployment variables using Anvil's test accounts:
export PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
export CONTRACT_ADDRESS=<DEPLOYED_CONTRACT_ADDRESS>
Step 3: Deploy to Anvil Using Forge Script
Run the forge script pointing to your local Anvil RPC URL:
forge script script/Crowdfunding.s.sol:CrowdfundingScript \
--fork-url http://127.0.0.1:8545 \
--broadcast \
--private-key $PRIVATE_KEY
Note: Make sure your script file is named properly (e.g., Crowdfunding.s.sol) and that you executed forge build prior to running the script.
8. Interacting with the Smart Contract Using Cast
With the contract deployed to Anvil, use cast to execute state-changing transactions and view calls.
A. Creating a Campaign
cast send $CONTRACT_ADDRESS \
"create_campaign(address,string,string,uint256,uint256,string)" \
0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 \
"Clean Water Initiative" \
"Building wells in rural communities" \
1000000000000000000 \
1698765432 \
"https://image.url" \
--private-key $PRIVATE_KEY \
--rpc-url http://127.0.0.1:8545
B. Fetching Campaign Details
cast call $CONTRACT_ADDRESS "getCampaign(uint256)(address,string,string,uint256,uint256,uint256,string)" 0
C. Donating to a Campaign
cast send $CONTRACT_ADDRESS \
"donateToCampaign(uint256)" 0 \
--value 1ether \
--private-key <DONOR_PRIVATE_KEY> \
--rpc-url http://127.0.0.1:8545
D. Checking Account Balances
cast balance <ACCOUNT_ADDRESS> --rpc-url http://127.0.0.1:8545
9. Deploying and Verifying on the Goerli Testnet
Once tested locally, migrating to Goerli requires switching out your local RPC endpoint for your Alchemy Goerli HTTPS URL, supplying your MetaMask private key, and adding your Etherscan API key for automatic source verification.
Comprehensive Goerli Deployment Command
export GOERLI_URL="https://eth-goerli.g.alchemy.v2/YOUR_ALCHEMY_API_KEY"
export METAMASK_PRIVATE_KEY="YOUR_METAMASK_EXPORTED_PRIVATE_KEY"
export ETHERSCAN_API_KEY="YOUR_ETHERSCAN_API_KEY"
forge create \
--rpc-url $GOERLI_URL \
--private-key $METAMASK_PRIVATE_KEY \
src/Crowdfunding.sol:Crowdfunding \
--etherscan-api-key $ETHERSCAN_API_KEY \
--verify
Production Troubleshooting & Verification Flow
10. Summary and Key Takeaways
- Foundry Ecosystem Mastery: Leveraged Forge for compiling and testing, Anvil for a local high-speed blockchain environment, and Cast for command-line contract interaction.
- Supernode Integration: Utilized Alchemy JSON-RPC endpoints to bypass running local validator nodes while deploying seamlessly.
- Testnet Economy: Employed Goerli faucets to acquire test ETH for gas deployment without risking real capital.
- Smart Contract Lifecycle: Designed, compiled, tested, deployed locally, and verified a full-fledged Solidity smart contract (
Crowdfunding.sol) containing structs, mappings, and payable functions. - Verification Automation: Streamlined production trust by programmatically verifying contract source code on Etherscan using API integration during deployment.
- Environment Management: Maintained security hygiene by managing sensitive credentials (private keys, API keys) via terminal environment variables rather than hardcoding them into source files.