From b58f3e2aa1044c9bed8fe0a5b7b74d76d2f97b54 Mon Sep 17 00:00:00 2001 From: Patricio Palladino Date: Sun, 15 Oct 2023 16:49:19 +0000 Subject: [PATCH] Complete the first version of Creating modules --- .../ignition/docs/guides/creating-modules.md | 381 ++++++++---------- 1 file changed, 174 insertions(+), 207 deletions(-) diff --git a/docs/src/content/ignition/docs/guides/creating-modules.md b/docs/src/content/ignition/docs/guides/creating-modules.md index 92c3a1111a..eb8949349e 100644 --- a/docs/src/content/ignition/docs/guides/creating-modules.md +++ b/docs/src/content/ignition/docs/guides/creating-modules.md @@ -1,329 +1,296 @@ # Creating Ignition Modules -- You define your deployment using modules -- What is a module -- buildModule -- Module id -- We recommend 1 module per file, with the name of the file matching the id -- `ModuleBuilder` and its methods -- They create futures -- Different kind of futures - - Deploying a contract - - Instantiating a contract - - Call - - Static call - - Red event argument - - Libraries -- Dependencies between futures and after -- Future ids -- Using parameters -- Passing ETH -- From and accounts -- Submodules -- Using existing artifacts -- Linking libraries - ---- - -Previous content: - -A Hardhat Ignition deployment is composed of modules. A module is a set of related smart contracts to be deployed, with accompanying contract calls, expressed through Hardhat Ignition's declarative Module API. - -For example, this is a minimal module `MyModule` that deploys an instance of a `Token` contract and exports it to consumers of `MyModule`: +When using Hardhat Ignition, you define your deployments using Ignition Modules. An Ignition module is an abstraction you use to describe the system you want to deploy. Each Ignition Module groups a set of smart contract instances of your system. -```javascript -const { buildModule } = require("@nomicfoundation/hardhat-ignition"); +This guide will explain you how to create Ignition Modules. + +## The module definition API + +To create an Ignition Module, you need to import the `buildModule` function from `@nomicfoundation/hardhat-ignition/modules` and call it passing a `string` that will be use as the module id, and a callback that defines the content of the module. + +For example, this is a module which will have the string `"MyToken"` as id: -module.exports = buildModule("MyModule", (m) => { - const token = m.contract("Token"); +::::tabsgroup{options="TypeScript,JavaScript"} + +:::tab{value="TypeScript"} + +```typescript +import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; + +export default buildModule("MyToken", (m) => { + const token = m.contract("Token", ["My Token", "TKN", 18]); return { token }; }); ``` -Modules can be deployed: directly at the command-line with the `deploy` task, within Hardhat tests (see [Using Hardhat Ignition in tests](./tests.md)) or consumed by other modules to allow for more complex deployments. +::: + +:::tab{value="JavaScript"} -## Deploying a contract +```javascript +const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules"); -Hardhat Ignition is aware of the contracts within the `./contracts` Hardhat folder. Hardhat Ignition can deploy any compilable local contract by name: +module.exports = buildModule("My token", (m) => { + const token = m.contract("Token", ["My Token", "TKN", 18]); -```tsx -const token = m.contract("Token"); + return { 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: +While you can create as many modules in a file as you want as long as their ids are unique, to deploy a module, you need to export it using `module.exports =` or `export default`. We recommend creating a single module per file, and using the module id as file name. -```tsx -const token = m.contract("Token", ["My Token", "TKN", 18]); -``` +The second argument we passed to `buildModule` is a module definition callback, which receives a `ModuleBuilder` object. This object has mehtods you use to define the contents of the module. For example, we used the `contract` method to define an instance of the contract `Token`. -### Adding an endowment of _ETH_ +Calling a methods of `ModuleBuilder` won't deploy any contract nor interact with the network in any way. Instead, it will create a `Future`, register it within the module, and return it. -The deployed contract can be given an endowment of _ETH_ by passing the value of the endowment in _wei_ as a `BigInt`, under the options object: +A `Future` is an object representing the result of an execution step that Hardhat Ignition needs to run to deploy a contract or interact with an existing one. To deploy a module, Hardhat Ignition executes every one of its future, running its execution step once, and storing its results. -```tsx -const token = m.contract("Token", [], { - value: BigInt(1_000_000_000), -}); -``` +Finally, `Future`s representing contract instances can be returned by the module defintion callback to expose one or more contracts to other modules and tests, just like we returned `token` in our example. -### Dependencies between contracts +## The different kinds of `Future` -If a contract needs the address of another contract as a constructor argument, the contract future can be used: +This section will explore the different kind of `Future` Hardhat Ignition supports, and how to defined them using a `ModuleBuilder`. -```tsx -const a = m.contract("A"); -const b = m.contract("B", [a]); -``` +### Deploying a contract -You can think of this as `b` being the equivalent of a promise of an address, although _futures are not promises_. +As we saw in our example above, to deploy an instance of a contract, you need to create a `Future` using `m.contract`. -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: +Hardhat Ignition is aware of the contracts in your Hardhat project, so you can refer to them by their name, like you would do in a test. -```tsx -const a = m.contract("A"); -const b = m.contract("B", [], { - after: [a], -}); +Let's look at the example again: + +```js +const token = m.contract("Token", ["My Token", "TKN", 18]); ``` -### Deploying from an artifact +Here we call `m.contract` and pass the contract name as the first argument. Then, we pass an array with the arguments that the constructor should receive. -To allow you to use your own mechanism for getting the contract artifact, `contract` supports passing an `Artifact` as the second parameter: +If you want to use the value that a `Future` represents as an argument, all you need to do is passing the `Future` itself. Hardhat Ignition will know how to resolve it during execution. -```javascript -const artifact = hre.artifacts.readArtifactSync("Foo"); +For example, we can use the address of `token` like this: -const userModule = buildModule("MyModule", (m) => { - m.contract("Foo", artifact, [0]); -}); +```js +const foo = m.contract("ReceivesAnAddress", [token]); ``` -### Using an existing contract - -A user might need to execute a method in a contract that wasn't deployed by Hardhat Ignition. An existing contract can be leveraged by passing an address and artifact: - -```tsx -const uniswap = m.contractAt("UniswapRouter", "0x0...", artifact); +If you need to send ETH to the constructor, you can pass an object with options as third argument to `m.contract`, and use its `value` field: -m.call(uniswap, "addLiquidity", [ - /*...*/ -]); +```js +const bar = m.contract("ReceivesETH", [], { + value: 1_000_000_000n, // 1gwei +}); ``` -### Linking libraries +### Using an existing contract -A library can be deployed and linked to a contract by passing the library's future as a named entry under the libraries option: +If you need to interact with existing contract you can create a `Future` to represent it like this: -```tsx -const safeMath = m.library("SafeMath"); -const contract = m.contract("Contract", [], { - libraries: { - SafeMath: safeMath, - }, -}); +```js +const existingToken = m.contractAt("Token", "0x..."); ``` -A library is deployed in the same way as a contract. +Just like with `m.contract`, the first value is the name of the contract, and the second value is its address. -## Calling contract methods +You can also use another `Future` as its address, which can be useful when using a factory, or to create a contract `Future` with a different interface (e.g. deploying a proxy instantiating it as its implementation). -Not all contract configuration happens via the constructor. To configure a contract through a call to a contract method: +### Calling contract methods -```tsx -const token = m.contract("Token"); -const exchange = m.contract("Exchange"); +If you need to call a method of an contract all you need to do is -m.call(exchange, "addToken", [token]); +```js +m.call(token, "transfer", [receiver, amount]); ``` -### Transferring _ETH_ as part of a call +Here the first argument is the contract we want to call, the second one the method name, and the third one is an array of arguments. The array of arguments can contain other `Future`s and Hardhat Ignition will know how to resolve them. -Similar to `ethers`, a call can transfer _ETH_ by passing a value in _wei_ as a `BigInt` under the options: +This method returns a `Future` which we aren't assigning to any variable. This isn't a problem, as Hardhat Ignition will execute every `Future` within a module. -```tsx -m.call(exchange, "deposit", [], { - value: BigInt(1_000_000_000), +Finally, if you need to send ETH while calling this method, you can pass an object with options as third argument to `m.contract`, and use its `value` field: + +```js +m.call(myContract, "receivesEth" [], { + value: 1_000_000_000n, // 1gwei }); ``` -### Transferring _Eth_ outside of a call +### Reading a value from a contract -It's also possible to transfer `ETH` to a given address via a regular Ethereum transaction: +If you need to call a `view` or `pure` method of a contract to retreive a value, you can do it with `m.staticCall`: -```tsx -m.sendETH(exchange, { - value: BigInt(1_000_000_000), -}); +```js +const balance = m.staticCall(token, "balanceOf", [address]); ``` -### Using the results of statically calling a contract method +Just like with `m.call`, `m.staticCall`'s first three arguments are the contract, the method name, and its argumetns, and it returns a `Future` representing the value returned by the method. -A contract might need the result of some other contract method as an input: +If the method you are calling returns more than one value, it will return the first one by default. You can customize this by passing an index or name as the forth value. -```tsx -const token = m.contract("Token"); -const totalSupply = m.staticCall(token, "totalSupply"); +To execute this `Future`, Hardhat Ignition won't send any transaction, and use `eth_call` instead. Like every `Future`, it only gets executed once, and its result is recorded. -const someContract = m.contract("ContractName", [totalSupply]); -``` +#### Reading a value from an event emitted by a contract -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 you need to read a value that was generated by a contract and exposed through Solidity events, you can use `m.readEventArgument`: ```tsx -if (totalSupply > 0) { - ... -} -``` +const transfer = m.call(token, "transfer", [receiver, amount]); -Because `totalSupply` is not a number, it is a future. +const value = m.readEventArgument(transfer, "Transfer", "_value"); +``` -## Retrieving data from events +Here, you pass the `Future` whose execution will emit the event, the event name, and the event argument (index or name) you want to read. -Important data and values generated by contract calls are often exposed through Solidity events. Hardhat Ignition allows you to retrieve event arguments and use them in subsequent contract calls: +You can also pass an object with options, which can contain: -```tsx -const multisig = m.contract("Multisig", []); +- `emitter`: A `Future` representing the contract instance that emits the event. This defaults to the contract you are interacting with in the `Future` you pass as first argument. +- `eventIndex`: If the are multiple events with the same name emitted by the `emitter`, you can use this parameter to select one of them. It defaults to `0`. -const call = m.call(multisig, "authorize"); +### Sending ETH or data to an account -const authorizer = m.readEventArgument( - call, - "AuthorizedBy", // Event name - "Authorizer" // Event argument name -); +If you need to send ETH or data to an account, you can do it like this -m.call(multisig, "execute", [authorizer]); +```js +const send = m.send("SendingEth", address, 1_000_000n); +const send = m.send("SendingData", address, undefined, "0x16417104"); ``` -## Network Accounts Management +The first argumetn of `m.send` is the id of the `Future`. To learn more about them jump to [this section](#future-ids). -All accounts configured for the current network can be accessed from within an Hardhat Ignition module via `m.getAccount(index)`: +The second argument is the address of the account you want to send the ETH or data to. -```tsx -module.exports = buildModule("Multisig", (m) => { - const owner = m.getAccount(0); - // ... -}); +The third and forth one are optional, and are the amount of ETH to send, and the data. + +### Deploying a library + +If you need to deploy a library, you can do it with + +```js +const myLib = m.library("MyLib"); ``` -You can then use these addresses in 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: +To learn how to link them, please read [this section](#linking-libraries) -```tsx -module.exports = buildModule("Multisig", (m) => { - const owner = m.getAccount(0); - const alsoAnOwner = m.getAccount(1); - const notAnOwner = m.getAccount(2); +## `Future` ids - const multisig = m.contract("Multisig", [owner, alsoAnOwner], { - from: owner, - }); +Each `Future` inside your should have a unique id. Normally, Hardhat Ignition will automatically generate an id for you, based on some of the parameters you pass when creating the `Future`. - const value = BigInt(1_000_000_000); - const fund = m.send("fund", multisig, value, undefined, { from: notAnOwner }); +In some cases, this automatic process may lead to a clash with an existing `Future`. If that happens, Hardhat Ignition won't try to resolve the clash, and you'd have to define an id manually. Every method of `ModuleBuilder` accepts an options object as last argument, which has an `id` field that can be used like this: - const call = m.call(multisig, "authorize", [], { from: alsoAnOwner }); +```js +const token = m.contract("Token", ["My Token 2", "TKN2", 18], { + id: "MyToken2", }); ``` -Note that if `from` is not provided, Hardhat Ignition will default to sending transactions using the first configured account (`accounts[0]`). +They are used to continue the execution of a deployment if it failed or if you want to modify it. -## Including modules within modules +The `Future` ids are used to organize your deployment results, artifacts, and to resume a deployment after it failed or you extended it. For this reason, you should avoid changing your ids after running a deployment. -Modules can be deployed and consumed within other modules via `m.useModule(...)`: +## Dependencies between futures -```tsx -module.exports = buildModule("`TEST` registrar", (m) => { - // ... - - const { ens, resolver, reverseRegistrar } = m.useModule(setupENSRegistry); +If you pass a `Future` as an argument when constructing a new one, a dependency from thew new one to the existing one is created. - // Setup registrar - const registrar = m.contract("FIFSRegistrar", [ens, tldHash]); +Dependencies are used by Hardhat Ignition to understand in which order it needs to execute the `Future`s. - // ... +You can also decleare dependencies between `Future`s explictly. To do this, you can use the options object that all the methods to construct `Future`s accept. For example: - return { ens, resolver, registrar, reverseRegistrar }; +```js +const a = m.contract("A"); +const b = m.contract("B", [], { + after: [a], }); ``` -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: +When you define your Ignition Modules you may want to use parameters to tweak some values during deployment. -```tsx -const symbol = m.getParameter("tokenSymbol"); -const name = m.getParameter("tokenName"); +You can do this by calling `m.getParamter`, and using its return value to define your `Future`s. -const token = m.contract("Token", { - args: [symbol, name, 1_000_000], -}); +For example, we make our token name parametric like this: + +```js +const tokenName = m.getParamter("name"); +const token = m.contract("Token", [tokenName, "TKN2", 18]); ``` -When a module is deployed, the proper parameters must be provided, indexed by the `ModuleId`. If they are not available, the deployment won't be executed and will error. +Now, when we deploy the module, we can provide a custom name. To learn how to do this, please read the [Deploying a module guide](./deploy.md). -You can use optional params by providing default values: +## Using submodules -```tsx -const symbol = m.getParameter("tokenSymbol", "TKN"); -``` +You can organize your deployment into different Ignition Modules, which can make them easier to write, read and reason about. -Previous parameters content: +When you are defining a module, you can access other modules as submodules and use their result `Future`s. To do it, you need to call `m.useModule` passing the module, as returned by `buildModule`: -# Using parameters +```js +const TokenModule = buildModule("TokenModule", (m) => { + const token = m.contract("Token", ["My Token", "TKN2", 18]); -When you define your Ignition Modules you may want to use parameters to tweak some values during deployment. + return { token }; +}); -You can do this by calling `m.getParamter`, and using its return value to define your `Future`s. +const TokenOwnerModule = buildModule("TokenOwnerModule", (m) => { + const { token } = m.useModule(TokenModule); -For example, we can modify the `Apollo` module from the [Quick Start guide](../getting-started/index.md#quick-start), by making the `Rocket`'s name a parameter: + const owner = m.contract("TokenOwner", [token]); + m.call(token, "transferOwnership", [owner]); -::::tabsgroup{options="TypeScript,JavaScript"} + return { owner }; +}); +``` -:::tab{value="TypeScript"} +If you use a `Future` from a submodule to create a new `Future`, the new one will have a dependency on every `Future` within the submodule. This means that any possible initialization within the submodule will be completed by the time your new `Future` gets executed. -**ignition/modules/Apollo.ts** +Calling multiple times to `m.useModule` with the same Ignition Module doesn't lead to multiple deployments. Hardhat Ignition only executes `Future`s once. -```typescript -import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; +## Deploying and calling contracts from different accounts -export default buildModule("Apollo", (m) => { - const apollo = m.contract("Rocket", m.getParamter("name", "Apollo")); +If you need to change the sender of a deployment, call, or another future, you can do it by providing a `from` option. - m.call(apollo, "launch", []); +For example, to deploy a contract from a different account you can do - return { apollo }; -}); +```js +const token = m.contract("Token", ["My Token", "TKN2", 18], { from: "0x...." }); ``` -::: +You can also define a module that uses the accounts that Hardhat has available during the deployment. To do it, you can use `m.getAccount(index)`, like this: -:::tab{value="JavaScript"} +```js +const account1 = m.getAccount(1); +const token = m.contract("Token", ["My Token", "TKN2", 18], { from: account1 }); +``` -**ignition/modules/Apollo.js** +## Using existing artifacts -```javascript{4} -const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules"); +If you need to deploy or interact with a contract that isn't part of your Hardhat project, you can provide your own artifacts. -module.exports = buildModule("Apollo", (m) => { - const apollo = m.contract("Rocket", m.getParamter("name", "Apollo")); +All the methods that create `Future`s that represent contracts have overloads that accept artifacts. Here are examples of all of them: - m.call(apollo, "launch", []); +```js +const token = m.contract("Token", TokenArtifact, ["My Token", "TKN2", 18]); - return { apollo }; -}); +const myLib = m.library("MyLib", MyLibArtifact); + +const token2 = m.contractAt("Token", token2Address, TokenArtifact); ``` -::: +In this case, the name of the contract is only used to generate [`Future` ids](#future-ids), and not to load any artifact. -:::: +## Linking libraries -Now, when we deploy the module, we can provide a custom name, or use the default, `"Apollo"`. +If you need to link a library when deploying a contract, you can do it by passing them in the options object when calling `m.contract` or `m.library`. + +For example, you can do + +```js +const myLib = m.library("MyLib"); +const myContract = m.contract("MyContract", [], { + libraries: { + MyLib: myLib, + }, +}); +```