Deploy
Deploying a smart contract is a one-way commit: the bytecode is immutable, the constructor runs once, the address is permanent. Use a deterministic deployer, verify on Etherscan / Sourcify, plan upgradeability up front (or commit to immutability), and keep deploy scripts in source control.
Hardhat / Foundry deploy + verification
EXAMPLE
// ===== Foundry: forge create =====
// Cargo-clean syntax, source goes to Etherscan via verify
forge create src/Token.sol:Token \
--rpc-url $RPC_URL \
--private-key $DEPLOYER_KEY \
--constructor-args 1000000000000000000000 \
--verify --etherscan-api-key $ETHERSCAN_KEY
// ===== Foundry: a deploy script =====
// script/Deploy.s.sol
pragma solidity ^0.8.24;
import 'forge-std/Script.sol';
import { Token } from '../src/Token.sol';
contract Deploy is Script {
function run() external {
uint256 pk = vm.envUint('DEPLOYER_KEY');
vm.startBroadcast(pk);
uint256 supply = 1_000 ether;
Token token = new Token(supply);
console.log('Token deployed to:', address(token));
vm.stopBroadcast();
}
}
// Run
// forge script script/Deploy.s.sol --rpc-url $RPC_URL --broadcast --verify
// ===== Hardhat: ethers v6 =====
// scripts/deploy.ts
import { ethers, network, run } from 'hardhat';
async function main() {
const [deployer] = await ethers.getSigners();
console.log('Deploying from', deployer.address);
const supply = ethers.parseEther('1000');
const Token = await ethers.getContractFactory('Token');
const token = await Token.deploy(supply);
await token.waitForDeployment();
const address = await token.getAddress();
console.log('Token deployed to:', address);
// Wait a few blocks before verifying (Etherscan indexing lag)
if (network.name !== 'hardhat' && network.name !== 'localhost') {
await new Promise((r) => setTimeout(r, 30_000));
await run('verify:verify', {
address,
constructorArguments: [supply],
});
}
}
main().catch((e) => { console.error(e); process.exit(1); });
// Run
// npx hardhat run scripts/deploy.ts --network sepolia
// ===== OpenZeppelin Upgradeable proxy =====
// scripts/deploy-upgradeable.ts
import { upgrades, ethers } from 'hardhat';
async function main() {
const Box = await ethers.getContractFactory('Box');
const proxy = await upgrades.deployProxy(Box, [42], { kind: 'uups' });
await proxy.waitForDeployment();
console.log('Proxy:', await proxy.getAddress());
// Later upgrade:
// const BoxV2 = await ethers.getContractFactory('BoxV2');
// await upgrades.upgradeProxy(proxyAddress, BoxV2);
}
// ===== Verification checklist =====
// - source code verified on Etherscan / Sourcify
// - constructor args MUST match exactly (encoded the same way)
// - compiler version + optimizer settings recorded
// - libraries linked correctly if any
// - proxy admin / owner set to a multisig (not your hot wallet)
// ===== Production deployment checklist =====
// 1) Tests passing AND audits complete
// 2) Test networks deployed + tested (Sepolia / Goerli replacement / Base Sepolia)
// 3) Deploy via a multisig (Gnosis Safe) — not from a hot wallet
// 4) Constructor args double-checked; transferOwnership to multisig
// 5) Source verified on Etherscan + Sourcify
// 6) Frontend, indexer, and subgraph updated with the new address
// 7) Announce the contract address via official channels signed by the multisig
// 8) Set up monitoring (Tenderly, Defender, Forta) for unusual activity
// ===== Cost +chain considerations =====
// L1 mainnet: expensive; verify gas budget twice
// L2 (Base, Arbitrum, Optimism): cheap deploys; same toolchain
// Sidechain (Polygon PoS): cheap; lower security guarantees
// Testnets: free; spotty third-party tool support
// ===== Common pitfalls =====
// - Deploying from a wallet you control; ownership rotation later is risky
// - Hard-coded addresses in constructors (changes per chain) — pass via args
// - Missing verification -> users cannot read the source on Etherscan
// - 'I'll add upgradability later' — you cannot. Decide BEFORE deploy.
// - Re-deploying after a 'small fix' -> users hold the OLD token; coordinate migration
Why it matters
Treat smart-contract deploys like database migrations: plan, test on testnets, verify the source code, and transfer ownership to a multisig immediately. Once you push to mainnet, the bytecode is permanent; the only "rollback" is "deploy a new contract and migrate everyone over", which is far more expensive than testing twice.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// scripts/deploy.ts (Hardhat)
import { ethers } from 'hardhat';
async function main() {
const Token = await ethers.getContractFactory('MyToken');
const token = await Token.deploy();
await token.waitForDeployment();
console.log('deployed:', await token.getAddress());
}
main().catch(e => { console.error(e); process.exit(1); });
Try it Yourself »
Discussion
Loading…