Hardhat from zero to hero: ignition, tasks and scripts (+ Chainlink Price Feed)

Publishing a Public Sale contract using the most powerful Hardhat features

Lee Marreros
28 min readJun 4, 2024

Hardhat overview
Key features of Hardhat
Steps for installing Hardhat
Project
1. Deploy the two smart contracts a to a local node
Exercises
2. Publish and verify the smart contracts to Sepolia Network
Exercises
3. Deploy the smart contracts using a Deployment Id and then reset its journal
Exercises
4. Create a task that calls the approve method from BBites Token
Exercises
5. Create a task (submodule) that sets up al smart contracts
Exercises:
6. Create a task where the owner purchases BBites Token with N amount of USDC (in Sepolia)
Exercises:
7. Fork Ethereum Mainnet in a local Hardhat node and publish the smart contracts
Exercises:
8. Develop a Hardhat script for purchasing BBites token against a Mainnet Ethereum fork with account impersonation
Exercises:

Hardhat overview

The main component of Hardhat is the Hardhat Runner. That component is an extensible task runner which helps to orchestrate complex tasks or task flows.

The Hardhat Runner uses at its core tasks and plugins. Tasks can call another tasks. Plugins could override other tasks. Those are the foundations for creating extendable and customizable workflows.

Hardhat also offers two powerful features called the ignition module and scripts. This article covers several use cases where each Hardhat feature is demonstrated.

Key features of Hardhat

  • It is a Development Environment: offers a local Ethereum node for fast testing and experimentation. Launch a testing node an deploy smart contracts against it. This could replace Ganache.
  • Task runners for automation: build automation flows for repetitive tasks. Fired flows by using a single command line. This saves you lot of time.
  • Plugin Ecosystem: Extend the power of Hardhat by using plugins (e.g. upgradeable smart contracts, gas reporter, contract verification, etc.)
  • Testing Framework: By using Mocha and Chai libraries, build different testing scenarios for smart contracts.
  • Debugging Tool: When using a Hardhat node or running tests, the method console.log could give you insights about what is happening at each state.
  • Network Management: By using the hardhat.config.js file, set up connections with different networks (mainnet and testnet). Then deploy and test against those networks.
  • Compilation: Whenever the tests, scripts or tasks are run, Hardhat compile the smart contracts automatically, providing the respective bytecode and ABI.
  • Execution Scripts: By using scripts, you could automate the deployment of smart contracts. More importantly, by using a script the set up required to prepare the initial values of a smart smart contract could be automated.
  • REPL: Hardhat can launch a live local Ethereum node. Then you could interact with for firing smart contract methods or publishing more smart contracts. The state is kept while the node is alive.

Steps for installing Hardhat

  1. Create a folder called hardhat-from-scratch. Open a terminal pointing to this folder and run npm init -y.
  2. Run npx hardhat init. Make sure you are using npm 7+ and node 18+. You will get something like this:
$ npx hardhat init

888 888 888 888 888
888 888 888 888 888
888 888 888 888 888
8888888888 8888b. 888d888 .d88888 88888b. 8888b. 888888
888 888 "88b 888P" d88" 888 888 "88b "88b 888
888 888 .d888888 888 888 888 888 888 .d888888 888
888 888 888 888 888 Y88b 888 888 888 888 888 Y88b.
888 888 "Y888888 888 "Y88888 888 888 "Y888888 "Y888

👷 Welcome to Hardhat v2.22.1 👷‍

? What do you want to do? …
❯ Create a JavaScript project
Create a TypeScript project
Create a TypeScript project (with Viem)
Create an empty hardhat.config.js
Quit

3. Press enter to select the option Create a Javascript project. Then you will get the following questions (keep pressing enter to keep the default configuration):

✔ What do you want to do? · Create a JavaScript project
✔ Hardhat project root: · /Users/.../hardhat-from-scratch
✔ Do you want to add a .gitignore? (Y/n) · y
✔ Do you want to install this sample project's dependencies with npm (hardhat @nomicfoundation/hardhat-toolbox)? (Y/n) · y

This will create a Hardhat project and install the plugin hardhat-toolbox, which is the first plugin to be installed. It helps to put all the Hardhat project together. After the installation is finished, we’ll have the following content in our folder hardhat-from-scratch:

contracts
ignition
node_modules
test
.gitignore
hardhat.config.js
package-lock.json
package.json
README.md

Project

To better understand all the Hardhat intricacies, we are going to build a Public Sale contract that will use most of the Hardhat features.

This contracts either receives Ether or USDC for purchasing BBites Tokens. By giving 1 USDC you will receive 10,000 BBites tokens. Chainlink Price Data Feed is used in order to calculate the exchange rate from Ether to USD. We are going to assume that 1 USD is the same as 1 USDC. Typically they differ by a very small difference.

Within the contracts folder create two files called PublicSale.sol and BBitesToken.sol.

Paste the following code in PublicSale.sol:

// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.0.0
pragma solidity 0.8.24;

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";
import {BBitesToken} from "./BBitesToken.sol";
import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol";

contract PublicSale is Ownable {
// 1 USDC === 10,000 BBites tokens
// 10 ** 12: decimals difference between BBitokens (18) and USDC (6)
uint256 public constant EXCHANGE_RATE_USDC_TO_TOKEN = 10_000 * 10 ** 12;

// Time frame for closing Public Sale
uint256 public TIME_FRAME = block.timestamp + 10 days;

// Tokens
BBitesToken bbitesToken;
IERC20 usdcToken;

uint256 totalBBitesSold;

// Chainlink: consults exchange ratio between ETH and USD
AggregatorV3Interface aggregator;

modifier whileOpen() {
require(block.timestamp <= TIME_FRAME, "Public Sale ended");
_;
}

event TokenPurchaseEther(
uint256 etherAmount,
address indexed purchaser,
uint256 indexed amount
);
event TokenPurchaseUSDC(
uint256 usdcAmount,
address indexed purchaser,
uint256 indexed amount
);

constructor(
address _owner,
address _ethUsdcPriceFeed,
address _bbitesTokenAddress,
address _usdcTokenAddress
) Ownable(_owner) {
aggregator = AggregatorV3Interface(_ethUsdcPriceFeed);
bbitesToken = BBitesToken(_bbitesTokenAddress);
usdcToken = IERC20(_usdcTokenAddress);
}

function purchaseTokenWithEther() public payable whileOpen {
uint256 _usdcAmount = _getUsdcFromEther(msg.value);
uint256 bbitesTokenQ = _transferBBitesTokens(_usdcAmount);

emit TokenPurchaseEther(msg.value, msg.sender, bbitesTokenQ);
}

function purchaseTokenWithUSDC(uint256 _usdcAmount) public whileOpen {
usdcToken.transferFrom(msg.sender, address(this), _usdcAmount);

uint256 bbitesTokenQ = _transferBBitesTokens(_usdcAmount);

emit TokenPurchaseUSDC(_usdcAmount, msg.sender, bbitesTokenQ);
}

function _transferBBitesTokens(
uint256 _usdcAmount
) internal returns (uint256) {
uint256 _bbitesTokenQ = _usdcAmount * EXCHANGE_RATE_USDC_TO_TOKEN;
totalBBitesSold += _bbitesTokenQ;

bbitesToken.mint(msg.sender, _bbitesTokenQ);

return _bbitesTokenQ;
}

function _getUsdcFromEther(
uint256 _etherAmount
) internal view returns (uint256 _usdcAmount) {
(, int256 ans, , , ) = aggregator.latestRoundData();
uint8 decimals = aggregator.decimals();

_usdcAmount =
(_etherAmount * uint256(ans) * 10 ** 6) /
10 ** decimals /
10 ** 18;
}
}

Paste the following code in BBitesToken.sol:

// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.0.0
pragma solidity 0.8.24;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";

contract BBitesToken is ERC20, AccessControl {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");

constructor(
address defaultAdmin,
address minter
) ERC20("BBites Token", "MTK") {
_grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin);
_grantRole(MINTER_ROLE, minter);
}

function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) {
_mint(to, amount);
}
}

Install any necessary library starting with these:

$ npm install --save-dev @chainlink/contracts @openzeppelin/contracts dotenv @nomicfoundation/hardhat-ignition-ethers

Now let’s explore several use cases with Hardhat:

1. Deploy the two smart contracts a to a local node

We need to start by creating an ignition module. For that, create a file called PublicSale.js at ignition/modules/PublicSale.js. Thanks to it the smart contracts will be deployed.

Within the file PublicSale.js, let’s put the following:

  • Let’s import the method buildModule from the library hardhat-ignition like this:
const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules");

This method will help us to create an ignition module in a declarative way. Later we’ll be able to fire this module from the terminal.

  • The method buildModule is called with two arguments: the name of the module and a callback that has the m argument. m stands for module ans has several useful properties. Let’s call this module DeployPublicSale (module ID). Note that the output of buildModule is exported out. Let’s see:
module.exports = buildModule("DeployPublicSale", (m) => {
// ...
});
  • Within the callback we’ll write the deployment process in a declarative way. We have two smart contracts that are PublicSale and BBitesToken: they need to be deployed. For that we do this:
module.exports = buildModule("DeployPublicSale", (m) => {
const bbitesToken = m.contract("BBitesToken", [
/** constructor args */
]);
const publicSale = m.contract("PublicSale", [
/** constructor args */
]);

return { bbitesToken, publicSale };
});

By using the m object from the callback we use its contract method in order to register two contracts for deployment. Then we return the outputs so that other modules will be able to use them. We are only missing the constructor arguments.

  • The first contract BBitesToken requires two arguments that are the defaultAdmin and minter. Arbitrarily they will be the same. By using m.getAccount(0); we retrieve the first account from the testing accounts given by the local hardhat node. Let’s see it in code:
module.exports = buildModule("DeployPublicSale", (m) => {
const owner = m.getAccount(0);
const bbitesToken = m.contract("BBitesToken", [owner, owner]);

// ...
});
  • Since we are going to interact with Ethereum and Mainnet (later), we need two different addresses for both Chainlink Price Data Feed and USDC token. It is possible to define only variables within the ignition module and then override them later by using a parameters file. We’ll do that since our target chain (testnet or mainnet) could change. First let’s create two files within the ignition folder called paramsEthereum.json and paramsSepolia.json. Then structure as follows:

paramsEthereum.json

{
"DeployPublicSale": {
"priceFeedAddress": "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419",
"usdcAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
}
}

paramsSepolia.json

{
"DeployPublicSale": {
"priceFeedAddress": "0x694AA1769357215DE4FAC081bf1f309aDC325306",
"usdcAddress": "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238"
}
}
  • Now it’s time to define variables that could be overridden by using a parameters file.
module.exports = buildModule("DeployPublicSale", (m) => {
// ...
const bbitesToken = m.contract("BBitesToken", [owner, owner]);

const priceFeedAddress = m.getParameter("priceFeedAddress");
const usdcAddress = m.getParameter("usdcAddress");
// ...
});

Now the value of priceFeedAddress and usdcAddress will change depending on which parameter file we are using. That gives us a lot of flexibility. It is time to populate the constructor arguments for PublicSale smart contract.

  • PublicSale requires four inputs: the owner of the smart contract, the Chainlink Price Data Feed (ETH / USD) address, the BBites Token address and the USDC Token address. Also, at the end of the module, let’s return both smart contracts. That output could be used later by other modules if required. Let’s see it in code:
module.exports = buildModule("DeployPublicSale", (m) => {
const owner = m.getAccount(0);
const bbitesToken = m.contract("BBitesToken", [owner, owner]);

const priceFeedAddress = m.getParameter("priceFeedAddress");
const usdcAddress = m.getParameter("usdcAddress");

const publicSale = m.contract("PublicSale", [
owner,
priceFeedAddress,
bbitesToken,
usdcAddress,
]);

return { bbitesToken, publicSale };
});
  • The BBitesToken smart contract has protected its mint method by the role MINTER_ROLE. That means that before the PublicSale is able to call mint, the BBitesToken smart contract must grant the role MINTER_ROLE to PublicSale.
function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) ...

We can add that instruction in a declarative way within the ignition module like this:

const getRole = (role) => ethers.keccak256(ethers.toUtf8Bytes(role));
const MINTER_ROLE = getRole("MINTER_ROLE");

module.exports = buildModule("DeployPublicSale", (m) => {
// ...

m.call(bbitesToken, "grantRole", [MINTER_ROLE, publicSale]);

return { bbitesToken, publicSale };
});

First note that we should get the MINTER_ROLE expressed in 32 bytes (bytes32 data type) because that is the input for the method grantRole. We achieve that by using the methods keccak256 and toUtf8Bytes from ethers.

Second, by using m.call, the grantRole method from bbitesToken contract will be fired with the arguments MINTER_ROLE and publicSale, giving the MINTER_ROLE to the Public Sale smart contract.

  • It is time to launch a local hardhat node. The ignition module will be run against this node later. Open another terminal and run the following command to launch a node:
$ npx hardhat node

You will see something like this:

Started HTTP and WebSocket JSON-RPC server at http://127.0.0.1:8545/

Accounts
========

WARNING: These accounts, and their private keys, are publicly known.
Any funds sent to them on Mainnet or any other live network WILL BE LOST.

Account #0: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 (10000 ETH)
Private Key: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

Account #1: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 (10000 ETH)
Private Key: 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d

...

Note the Account #0: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 is given by the local Hardhat node. This is the same as the one requested at m.getAccount(0) within the ignition module. This is a functional node used for deployment and transaction testing. Before publishing to testnet or mainnet, it’s a great idea to first deploy to a local node and solve any issues that could come up.

  • In another terminal we are going to run the ignition module against the local hardhat node and, at the same time, use a particular parameter file. Run the following code:
$ npx hardhat ignition deploy ignition/modules/PublicSale.js --network localhost --parameters ignition/paramsSepolia.json

Note that --network localhost means interacting with the local hardhat node; --parameters ignition/paramsSepolia.json means to pick up a particular parameter file. For now it’s not important to distinguish the parameter file because it is a local node. It will become important when we either publish against testnet or mainnet.

  • The result will be something similar to this:
Hardhat Ignition 🚀

Deploying [ DeployPublicSale ]

Batch #1
Executed DeployPublicSale#BBitesToken

Batch #2
Executed DeployPublicSale#PublicSale

Batch #3
Executed DeployPublicSale#BBitesToken.grantRole

[ DeployPublicSale ] successfully deployed 🚀

Deployed Addresses

DeployPublicSale#BBitesToken - 0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9
DeployPublicSale#PublicSale - 0x5FC8d32690cc91D4c39d9d3abcBD16989F875707

As you can see, there are three batches: Batch #1, Batch #2 and Batch #2. The reason behind this is that some contracts have dependencies on other contracts. For instance, the Public Sale smart contract depends on the address of BBites Token, which should be published first. Therefore, BBites Token is published first in Batch #1, then Public Sale smart contract is published second in Batch #2. Finally, after publishing Public Sale, then the BBites Token is able to grant him the role MINTER_ROLE.

Exercises

  • Fire several local hardhat nodes in different ports
  • Given any mnemonic (seed phrase), figure out how to provide the same 20 accounts every time the hardhat local node is launched

2. Publish and verify the smart contracts to Sepolia Network

Thanks to Hardhat we could set up different networks as targets to easily publish our smart contracts. We are going to start by finding the right RPC for each network. The RPC is a way to connect to a particular blockchain node for sending transactions, like publishing smart contracts. Then, we need to add two target chains that are Sepolia and Ethereum in the hardhat.config.js. Finally, we’ll obtain a key from Etherscan that will be used to verify smart contracts programmatically.

  • Let’s find an RPC for Sepolia and Ethereum. Chose any RPC that has low latency, and has the Score and Privacy with green checks. Arbitrarily, let’s choose https://eth-sepolia.public.blastapi.io and https://eth-pokt.nodies.app for Sepolia and Ethereum network respectively. It’s advisable to get a private RPC from companies like Alchemy, Infura or Moralis. Private RPC diminish latencies and enhance the communication between clients and nodes.

Now let’s add these RPCs to the hardhat.config.js file. Hardhat will use them when it is time to propagate the transaction object to the blockchain network. Note that we have added the property networks. Within it, we did include set ups for Sepolia and Ethereum networks. For now let’s start by pasting the RPC in the url field:

require("@nomicfoundation/hardhat-toolbox");

/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
solidity: "0.8.24",
networks: {
sepolia: {
url: "https://eth-sepolia.public.blastapi.io",
},
ethereum: {
url: "https://eth-pokt.nodies.app",
},
},
};
  • Our Ethereum address must have a positive balance of SepoliaEth for paying the gas of publishing smart contracts. Obtain SepoliaEth from any Faucet. Then extract the private key linked to that Ethereum address. This could be done through Metamask.

Note: generate a new private key only for testing purposes. DO NOT USE YOUR PERSONAL PRIVATE KEY!

Create a file called .env at the root of the project and paste the private key in that file like this:

PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

Install the library dotenv by running the following command:

$ npm install --save dotenv

At the top of your hardhat.config.js add the following line for being able to use the secret variables within this file:

require("@nomicfoundation/hardhat-toolbox");
require("@nomicfoundation/hardhat-ignition-ethers");
require("dotenv").config();

// ...

Now let Hardhat know which account should use to pay the gas for deploying transactions. For this, the accounts property (which expects an array) should be included for each set up like this:

sepolia: {
url: "https://eth-sepolia.public.blastapi.io",
accounts: [process.env.PRIVATE_KEY],
},
ethereum: {
url: "https://eth-pokt.nodies.app",
accounts: [process.env.PRIVATE_KEY],
},

More private keys could be added in the accounts array. Then, from the ignition module you will be able to retrieve particular accounts by using m.getAccount(0);, where 0 could point to another index of the accounts array.

  • Let’s run the DeployPublicSale ignition module again. This time let’s point to Sepolia network. There is no need to launch a local Hardhat node (turn it off in case it’s running). The command is the following:
$ npx hardhat ignition deploy ignition/modules/PublicSale.js --network sepolia --parameters ignition/paramsSepolia.json

Note that in this particular command --network sepolia points to the set up we just created in the hardhat.config.js file. Once the terminal process finished, a result like this will be given:

✔ Confirm deploy to network sepolia (11155111)? … yes
Hardhat Ignition 🚀

Deploying [ PublicSale ]

Batch #1
Executed DeployPublicSale#BBitesToken

Batch #2
Executed DeployPublicSale#PublicSale

Batch #3
Executed DeployPublicSale#BBitesToken.grantRole

[ PublicSale ] successfully deployed 🚀

Deployed Addresses

DeployPublicSale#BBitesToken - 0x82692Bd247fCb2587118cBAC959612289e39C09a
DeployPublicSale#PublicSale - 0x9148Eab4E1893442D7B76832f093DdeEf15A8275

Note: if the terminal command gives you the following error “Artifact bytecodes have been changed”, remove the ignition/deployments folder and run the command again.

The above output means that both smart contracts BBites Token and Public Sale are already published on the Sepolia Network. However, by checking the Contract tab we realize that they are not verify. Let’s do that now.

  • Go to Etherscan and Sign In. Create an account if you don’t have one. Go to My Profile by hovering the cursor over your name (or click here). On the left column, search for the API Keys button. Click on it. Click on Add (blue button). Put a name for your project. Then click on Create New API Key. Then copy the API Key Token in the .env file like this:
PRIVATE_KEY=9c2fb809ce707dc56b93a12c5ada46537290de968ec2822298267faeaa12af6f
API_KEY_ETHERSCAN=Q43KYKH2FRHFXT1J2PBCCFFF9JF4D4SYH6

Now we need to import this API Key within the hardhat.config.js file. Let’s add a new entry:

//...

module.exports = {
solidity: "0.8.24",
networks: {
/** ... */
},
etherscan: {
apiKey: process.env.API_KEY_ETHERSCAN,
},
};
  • In order to verify the smart contracts we published before, let’s add another piece to the terminal command: --verify. The final command looks like this:
$ npx hardhat ignition deploy ignition/modules/PublicSale.js --network sepolia --parameters ignition/paramsSepolia.json --verify

The output of that command states this:

✔ Confirm deploy to network sepolia (11155111)? … yes
[ PublicSale ] Nothing new to deploy based on previous execution stored in ./ignition/deployments/chain-11155111

Deployed Addresses

DeployPublicSale#BBitesToken - 0x82692Bd247fCb2587118cBAC959612289e39C09a
DeployPublicSale#PublicSale - 0x9148Eab4E1893442D7B76832f093DdeEf15A8275

Verifying deployed contracts

Verifying contract "contracts/BBitesToken.sol:BBitesToken" for network sepolia...
Contract contracts/BBitesToken.sol:BBitesToken already verified on network sepolia:
- https://sepolia.etherscan.io/address/0x82692Bd247fCb2587118cBAC959612289e39C09a#code

Verifying contract "contracts/PublicSale.sol:PublicSale" for network sepolia...
Contract contracts/PublicSale.sol:PublicSale already verified on network sepolia:
- https://sepolia.etherscan.io/address/0x9148Eab4E1893442D7B76832f093DdeEf15A8275#code

Note that the smart contracts were not published again. Therefore, new addresses were not created. Instead, Hardhat was able to pick up from the last step. Hardhat recognized that the same two smart contracts were published and added the verification to them. This is part of a Hardhat’s feature called journal. Let’s get back to the same address in Sepolia Etherscan: BBites Token and Public Sale. Now will see the green check ✅ on the Contract tab, which indicates that the smart contract was successfully verified.

Exercises

  • Complete the set up for publishing and verifying the smart contracts on the following target chains: Moonbase Alpha, Amoy, Fuji, Binance Smart Chain and Optimism.
  • Use the RPC service from two different providers. Choose two out of the following three: Alchemy, Moralis and Infura. Use the RPC of one provider to publish to Moonbase, Amoy and Fuji. Use another RPC from another provider to publish to Binance Smart Chain and Optimism.

3. Deploy the smart contracts using a Deployment Id and then reset its journal

  • Let’s create a deployment id for our previous deployment. This will help us to reference it later for several purposes. For instance, we could grab the addresses of the deployed smart contracts. The first time we add the deployment id, a fresh deployment process will start, creating a separate history of transactions. For that, lets add the following to our script: --deployment-id PublicSaleDepID. Here we are specifying the parameter --deployment-id called PublicSaleDepID. Now the script look like this:
$ npx hardhat ignition deploy ignition/modules/PublicSale.js --network sepolia --parameters ignition/paramsSepolia.json --verify --deployment-id PublicSaleDepID

After running this script, besides the expected output (batches, verification, etc.), a new folder will be created at ignition/deployments/PublicSaleDepID. Notice that this folder has the same name as the deployment id used.

  • Within the folder at ignition/deployments/PublicSaleDepID there is file called journal.jsonl. Let’s open it. This is relevant information regarding the deployment process. Let’s focus on something called futureId. For now let’s notice that there are two futures ids: DeployPublicSale#BBitesToken and DeployPublicSale#PublicSale. These pieces of information are useful when you would want to reset the journal steps. To wipe out the deployment information regarding a particular contract, you could execute one of the following commands:

Reset information about the smart contract PublicSale:

$ npx hardhat ignition wipe PublicSaleDepID DeployPublicSale#PublicSale

Reset information about the smart contract BBitesToken:

$ npx hardhat ignition wipe PublicSaleDepID DeployPublicSale#BBitesToken

Let’s try them now. The result will be this:

DeployPublicSale#PublicSale state has been cleared

DeployPublicSale#BBitesToken state has been cleared
  • Review again the file journal.jsonl to see the changes. For the two future ids we’ll see something like this:
{"futureId":"DeployPublicSale#PublicSale","type":"WIPE_APPLY"}
{"futureId":"DeployPublicSale#BBitesToken","type":"WIPE_APPLY"}

Meaning that the information regarding the deployment of those smart contracts has been erased.

  • Let’s run again the deployment script using the same deployment id. This will give us a fresh journal and new addresses:
$ npx hardhat ignition deploy ignition/modules/PublicSale.js --network sepolia --parameters ignition/paramsSepolia.json --verify --deployment-id PublicSaleDepID
  • The future ids from both contracts Public Sale and BBItes Token could be customize within the ignition module. The m.contract method could receive up to three arguments. The last one will be the id that will be part of the future id. This is the code:
module.exports = buildModule("DeployPublicSale", (m) => {
// ...
const bbitesToken = m.contract("BBitesToken", [owner, owner], {
id: "fiBBites",
});

//...

const publicSale = m.contract(
"PublicSale",
[owner, priceFeedAddress, bbitesToken, usdcAddress],
{ id: "fiPublicSale" }
);

// ...
});
  • Let’s deploy the ignition module again. Before that, let’s change the deployment id to something else like PublicSaleDepID2. This is important! Otherwise, if we use the previous deployment id, Hardhat will get confused:
npx hardhat ignition deploy ignition/modules/PublicSale.js --network sepolia --parameters ignition/paramsSepolia.json --verify --deployment-id PublicSaleDepID2
  • By inspecting the journal.jsonl at igntion/deployments/PublicSaleDepID2/journal.jsonl. The new future ids are DeployPublicSale#fiBBites and DeployPublicSale#fiPublicSale for the BBites Token and Public Sale contracts respectively.

Exercises

  • Create another deployment id with different names for the futures ids. In regards to the ignition module, comment the second half of it and run the deployment script against Sepolia. Then uncomment the second half and run the deployment script. You will notice how, thanks to the journal, Hardhat won’t run the first half again. It will continue only with the second half of the script.

4. Create a task that calls the approve method from BBites Token

  • Tasks are a handy way of automating particular operations within and between smart contracts. Tasks could be used by other tasks, which allows you to create complex scenarios. Before purchasing any amount of BBites tokens with USDC, a user must give approval to the Public Sale smart contract to manipulate his USDC tokens. Let’s do that in a task.
  • In the file hardhat.config.js, let’s create a task. A task receives two inputs: the name of the task and a description. Let’s see:
// ...
require("dotenv").config();

task(
"callUsdcApprove",
"Owner gives approve of USDC to Public Sale"
).setAction(async (taskArgs, hre) => {});

// ...

The task(...) method, has a property called setAction within which the operations will be defined. setAction receives a callback with two arguments: the arguments that are passed through the command line and the Hardhat Running Environment (hre). Now let’s define some logic.

  • First of all, let’s grab all the deployed address of the deployment with id PublicSaleDepID2. Notice that all deployed addresses are within the file deployed_addresses.json at ./ignition/deployments/PublicSaleDepID2/deployed_addresses.json. Also, let’s grab the future id from Public Sale contract. Combining these two we obtain the address from Public Sale contract. The code is as follows:
setAction(async (taskArgs, hre) => {
// Public Sale address
var addressList = require("./ignition/deployments/PublicSaleDepID2/deployed_addresses.json");
var futureIdPublicSale = "DeployPublicSale#fiPublicSale";
var publicSaleAddress = addressList[futureIdPublicSale];
});
  • Second, let’s create a reference of the USDC smart contract since the owner will be calling the approve method from it. To do that, we need to use a method from the ethers library called getContractAt. This last method has this signature:
function getContractAt(
abi: any[],
address: string,
signer?: ethers.Signer
): Promise<ethers.Contract>;

Therefore, we require the USDC abi, the USDC address and, optionally, the signer that will become the msg.sender when we call any method from this contract. While you can copy the entire USDC abi from Sepolia Etherscan, We’ll use a simpler abi. The signer will be retrieved from ethers.getSigners(). Let’s see:

setAction(async (taskArgs, hre) => {
// ...
var publicSaleAddress = addressList[futureIdPublicSale];

// USDC contract reference
var [owner] = await ethers.getSigners();
var usdcAddress = "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238"; // sepolia
var usdcAbi = [
"function approve(address spender, uint256 value) public returns(uint256)",
];
var usdcContract = await hre.ethers.getContractAt(
usdcAbi,
usdcAddress,
owner
);
});
  • Now that we have a reference to the USDC contract, let’s call it’s approve method using the owner as the msg.sender. The approve method expects the spender address and the amount of approval. The owner will give approval to the Public Sale contract in the amount of 10 USDC. Lets’ see that:
setAction(async (taskArgs, hre) => {
// ...
var usdcContract = await hre.ethers.getContractAt(
usdcAbi,
usdcAddress,
owner
);

var value = 10000000n; // USDC has 6 decimals
var tx = await usdcContract.approve(publicSaleAddress, value);
await tx.wait(); // wait for 1 confirmation

console.log("Transaction hash:", tx.hash);

return { usdcContract };
});
  • Time to execute this task in the terminal. Let’s keep in mind that this task should be executed against Sepolia network. It looks like this:
$ npx hardhat callUsdcApprove --network sepolia

The output of that process looks like this:

Transaction hash: 0x6683d8d24279c5f3b1ec4270fb3e2c5704a08b1cff6c041aa266fd70535f561f

That is the transaction hash of the last process.

Exercises

  • After the approve method is called within the ignition module, call the method allowance from USDC, which will helps us to double check whether the owner address has given allowance to the Public Sale smart contract. Print out the allowance given. Run the script against Sepolia again.

5. Create a task (submodule) that sets up al smart contracts

  • We can create a task where all smart contracts are set up. Then, we can call that task from another task. So far we have three smart contracts: BBites Token, Public Sale and USDC. The last one be used that much.
  • Let’s create another task within the hardhat.config.js file. Let’s start by grabbing the address of the deployed contracts from the deployment id PublicSaleDepID2. We know already the address are at deployed_addresses.json. Also, by using getContractAt, let’s create references to Public Sale, BBites Token and USDC contracts. Finally, let’s return the three smart contracts. Later this output will be used by another tasks. Let’s see:
task("setUpContracts", "Sets up all smart contracts (submodule)").setAction(
async (taskArgs, hre) => {
// addresses
var addressList = require("./ignition/deployments/PublicSaleDepID2/deployed_addresses.json");

var futureIdPublicSale = "DeployPublicSale#fiPublicSale";
var futureIdBBitesToken = "DeployPublicSale#fiBBites";

var publicSaleAddress = addressList[futureIdPublicSale];
var bbitesTokenAddress = addressList[futureIdBBitesToken];

// signer
var [owner] = await ethers.getSigners();

// contract
var publicSaleContract = await ethers.getContractAt(
"PublicSale",
publicSaleAddress,
owner
);

var bbitesContract = await ethers.getContractAt(
"BBitesToken",
bbitesTokenAddress,
owner
);

var usdcAddress = "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238"; // sepolia
var usdcAbi = [
"function approve(address spender, uint256 value) public returns(uint256)",
];
var usdcContract = await ethers.getContractAt(
usdcAbi,
usdcAddress,
owner
);

return { publicSaleContract, bbitesContract, usdcContract };
}
);
  • Now let’s run the command that will fire this task. There is not a console.log within this task so there won’t be any output. If something goes wrong, the error will come up.
$ npx hardhat setUpContracts --network sepolia

Everything wen’t fine because no error showed up.

Exercises:

  • Instead of hard coding the USDC ABI within the task, go to the USDC implementation contract and copy the entire ABI. Thi is the USDC address. However, that address is the Proxy of the USDC contract, not the address of the USDC implementation. Find the implementation contract and copy all the ABI. Create a new folder at the root of your project called externalArtifacts and a file called usdcArtifact.js within it. Paste the ABI in that file. Then import the ABI from the task.

6. Create a task where the owner purchases BBites Token with N amount of USDC (in Sepolia)

  • This step #6 assumes that the owner has given approval (#4) of USDC tokens in favor of the Public Sale smart contract. It is possible to call task #4 within task #6 if needed.
  • The token USDC used as the currency for purchasing BBites Tokens has this address: 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238. There is also a faucet for asking 10 USDC. Ask for USDC by using the same Ethereum address from which the private key you pasted in the .env file. Having a positive balance of USDC is necessary to complete the purchase.
  • Let’s use the task that publishes all smart contracts (from #5) within the task that does the purchase of BBites Tokens with USDC. This task will be called purchaseBBitesToken with description Purchases BBites Token with N amount of USDC. By using the method run from Hardhat, we can call another task by using the task name. Before doing the purchase, we need to create references of all smart contracts: this was done at setUpContracts task (#5). To do that, do the following:
task(
"purchaseBBitesToken",
"Purchases BBites Token with N amount of USDC"
).setAction(async (taskArgs, hre) => {
const { publicSaleContract, bbitesContract, usdcContract } = await run(
"setUpContracts"
);
});

Now we got references to all smart contracts.

  • The amount of USDC to be spent will be dynamic, meaning that the amount will be defined when writing the the command at the terminal (a task is able to receive inputs directly from the terminal). The variable that captures the inputs within the task is taskArgs, which is part of the callback of setAction. Let’s use .addParam for specifying the expected input by the task and its description. Let’see that in code:
task("purchaseBBitesToken", "Purchases BBites Token with N amount of USDC")
.addParam("usdcQ", "Amount of USDC to spent")
.setAction(async (taskArgs, hre) => {
const { publicSaleContract, bbitesContract, usdcContract } = await run(
"setUpContracts"
);

var { usdcQ } = taskArgs;
console.log("USDC quantity", BigInt(usdcQ));
});
  • Hardhat provides helpful information about any task and its expected parameters. Let’s ask for help in regards to previous task:
$ npx hardhat help purchaseBBitesToken

The output looks like this:

Hardhat version 2.22.4

Usage: hardhat [GLOBAL OPTIONS] purchaseBBitesToken --usdc-q <STRING>

OPTIONS:

--usdc-q Amount of USDC to spent

purchaseBBitesToken: Purchases BBites Token with N amount of USDC

For global options help run: hardhat help

According to this information, in order to pass the expected parameter to the task purchaseBBitesToken, it is required to use --usdc-q followed by the amount of USDC. Let’s do that next.

  • For the sake of testing, let’s run the task, including its additional parameter required. Considering the information from help, let’s build the terminal command specifying the amount of USDC. For that, follow this:
$ npx hardhat purchaseBBitesToken --usdc-q 100000000000000000000000  --network sepolia

Remember that anything pass through the command line as a parameter is passed as string. Therefore, in order to convert a string to an appropriate number that a smart contract could use, the parameter should be converted to BigInt data type. Then, the output is this. Notice the n at the end, showing that it is in fact a BigInt:

USDC quantity 100000000000000000000000n

That proves that everything is working fine so far. Only remember that the USDC token has 6 decimals.

  • Now it’s time to call the method purchaseTokenWithUSDC from the PublicSale smart contract. That method receives a uint256 argument. This is its signature:
function purchaseTokenWithUSDC(uint256 _usdcAmount) public ...

With this method, we give up USDC tokens in exchange of BBites tokens. By using the method balanceOf from the BBites token and USDC token, let’s corroborate what happens with the balances from both tokens. For this last piece to work, let’s add first the method balanceOf to the USDC’s abi within the task setUpContracts.

var usdcAbi = [
"function approve(address spender, uint256 value) public returns(uint256)",
"function balanceOf(address account) external view returns (uint256)",
];

Now let’s complete the purchase at purchaseBBitesToken task:

task("purchaseBBitesToken", "Purchases BBites Token with N amount of USDC")
.addParam("usdcQ", "Amount of USDC to spent")
.setAction(async (taskArgs, hre) => {
// ...
console.log("USDC quantity", BigInt(usdcQ));

// purchase
var tx = await publicSaleContract.purchaseTokenWithUSDC(BigInt(usdcQ));
await tx.wait(); // wait for one confirmation
console.log("Sepolia transaction hash", tx.hash);

// balances
var [owner] = await hre.ethers.getSigners();

var bbitesBalance = await bbitesContract.balanceOf(owner.address);
console.log(
"BBites Tokens Balance",
hre.ethers.formatUnits(bbitesBalance, 18)
);

var usdcBalance = await usdcContract.balanceOf(owner.address);
console.log(
"USDC Tokens Balance",
hre.ethers.formatUnits(usdcBalance, 6)
);
});

Notice how helpful is the method hre.ethers.formatUnits. It formats the output taking into consideration the amount of decimals of each token. BBites tokens has 18 decimals and USDC 6 decimals.

  • Let’s run this task from the terminal passing 1 USDC (with 6 decimals). Use the following command:
$  npx hardhat purchaseBBitesToken --usdc-q 1000000 --network sepolia

The output obtained is this:

USDC quantity 1000000n
BBites Tokens Balance 10000.0
USDC Tokens Balance 9.0

That means the USDC tokens from the owner has decreased in 1 USDC. As a result, the owner has obtain 1000 BBites tokens, which means the purchase was successful.

Exercises:

  • Create another task called purchaseBBitesTokenWithEther. This tasks gives up Ether in exchange of USDC. The amount of Ether is passed as a parameter in the command line. Make sure to capture this value from within the task. Output the BBites balance before and after to make sure the exchange actually happened. Run this in Sepolia network.
  • Create a new task called transferAndPurchase. Add another private key within the array accounts of Sepolia set up network (hardhat.config.js). Let’s call this signer alice and import it by using hre.ethers.getSigners(); within transferAndPurchase task. Make the owner transfer 5 USDC to alice. Make alice purchase BBites tokens by using 5 USDC. Figure out if other tasks could be used to simplify this purchase.

7. Fork Ethereum Mainnet in a local Hardhat node and publish the smart contracts

  • Forking a network means to replicate the state of a particular network (starting a particular block) within a local Hardhat node. All smart contracts (belonging to that forked network) and their state become available for interaction. This is useful for simulating certain transactions against the local node as if those transactions were made against the forked network (e.g. Mainnet). As a result, the speed of transactions increases and funds do not get spend. Also, Hardhat offers features such funding a particular signer with Ether, impersonating another wealthy accounts, mining blocks and manipulating the timestamp. These features help you create richer scenarios.
  • In order to fork a network starting at the most recent mined block, we need to get a RPC from either Alchemy or Infura. Go to either site and obtain a RPC that points to Ethereum Mainnet. In Alchemy, after you sign up, click con Create new app. For Chain choose Ethereum and for Network choose Ethereum Mainnet. Type Hardhat from scratch as the Name for the project. Then click on Create app. To get the RPC link click on API Key. A pop up will appear. Copy the value at HTTPS, which is the link for your RPC node. Save this link for later. Mine looks like this:
https://eth-mainnet.g.alchemy.com/v2/LTBnYT9QU2ckIEK9AxI3xYIy4jrlK5Bw
  • Now let’s run a local Hardhat node that forks Ethereum Mainnet. Fire the following command line in a terminal:
$ npx hardhat node --fork https://eth-mainnet.g.alchemy.com/v2/LTBnYT9QU2ckIEK9AxI3xYIy4jrlK5Bw

The output will be something like this:

Started HTTP and WebSocket JSON-RPC server at http://127.0.0.1:8545/

Accounts
========

WARNING: These accounts, and their private keys, are publicly known.
Any funds sent to them on Mainnet or any other live network WILL BE LOST.

Account #0: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 (10000 ETH)
Private Key: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

...

Notice how this node is running at a particular port in our machine: 8545. Let’s create a another set up in hardhat.config.js. This will allows us to make transactions to the local Hardhat node, which in turn will interact with a copy of Ethereum Mainnet.

  • Open hardhat.config.js and add another entry to networks. When we run the ignition module, the --network value will be fork:
module.exports = {
//...
networks: {
// ...
fork: {
url: "http://127.0.0.1:8545",
},
},
// ...
};
  • Open another terminal. Remember that we are running the PublicSale ignition module against the local Hardhat node that is running at the fork set up. Also, use the appropriate params file called ignition/paramsEthereum.json because we are in mainnet. Let’s use another --deployment-id called PublicSaleFork for this process. This is the command:
$ npx hardhat ignition deploy ignition/modules/PublicSale.js --network fork --parameters ignition/paramsEthereum.json --deployment-id PublicSaleFork
  • The outputs are quite clear. The terminal that runs the node shows something like this:
eth_getBlockByNumber
eth_chainId
eth_maxPriorityFeePerGas - Method not supported
eth_estimateGas
eth_call
Contract deployment: BBitesToken
Contract address: 0x776d6996c8180838dc0587ae0de5d614b1350f37
From: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266

Those commands starting with eth_ are instructions for publishing the smart contracts against a network.

The terminal where we run the ignition module, shows something like this:

Hardhat Ignition 🚀

Deploying [ DeployPublicSale ]

Batch #1
Executed DeployPublicSale#fiBBites

Batch #2
Executed DeployPublicSale#fiPublicSale

Batch #3
Executed DeployPublicSale#fiBBites.grantRole

[ DeployPublicSale ] successfully deployed 🚀

Deployed Addresses

DeployPublicSale#fiBBites - 0x776D6996c8180838dC0587aE0DE5D614b1350f37
DeployPublicSale#fiPublicSale - 0x3A906C603F080D96dc08f81CF2889dAB6FF299dE

Also notice that there is another folder created at ignition/deployments/PublicSaleFork that contains information about this very last deployment process.

  • While the local Hardhat node is still alive, the changes in the state made within those contracts will remain. Once the local node is shut down, the state is gone. To shut down the local node press Ctrl + C.

Exercises:

  • Fork any other available Mainnet (Polygon, Binance Smart Chain, Moonbean, etc.) and repeat the process from above. The only limitation is given by the RPC providers because they do not always offer Mainnet RPCs.

8. Develop a Hardhat script for purchasing BBites token against a Mainnet Ethereum fork with account impersonation

  • The following interaction could be done by using tasks in the same way we did it before. However, this time let’s cover another feature from Hardhat: scripts. Scripts also allow the creation of complex scenarios in the same way tasks do it. Only that scripts are less customizable, more specific and not reusable. On other side, Hardhat tasks are more flexible and can be reused. Tasks also take parameters and are composed with other tasks. Tasks are ideal for integration with CI/CD pipelines.
  • (If not done) Open a terminal and run a fork of Ethereum Mainnet in a local node by running the following command:
$ npx hardhat node --fork https://eth-mainnet.g.alchemy.com/v2/LTBnYT9QU2ckIEK9AxI3xYIy4jrlK5Bw
  • (if not done) In another terminal, run the ignition module that helps to publish the smart contracts against Hardhat local node. Run the following command:
$ npx hardhat ignition deploy ignition/modules/PublicSale.js --network fork --parameters ignition/paramsEthereum.json --deployment-id PublicSaleFork

This will create a file at ignition/deployment/PublicSaleFork/deployed_addresses.json with the smart contract addresses. Looks like this:

{
"DeployPublicSale#fiBBites": "0x776D6996c8180838dC0587aE0DE5D614b1350f37",
"DeployPublicSale#fiPublicSale": "0x3A906C603F080D96dc08f81CF2889dAB6FF299dE"
}

These smart contracts have been published in a Ethereum Mainnet fork.

  • Create folder at the root of the project called scripts and add a file called purchaseBBitesTokens.js. Let’s start with the script skeleton:
const { ethers } = require("hardhat");

async function main() {
console.log("This is a hardhat script!");
}

main().catch((error) => {
console.error(error);
process.exitCode = 1;
});

Basically we define a function and then we execute it. Note the catch method. This helps to output any error during the method execution. exitCode = 1, in the context of Node Js, means that something went wrong

  • Now let’s fire this script from the terminal. If you add --network fork to this script, you will be running it against the local node:
npx hardhat run scripts/purchaseBBitesTokens.js --network fork

The output is this:

This is a hardhat script!
  • Since we are dealing with a replica of Mainnet Ethereum, in order to purchase BBites tokens with USDC, it is required to have a positive balance of USDC. As we know, there is not a USDC faucet in Mainnet. Instead, we could pretend that we are someone else who has lots of USDC. This feature in Hardhat is called impersonation. It helps to impersonate another accounts from which we do not have their private key and still be able to sign transactions like them. Let’s take a look at the list of USDC holders in Ethereum Mainnet. Let’s choose the address at the top that is 0x4B16c5dE96EB2117bBE5fd171E4d203624B014aa and has 1 billion USDC (USDC whale). Also, let’s connect with the contract USDC at mainnet using the impersonated account. Then, let’s transfer 1 USDC to our account to test it:
const { ethers } = require("hardhat");

async function main() {
// Hardhat account impersonation
var usdcWhale = "0x4B16c5dE96EB2117bBE5fd171E4d203624B014aa";
await hre.network.provider.request({
method: "hardhat_impersonateAccount",
params: [usdcWhale],
});
const impersonatedSigner = await ethers.getSigner(usdcWhale);

// USDC Mainnet contract
var usdcAddress = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
var usdcAbi = [
"function approve(address spender, uint256 value) public returns(uint256)",
"function balanceOf(address account) external view returns (uint256)",
"function transfer(address recipient, uint256 amount) public returns (bool)",
];
var usdcContract = await ethers.getContractAt(
usdcAbi,
usdcAddress,
impersonatedSigner
);

// USDC transfer to our account
var [owner] = await hre.ethers.getSigners();
await usdcContract.transfer(owner.address, 1000000n);
var balance = await usdcContract.balanceOf(owner.address);
console.log("Our USDC balance is", balance);
}

main().catch((error) => {
console.error(error);
process.exitCode = 1;
});

Now let’s run this task against the fork at the local Hardhat node with this:

$ npx hardhat run scripts/purchaseBBitesTokens.js --network fork

Output:

Our USDC balance is 1000000n

Which proves that effectively the USDC whale was impersonated since our account has received 1 USDC from the whale.

  • Let’s proceed with the rest of the script that purchase BBites token with USDC tokens. Let’s set up the required smart contracts at the top. Then let’s do the approval: the whale gives allowance to the Public Sale smart contract. Next, the whale calls the Public Sale contract an purchases BBites token by using USDC. Finally, let’s calculate the whale’s balance of BBites tokens. Let’s do that in code:
async function main() {
// Hardhat account impersonation
var usdcWhale = "0x4B16c5dE96EB2117bBE5fd171E4d203624B014aa";
await hre.network.provider.request({
method: "hardhat_impersonateAccount",
params: [usdcWhale],
});
const impersonatedSigner = await hre.ethers.getSigner(usdcWhale);

// Smart Contracts
// USDC
var usdcAddress = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
var usdcAbi = [
"function approve(address spender, uint256 value) public returns(uint256)",
"function balanceOf(address account) external view returns (uint256)",
"function transfer(address recipient, uint256 amount) public returns (bool)",
];
var usdcContract = await hre.ethers.getContractAt(
usdcAbi,
usdcAddress,
impersonatedSigner
);

// BBites
var [owner] = await hre.ethers.getSigners();
var bbitesTokenAddress = "0x776D6996c8180838dC0587aE0DE5D614b1350f37";
var bbitesContract = await ethers.getContractAt(
"BBitesToken",
bbitesTokenAddress,
impersonatedSigner
);

// Public Sale
var publicSaleAddress = "0x3A906C603F080D96dc08f81CF2889dAB6FF299dE";
var publicSaleContract = await ethers.getContractAt(
"PublicSale",
publicSaleAddress,
impersonatedSigner
);

// Approval
var tx;
const TEN_USDC = 10000000n;
tx = await usdcContract.approve(publicSaleAddress, TEN_USDC);
await tx.wait();

// Purchase
tx = await publicSaleContract.purchaseTokenWithUSDC(TEN_USDC);
await tx.wait();

// Balance
var balance = await bbitesContract.balanceOf(usdcWhale);
console.log("Balance BBites Token", hre.ethers.formatUnits(balance, 18));
}

Let’s fire the Hardhat script against the forked node by executing the following command:

$ npx hardhat run scripts/purchaseBBitesTokens.js --network fork

The output is as follows:

Balance BBites Token 100000.0

And that demonstrates that everything went fine.

Exercises:

  • Do the script from above by using tasks instead of scripts
  • Do the script from above by using a ignition module instead of scripts

--

--

Lee Marreros

Senior Blockchain Developer | Speaker | Chainlink Developer Expert