Skip to content

wighawag/template-ethereum-contracts

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

326 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Ethereum Smart Contract Development Template

A production-ready template for developing EVM smart contracts using Hardhat v3 and hardhat-deploy v2 with the rocketh deployment system.

Why This Template?

hardhat-deploy + rocketh vs Ignition

While Hardhat's official Ignition plugin offers a robust deployment system, it comes with a rigid DSL that limits flexibility. This template uses hardhat-deploy + rocketh which provides:

  • Hot Contract Replacement (HCR): The equivalent of HMR (Hot Module Replacement) for smart contracts. Edit your contracts and see changes live while developing your app or game. This uses proxy patterns with a set of conventions to make it work seamlessly.
  • Intuitive Deployment Scripts: Write deployment logic in plain TypeScript.
  • Flexible Proxy Patterns: Declarative proxy deployment with deployViaProxy for upgradeable contracts.
  • Universal Deploy Scripts: Thanks to rocketh, the deploy script can run in any environment, nide, browser, etc...

Monorepo Structure

This template is organized as a monorepo, making it easy to:

  • Add a web frontend in a separate web/ folder
  • Import contract artifacts, ABIs, and types from the contracts package
  • Share deployment information across packages
  • Publish contracts as an npm package for external consumption

Project Structure

.
├── contracts/                    # Smart contracts package
│   ├── src/                      # Solidity source files
│   ├── deploy/                   # Deployment scripts
│   ├── deployments/              # Deployment artifacts per network
│   ├── generated/                # Auto-generated artifacts and ABIs
│   ├── rocketh/                  # Rocketh configuration
│   │   ├── config.ts             # Account & extension configuration
│   │   ├── deploy.ts             # Deploy script setup
│   │   └── environment.ts        # Environment setup for tests/scripts
│   ├── scripts/                  # Utility scripts
│   └── test/                     # TypeScript tests
├── package.json                  # Root monorepo configuration
└── pnpm-workspace.yaml           # PNPM workspace definition

Initial Setup

Prerequisites

Installation

pnpm i

We also recommend installing Zellij for an optimal development experience with pnpm start.

Usage

Compile Contracts

pnpm contracts:compile

This runs both Solidity and TypeScript compilation.

Watch Mode (Auto-Rebuild)

Run in a separate terminal for automatic recompilation on changes:

async: run this in a separate terminal

pnpm contracts:compile:watch

Run Tests

pnpm contracts:test

This runs both:

  • Solidity tests (forge-style, using forge-std)
  • TypeScript tests (using Node.js test runner with earl assertions)

Local Development

Start a local Ethereum node:

async: run this in a separate terminal

pnpm contracts:local_node

Deploy to localhost:

pnpm contracts:deploy localhost --skip-prompts

Deploy to Networks

  1. Configure your environment variables in .env.local:
MNEMONIC_<network>="your mnemonic phrase"
ETHERSCAN_API_KEY=<api-key>  # For verification

Or use Hardhat's secret store for sensitive data.

  1. Deploy:
pnpm contracts:deploy <network>

Execute Scripts

Run scripts against a deployed contract:

pnpm contracts:execute <network> scripts/setMessage.ts "hello"

Or execute in a forked environment:

pnpm contracts:fork:execute <network> scripts/setMessage.ts "Hello world"

Verify Contracts

pnpm contracts:verify <network>

Zellij Development Environment

Zellij is a terminal multiplexer (like tmux) with a preconfigured layout for this template.

Start the full development environment:

pnpm start

This launches:

  • A local Ethereum node
  • Auto-compilation on file changes
  • Auto-deployment on changes
  • Auto-testing on changes
  • An interactive shell for running scripts

Configuration

Named Accounts

Configure accounts in contracts/rocketh/config.ts:

export const config = {
  accounts: {
    deployer: { default: 0 }, // First account from mnemonic
    admin: { default: 1 }, // Second account
  },
  // ...
} as const satisfies UserConfig;

Network Configuration

Networks are configured in contracts/hardhat.config.ts using helper functions:

  • addNetworksFromEnv(): Auto-configure networks from ETH_NODE_URI_* environment variables
  • addNetworksFromKnownList(): Add configurations for well-known networks
  • addForkConfiguration(): Enable forking mode via HARDHAT_FORK env var

Rocketh Extensions

Extensions provide deployment functionality. Configure in contracts/rocketh/config.ts:

import * as deployExtension from "@rocketh/deploy";
import * as deployProxyExtension from "@rocketh/proxy";
import * as readExecuteExtension from "@rocketh/read-execute";
import * as viemExtension from "@rocketh/viem";

const extensions = {
  ...deployExtension, // Basic deploy function
  ...readExecuteExtension, // read/execute helpers
  ...deployProxyExtension, // deployViaProxy for upgradeable contracts
  ...viemExtension, // viem client integration
};

Writing Deploy Scripts

Deploy scripts are located in contracts/deploy/ and are executed in order (prefixed with numbers):

import { deployScript, artifacts } from "../rocketh/deploy.js";

export default deployScript(
  async (env) => {
    const { deployer, admin } = env.namedAccounts;

    // Deploy an upgradeable contract
    const deployment = await env.deployViaProxy(
      "GreetingsRegistry",
      {
        account: deployer,
        artifact: artifacts.GreetingsRegistry,
        args: ["prefix:"],
      },
      {
        owner: admin,
        linkedData: {
          /* metadata stored with deployment */
        },
      },
    );

    // Interact with the deployed contract
    const contract = env.viem.getContract(deployment);
    const message = await contract.read.messages([deployer]);
  },
  { tags: ["GreetingsRegistry"] },
);

Writing Tests

TypeScript Tests

Located in contracts/test/, using Node.js test runner and earl assertions:

import { expect } from "earl";
import { describe, it } from "node:test";
import { network } from "hardhat";
import { setupFixtures } from "./utils/index.js";

const { provider, networkHelpers } = await network.connect();
const { deployAll } = setupFixtures(provider);

describe("GreetingsRegistry", function () {
  it("should set and retrieve messages", async function () {
    const { env, GreetingsRegistry, unnamedAccounts } =
      await networkHelpers.loadFixture(deployAll);

    const greeter = unnamedAccounts[0];
    await env.execute(GreetingsRegistry, {
      functionName: "setMessage",
      args: ["hello"],
      account: greeter,
    });

    const message = await env.read(GreetingsRegistry, {
      functionName: "messages",
      args: [greeter],
    });
    expect(message).toEqual("hello");
  });
});

Solidity Tests

Located alongside contracts with .t.sol extension, using forge-std:

import {Test} from "forge-std/Test.sol";
import {GreetingsRegistry} from "./GreetingsRegistry.sol";

contract GreetingsRegistryTest is Test {
    GreetingsRegistry internal registry;

    function setUp() public {
        registry = new GreetingsRegistry("");
    }

    function test_setMessageWorks() public {
        registry.setMessage("hello");
        assertEq(registry.messages(address(this)), "hello");
    }
}

Linting

Solidity linting is configured with slippy:

pnpm contracts:lint

Publishing & Consuming Contracts

Package Exports

The contracts package exposes multiple entry points:

{
  "exports": {
    "./deploy/*": "./dist/deploy/*",
    "./rocketh/*": "./dist/rocketh/*",
    "./artifacts/*": "./dist/generated/artifacts/*",
    "./abis/*": "./dist/generated/abis/*",
    "./deployments/*": "./deployments/*",
    "./src/*": "./src/*"
  }
}

Using in Another Package

// Import ABIs
import { Abi_GreetingsRegistry } from "template-ethereum-contracts/abis/GreetingsRegistry.js";

// Import deployment info
import GreetingsRegistry from "template-ethereum-contracts/deployments/sepolia/GreetingsRegistry.json";

// Import Solidity sources (for inheritance or verification)
// Reference: template-ethereum-contracts/src/GreetingsRegistry/GreetingsRegistry.sol

Building for Publication

pnpm contracts:build

Environment Variables

Variable Description
ETH_NODE_URI_<network> RPC endpoint for the network
MNEMONIC_<network> Mnemonic for account derivation
MNEMONIC Fallback mnemonic if network-specific not set
ETHERSCAN_API_KEY API key for contract verification

Set SECRET as the value to use Hardhat's secret store:

ETH_NODE_URI_mainnet=SECRET  # Uses configVariable('SECRET_ETH_NODE_URI_mainnet')

Adding a Web Frontend

Since this is a monorepo, you can easily add a web frontend:

  1. Create a web/ directory with your frontend framework
  2. Add it to pnpm-workspace.yaml:
    packages:
      - "contracts"
      - "web"
  3. Import contracts in your frontend:
  • ABIs

    import { Abi_GreetingsRegistry } from "template-ethereum-contracts/abis/GreetingsRegistry.js";
  • Artifacts

    import { Artifact_GreetingsRegistry } from "template-ethereum-contracts/artifacts/GreetingsRegistry.js";
  • Deployments

    import GreetingsRegistry from "template-ethereum-contracts/deployments/sepolia/GreetingsRegistry.js";
  • or event Deploy Scripts

    import DeployScript from "template-ethereum-contracts/deploy/001_deploy_greetings_registry.js";
  1. Use the export script to generate deployment info:
    pnpm contracts:export <network> --ts ../web/src/lib/deployments.ts

License

MIT

About

Template to develop ethereum smart contracts

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors