Smart Contract Deployment Scripts Development
One-time deployment via forge create or npx hardhat deploy with hardcoded parameters — this is technical debt. When you need to deploy to 5 chains, then reproduce on testnet for auditors, then repeat in 3 months for a new version — it turns out nobody remembers the exact order of calls, which contracts need initialization after deployment, and on what block verification happened.
Forge Script as Standard
Foundry Script (.s.sol) — Solidity file executed as a deployment script. Key advantage: one language for contracts and deployment, compiler type checking, ability to test the script itself.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Script, console} from "forge-std/Script.sol";
import {MyProtocol} from "../src/MyProtocol.sol";
import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
contract DeployMyProtocol is Script {
function run() external {
uint256 deployerKey = vm.envUint("PRIVATE_KEY");
address deployer = vm.addr(deployerKey);
vm.startBroadcast(deployerKey);
ProxyAdmin admin = new ProxyAdmin(deployer);
MyProtocol implementation = new MyProtocol();
bytes memory initData = abi.encodeCall(
MyProtocol.initialize,
(vm.envAddress("TREASURY"), vm.envUint("FEE_BPS"))
);
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
address(implementation),
address(admin),
initData
);
console.log("ProxyAdmin:", address(admin));
console.log("Implementation:", address(implementation));
console.log("Proxy:", address(proxy));
vm.stopBroadcast();
}
}
Run: forge script script/DeployMyProtocol.s.sol --rpc-url $RPC --broadcast --verify.
The --verify flag automatically verifies all deployed contracts via Etherscan API. --slow adds a delay between transactions — needed for RPC providers with rate limits.
Parameterization via Environment
No hardcoded addresses in the script. Everything via environment variables:
address treasury = vm.envAddress("TREASURY");
uint256 fee = vm.envUint("FEE_BPS");
bool isMainnet = vm.envBool("IS_MAINNET");
For different environments: files .env.sepolia, .env.mainnet, .env.polygon. The deployment script is the same.
Logging Deployed Contract Addresses
After deployment addresses must be fixed. Approaches:
JSON file via foundry's --json flag. forge script ... --json > deployments/sepolia.json. Structured output with addresses, transaction hashes, block numbers.
Custom logging in script. Via vm.writeJson() and vm.writeFile():
string memory json = vm.serializeAddress("deployment", "proxy", address(proxy));
vm.writeJson(json, string.concat("deployments/", vm.toString(block.chainid), ".json"));
Deployment files are committed to the repository — they serve as the source of truth for frontend, analytics, and future upgrade scripts.
Upgrade Scripts
For UUPS and Transparent Proxy patterns — separate script for each upgrade:
contract UpgradeV2 is Script {
function run() external {
address proxy = vm.envAddress("PROXY_ADDRESS");
address admin = vm.envAddress("PROXY_ADMIN");
vm.startBroadcast(vm.envUint("PRIVATE_KEY"));
MyProtocolV2 newImpl = new MyProtocolV2();
ProxyAdmin(admin).upgradeAndCall(
ITransparentUpgradeableProxy(proxy),
address(newImpl),
""
);
vm.stopBroadcast();
}
}
Each upgrade script has a name with version (UpgradeToV2.s.sol) and is stored in repository history.
Multi-chain Deployment
Script runs sequentially for each chain:
forge script script/Deploy.s.sol --rpc-url $ETHEREUM_RPC --broadcast --verify
forge script script/Deploy.s.sol --rpc-url $POLYGON_RPC --broadcast --verify --verifier-url $POLYGONSCAN_API
forge script script/Deploy.s.sol --rpc-url $ARBITRUM_RPC --broadcast --verify
Or via Makefile / shell script with iteration over RPC endpoints array.
Timeline
Writing a basic deployment script with parameterization and logging: 1 day. Full deployment infrastructure with upgrade scripts, multi-chain support and CI integration: 2-3 days.
Cost is calculated after clarifying the number of contracts and target networks.







