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
deployViaProxyfor 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
contractspackage - 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
earlassertions)
Local Development
Start a local Ethereum node:
async: run this in a separate terminal
pnpm contracts:node:local
Deploy to localhost:
pnpm contracts:deploy localhost --skip-prompts
Deploy to Networks
- Configure your environment variables in
.env.local:
bash skip
MNEMONIC_="your mnemonic phrase"
ETHERSCAN_API_KEY= # For verification
Or use Hardhat's secret store for sensitive data.
- Deploy:
bash skippnpm contracts:deploy
### Execute Scripts
Run scripts against a deployed contract:
bash skip
pnpm contracts:execute scripts/setMessage.ts "hello"
Or execute in a forked environment:
bash skip
pnpm contracts:fork:execute scripts/setMessage.ts "Hello world"
### Verify Contracts
bash skip
pnpm contracts:verify
## Zellij Development Environment
Zellij is a terminal multiplexer (like tmux) with a preconfigured layout for this template.
Start the full development environment:
bash skip
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
contracts/rocketh/config.ts:
typescript skip
export const config = {
accounts: {
deployer: { default: 0 }, // First account from mnemonic
admin: { default: 1 }, // Second account
},
// ...
} as const satisfies UserConfig;
### Network Configuration
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
contracts/rocketh/config.ts:
typescript skip
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):
typescript skip
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:
typescript skip
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:
solidity
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:
bash
pnpm contracts:lint
## Publishing & Consuming Contracts
Package Exports
The contracts package exposes multiple entry points:
json
{
"exports": {
"./deploy/": "./dist/deploy/",
"./rocketh/": "./dist/rocketh/",
"./artifacts/": "./dist/generated/artifacts/",
"./abis/": "./dist/generated/abis/",
"./deployments/": "./deployments/",
"./src/": "./src/"
}
}
### Using in Another Package
typescript skip
// 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
bash
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:
bash skip
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:
- Create a
web/ directory with your frontend framework
Add it to pnpm-workspace.yaml:
yaml packages:
- "contracts"
- "web"
3. Import contracts in your frontend:
- ABIs
typescript skip import { Abi_GreetingsRegistry } from "template-ethereum-contracts/abis/GreetingsRegistry.js";
- Artifacts
typescript skip
import { Artifact_GreetingsRegistry } from "template-ethereum-contracts/artifacts/GreetingsRegistry.js";
- Deployments
typescript skip
import GreetingsRegistry from "template-ethereum-contracts/deployments/sepolia/GreetingsRegistry.js";
- or event Deploy Scripts
typescript skip
import DeployScript from "template-ethereum-contracts/deploy/001_deploy_greetings_registry.js";
4. Use the export script to generate deployment info:
bash skip
pnpm contracts:export --ts ../web/src/lib/deployments.ts
``
License
MIT