Deployment Scripts

Difficulty: Advanced

Smart contract deployment submits compiled bytecode to the blockchain where it is stored permanently and assigned an address. Unlike traditional software, deployment is irreversible - once deployed, code cannot be changed unless using proxy patterns. This makes the deployment process critically important and worth automating with well-tested scripts.

Hardhat Ignition is Hardhat's declarative deployment system. Instead of step-by-step instructions, Ignition uses modules that describe the desired end state. It handles dependency resolution, parallel deployment of independent contracts, transaction management, failure resumption, and artifact storage. Each module defines contract deployments and their relationships.

For multi-contract deployments, Ignition resolves the dependency graph automatically. If contract B requires A's address, Ignition deploys A first, waits for confirmation, then deploys B. Independent contracts deploy in parallel. This is critical for DeFi protocols with many interconnected contracts.

Deterministic deployment via CREATE2 ensures a contract gets the same address across different networks. The address is computed from the deployer address, salt, and bytecode hash, independent of the nonce. The same inputs produce the same address on any EVM chain, enabling cross-chain protocols.

Best practices: use a hardware wallet for the deployer account, deploy to testnet first, verify all interactions, check gas prices, verify contracts on explorers immediately, document all addresses and constructor arguments, and have a deployment checklist.

Code examples

Hardhat Ignition Module

// ignition/modules/Protocol.ts
import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";

const ProtocolModule = buildModule("ProtocolModule", (m) => {
  const owner = m.getParameter("owner", "0x...");
  const feeBps = m.getParameter("feeBps", 250);

  // Deploy token first
  const token = m.contract("GovernanceToken", [
    "Protocol Token", "PTK", ethers.parseEther("1000000")
  ]);

  // Treasury depends on token
  const treasury = m.contract("Treasury", [token, feeBps]);

  // Vault depends on both
  const vault = m.contract("Vault", [token, treasury]);

  // Staking depends on token
  const staking = m.contract("StakingRewards", [token, token]);

  // Post-deployment configuration
  m.call(token, "grantRole", [
    ethers.keccak256(ethers.toUtf8Bytes("MINTER_ROLE")), vault
  ], { id: "grant_minter" });

  m.call(token, "transferOwnership", [owner], { id: "transfer_ownership" });

  return { token, treasury, vault, staking };
});

export default ProtocolModule;

// Deploy:
// npx hardhat ignition deploy ignition/modules/Protocol.ts --network sepolia

Ignition modules declare what to deploy. Ignition resolves dependencies automatically, deploys independent contracts in parallel, and handles failures with resumption. Post-deployment configuration calls are part of the module.

Traditional Deployment Script

// scripts/deploy.ts
import { ethers, run, network } from "hardhat";
import * as fs from "fs";

async function main() {
  const [deployer] = await ethers.getSigners();
  console.log("Deployer:", deployer.address);
  console.log("Network:", network.name);

  // Deploy Token
  const Token = await ethers.getContractFactory("GovernanceToken");
  const token = await Token.deploy("Protocol Token", "PTK", ethers.parseEther("1000000"));
  await token.waitForDeployment();
  const tokenAddr = await token.getAddress();
  console.log("Token:", tokenAddr);

  // Deploy Vault (depends on token)
  const Vault = await ethers.getContractFactory("Vault");
  const vault = await Vault.deploy(tokenAddr);
  await vault.waitForDeployment();
  const vaultAddr = await vault.getAddress();
  console.log("Vault:", vaultAddr);

  // Configure: grant role
  const MINTER_ROLE = ethers.keccak256(ethers.toUtf8Bytes("MINTER_ROLE"));
  await (await token.grantRole(MINTER_ROLE, vaultAddr)).wait();
  console.log("Granted MINTER_ROLE to Vault");

  // Verify on Etherscan
  if (network.name !== "hardhat" && network.name !== "localhost") {
    await token.deploymentTransaction()?.wait(5);
    try {
      await run("verify:verify", {
        address: tokenAddr,
        constructorArguments: ["Protocol Token", "PTK", ethers.parseEther("1000000")],
      });
    } catch (e: any) { console.log(e.message); }

    try {
      await run("verify:verify", {
        address: vaultAddr,
        constructorArguments: [tokenAddr],
      });
    } catch (e: any) { console.log(e.message); }
  }

  // Save deployment
  const deployment = {
    network: network.name,
    deployer: deployer.address,
    timestamp: new Date().toISOString(),
    contracts: { token: tokenAddr, vault: vaultAddr },
  };
  fs.mkdirSync("deployments", { recursive: true });
  fs.writeFileSync(`deployments/${network.name}.json`, JSON.stringify(deployment, null, 2));
}

main().catch(console.error);
// Run: npx hardhat run scripts/deploy.ts --network sepolia

This script sequentially deploys contracts, configures roles, verifies on Etherscan, and saves deployment addresses. Includes error handling, block confirmations, and artifact storage.

CREATE2 Deterministic Deployment

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract DeterministicDeployer {
    event Deployed(address indexed deployed, bytes32 indexed salt);

    function deploy(bytes32 salt, bytes memory bytecode)
        external payable returns (address deployed)
    {
        assembly {
            deployed := create2(callvalue(), add(bytecode, 0x20), mload(bytecode), salt)
            if iszero(extcodesize(deployed)) { revert(0, 0) }
        }
        emit Deployed(deployed, salt);
    }

    function computeAddress(bytes32 salt, bytes32 bytecodeHash)
        external view returns (address)
    {
        return address(uint160(uint256(keccak256(
            abi.encodePacked(bytes1(0xff), address(this), salt, bytecodeHash)
        ))));
    }
}

// Usage:
// const salt = ethers.keccak256(ethers.toUtf8Bytes("my-contract-v1"));
// const bytecodeHash = ethers.keccak256(bytecodeWithArgs);
// const predictedAddr = deployer.computeAddress(salt, bytecodeHash);
// // Same address on every EVM chain!

CREATE2 computes deployment address from deployer + salt + bytecode hash. Same inputs = same address on any EVM chain. Essential for cross-chain protocols and infrastructure contracts.

Key points

Concepts covered

Hardhat Ignition, Deployment, Constructor Args, Multi-Contract, Deterministic Deployment