Skip to content

Using Hardhat to Deploy To Moonbeam

Introduction

Hardhat is a flexible and extensible Ethereum development environment that streamlines the smart contract development process. Since Moonbeam is Ethereum-compatible, you can use Hardhat to develop and deploy smart contracts on Moonbeam.

Hardhat takes a task-based approach to development, where you can define and execute tasks that perform specific actions. These actions include compiling and deploying contracts, running tests, and more. Tasks are highly configurable, so you can create, customize, and execute tasks that are tailored to meet your needs.

You can also extend Hardhat's functionality through the use of plugins. Plugins are external extensions that integrate with Hardhat to provide additional features and tools for your workflow. For example, there are plugins for common Ethereum libraries, like Ethers.js and viem, a plugin that extends the Chai assertion library to include Ethereum-specific functionality, and more. All of these plugins can be used to extend your Hardhat project on Moonbeam.

This guide will provide a brief introduction to Hardhat and show you 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 a Moonbeam development node.

Please note that although Hardhat comes with a Hardhat Network component, which provides a local development environment, you should use a local Moonbeam development node instead. You can connect a Moonbeam development node to Hardhat just like you would with any other network.

Checking Prerequisites

To get started, you will need the following:

Create 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:

  1. Create a directory for your project

    mkdir hardhat && cd hardhat
    
  2. Initialize the project, which will create a package.json file

    npm init -y
    
  3. Install Hardhat

    npm install hardhat
    
  4. Create a Hardhat project

    npx hardhat init
    

    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.

  5. 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, which will create a Hardhat configuration file for your project

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.2 πŸ‘·β€
 What do you want to do? …   Create a JavaScript project   Create a TypeScript project   Create a TypeScript project (with Viem)   Quit

Hardhat Configuration File

The Hardhat configuration file is the entry point into your Hardhat project. It defines various settings and options for your Hardhat project, such as the Solidity compiler version to use and the networks you can deploy your contracts to.

To start, your hardhat.config.js should resemble the following:

/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
  solidity: '0.8.20',
};

For this example, you can leave the Solidity compiler version to 0.8.20; however, if you are using a different contract that requires a newer version, don't forget to update the version here.

Next, you'll need to modify your configuration file to add the network configurations for the network you want to deploy your contract to. For Moonbeam networks, you'll need to specify the following:

  • url - the RPC endpoint of the node
  • chainId - the chain ID, which is used to validate the network
  • accounts - the accounts that can be used to deploy and interact with contracts. You can either enter an array of the private keys for your accounts or use an HD Wallet

For this example, the network will be Moonbase Alpha, but you can modify the configuration to use any of the Moonbeam networks:

module.exports = {
  solidity: '0.8.20',
  networks: {
    moonbeam: {
      url: 'INSERT_RPC_API_ENDPOINT', // Insert your RPC URL here
      chainId: 1284, // (hex: 0x504),
      accounts: ['INSERT_PRIVATE_KEY'],
    },
  },
};
module.exports = {
  solidity: '0.8.20',
  networks: {
    moonriver: {
      url: 'INSERT_RPC_API_ENDPOINT', // Insert your RPC URL here
      chainId: 1285, // (hex: 0x505),
      accounts: ['INSERT_PRIVATE_KEY'],
    },
  },
};
module.exports = {
  solidity: '0.8.20',
  networks: {
    moonbase: {
      url: 'https://rpc.api.moonbase.moonbeam.network', // Insert your RPC URL here
      chainId: 1287, // (hex: 0x507),
      accounts: ['INSERT_PRIVATE_KEY'],
    },
  },
};
module.exports = {
  solidity: '0.8.20',
  networks: {
    dev: {
      url: 'http://127.0.0.1:9944', // Insert your RPC URL here
      chainId: 1281, // (hex: 0x501),
      accounts: ['INSERT_PRIVATE_KEY'],
    },
  },
};

Remember

This is for demo purposes only. Never store your private key in a JavaScript file.

If you are planning on using any plugins with your project, you'll need to install the plugin and import it into the hardhat.config.js file. Once a plugin has been imported, it becomes part of the Hardhat Runtime Environment, and you can leverage the plugin's functionality within tasks, scripts, and more.

For this example, you can install the hardhat-ethers plugin and import it into the configuration file. This plugin provides a convenient way to use the Ethers.js library to interact with the network.

npm install @nomicfoundation/hardhat-ethers ethers

Additionally, you'll need to install the hardhat-ignition-ethers plugin to enable deployment of smart contracts with Hardhat Ignition. You can install it with the following command:

npm install --save-dev @nomicfoundation/hardhat-ignition-ethers

To import both plugins, add the following require statements to the top of the Hardhat configuration file:

/** @type import('hardhat/config').HardhatUserConfig */
require('@nomicfoundation/hardhat-ethers');
require('@nomicfoundation/hardhat-ignition-ethers');

const privateKey = 'INSERT_PRIVATE_KEY';

module.exports = {
  solidity: '0.8.20',
  networks: {
    moonbase: {
      url: 'https://rpc.api.moonbase.moonbeam.network',
      chainId: 1287, // 0x507 in hex,
      accounts: [privateKey]
    }
  }
};

For more information on the available configuration options, please refer to Hardhat's documentation on Configuration.

The Contract File

Now that you've configured your project, you can begin the development process by creating your smart contract. The contract will be a simple one that will let you store a value that can be retrieved later, called Box.

To add the contract, you'll take the following steps:

  1. Create a contracts directory

    mkdir contracts
    
  2. Create a Box.sol file

    touch contracts/Box.sol
    
  3. Open the file and add the following contract to it:

    // contracts/Box.sol
    // SPDX-License-Identifier: MIT
    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;
        }
    }
    

Compile the Contract

The next step is to compile the Box.sol smart contract. For this, you can use the built-in compile task, which will look for Solidity files in the contracts directory and compile them using the version and compiler settings defined in the hardhat.config.js file.

To use the compile task, all you have to do is run:

npx hardhat compile
npx hardhat compile Compiled 1 Solidity files successfully (evm target: paris). ls -l artifacts cache contracts hardhat.config.js node_modules package.json package-lock.json

After compilation, an artifacts directory is created that holds the bytecode and metadata of the contract, which are .json files. It’s a good idea to add this directory to a .gitignore file.

If you make changes to the contract after you've compiled it, you can compile it again using the same command. Hardhat will look for any changes and recompile the contract. If no changes are found, nothing will be compiled. If needed, you can force a compilation using the clean task, which will clear the cache and delete the old artifacts.

Deploy the Contract

To deploy the contract, you'll use Hardhat Ignition, a declarative framework for deploying smart contracts. Hardhat Ignition is designed to make it easy to manage recurring tasks surrounding smart contract deployment and testing. For more information, be sure to check out the Hardhat Ignition docs.

To set up the proper file structure for your Ignition module, create a folder named ignition and a subdirectory called modules. Then add a new file to it called Box.js. You can take all three of these steps with the following command:

mkdir ignition ignition/modules && touch ignition/modules/Box.js

Next, you can write your Hardhat Ignition module. To get started, take the following steps:

  1. Import the buildModule function from the Hardhat Ignition module
  2. Export a module using buildModule
  3. Use the getAccount method to select the deployer account
  4. Deploy the Box contract
  5. Return an object from the module. This makes the Box contract accessible for interaction in Hardhat tests and scripts
// 1.  Import the `buildModule` function from the Hardhat Ignition module
const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules");

// 2. Export a module using `buildModule`
module.exports = buildModule("BoxModule", (m) => {

  // 3. Use the `getAccount` method to select the deployer account
  const deployer = m.getAccount(0);

  // 4. Deploy the `Box` contract
  const box = m.contract("Box", [], {
    from: deployer,
  });

  // 5. Return an object from the module 
  return { box };
});

To run the script and deploy the Box.sol contract, use the following command, which requires you to specify the network name as defined in your hardhat.config.js. If you don't specify a network, hardhat will deploy the contract to a local hardhat network by default.

npx hardhat ignition deploy ./ignition/modules/Box.js --network moonbase

Note

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 file.

You'll be prompted to confirm the network you wish to deploy to. After a few seconds after you confirm, the contract is deployed, and you'll see the contract address in the terminal.

npx hardhat ignition deploy ./ignition/modules/Box.js --network moonbase
βœ… Confirm deploy to network moonbase (1287)? … yes Hardhat Ignition πŸš€
Deploying [ BoxModule ]
Batch #1 Executed BoxModule#Box
[ BoxModule ] successfully deployed πŸš€
Deployed Addresses
BoxModule#Box - 0xfBD78CE8C9E1169851119754C4Ea2f70AB159289

Congratulations, your contract is live! Save the address, as you will use it to interact with this contract instance in the next step.

Interact with the Contract

There are a couple of ways that you can interact with your newly deployed contract using Hardhat: you can use the console task, which spins up an interactive JavaScript console, or you can create another script and use the run task to execute it.

Using the Hardhat Console

The Hardhat console uses the same execution environment as the tasks and scripts, so it automatically uses the configurations and plugins defined in the hardhat.config.js.

To launch the Hardhat console, you can run:

npx hardhat console --network moonbase

Next, you can take the following steps, entering one line at a time:

  1. Create a local instance of the Box.sol contract

    const Box = await ethers.getContractFactory('Box');
    
  2. Connect the local instance to the deployed contract, using the address of the contract

    const box = await Box.attach('0xfBD78CE8C9E1169851119754C4Ea2f70AB159289');
    
  3. Interact with the attached contract. For this example, you can call the store method and store a simple value

    await box.store(5);
    

The transaction will be signed by your account configured in the hardhat.config.js file and broadcasted to the network. The output should look similar to:

npx hardhat console --network moonbase
Welcome to Node.js v20.9.0. Type ".help" for more information. const Box = await ethers.getContractFactory('Box'); undefined
const box = await Box.attach('0xfBD78CE8C9E1169851119754C4Ea2f70AB159289'); undefined
await box.store(5); ContractTransactionResponse {
provider: HardhatEthersProvider { ... },
blockNumber: null,
blockHash: null,
index: undefined,
hash: '0x1c49a64a601fc5dd184f0a368a91130cb49203ec0f533c6fcf20445c68e20264',
type: 2,
to: '0xa84caB60db6541573a091e5C622fB79e175E17be',
from: '0x3B939FeaD1557C741Ff06492FD0127bd287A421e',
nonce: 87,
gasLimit: 45881n,
gasPrice: 1107421875n,
maxPriorityFeePerGas: 1n,
maxFeePerGas: 1107421875n,
data: '0x6057361d0000000000000000000000000000000000000000000000000000000000000005',
value: 0n,
chainId: 5678n,
signature: Signature { r: "0x9233b9cc4ae6879b7e08b9f1a4bfb175c8216eee0099966eca4a305c7f369ecc", s: "0x7663688633006b5a449d02cb08311569fadf2f9696bd7fe65417860a3b5fc57d", yParity: 0, networkV: null },
accessList: [],
blobVersionedHashes: null
}
await box.retrieve(); 5n

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 initially stored.

Using a Script

Similarly to the deployment script, you can create a script to interact with your deployed contract, store it in the scripts directory, and run it using the built-in run task.

To get started, create a set-value.js file in the scripts directory:

mkdir scripts && touch scripts/set-value.js

Now paste the following contract into the set-value.js file:

// scripts/set-value.js
async function main() {
  // Create instance of the Box contract
  const Box = await ethers.getContractFactory('Box');

  // Connect the instance to the deployed contract
  const box = await Box.attach('0xfBD78CE8C9E1169851119754C4Ea2f70AB159289');

  // Store a new value
  await box.store(2);

  // Retrieve the value
  const value = await box.retrieve();
  console.log(`The new value is: ${value}`);
}

main()
  .then(() => process.exit(0))
  .catch(error => {
    console.error(error);
    process.exit(1);
  });

To run the script, you can use the following command:

npx hardhat run --network moonbase scripts/set-value.js

The script should return 2 as the value.

npx hardhat run --network moonbase scripts/set-value.js
The new value is: 2

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 or 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 an 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:

Private Key: Oxdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97 Account #9: Oxa0Ee7A142d267C1f36714E4a8F75612F20a79720 (10000 ETH) Private Key: 0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6 Account #10: OxBcd4042DE499D14e55001CcbB24a551F3b954096 (10000 ETH) Private Key: Oxf214f2b2cd398c806f84e317254e0f0b801d0643303237d97a22a48e01628897 Account #11: 0x71bE63f3384f5fb98995898A86B02Fb2426c5788 (10000 ETH) Private Key: 0x701b615bbdfb9de65240bc28bd21bbc0d996645a3dd57e7b12bc2bdf6f192c82 Account #12: OxFABBOac9d68B0B445fB7357272F202C5651694a (10000 ETH) Private Key: Oxa267530f49f8280200edf313ee7af6b827f2a8bce2897751d06a843f644967b1 Account #13: 0x1CBd3b2770909D4e10f157cABC84C7264073C9Ec (10000 ETH) Private Key: 0x47c99abed3324a2707c28affff1267e45918ec8c3f20b8aa892e8b065d2942dd Account #14: OxdF3e18d64BC6A983f673Ab319CCaE4f1a5707097 (10000 ETH) Private Key: Oxc526ee95bf44d8fc405a158bb884d9d1238d990612e9f33d006bb0789009aaa Account #15: Oxcd3B766CCDd6AE721141F452C550Ca635964ce71 (10000 ETH) Private Key: 0x8166f546bab6da521a8369cab06c5d2b9e46670292d85c875ee9ec20e84ffb61 Account #16: 0Γ—2546BcD3c84621e976D8185a91A922aE77ECEc30 (10000 ETH) Private Key: Oxea6c44ac03bff858b476bba40716402b03e41b8e97e276d1baec7c37d42484a0 Account #17: OxbDA5747bFD65F08deb54cb465eB87D40e51B197E (10000 ETH) Private Key: 0x689af8efa8c651a91ad287602527f3af2fe9f6501a7ac4b06166765a93e037fd Account #18: OxdD2FD4581271e230360230F9337D5c0430Bf44C0 (10000 ETH) Private Key: Oxde9be858da4a475276426320d5e9262ecfc3ba460bfac56360bfa6c4c28b4ee0 Account #19: 0Γ—8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199 (10000 ETH) Private Key: Oxdf57089febbacf7ba0bc227dafbffa9fc08a93fdc68e1e42411a14efcf23656e 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.

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.JsonRpcProvider(
    '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;
});
The information presented herein has been provided by third parties and is made available solely for general information purposes. Moonbeam does not endorse any project listed and described on the Moonbeam Doc Website (https://docs.moonbeam.network/). Moonbeam Foundation does not warrant the accuracy, completeness or usefulness of this information. Any reliance you place on such information is strictly at your own risk. Moonbeam Foundation disclaims all liability and responsibility arising from any reliance placed on this information by you or by anyone who may be informed of any of its contents. All statements and/or opinions expressed in these materials are solely the responsibility of the person or entity providing those materials and do not necessarily represent the opinion of Moonbeam Foundation. The information should not be construed as professional or financial advice of any kind. Advice from a suitably qualified professional should always be sought in relation to any particular matter or circumstance. The information herein may link to or integrate with other websites operated or content provided by third parties, and such other websites may link to this website. Moonbeam Foundation has no control over any such other websites or their content and will have no liability arising out of or related to such websites or their content. The existence of any such link does not constitute an endorsement of such websites, the content of the websites, or the operators of the websites. These links are being provided to you only as a convenience and you release and hold Moonbeam Foundation harmless from any and all liability arising from your use of this information or the information provided by any third-party website or service.
Last update: October 2, 2024
| Created: March 1, 2022