Deploying to Mainnet
The Deployment Path
A deployment creates on-chain code and state. Replacing its behavior later may require an upgrade mechanism or a migration, so test the deployment sequence as well as the contract functions:
Local tests → Testnet deployment → Testnet verification → Mainnet deployment → Mainnet verification
Record the configuration, compiler settings, addresses, and permissions used at each stage.
Step 1: Prepare Your Environment
Before touching mainnet, set up your deployment configuration.
Hardhat Config
// hardhat.config.js
require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();
module.exports = {
solidity: "0.8.20",
networks: {
sepolia: {
url: process.env.SEPOLIA_RPC_URL,
accounts: [process.env.DEPLOYER_PRIVATE_KEY],
},
mainnet: {
url: process.env.MAINNET_RPC_URL,
accounts: [process.env.DEPLOYER_PRIVATE_KEY],
},
},
etherscan: {
apiKey: process.env.ETHERSCAN_API_KEY,
},
};
Critical Rule: Never Expose Keys
Keep private keys out of source files and version control. If local environment files contain credentials, exclude them from Git and check that they have not already been committed.
# .env (NEVER commit this file)
DEPLOYER_PRIVATE_KEY=0xabc123...
SEPOLIA_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/your-key
MAINNET_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/your-key
ETHERSCAN_API_KEY=your-etherscan-api-key
Treat a published private key as compromised. Removing it from a file does not invalidate copies or remove it from repository history.
Step 2: Write the Deployment Script
// scripts/deploy.js
const hre = require("hardhat");
async function main() {
const initialSupply = hre.ethers.parseEther("1000000");
console.log("Deploying SimpleToken...");
const token = await hre.ethers.deployContract("SimpleToken", [initialSupply]);
await token.waitForDeployment();
const address = await token.getAddress();
console.log(`SimpleToken deployed to: ${address}`);
// Wait for 5 block confirmations before verifying
console.log("Waiting for block confirmations...");
await token.deploymentTransaction().wait(5);
// Verify on Etherscan
console.log("Verifying on Etherscan...");
await hre.run("verify:verify", {
address: address,
constructorArguments: [initialSupply],
});
console.log("Deployment and verification complete.");
}
main().catch(console.error);
Step 3: Deploy to Testnet First
npx hardhat run scripts/deploy.js --network sepolia
After deployment:
- Check the contract on Sepolia Etherscan.
- Call every function. Transfer tokens. Test edge cases.
- Have someone else test it. Fresh eyes catch what you missed.
Step 4: Estimate Mainnet Costs
Before deploying to mainnet, estimate the gas cost:
const factory = await hre.ethers.getContractFactory("SimpleToken");
const deployTx = await factory.getDeployTransaction(initialSupply);
const estimatedGas = await hre.ethers.provider.estimateGas(deployTx);
const gasPrice = await hre.ethers.provider.getFeeData();
const costInWei = estimatedGas * gasPrice.gasPrice;
const costInEth = hre.ethers.formatEther(costInWei);
console.log(`Estimated deployment cost: ${costInEth} ETH`);
Typical costs (at 20 gwei gas price):
- Simple ERC-20: ~$30-80
- ERC-721 (NFT): ~$50-150
- Complex DeFi protocol (multiple contracts): ~$2,000-10,000
Step 5: Deploy to Mainnet
npx hardhat run scripts/deploy.js --network mainnet
Before broadcasting, verify the chain, sender, constructor arguments, linked addresses, and expected permissions. Compare the resulting deployment with the recorded configuration.
Post-Deployment Checklist
| Step | Action | Why |
|---|---|---|
| 1 | Verify on Etherscan | Users need to read and trust the source code |
| 2 | Test with small amounts | Confirm functions work on mainnet |
| 3 | Record addresses | Document every contract address in your repo |
| 4 | Transfer ownership | Move admin keys to a multisig (like Gnosis Safe) |
| 5 | Monitor | Set up alerts for unusual transactions (via Tenderly or OpenZeppelin Defender) |
Foundry Deployment
If you use Foundry instead of Hardhat:
# Deploy
forge create --rpc-url $MAINNET_RPC_URL \
--private-key $DEPLOYER_PRIVATE_KEY \
src/SimpleToken.sol:SimpleToken \
--constructor-args 1000000000000000000000000
# Verify
forge verify-contract $CONTRACT_ADDRESS \
src/SimpleToken.sol:SimpleToken \
--etherscan-api-key $ETHERSCAN_API_KEY
Key takeaways
- Always deploy to a testnet first. Mainnet deployments are irreversible.
- Never expose private keys in code or Git history. Use environment variables and
.gitignore. - Verify your contract on Etherscan so users can audit the source code.
- After mainnet deployment, transfer ownership to a multisig wallet. A single compromised key should never control the protocol.
Quiz: Deploying to Mainnet
1 / 5Why should you deploy to a testnet before mainnet?