Deploying Smart Contracts to Ethereum Mainnet
Deploying to mainnet is not repeating a command from testnet with a different RPC. It is a point of no return: a contract without an upgradeable pattern cannot be changed, logic errors cost real money, and incorrectly set gas parameters can result in a stuck transaction during network peak load.
Pre-Deployment Checklist
Before spending ETH on deployment, complete mandatory steps:
Audit and Testing:
- Test coverage ≥ 95% by statement coverage (Hardhat Coverage or Foundry
forge coverage) - Run Slither — static analyzer finds reentrancy, integer overflow, unused return values
- Check Mythril or Aderyn for deep symbolic execution
- For contracts with TVL > $100k — mandatory external audit
Compiler Verification:
- Fixed Solidity version without
^(not^0.8.20, but=0.8.20) - Optimizer runs — standard 200 for balance of deployment and call gas; for contracts with very frequent calls — 1000+
- Check bytecode determinism: compiling twice should yield identical bytecode
Deployment Process via Hardhat
// hardhat.config.ts
const config: HardhatUserConfig = {
networks: {
mainnet: {
url: process.env.MAINNET_RPC_URL!, // Infura/Alchemy/Quicknode
accounts: [process.env.DEPLOYER_PRIVATE_KEY!],
gasPrice: 'auto',
},
},
etherscan: {
apiKey: process.env.ETHERSCAN_API_KEY!,
},
};
// deploy script
async function main() {
const [deployer] = await ethers.getSigners();
console.log('Deployer balance:', ethers.formatEther(
await deployer.provider.getBalance(deployer.address)
));
const Contract = await ethers.getContractFactory('MyContract');
const contract = await Contract.deploy(/* constructor args */);
await contract.waitForDeployment();
const address = await contract.getAddress();
console.log('Deployed to:', address);
// Verification on Etherscan
await run('verify:verify', {
address,
constructorArguments: [/* args */],
});
}
Gas and Priority Fees (EIP-1559)
After the London hard fork, transactions use maxFeePerGas and maxPriorityFeePerGas instead of a single gasPrice. For deployment use dynamic estimation:
const feeData = await provider.getFeeData();
// maxFeePerGas: baseFee * 2 + maxPriorityFeePerGas (2x buffer for baseFee growth)
// maxPriorityFeePerGas: 1-3 Gwei for normal deployment
For urgent deployment during network congestion — increase maxPriorityFeePerGas to 5–10 Gwei. Monitor current network state via eth_gasPrice through Blocknative or ethgasstation API.
Managing Deployer Private Key
Never deploy with a key used for other operations. Schema:
- Separate wallet for deployment only
- Funding from multisig (Safe) with exact amount for deployment + small buffer
- After deployment — transfer ownership to multisig:
contract.transferOwnership(safeAddress) - Deployer private key — in secrets manager (AWS Secrets Manager, HashiCorp Vault), not in
.env
After Deployment
- Verify source code on Etherscan (via
hardhat-etherscanorhardhat-verify) - Record contract address in deployment manifest with chainId, blockNumber, txHash
- Check all view functions through Etherscan Read Contract
- Test call of each write function with minimal parameters
- Set up monitoring (Tenderly Alerts or OpenZeppelin Defender) for critical events
Upgradeable Contracts
If update capability is needed — deploy via proxy pattern (UUPS or Transparent Proxy from OpenZeppelin). This adds complexity and gas overhead, but enables bug fixes after deployment. UUPS is preferable to Transparent Proxy — upgrade logic in implementation contract is ~30% cheaper on gas per call through proxy.







