A production-ready template for developing EVM smart contracts using Hardhat v3 and hardhat-deploy v2 with the rocketh deployment system.
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...
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
.
├── 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
pnpm iWe also recommend installing Zellij for an optimal development experience with pnpm start.
pnpm contracts:compileThis runs both Solidity and TypeScript compilation.
Run in a separate terminal for automatic recompilation on changes:
async:
run this in a separate terminal
pnpm contracts:compile:watchpnpm contracts:testThis runs both:
- Solidity tests (forge-style, using
forge-std) - TypeScript tests (using Node.js test runner with
earlassertions)
Start a local Ethereum node:
async:
run this in a separate terminal
pnpm contracts:local_nodeDeploy to localhost:
pnpm contracts:deploy localhost --skip-prompts- Configure your environment variables in
.env.local:
MNEMONIC_<network>="your mnemonic phrase"
ETHERSCAN_API_KEY=<api-key> # For verificationOr use Hardhat's secret store for sensitive data.
- Deploy:
pnpm contracts:deploy <network>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"pnpm contracts:verify <network>Zellij is a terminal multiplexer (like tmux) with a preconfigured layout for this template.
Start the full development environment:
pnpm startThis launches:
- A local Ethereum node
- Auto-compilation on file changes
- Auto-deployment on changes
- Auto-testing on changes
- An interactive shell for running scripts
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;Networks are configured in contracts/hardhat.config.ts using helper functions:
addNetworksFromEnv(): Auto-configure networks fromETH_NODE_URI_*environment variablesaddNetworksFromKnownList(): Add configurations for well-known networksaddForkConfiguration(): Enable forking mode viaHARDHAT_FORKenv var
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
};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"] },
);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");
});
});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");
}
}Solidity linting is configured with slippy:
pnpm contracts:lintThe 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/*"
}
}// 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.solpnpm contracts:build| 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')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:packages: - "contracts" - "web"
- 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";
- Use the export script to generate deployment info:
pnpm contracts:export <network> --ts ../web/src/lib/deployments.ts
MIT