Beginner • Workflow Localhost → Testnet → Mainnet

Learn a clean deployment workflow
in ~45 minutes.

This guide is not about writing a token. It’s about shipping contracts safely: how to move from local development to testnets and finally to mainnet — with a repeatable structure and zero chaos.

Level · Beginner
Stack · Hardhat 3, TS
Time · ~45 minutes
What you’ll build WORKFLOW
Networks Local · Sepolia · Mainnet
Scripts Deploy · Verify · Dry-run
Structure /scripts /deployments
scripts/ deploy.ts verify.ts deployments/ sepolia.json mainnet.json .env (ignored)

What you’ll learn

Many beginners can write a contract… and then get stuck when it’s time to deploy. This guide fixes that by teaching a simple, real-world deployment story.

  • How to think about networks: local vs testnet vs mainnet
  • A clean project structure for deploy flow
  • How to configure RPC + keys safely
  • How to deploy with repeatable scripts
  • How to verify and record addresses
  • How to avoid “oops I deployed wrong” moments
No token, no NFT, no theory dump

We assume you already have any contract to deploy. The focus here is workflow, not Solidity basics.

Prerequisites
You should be comfortable with:
  • Running Hardhat projects
  • Editing config + scripts
  • Basic terminal usage
Optional:
  • MetaMask installed
  • Sepolia RPC URL
Hardhat 3 networks
Deployment scripts
.env safety
Verification
Mainnet checklist

Build the right mental model

Before touching config, you need to understand what a “network” really means in dev. Think of deployment as a ladder:

  • Localhost — instant feedback, zero money, fake ETH.
  • Testnet — real chain rules, fake ETH, public explorer.
  • Mainnet — real money, real users, no undo button.
Why not deploy straight to testnet?

Because you want fast iteration first. Local lets you break things without waiting for confirmations or faucets. When local is stable, testnet validates your real-world setup.

The workflow we’ll follow:

1) Local deploy (fast sanity check)
2) Testnet deploy (real chain rules)
3) Verify + record addresses
4) Dry-run mainnet deployment
5) Mainnet deploy (with checklist)

Set up a clean deployment structure

A small but powerful rule: anything repeatable should be a script.

Here’s a beginner-friendly structure that scales:

.
├─ contracts/
├─ scripts/
│  ├─ deploy.ts
│  ├─ verify.ts
│  └─ dry-run.ts
├─ deployments/
│  ├─ localhost.json
│  ├─ sepolia.json
│  └─ mainnet.json
├─ hardhat.config.ts
├─ .env              (ignored)
└─ package.json
  • scripts/deploy.ts deploys contracts.
  • scripts/verify.ts verifies them.
  • deployments/*.json stores addresses + metadata.
Why store deployments?

So your frontend, tests, or future scripts can read the latest address without you hunting in terminal history.

Rules of the folder
  • One deploy script per project
  • Deployments saved per network
  • Never hardcode private keys
  • Verify only on public nets

This layout is enough for 90% of real projects. You can expand later with Ignition modules, but don’t start there.

Configure networks safely

In Hardhat 3, networks are typed by purpose:

  • edr-simulated — local simulated networks
  • http — real RPC networks

Here’s a minimal but correct config skeleton:

// hardhat.config.ts
import { defineConfig, configVariable } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox-ethers";

export default defineConfig({
  solidity: {
    version: "0.8.28",
    settings: { optimizer: { enabled: true, runs: 200 } },
  },

  networks: {
    // local simulated chain
    hardhatMainnet: {
      type: "edr-simulated",
      chainType: "l1",
    },

    // real testnet via RPC
    sepolia: {
      type: "http",
      chainType: "l1",
      url: configVariable("SEPOLIA_RPC_URL"),
      accounts: [configVariable("SEPOLIA_PRIVATE_KEY")],
    },

    // mainnet via RPC
    mainnet: {
      type: "http",
      chainType: "l1",
      url: configVariable("MAINNET_RPC_URL"),
      accounts: [configVariable("MAINNET_PRIVATE_KEY")],
    },
  },
});
Config variables ≠ .env by default

Hardhat 3 supports configuration variables. If you prefer classic dotenv, that’s fine too — just don’t commit secrets.

Store secrets like a grown-up

Whether you use Hardhat config variables or dotenv, the rule is the same: keys and RPC URLs must live outside Git.

Classic beginner-friendly approach:

// .env (never commit)
SEPOLIA_RPC_URL=https://sepolia.infura.io/v3/xxxx
SEPOLIA_PRIVATE_KEY=0xYOUR_TESTNET_KEY

MAINNET_RPC_URL=https://mainnet.infura.io/v3/xxxx
MAINNET_PRIVATE_KEY=0xYOUR_MAINNET_KEY

ETHERSCAN_API_KEY=XXXX

Add this to .gitignore:

.env
deployments/*.json
Two keys, not one

Use a dedicated burner key for testnets. Mainnet key should be separate and only loaded when needed.

Deploy locally first

Your deploy script should work on local and public networks without edits. Example deploy script:

// scripts/deploy.ts
import { network } from "hardhat";
import fs from "fs";

async function main() {
  const { ethers, networkName } = await network.connect();

  console.log("Deploying to:", networkName);

  // Example: deploy any existing contract
  const Contract = await ethers.getContractFactory("MyContract");
  const contract = await Contract.deploy();
  await contract.waitForDeployment();

  const address = await contract.getAddress();
  console.log("Deployed at:", address);

  // Save deployment
  const out = {
    address,
    deployedAt: new Date().toISOString(),
    network: networkName,
  };

  fs.mkdirSync("deployments", { recursive: true });
  fs.writeFileSync(`deployments/${networkName}.json`, JSON.stringify(out, null, 2));
  console.log("Saved deployments/" + networkName + ".json");
}

main().catch((e) => {
  console.error(e);
  process.exit(1);
});

Run on local simulated chain:

npx hardhat run scripts/deploy.ts --network hardhatMainnet
Why specify network?

Because it forces you to be explicit. Beginners often deploy to the wrong network by accident. Explicit is safe.

Deploy to Sepolia testnet

Once local deploy succeeds, move to testnet. Same script, different network:

npx hardhat run scripts/deploy.ts --network sepolia

If your deploy fails here, it’s usually one of these:

  • RPC URL is wrong / rate limited
  • Private key has no Sepolia ETH
  • Constructor args mismatch
Save your Sepolia address

It’s now stored in deployments/sepolia.json. This is your “source of truth” for the token or app you’re building.

Verify on a block explorer

Verification is optional but recommended. It makes your source code public and readable.

If you use toolbox-ethers, verify plugin is included. Add Etherscan API key in env, then:

npx hardhat verify --network sepolia <address> <constructorArgs...>

You can automate this in scripts/verify.ts:

// scripts/verify.ts
import { network, run } from "hardhat";
import fs from "fs";

async function main() {
  const { networkName } = await network.connect();
  const p = `deployments/${networkName}.json`;

  if (!fs.existsSync(p)) {
    throw new Error("No deployments file: " + p);
  }

  const { address } = JSON.parse(fs.readFileSync(p, "utf8"));

  console.log("Verifying:", address);

  await run("verify:verify", {
    address,
    constructorArguments: [],
  });

  console.log("Verified!");
}

main().catch((e) => {
  console.error(e);
  process.exit(1);
});

Run it:

npx hardhat run scripts/verify.ts --network sepolia

Do a “dry-run” before mainnet

A dry-run is a final rehearsal: you deploy with mainnet config but to a forked state locally.

Add forking to your simulated net:

// hardhat.config.ts (snippet)
hardhatMainnet: {
  type: "edr-simulated",
  chainType: "l1",
  forking: {
    url: configVariable("MAINNET_RPC_URL"),
    blockNumber: 20000000,
  },
},

Now run deploy on fork:

npx hardhat run scripts/deploy.ts --network hardhatMainnet
Why fork matters

Your contract runs against real mainnet state: real tokens, real liquidity pools, real addresses — but locally.

Mainnet deployment checklist

Mainnet is not scary if you follow a strict checklist. Here’s a beginner-safe one:

  • ✅ Script deployed successfully on localhost
  • ✅ Script deployed successfully on Sepolia
  • ✅ You verified on Sepolia
  • ✅ You dry-ran on mainnet fork
  • ✅ You reviewed constructor args (twice)
  • ✅ You checked deployer balance

When ready, deploy:

npx hardhat run scripts/deploy.ts --network mainnet
Take your time

If you’re unsure — stop. Re-run fork and Sepolia again. The cost of waiting is tiny compared to the cost of mistakes.

Mainnet safety tips
  • Use a fresh deployer wallet
  • Keep backups of .env
  • Don’t deploy at 3AM
  • Save tx hash + address
  • Verify right after deploy

Upgradeable deployments (mini-intro)

Some real projects use upgradeable proxies (UUPS / Transparent). You don’t need them for learning — but you should know the flow exists.

Install upgrades plugin:

npm install --save-dev @openzeppelin/hardhat-upgrades

Enable it:

// hardhat.config.ts
import "@openzeppelin/hardhat-upgrades";

Deploy as a proxy:

// scripts/deploy.ts (snippet)
const Box = await ethers.getContractFactory("Box");
const proxy = await upgrades.deployProxy(Box, [42], { kind: "uups" });
await proxy.waitForDeployment();

console.log("Proxy address:", await proxy.getAddress());
When to learn upgradeables?

After you’re confident with normal deployments. Proxies add complexity and should not be your step zero.

You now have a real workflow

You’ve learned:

  • How to treat networks as a ladder
  • How to structure scripts and deployments
  • How to deploy locally, then on testnet, then mainnet
  • How to verify and record addresses
  • How to dry-run before spending real ETH

With this, you can ship almost any Solidity project in a clean way. Next, you can pick any guide (ERC-721, DeFi, Security) and your deploy story will stay stable.

Suggested next guides
  • Hardhat deep dive (networks & forking)
  • Security basics (reentrancy, access control)
  • OpenZeppelin extensions in practice

Link these to pages like hardhat.html, security.html, oz.html to build a learning path.

0% · guide progress