hardhat-deploy
A Hardhat Plugin For Replicable Deployments And Easy Testing
Note: This is the documentation for hardhat-deploy v2 that works with hardhat v3
Documentation for hardhat-deploy v1 can be found on the v1 branch
hardhat-deploy make it easy for you to deploy your EVM smart contracts across many chains.
The version 2 is a full rewrite that has been already used in production for several years. It does not support yet all the features of version 1 but is fully modular and it is thus much easier to contribute to new deployment mechanism.
Under the hood, hardhat-deploy uses rocketh a framework agnostic system that provide a minimal api to save and load deployment.
Everything else is handled by external module. For example @rocketh/deploy
give you a deploy function that you can use to deploy contracts. But you can provide your own module for advanced use case.
There are already a few like @rocketh/proxy
to deploy proxy declaratively like in hardhat-deploy v1 or @rocketh/diamond
to deploy diamond declaratively.
What is it for?
This hardhat plugin adds a mechanism to deploy contracts to any network, keeping track of them and replicating the same environment for testing.
It also adds a mechanism to associate names to addresses, so test and deployment scripts can be reconfigured by simply changing the address a name points to, allowing different configurations per network. This also results in much clearer tests and deployment scripts (no more accounts[0] in your code).
This plugin contains a lot more features too, all geared toward a better developer experience :
- chain configuration export (via @rocketh/export) (listing deployed contracts' addresses and their abis (useful for web apps))
- library linking at the time of deployment.
- deterministic deployment across networks.
- support for specific deploy script per network (L1 vs L2 for example)
- deployment dependency system (allowing you to only deploy what is needed).
- deployment retrying (by saving pending tx): so you can feel confident when making a deployment that you can always recover.
- deployments as test fixture via hardhat helpers
- contains helpers to read and execute transaction on deployed contract referring to them by name.
- save metadata of deployed contract so they can always be fully verified, via sourcify or etherscan.
- ability to submit contract source to etherscan and sourcify for verification at any time. (Because hardhat-deploy will save all the necessary info, it can be executed at any time.)
- support hardhat's fork feature so deployment can be accessed even when run through fork.
- declarative proxy deployment with ability to upgrade them transparently, only if code changes.
- this include support for openzeppelin transparent proxies
- diamond deployment with facets, allowing you to focus on what the new version will be. It will generate the diamondCut necessary to reach the new state.
- support HRC (Hot Contract Replacement) via special proxy mechanism and file watch setup
hardhat-deploy in a nutshell
Before going into the details, here is a very simple summary of the basic feature of hardhat-deploy.
hardhat-deploy allows you to write deploy scripts
in the deploy
folder. Each of these files that look as follows will be executed in turn when you execute the following task: hardhat --network <networkName> deploy
// we import what we need from the @rocketh alias, see below for setup
import { execute, artifacts } from "@rocketh";
export default execute(
// this allow us to define our deploy function which takes as first argument an environment object
// This contaisn the function provided by the modules imported in 'rocketh.ts'
// along with other built-in functions and the named accounts
async ({ deployViaProxy, namedAccounts }) => {
const { deployer, admin } = namedAccounts;
const prefix = "proxy:";
await deployViaProxy(
"GreetingsRegistry",
{
account: deployer,
artifact: artifacts.GreetingsRegistry,
args: [prefix],
},
{
owner: admin,
linkedData: {
prefix,
admin,
},
}
);
},
// execute takes as a second argument an options object where you can specify tags and dependencies
{ tags: ["GreetingsRegistry", "GreetingsRegistry_deploy"] }
);
Furthermore you can also ensure these scripts are executed in test too by calling await loadAndExecuteDeployments({provider: provider,});
in your tests.
Amd you can call these in hardhat test fixture to benefit from caching optimization.
This is a huge benefit for testing since you are not required to replicate the deployment procedure in your tests. The tag feature (as seen in the script above) and dependencies will also make your life easier when writing complex deployment procedures.
You can even group deploy scripts in different sub-folders and ensure they are executed in their logical order.
Installation
Here is the basic for getting started
Note: hardhat-deploy is alpha, see @next to get alpha version, if you do not specify @nextyou'll get v1
npm install -D hardhat-deploy@next rocketh @rocketh/deploy @rocketh/read-execute
pnpm add -D hardhat-deploy@next rocketh @rocketh/deploy @rocketh/read-execute
but you can also add these that provide more features
npm install -D @rocketh/proxy @rocketh/diamond @rocketh/export @rocketh/verifier @rocketh/doc
pnpm add -D @rocketh/proxy @rocketh/diamond @rocketh/export @rocketh/verifier @rocketh/doc
Then you need import them in your deploy script.
But we recommend you import them in one location that you then import in your deploy script so you can share it to all of them.
We recommend to actuall use the rocketh.ts/js
config file to do that in one place.
if you use typescript, we also recommend you add the to tsconfig.json
so you can import with import .. from "@rocketh"
it from anywhere:
{
"compilerOptions": {
...
"paths": {
"@rocketh": ["./rocketh.ts"]
}
}
}
Example of rocketh.ts
file:
// ------------------------------------------------------------------------------------------------
// Typed Config
// ------------------------------------------------------------------------------------------------
import { UserConfig } from "rocketh";
export const config = {
accounts: {
deployer: {
default: 0,
},
},
} as const satisfies UserConfig;
// ------------------------------------------------------------------------------------------------
// Imports and Re-exports
// ------------------------------------------------------------------------------------------------
// We regroup all what is needed for the deploy scripts
// so that they just need to import this file
// We also added an alias (@rocketh) in tsconfig.json
// so they just need to do `import {execute, artifacts} from '@rocketh';`
// and this work anywhere in the file hierarchy
// ------------------------------------------------------------------------------------------------
// we add here the module we need, so that they are available in the deploy scripts
import "@rocketh/deploy"; // this one provide a deploy function
import "@rocketh/read-execute"; // this one provide read,execute functions
// ------------------------------------------------------------------------------------------------
// we re-export the artifacts, so they are easily available from the alias
import artifacts from "./generated/artifacts.js";
export { artifacts };
// ------------------------------------------------------------------------------------------------
// while not necessary, we also converted the execution function type to know about the named accounts
// this way you get type safe accounts
import {
execute as _execute,
loadAndExecuteDeployments,
type NamedAccountExecuteFunction,
} from "rocketh";
const execute = _execute as NamedAccountExecuteFunction<typeof config.accounts>;
export { execute, loadAndExecuteDeployments };
You can them create a deploy script in the deploy
folder like so:
// we import what we need from the @rocketh alias, see ../rocketh.ts
import { execute, artifacts } from "@rocketh";
export default execute(
async ({ deploy, namedAccounts }) => {
const { deployer } = namedAccounts;
await deploy("GreetingsRegistry", {
account: deployer,
artifact: artifacts.GreetingsRegistry,
args: [""],
});
},
// finally you can pass tags and dependencies
{ tags: ["GreetingsRegistry", "GreetingsRegistry_deploy"] }
);
See a template that uses hardhat-deploy here: https://github.com/wighawag/template-ethereum-contracts
Migrating from hardhat-deploy v1
in v1 you would create a deploy file like this:
// deploy/00_deploy_my_contract.js
// export a function that get passed the Hardhat runtime environment
module.exports = async ({ getNamedAccounts, deployments }) => {
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();
await deploy("MyContract", {
from: deployer,
args: ["Hello"],
log: true,
});
};
// add tags and dependencies
module.exports.tags = ["MyContract"];
and you would have configuraiton in hardhat.config.ts
namedAccounts: {
deployer: 0,
...
},
in v2 you will do this instead:
/// we import what we need from the @rocketh alias, see ../rocketh.ts
import { execute, artifacts } from "@rocketh";
export default execute(
async ({ deploy, namedAccounts }) => {
const { deployer } = namedAccounts;
await deploy("MyContract", {
account: deployer,
artifact: artifacts.MyContract,
args: ["Hello"],
});
},
// finally you can pass tags and dependencies
{ tags: ["MyContract"] }
);
and you would have a rocketh.ts/js
file as described in the Setup
Migrating existing deployment to hardhat-deploy
Only needed for an existing project that already deployed contracts and has the deployment information available (at minimum, address, abi, bytecode and metadata)
If you come from hardhat-deploy v1, there is nothing you should need to do as hardhat-deploy v2 will read your deployment files as usual.
You might want to switch your current deployment process to use hardhat-deploy. In that case you probably have some deployments saved elsewhere.
In order to port them to hardhat-deploy, you'll need to create one .json
file per contract in the deployments/<network>
folder (configurable via paths config).
The network folder is simply the hardhat network name (as configured in hardhat.config.js). Such folder need to have a file named .chain
containing both the chainId as decimal and the genesisHash. If coming from hardhat-deploy v1, it will also accept a .chainId
file containing only the chainId but will convert it
For example for a network named "sepolia" (for the corresponding network) the file deployments/rinkeby/.chain
would be
{"chainId":"11155111","genesisHash":"0x25a5cc106eea7138acab33231d7160d69cb777ee0c2c553fcddf5138993e6dd9"}
Each contract file must follow this type (as defined in types.ts) :
export type Deployment<TAbi extends Abi> = {
readonly address: EIP1193Account;
readonly abi: Narrow<TAbi>;
readonly transaction?: {
readonly hash: EIP1193DATA;
readonly origin?: EIP1193Account;
readonly nonce?: EIP1193DATA;
};
readonly receipt?: {
confirmations: number;
blockHash: EIP1193DATA;
blockNumber: EIP1193QUANTITY;
transactionIndex: EIP1193QUANTITY;
};
readonly bytecode: EIP1193DATA;
readonly argsData: EIP1193DATA;
readonly metadata: string;
readonly numDeployments?: number;
readonly libraries?: Libraries;
readonly linkedData?: any; // TODO
readonly deployedBytecode?: EIP1193DATA;
readonly linkReferences?: any; // TODO
readonly deployedLinkReferences?: any; // TODO
readonly contractName?: string;
readonly sourceName?: string; // relative path
readonly devdoc?: DevDoc;
readonly evm?: {
readonly gasEstimates?: GasEstimates | null;
} & any;
readonly storageLayout?: StorageLayout;
readonly userdoc?: UserDoc;
} & Record<string, unknown>;
As you can see, not all fields are mandatory. But having the other fields allow more feature. For example, metadata and args allow you to benefit from contract code verification.
Here is an example:
Let's say you have:
- 2 Contract named Greeter and Registry deployed on rinkeby
- 1 contract named Greeter on mainnet
- 2 Contract named Greeter and Registry deployed on a network named rinkeby2
You would get the following folder structure:
deployments/
mainnet/
.chain
Greeter.json
rinkeby/
.chain
Greeter.json
Registry.json
rinkeby2/
.chain
Greeter.json
Registry.json
The reason why hardhat-deploy save chainId and genesisHash in the .chain
file is both for
- safety: so that if you were to change the network name to point to a different chain, it would not attempt to read the wrong folder and assume that a contract has been deployed while it has not.
- ability to know the chainId without requiring to be connected to a node (and so not dependent on hardhat.config.js settings). Useful for
export
task.
Hardhat Tasks Available/Updated
hardhat deploy currently add one task to hardhat. But since hardhat-deploy is now just a tiny wrapper arround rocketh, you can use rocketh commands too
1. hardhat deploy
This plugin adds the deploy task to Hardhat.
This task will execute the scripts in the deploy
folder and save the contract deployments to disk. These deployments are supposed to be saved for example in a git repository. This way they can be accessed later. But you are free to save them elsewhere and get them back via your mechanism of choice.
With the deployment saved, it allows you to deploy a contract only if changes were made.
Deploy scripts (also called Deploy functions) can also perform arbitrary logic.
For further details on how to use it and write deploy script, see section below.
2. rocketh-verify
This command is provided by the optional package @rocketh/verify
This command will submit the contract source and other info of all deployed contracts to allow services like etherscan, blockscout or sourcify to verify and record the sources.
To execute that command, you need to specify the network to run against :
pnpm rocketh-verify -n <network-name> <etherscan|sourcify|blockscout>
3. rocketh-export
This command is provided by the optional package @rocketh/export
This command will export the contract deployed (saved in deployments
folder) to a file with a simple format containing only contract addresses and abi, useful for web apps.
One of the following options need to be set for this task to have any effects :
rocketh-export -n <network-name> --ts <path-to-contracts.ts>
Environment object and extension
Each deploy function is given a environment object as first argument.
it contains at least the following fields :
export interface Environment<
NamedAccounts extends
UnresolvedUnknownNamedAccounts = UnresolvedUnknownNamedAccounts,
Deployments extends UnknownDeployments = UnknownDeployments,
> {
network: {
chain: Chain;
name: string;
tags: { [tag: string]: boolean };
provider: TransactionHashTracker;
};
namedAccounts: ResolvedNamedAccounts<NamedAccounts>;
namedSigners: ResolvedNamedSigners<ResolvedNamedAccounts<NamedAccounts>>;
unnamedAccounts: EIP1193Account[];
addressSigners: { [name: `0x${string}`]: Signer };
save<TAbi extends Abi = Abi>(
name: string,
deployment: Deployment<TAbi>,
options?: { doNotCountAsNewDeployment?: boolean }
): Promise<Deployment<TAbi>>;
savePendingDeployment<TAbi extends Abi = Abi>(
pendingDeployment: PendingDeployment<TAbi>
): Promise<Deployment<TAbi>>;
savePendingExecution(
pendingExecution: PendingExecution
): Promise<EIP1193TransactionReceipt>;
get<TAbi extends Abi>(name: string): Deployment<TAbi>;
getOrNull<TAbi extends Abi>(name: string): Deployment<TAbi> | null;
fromAddressToNamedABI<TAbi extends Abi>(
address: Address
): { mergedABI: TAbi; names: string[] };
fromAddressToNamedABIOrNull<TAbi extends Abi>(
address: Address
): { mergedABI: TAbi; names: string[] } | null;
showMessage(message: string): void;
showProgress(message?: string): ProgressIndicator;
hasMigrationBeenDone(id: string): boolean;
}
And it is expanded by each module you import like @rocketh/deploy
which adds a deploy function
Configuration
Configuration of network or other hardhat specific config is done via hardhat.config.ts
Else rocketh.ts/js is where you can configure hardhat-deploy/rocketh specific configs.
1. namedAccounts (ability to name addresses)
The rocketh.js file need to export a config object
For example
import {UserConfig} from 'rocketh';
export const config = {
accounts: {
deployer: {
default: 0,
sepolia: 1
},
admin: {
default: 1,
},
},
} as const satisfies UserConfig;
// we add here the module we need, so that they are available in the deploy scripts
import '@rocketh/deploy'; // this one provide a deploy function
import '@rocketh/read-execute'; // this one provide read,execute functions
import '@rocketh/proxy'; // this one provide a deployViaProxy function that let you declaratively deploy proxy based contracts
import artifacts from './generated/artifacts.js';
export {artifacts};
This config file import modules too so that deploy script can simply import rocketh.js to have access to artifacts and specifc environment functions
The named account feature allow you to define accounts by name and have them configurable by network
In the above file, the deployer is set to be the first account on all network, except for sepolia where it is the second
4. deterministicDeployment (ability to specify a deployment factory)
The config also allow you to set an optional deterministicDeployment
field.
deterministicDeployment
allows you to associate information that are used on each network for deterministic deployment. The information for each deterministic deployment consist out of a factory
, a deployer
, the required funding
and a signedTx
to deploy the factory. The default deterministic deployment used is the Deterministic Deployment Proxy. The factory expects a 32 bytes salt
concatenated with the deployment data (see EIP-1014 for more information on these parameters).
Using the deterministicDeployment
it is possible to define a different setup for the deterministic deployment. One use case for this is the deterministic deployment on networks that required replay protection (such as Celo or Avalanche). The Deterministic Deployment Proxy can only be deployed on networks that don't enforce replay protection, therefore on other networks an alternative library has to be used. An example for this would be the Safe Singleton Factory that is an adjusted version of the Deterministic Deployment Proxy that contains signed transactions that include replay protection.
import {UserConfig} from 'rocketh';
export const config = {
accounts: {
deployer: {
default: 0,
sepolia: 1
},
admin: {
default: 1,
},
},
networks: {
sepolia: {
factory: '0x4e59b44847b379578588920ca78fbf26c0b4956c',
deployer: '0x3fab184622dc19b6109349b94811493bf2a45362',
funding: '10000000000000000',
signedTx:
'0xf8a58085174876e800830186a08080b853604580600e600039806000f350fe7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf31ba02222222222222222222222222222222222222222222222222222222222222222a02222222222222222222222222222222222222222222222222222222222222222',
}
}
} as const satisfies UserConfig;
How to Deploy Contracts
The deploy
Task
hardhat --network <networkName> deploy
This is a new task that the hardhat-deploy
adds. As the name suggests it deploys contracts. To be exact it will look for files in the folder deploy
It will scan for files in alphabetical order and execute them in turn.
- it will
import
each of these files and execute the exported function
To specify the network, you can use the builtin hardhat argument --network <network name>
⚠️ Note that running
hardhat deploy
without specifying a network will use the default network. If the default network is hardhat (the default's default) then nothing will happen as a result as everything happens in memory, but this can be used to ensure the deployment is without issues.
Deploy Scripts
The deploy scripts need to call the execute function exported by rocketh
The execute expect as first argument a function
For example this script will deploy the GreetingsRegistry
contract
import { execute, artifacts } from "@rocketh";
export default execute(
async ({ deploy, namedAccounts }) => {
const { deployer } = namedAccounts;
await deploy("GreetingsRegistry", {
account: deployer,
artifact: artifacts.GreetingsRegistry,
args: [""],
});
},
{ tags: ["GreetingsRegistry", "GreetingsRegistry_deploy"] }
);
The tags is a list of string that when the deploy task is executed with, the script will be executed (unless it skips). In other word if the deploy task is executed with a tag that does not belong to that script, that script will not be executed unless it is a dependency of a script that does get executed.
The dependencies is a list of tag that will be executed if that script is executed. So if the script is executed, every script whose tag match any of the dependencies will be executed first.
Finally the function can return true if it wishes to never be executed again. This can be useful to emulate migration scripts that are meant to be executed only once. Once such script return true (async), the id
field is used to track execution and if that field is not present when the script return true, it will fails.
In other words, if you want a particular deploy script to run only once, it needs to both return true (async) and have an id
set.
In any case, as a general advice every deploy function should be idempotent. This is so they can always recover from failure or pending transaction. This is what underpin most of hardhat-deploy philosophy.
This is why the deploy
function provided by @rocketh/deploy
will by default only deploy if the contract code has changed, making it easier to write idempotent script.
Environment and function provided by rocketh modules
By default rocketh environment only provide function to read and write deployments. It has no deploy
function on its own.
These are provided by external modules but few are already available like @rocketh/deploy
, @rocketh/proxy
and @rocketh/diamond
each with its own specific use case.
@rocketh/deploy
@rocketh/proxy
@rocketh/diamond
Handling contract using libraries
In the deploy function, one of the DeployOptions
field is the libraries
field. It allows you to associate external contract as libraries at the time of deployment.
First, you have deploy the library using the deploy
function, then when we deploy a contract that needs the linked library, we can pass the deployed library name and address in as an argument to the libraries
object.
First step: deploy the library:
const exampleLibrary = await deploy("ExampleLibary", {
artifact: artifacts.ExampleLibary,
from: <deployer>
});
ExampleLibrary is now deployed to whatever network was chosen (hardhat deploy --network <networkName>
)
For example, if we are deploying on Sepolia, this library will get deployed on sepolia, and the exampleLibrary
variable will be a deployment object that contains the abi as well as the deployed address for the contract.
Now that the library is deployed, we can link it in our next deployed contract.
const example = await deploy(
"Example",
{
account: deployer,
artifact: artifacts.Example,
args: ["example string argument for the 'Example' contract constructor"],
},
{
libraries: {
ExampleLibrary: exampleLibrary.address,
},
}
);
This libraries
object takes the name of the library, and its deployed address on the network. Multiple libraries can be passed into the libraries
object.