#Creating Modules for Deployments
An Ignition deployment is composed of modules. A module is a special javascript/typescript function that encapsulates several on-chain transactions (e.g. deploy a contract, invoke a contract function etc).
For example, this is a minimal module MyModule
that deploys an instance of a Token
contract and exposes it to any consumer of MyModule
:
const { buildModule } = require("@nomicfoundation/hardhat-ignition");
module.exports = buildModule("MyModule", (m) => {
const token = m.contract("Token");
return { token };
});
Modules can be deployed directly at the cli (with npx hardhat ignition deploy MyModule.js
), within Hardhat mocha tests (see Ignition in Tests) or consumed by other Modules to allow for complex deployments.
During a deployment, Ignition uses the module to generate an execution plan of the transactions to run and the order in which to run them based on their dependencies. A module uses the injected DeploymentBuilder
to specify the on-chain transactions that will eventually be run, and how they interdepend on each other.
# Deploying a contract
Ignition is aware of the contracts within the ./contracts
Hardhat folder. Ignition can deploy any compilable local contract by name:
const token = m.contract("Token");
token
here is called a contract future. It represents the contract that will eventually be deployed.
#Constructor arguments
In Solidity contracts may have constructor arguments that need satisfied on deployment. This can be done by passing an args
array as the second parameter:
const token = m.contract("Token", ["My Token", "TKN", 18]);
#Adding an endowment of Eth
The deployed contract can be given an endowment of Eth by passing the value of the endowment under the options object:
const token = m.contract("Token", [], {
value: BigInt(ethers.utils.parseUnits("1").toString()),
});
#Dependencies between contracts
If a contract needs the address of another contract as a constructor argument, the contract future can be used:
const a = m.contract("A");
const b = m.contract("B", [a]);
You can think of this as b
being the equivalent of a promise of an address, although futures are not promises.
If a contract does not directly depend through arguments on another contract, a dependency (don't deploy b
until a
is successfully deployed) can still be created using the after
array of options:
const a = m.contract("A");
const b = m.contract("B", [], {
after: [a],
});
#Deploying from an artifact
To allow you to use your own mechanism for getting the contract artifact, contract
supports passing an Artifact
as the second parameter:
const artifact = hre.artifacts.readArtifactSync("Foo");
const userModule = buildModule("MyModule", (m) => {
m.contract("Foo", artifact, [0]);
});
#Using an existing contract
A user might need to execute a method in a contract that wasn't deployed by Ignition. An existing contract can be leveraged by passing an address and artifact:
const uniswap = m.contractAt("UniswapRouter", "0x0...", artifact);
m.call(uniswap, "swap", [
/*...*/
]);
#Linking libraries
A library can be deployed and linked to a contract by passing the libraries contract future as a named entry under the libraries option:
const safeMath = m.library("SafeMath");
const contract = m.contract("Contract", [], {
libraries: {
SafeMath: safeMath,
},
});
A library is deployed in the same way as a contract.
# Calling contract methods
Not all contract configuration happens via the constructor. To configure a contract through a call to a contract method:
const token = m.contract("Token");
const exchange = m.contract("Exchange");
m.call(exchange, "addToken", [token]);
#Transferring Eth as part of a call
Similar to ethers
, a call can transfer Eth
by passing a value
under the options:
m.call(exchange, "deposit", [], {
value: BigInt(ethers.utils.parseUnits("1").toString()),
});
#Transferring Eth outside of a call
It's also possible to transfer Eth
to a given address via a regular Ethereum transaction:
m.sendETH(exchange, {
value: ethers.utils.parseUnits("1"),
});
#Using the results of statically calling a contract method
A contract might need the result of some other contract method as an input:
const token = m.contract("Token");
const totalSupply = m.staticCall(token, "totalSupply");
const someContract = m.contract("ContractName", [totalSupply]);
In this example, totalSupply
is called a deferred value. Similar to how a contract future is a contract that will eventually be deployed, a deferred value is some value that will eventually be available. That means you can't do this:
if (totalSupply > 0) {
...
}
Because totalSupply
is not a number, it is a future.
# Waiting for on-chain events
A deployment can be put on-hold
until an on-chain event has been emitted (for instance a timelock or multisig approval):
const multisig = m.contract("Multisig", []);
const call = m.call(multisig, "authorize");
const authorizerEventArg = m.readEventArgument(
call,
"AuthorizedBy", // Event name
"Authorizer" // Event arg name
);
m.call(multisig, "execute", [authorizerEventArg]);
The event
during deployment will check whether an event matching the given filter args has been emitted. If it has, the deployment will continue, if not the deployment will pause and listen for the event for a configurable period of time. If the event has not been detected within this listening period, the deployment stops in the on-hold
condition. A further run of the deployment will recheck the event
condition.
Upon execution, the EventFuture
will be resolved to the values of the requested parameter emitted by the given event. You can then use that value in tests or other modules as expected.
# Network Accounts Management
All accounts configured for the current network can be accessed from within an Ignition module via m.getAccount(index)
:
module.exports = buildModule("Multisig", (m) => {
const owner = m.getAccount(0);
});
You can then use these addresses anywhere you normally would, such as constructor or function args. Additionally, you can pass them as a value to the from
option in order to specify which account you would like a specific transaction sent from:
module.exports = buildModule("Multisig", (m) => {
const owner = m.getAccount(0);
const alsoAnOwner = m.getAccount(1);
const notAnOwner = m.getAccount(2);
const multisig = m.contract("Multisig", [owner, alsoAnOwner], {
from: owner,
});
const value = BigInt(ethers.utils.parseUnits("100").toString());
const fund = m.send("fund", multisig, value, undefined, { from: notAnOwner });
const call = m.call(multisig, "authorize", [], { from: alsoAnOwner });
});
Note that if from
is not provided, Ignition will default to sending transactions using the first configured account (accounts[0]
).
# Including modules within modules
Modules can be deployed and consumed within other modules via m.useModule(...)
:
module.exports = buildModule("`TEST` registrar", (m) => {
// ...
const { ens, resolver, reverseRegistrar } = m.useModule(setupENSRegistry);
// Setup registrar
const registrar = m.contract("FIFSRegistrar", [ens, tldHash]);
// ...
return { ens, resolver, registrar, reverseRegistrar };
});
Calls to useModule
memoize the results object.
Only contract or library types can be returned when building a module.
# Module parameters
Modules can have parameters that are accessed using the DeploymentBuilder
object:
const symbol = m.getParameter("tokenSymbol");
const name = m.getParameter("tokenName");
const token = m.contract("Token", {
args: [symbol, name, 1_000_000],
});
When a module is deployed, the proper parameters must be provided. If they are not available, the deployment won't be executed and will error.
You can use optional params by providing default values:
const symbol = m.getParameter("tokenSymbol", "TKN");
Next, let's take a look at using an Ignition module within Hardhat tests: