Foundry has become the standard toolchain for Solidity development. It is faster than Hardhat, runs tests in Solidity instead of JavaScript, and has first-class support for forking and fuzzing.
Prerequisites
- Basic Solidity knowledge
- Sepolia ETH in your wallet — see the How to get Sepolia ETH guide
- A wallet private key for deployment (use a dedicated dev wallet, never your mainnet key)
1. Install Foundry
curl -L https://foundry.paradigm.xyz | bash
foundryup
Verify the installation:
forge --version
cast --version
2. Create a new project
forge init hello-sepolia
cd hello-sepolia
Foundry creates the following structure:
hello-sepolia/
├── src/
│ └── Counter.sol # sample contract
├── test/
│ └── Counter.t.sol # sample test
├── script/
│ └── Counter.s.sol # deployment script
└── foundry.toml
3. Write the contract
Replace src/Counter.sol with a simple storage contract:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract HelloSepolia {
string public message;
address public owner;
event MessageUpdated(string newMessage, address updatedBy);
constructor(string memory _message) {
message = _message;
owner = msg.sender;
}
function setMessage(string calldata _message) external {
message = _message;
emit MessageUpdated(_message, msg.sender);
}
}
4. Write a test
Replace test/Counter.t.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
import {HelloSepolia} from "../src/HelloSepolia.sol";
contract HelloSepoliaTest is Test {
HelloSepolia public hello;
function setUp() public {
hello = new HelloSepolia("Hello, testnet!");
}
function test_InitialMessage() public view {
assertEq(hello.message(), "Hello, testnet!");
}
function test_SetMessage() public {
hello.setMessage("Updated");
assertEq(hello.message(), "Updated");
}
function test_OwnerIsDeployer() public view {
assertEq(hello.owner(), address(this));
}
}
Run the tests:
forge test -vv
5. Configure the network
Create a .env file (never commit this):
SEPOLIA_RPC_URL=https://rpc.sepolia.org
PRIVATE_KEY=0xYOUR_DEV_WALLET_PRIVATE_KEY
ETHERSCAN_API_KEY=YOUR_ETHERSCAN_API_KEY
Add to foundry.toml:
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
[rpc_endpoints]
sepolia = "${SEPOLIA_RPC_URL}"
[etherscan]
sepolia = { key = "${ETHERSCAN_API_KEY}", url = "https://api-sepolia.etherscan.io/api" }
6. Write the deployment script
Replace script/Counter.s.sol with script/Deploy.s.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Script} from "forge-std/Script.sol";
import {HelloSepolia} from "../src/HelloSepolia.sol";
contract Deploy is Script {
function run() external returns (HelloSepolia) {
vm.startBroadcast();
HelloSepolia hello = new HelloSepolia("Hello from Sepolia!");
vm.stopBroadcast();
return hello;
}
}
7. Deploy
Load environment variables and deploy:
source .env
forge script script/Deploy.s.sol \
--rpc-url sepolia \
--private-key $PRIVATE_KEY \
--broadcast \
--verify
The --verify flag automatically verifies the contract on Sepolia Etherscan.
8. Verify the deployment
cast call <DEPLOYED_ADDRESS> "message()(string)" --rpc-url sepolia
Or visit sepolia.etherscan.io/address/<DEPLOYED_ADDRESS> and interact via the Read Contract tab.
Send a transaction
cast send <DEPLOYED_ADDRESS> \
"setMessage(string)" "Hello from CLI!" \
--private-key $PRIVATE_KEY \
--rpc-url sepolia
Common errors
| Error | Cause | Fix |
|---|---|---|
insufficient funds |
Not enough Sepolia ETH | Get ETH from a faucet |
nonce too low |
Pending transaction | Wait or reset nonce with cast nonce |
execution reverted |
Contract logic error | Check the revert reason with cast run <txhash> |