
For anything more serious than toy contracts, you need proper tools. I've shipped contracts worth millions using both Hardhat and Foundry. Both integrate perfectly with Optimism's developer tools and testing networks. Here's what actually works:
Hardhat: The Battle-Tested Option
Hardhat is boring and reliable. That's exactly what you want when deploying to mainnet. The Hardhat docs are solid and their plugin ecosystem covers everything you need.
Setup that won't break:
mkdir my-optimism-project && cd my-optimism-project
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox dotenv
npx hardhat init
Config that I actually use:
require(\"@nomicfoundation/hardhat-toolbox\");
require(\"dotenv\").config();
module.exports = {
solidity: {
version: \"0.8.24\",
settings: {
optimizer: {
enabled: true,
runs: 200 // Good balance between deploy cost and runtime cost
}
}
},
networks: {
sepolia: {
url: \"https://sepolia.optimism.io\",
chainId: 11155420,
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : []
},
optimism: {
url: \"https://mainnet.optimism.io\",
chainId: 10,
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : []
}
},
etherscan: {
apiKey: {
optimism: process.env.OPTIMISM_API_KEY || \"dummy\"
},
customChains: [
{
network: \"sepolia\",
chainId: 11155420,
urls: {
apiURL: \"https://api-sepolia-optimistic.etherscan.io/api\",
browserURL: \"https://sepolia-optimistic.etherscan.io\"
}
}
]
}
};
Deployment script that handles failures:
async function main() {
const [deployer] = await ethers.getSigners();
const balance = await ethers.provider.getBalance(deployer.address);
console.log(\"Deploying with:\", deployer.address);
console.log(\"Balance:\", ethers.formatEther(balance), \"ETH\");
if (balance < ethers.parseEther(\"0.01\")) {
console.error(\"Insufficient balance. Need at least 0.01 ETH\");
process.exit(1);
}
try {
const Contract = await ethers.getContractFactory(\"YourContract\");
const contract = await Contract.deploy({
gasLimit: 1000000 // Set reasonable gas limit
});
await contract.waitForDeployment();
const address = await contract.getAddress();
console.log(\"Contract deployed to:\", address);
console.log(\"Verify with:\");
console.log(`npx hardhat verify --network ${network.name} ${address}`);
} catch (error) {
console.error(\"Deployment failed:\", error);
process.exit(1);
}
}
main().catch(console.error);
Foundry: Fast as Hell, Painful to Learn

I switched to Foundry for new projects because it's insanely fast. But the learning curve is steep if you're coming from JavaScript land. The Foundry book explains everything, but expect to spend time learning Solidity testing patterns and Rust toolchain quirks.
Setup (pray your Rust install doesn't break):
curl -L https://foundry.paradigm.xyz | bash
foundryup
forge init my-project --template https://github.com/foundry-rs/forge-template
cd my-project
foundry.toml that works:
[profile.default]
src = 'src'
out = 'out'
libs = ['lib']
optimizer = true
optimizer_runs = 200
[rpc_endpoints]
sepolia = \"https://sepolia.optimism.io\"
optimism = \"https://mainnet.optimism.io\"
[etherscan]
optimism = { key = \"${OPTIMISM_API_KEY}\" }
## This saves your sanity when debugging
[fmt]
line_length = 100
tab_width = 4
Deploy script (Deploy.s.sol):
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import \"forge-std/Script.sol\";
import \"../src/YourContract.sol\";
contract Deploy is Script {
function run() external {
uint256 deployerPrivateKey = vm.envUint(\"PRIVATE_KEY\");
vm.startBroadcast(deployerPrivateKey);
YourContract contract = new YourContract();
console.log(\"Contract deployed to:\", address(contract));
vm.stopBroadcast();
}
}
Deploy with style:
forge script script/Deploy.s.sol \
--rpc-url sepolia \
--broadcast \
--verify \
-vvvv
War Stories (Learn From My Pain)
Hardhat mysteriously broke during a Friday afternoon deployment (because of course it did). Error: "Cannot read property 'gasPrice' of undefined". Spent 3 fucking hours debugging, growing increasingly angry at my laptop. Issue? I had a circular import in my test files that somehow broke the deployer. The contracts folder was fine - it was test/helpers.js importing from test/mocks.js which imported helpers.js. Always check your damn imports before you lose your mind.
Foundry verification shitstorm - Contract deployed fine, verification kept failing with "bytecode mismatch". Turns out my Deploy.s.sol was using ^0.8.24 but my contract was ^0.8.20. Foundry compiled the script with 0.8.24 but used 0.8.20 for the actual contract. Match your pragma versions or you'll be pulling your hair out.
Gas estimation is complete fantasy - Both tools regularly lie about gas costs. Set gas limit to 1.5x whatever they estimate because L2s have weird spikes during congestion. I've had "estimated 200k gas" transactions fail with "out of gas" multiple times.
Etherscan rate limits bite hard - Spam the verification API and you're locked out for an hour. No warning, just silent failure. Always wait 30 seconds between retry attempts.
What I Actually Check Before Mainnet
Hardhat:
- TypeScript integration breaks randomly. Restart helps.
hardhat.config.ts
vs .js
matters for imports
- Plugin conflicts are hell to debug
- Network config must match exactly or you'll deploy to wrong chain
Foundry:
- Rust compilation errors are cryptic as fuck
forge install
sometimes corrupts your git repo
- Environmental variable loading is inconsistent
- Fuzzing tests can run forever if you let them
Both tools are production-ready, but Foundry is faster if you can handle the Rust learning curve. I use Hardhat for team projects (JavaScript devs exist) and Foundry for solo work (speed matters). Check Paradigm's comparison or this detailed MetaMask analysis for benchmarks. If you're working on Optimism Superchain projects, both tools support cross-chain deployment patterns.