Using Hardhat to Deploy To Moonbeam¶
Introduction¶
Hardhat is an Ethereum development environment that helps developers manage and automate the recurring tasks inherent to building smart contracts and DApps. Hardhat can directly interact with Moonbeam's Ethereum API so it can also be used to deploy smart contracts into Moonbeam.
This guide will cover how to use Hardhat to compile, deploy, and debug Ethereum smart contracts on the Moonbase Alpha TestNet. This guide can also be adapted for Moonbeam, Moonriver, or Moonbeam development node.
Checking Prerequisites¶
To get started, you will need the following:
- Have MetaMask installed and connected to Moonbase Alpha
- Have an account with funds. You can get DEV tokens for testing on Moonbase Alpha once every 24 hours from the Moonbase Alpha Faucet
- To test out the examples in this guide on Moonbeam or Moonriver, you will need to have your own endpoint and API key, which you can get from one of the supported Endpoint Providers
Creating a Hardhat Project¶
You will need to create a Hardhat project if you don't already have one. You can create one by completing the following steps:
-
Create a directory for your project
mkdir hardhat && cd hardhat
-
Initialize the project which will create a
package.json
filenpm init -y
-
Install Hardhat
npm install hardhat
-
Create a project
npx hardhat
Note
npx
is used to run executables installed locally in your project. Although Hardhat can be installed globally, it is recommended to install it locally in each project so that you can control the version on a project by project basis. -
A menu will appear which will allow you to create a new project or use a sample project. For this example, you can choose Create an empty hardhat.config.js
This will create a Hardhat config file (hardhat.config.js
) in your project directory.
Once you have your Hardhat project, you can also install the Ethers plugin. This provides a convenient way to use the Ethers.js library to interact with the network. To install it, run the following command:
npm install @nomicfoundation/hardhat-ethers ethers@6
The Contract File¶
With your empty project created, next you are going to create a contracts
directory. You can do so by running the following command:
mkdir contracts && cd contracts
The smart contract that you'll deploy as an example will be called Box
, it will let you store a value that can be retrieved later. In the contracts
directory, you can create the Box.sol
file:
touch Box.sol
Open the file and add the following contract to it:
// contracts/Box.sol
pragma solidity ^0.8.1;
contract Box {
uint256 private value;
// Emitted when the stored value changes
event ValueChanged(uint256 newValue);
// Stores a new value in the contract
function store(uint256 newValue) public {
value = newValue;
emit ValueChanged(newValue);
}
// Reads the last stored value
function retrieve() public view returns (uint256) {
return value;
}
}
Hardhat Configuration File¶
Before you can deploy the contract to Moonbase Alpha, you'll need to modify the Hardhat configuration file and create a secure file to store your private key in.
Next you can take the following steps to modify the hardhat.config.js
file and add Moonbase Alpha as a network:
- Import plugins
-
Create a variable for your private key(s)
Remember
This is for demo purposes only. Never store your private key in a JavaScript file.
-
Inside the
module.exports
, you need to provide the Solidity version -
Add the Moonbase Alpha network configuration. You can modify the
hardhat.config.js
file to use any of the Moonbeam networks:moonbeam: { url: 'INSERT_RPC_API_ENDPOINT', // Insert your RPC URL here chainId: 1284, // (hex: 0x504), accounts: [privateKey] },
moonriver: { url: 'INSERT_RPC_API_ENDPOINT', // Insert your RPC URL here chainId: 1285, // (hex: 0x505), accounts: [privateKey] },
moonbase: { url: 'https://rpc.api.moonbase.moonbeam.network', chainId: 1287, // (hex: 0x507), accounts: [privateKey] },
dev: { url: 'http://127.0.0.1:9944', chainId: 1281, // (hex: 0x501), accounts: [privateKey] },
// 1. Import the Ethers plugin required to interact with the contract
require('@nomicfoundation/hardhat-ethers');
// 2. Add your private key from your pre-funded Moonbase Alpha testing account
const privateKey = 'INSERT_PRIVATE_KEY';
module.exports = {
// 3. Specify the Solidity version
solidity: '0.8.1',
networks: {
// 4. Add the Moonbase Alpha network specification
moonbase: {
url: 'https://rpc.api.moonbase.moonbeam.network',
chainId: 1287, // 0x507 in hex,
accounts: [privateKey]
}
}
};
Congratulations! You are now ready for deployment!
Compiling Solidity¶
To compile the contract you can simply run:
npx hardhat compile
After compilation, an artifacts
directory is created: it holds the bytecode and metadata of the contract, which are .json
files. It’s a good idea to add this directory to your .gitignore
.
Deploying the Contract¶
In order to deploy the Box.sol
smart contract, you will need to write a simple deployment script. You can create a new directory for the script and name it scripts
and add a new file to it called deploy.js
:
mkdir scripts && cd scripts
touch deploy.js
Next, you need to write your deployment script which can be done using ethers
. Because you'll be running it with Hardhat, you don't need to import any libraries.
To get started, take the following steps:
- Create a local instance of the contract with the
getContractFactory
method - Use the
deploy
method that exists within this instance to instantiate the smart contract - Wait for the deployment by using
deployed
- Once deployed, you can fetch the address of the contract using the contract instance.
// scripts/deploy.js
async function main() {
// 1. Get the contract to deploy
const Box = await ethers.getContractFactory('Box');
console.log('Deploying Box...');
// 2. Instantiating a new Box smart contract
const box = await Box.deploy();
// 3. Waiting for the deployment to resolve
await box.waitForDeployment();
// 4. Use the contract instance to get the contract address
console.log('Box deployed to:', box.target);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
You can now deploy the Box.sol
contract using the run
command and specifying moonbase
as the network:
npx hardhat run --network moonbase scripts/deploy.js
If you're using another Moonbeam network, make sure that you specify the correct network. The network name needs to match how it's defined in the hardhat.config.js
.
After a few seconds, the contract is deployed, and you should see the address in the terminal.
Congratulations, your contract is live! Save the address, as you will use it to interact with this contract instance in the next step.
Interacting with the Contract¶
To interact with your newly deployed contract on Moonbase Alpha, you can launch the Hardhat console
by running:
npx hardhat console --network moonbase
Next you can take the following steps, entering in one line at a time:
-
Create a local instance of the
Box.sol
contractconst Box = await ethers.getContractFactory('Box');
-
Connect the local instance to the deployed contract, using the address of the contract
const box = await Box.attach('0x425668350bD782D80D457d5F9bc7782A24B8c2ef');
-
Interact with the attached contract. For this example, you can call the
store
method and store a simple valueawait box.store(5);
The transaction will be signed by your Moonbase account and be broadcasted to the network. The output should look similar to:
Notice your address labeled from
, the address of the contract, and the data
that is being passed. Now, you can retrieve the value by running:
await box.retrieve();
You should see 5
or the value you have stored initially.
Congratulations, you have successfully deployed and interacted with a contract using Hardhat!
Hardhat Forking¶
You can fork any EVM compatible chain using Hardhat, including Moonbeam. Forking simulates the live Moonbeam network locally, enabling you to interact with deployed contracts on Moonbeam in a local test environment. Since Hardhat forking is based on an EVM implementation, you can interact with the fork using standard Ethereum JSON-RPC methods supported by Moonbeam and Hardhat.
There are some limitations to be aware of when using Hardhat forking. You cannot interact with any of the Moonbeam precompiled contracts and their functions. Precompiles are a part of the Substrate implementation and therefore cannot be replicated in the simulated EVM environment. This prohibits you from interacting with cross-chain assets on Moonbeam and Substrate-based functionality such as staking and governance.
There is currently an issue related to forking Moonbeam, so in order to fix the issue you'll need to manually patch Hardhat first. You can find out more information by following the issue on GitHub as well as the related PR.
Patching Hardhat¶
Before getting started, you'll need to apply a temporary patch to workaround an RPC error until Hardhat fixes the root issue. The error is as follows:
Error HH604: Error running JSON-RPC server: Invalid JSON-RPC response's result.
Errors: Invalid value null supplied to : RpcBlockWithTransactions | null/transactions: RpcTransaction Array/0: RpcTransaction/accessList: Array<{ address: DATA, storageKeys: Array<DATA> | null }> | undefined, Invalid value null supplied to : RpcBlockWithTransactions | null/transactions: RpcTransaction Array/1: RpcTransaction/accessList: Array<{ address: DATA, storageKeys: Array<DATA> | null }> | undefined, Invalid value null supplied to : RpcBlockWithTransactions | null/transactions: RpcTransaction Array/2: RpcTransaction/accessList: Array<{ address: DATA, storageKeys: Array<DATA> | null }> | undefined
To patch Hardhat, you'll need to open the node_modules/hardhat/internal/hardhat-network/jsonrpc/client.js
file of your project. Next, you'll add a addAccessList
function and update the _perform
and _performBatch
functions.
To get started, you can remove the preexisting _perform
and _performBatch
functions and in their place add the following code snippet:
addAccessList(method, rawResult) {
if (
method.startsWith('eth_getBlock') &&
rawResult &&
rawResult.transactions?.length
) {
rawResult.transactions.forEach((t) => {
if (t.accessList == null) t.accessList = [];
});
}
}
async _perform(method, params, tType, getMaxAffectedBlockNumber) {
const cacheKey = this._getCacheKey(method, params);
const cachedResult = this._getFromCache(cacheKey);
if (cachedResult !== undefined) {
return cachedResult;
}
if (this._forkCachePath !== undefined) {
const diskCachedResult = await this._getFromDiskCache(
this._forkCachePath,
cacheKey,
tType
);
if (diskCachedResult !== undefined) {
this._storeInCache(cacheKey, diskCachedResult);
return diskCachedResult;
}
}
const rawResult = await this._send(method, params);
this.addAccessList(method, rawResult);
const decodedResult = (0, decodeJsonRpcResponse_1.decodeJsonRpcResponse)(
rawResult,
tType
);
const blockNumber = getMaxAffectedBlockNumber(decodedResult);
if (this._canBeCached(blockNumber)) {
this._storeInCache(cacheKey, decodedResult);
if (this._forkCachePath !== undefined) {
await this._storeInDiskCache(this._forkCachePath, cacheKey, rawResult);
}
}
return decodedResult;
}
async _performBatch(batch, getMaxAffectedBlockNumber) {
// Perform Batch caches the entire batch at once.
// It could implement something more clever, like caching per request
// but it's only used in one place, and those other requests aren't
// used anywhere else.
const cacheKey = this._getBatchCacheKey(batch);
const cachedResult = this._getFromCache(cacheKey);
if (cachedResult !== undefined) {
return cachedResult;
}
if (this._forkCachePath !== undefined) {
const diskCachedResult = await this._getBatchFromDiskCache(
this._forkCachePath,
cacheKey,
batch.map((b) => b.tType)
);
if (diskCachedResult !== undefined) {
this._storeInCache(cacheKey, diskCachedResult);
return diskCachedResult;
}
}
const rawResults = await this._sendBatch(batch);
const decodedResults = rawResults.map((result, i) => {
this.addAccessList(batch[i].method, result);
return (0, decodeJsonRpcResponse_1.decodeJsonRpcResponse)(
result,
batch[i].tType
);
});
const blockNumber = getMaxAffectedBlockNumber(decodedResults);
if (this._canBeCached(blockNumber)) {
this._storeInCache(cacheKey, decodedResults);
if (this._forkCachePath !== undefined) {
await this._storeInDiskCache(this._forkCachePath, cacheKey, rawResults);
}
}
return decodedResults;
}
Then you can use patch-package to automatically patch the package by running the following command:
npx patch-package hardhat
A patches
directory will be created and now you should be all set to fork Moonbeam without running into any errors.
Forking Moonbeam¶
You can fork Moonbeam from the command line or configure your Hardhat project to always run the fork from your hardhat.config.js
file. To fork Moonbeam or Moonriver, you will need to have your own endpoint and API key which you can get from one of the supported Endpoint Providers.
To fork Moonbeam from the command line, you can run the following command from within your Hardhat project directory:
npx hardhat node --fork INSERT_RPC_API_ENDPOINT
npx hardhat node --fork INSERT_RPC_API_ENDPOINT
npx hardhat node --fork https://rpc.api.moonbase.moonbeam.network
If you prefer to configure your Hardhat project, you can update your hardhat.config.js
file with the following configurations:
...
networks: {
hardhat: {
forking: {
url: 'INSERT_RPC_API_ENDPOINT',
}
}
}
...
...
networks: {
hardhat: {
forking: {
url: 'INSERT_RPC_API_ENDPOINT',
}
}
}
...
...
networks: {
hardhat: {
forking: {
url: 'https://rpc.api.moonbase.moonbeam.network',
}
}
}
...
When you spin up the Hardhat fork, you'll have 20 development accounts that are pre-funded with 10,000 test tokens. The forked instance is available at http://127.0.0.1:8545/
. The output in your terminal should resemble the following:
To verify you have forked the network, you can query the latest block number:
curl --data '{"method":"eth_blockNumber","params":[],"id":1,"jsonrpc":"2.0"}' -H "Content-Type: application/json" -X POST localhost:8545
If you convert the result
from hex to decimal, you should get the latest block number from the time you forked the network. You can cross reference the block number using a block explorer.
From here you can deploy new contracts to your forked instance of Moonbeam or interact with contracts already deployed by creating a local instance of the deployed contract.
To interact with an already deployed contract, you can create a new script in the scripts
directory using ethers
. Because you'll be running it with Hardhat, you don't need to import any libraries. Inside the script, you can access a live contract on the network using the following snippet:
const hre = require('hardhat');
async function main() {
const provider = new ethers.providers.StaticJsonRpcProvider('http://127.0.0.1:8545/');
const contract = new ethers.Contract(
'INSERT_CONTRACT_ADDRESS', 'INSERT_CONTRACT_ABI', provider
);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
| Created: March 1, 2022