Setting Up Automatic Deployment via Foundry Multi-Chain
Deploying a contract to five networks manually means running forge script five times, copying the address five times, verifying on the respective explorer five times. It's easy to forget one network or make a typo in the address — a classic story. Foundry multi-chain deploy solves this with one script.
Structure of a Foundry Deployment Script
A Foundry Script is a Solidity contract inheriting Script from forge-std. It contains the deployment logic that executes via forge script with the --broadcast flag.
For multi-chain deployment, the key is managing configuration across chains. Standard approach: a JSON file with parameters for each network and reading through vm.readFile + parsing via stdJson.
deployments/
config.json # { "arbitrum": { "fee": 100 }, "polygon": { ... } }
arbitrum.json # { "MyContract": "0x..." } (after deploy)
polygon.json
script/
Deploy.s.sol
In Deploy.s.sol we read the config via vm.readFile, deploy with needed parameters, write the address via vm.writeFile. After deployment, JSON files with addresses become the source of truth for frontend and npm package.
Running on Multiple Networks
A bash script wrapping forge script:
CHAINS=("arbitrum" "polygon" "optimism" "base" "bsc")
for chain in "${CHAINS[@]}"; do
forge script script/Deploy.s.sol \
--rpc-url $chain \
--broadcast \
--verify \
--etherscan-api-key $ETHERSCAN_API_KEY \
-vvv
done
RPC URLs are configured in foundry.toml:
[rpc_endpoints]
arbitrum = "${ARBITRUM_RPC_URL}"
polygon = "${POLYGON_RPC_URL}"
Verification (--verify) runs automatically after deployment. For networks with non-standard explorers (BSC → BscScan, Polygon → PolygonScan) — separate ETHERSCAN_API_KEY for each network via [etherscan] section in foundry.toml.
Deterministic Addresses via CREATE2
For cross-chain systems, it's often important that a contract has the same address on all chains. CREATE gives an address from sender + nonce — if nonce shifts on one of the networks (e.g., an erroneous transaction), addresses will diverge.
CREATE2 computes the address from deployer + salt + bytecode. The same deployer with the same salt gives the same address everywhere. Standard deployer: 0x4e59b44847b379578588920cA78FbF26c0B4956C (Foundry deterministic deployer, present on all EVM chains).
In Foundry: new MyContract{salt: bytes32("v1")}(constructorArg) automatically uses CREATE2 via this deployer.
CI/CD via GitHub Actions
- name: Deploy to all chains
env:
PRIVATE_KEY: ${{ secrets.DEPLOYER_PRIVATE_KEY }}
ARBITRUM_RPC_URL: ${{ secrets.ARBITRUM_RPC_URL }}
run: |
forge script script/Deploy.s.sol --rpc-url arbitrum --broadcast --verify
Private key comes from GitHub Secrets. For production, better: Hardhat Deploy with AWS KMS or a script with Safe Transaction Builder (deploy via multisig).
Timeline
Setting up Foundry multi-chain deployment (script, config, CI/CD) for a project with 1-3 contracts on 3-5 networks — 1 working day.







