Skip to main content

Using Hardhat with the Avalanche C-Chain

Introduction​

The goal of this guide is to lay out best practices regarding writing, testing and deployment of smart contracts to Avalanche's C-Chain. We'll be building smart contracts with development environment Hardhat.

Prerequisites​

NodeJS and Yarn​

First, install the LTS (long-term support) version of NodeJS. This is 18.x at the time of writing. NodeJS bundles npm.

Next, install yarn:

npm install -g yarn

AvalancheGo and Avalanche Network Runner​

AvalancheGo is an Avalanche node implementation written in Go. Avalanche Network Runner is a tool to quickly deploy local test networks. Together, you can deploy local test networks and run tests on them.

Solidity and Avalanche​

It is also helpful to have a basic understanding of Solidity and Avalanche.

Dependencies​

Clone the quickstart repository and install the necessary packages via yarn.

git clone https://github.com/ava-labs/avalanche-smart-contract-quickstart.git
cd avalanche-smart-contract-quickstart
yarn
info

The repository cloning method used is HTTPS, but SSH can be used too:

git clone [email protected]:ava-labs/avalanche-smart-contract-quickstart.git

You can find more about SSH and how to use it here.

Write Contracts​

Edit the ExampleERC20.sol contract in contracts/. ExampleERC20.sol is an Open Zeppelin ERC20 contract. ERC20 is a popular smart contract interface. You can also add your own contracts.

Hardhat Config​

Hardhat uses hardhat.config.js as the configuration file. You can define tasks, networks, compilers and more in that file. For more information see here.

Here is an example pre-configured hardhat.config.ts.

import { task } from "hardhat/config";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
import { BigNumber } from "ethers";
import "@nomiclabs/hardhat-waffle";

// When using the hardhat network, you may choose to fork Fuji or Avalanche Mainnet
// This will allow you to debug contracts using the hardhat network while keeping the current network state
// To enable forking, turn one of these booleans on, and then run your tasks/scripts using ``--network hardhat``
// For more information go to the hardhat guide
// https://hardhat.org/hardhat-network/
// https://hardhat.org/guides/mainnet-forking.html
const FORK_FUJI = false;
const FORK_MAINNET = false;
const forkingData = FORK_FUJI
? {
url: "https://api.avax-test.network/ext/bc/C/rpc",
}
: FORK_MAINNET
? {
url: "https://api.avax.network/ext/bc/C/rpc",
}
: undefined;

export default {
solidity: {
compilers: [
{
version: "0.5.16",
},
{
version: "0.6.2",
},
{
version: "0.6.4",
},
{
version: "0.7.0",
},
{
version: "0.8.0",
},
],
},
networks: {
hardhat: {
gasPrice: 225000000000,
chainId: !forkingData ? 43112 : undefined, //Only specify a chainId if we are not forking
forking: forkingData,
},
local: {
url: "http://localhost:9650/ext/bc/C/rpc",
gasPrice: 225000000000,
chainId: 43112,
accounts: [
"0x56289e99c94b6912bfc12adc093c9b51124f0dc54ac7a766b2bc5ccf558d8027",
"0x7b4198529994b0dc604278c99d153cfd069d594753d471171a1d102a10438e07",
"0x15614556be13730e9e8d6eacc1603143e7b96987429df8726384c2ec4502ef6e",
"0x31b571bf6894a248831ff937bb49f7754509fe93bbd2517c9c73c4144c0e97dc",
"0x6934bef917e01692b789da754a0eae31a8536eb465e7bff752ea291dad88c675",
"0xe700bdbdbc279b808b1ec45f8c2370e4616d3a02c336e68d85d4668e08f53cff",
"0xbbc2865b76ba28016bc2255c7504d000e046ae01934b04c694592a6276988630",
"0xcdbfd34f687ced8c6968854f8a99ae47712c4f4183b78dcc4a903d1bfe8cbf60",
"0x86f78c5416151fe3546dece84fda4b4b1e36089f2dbc48496faf3a950f16157c",
"0x750839e9dbbd2a0910efe40f50b2f3b2f2f59f5580bb4b83bd8c1201cf9a010a",
],
},
fuji: {
url: "https://api.avax-test.network/ext/bc/C/rpc",
gasPrice: 225000000000,
chainId: 43113,
accounts: [],
},
mainnet: {
url: "https://api.avax.network/ext/bc/C/rpc",
gasPrice: 225000000000,
chainId: 43114,
accounts: [],
},
},
};

This configures necessary network information to provide smooth interaction with Avalanche. There are also some pre-defined private keys for testing on a local test network.

info

The port in this tutorial uses 9650. Depending on how you start your local network, it could be different.

Hardhat Tasks​

You can define custom hardhat tasks in hardhat.config.ts. There are two tasks included as examples: accounts and balances.

task(
"accounts",
"Prints the list of accounts",
async (args, hre): Promise<void> => {
const accounts: SignerWithAddress[] = await hre.ethers.getSigners();
accounts.forEach((account: SignerWithAddress): void => {
console.log(account.address);
});
}
);

task(
"balances",
"Prints the list of AVAX account balances",
async (args, hre): Promise<void> => {
const accounts: SignerWithAddress[] = await hre.ethers.getSigners();
for (const account of accounts) {
const balance: BigNumber = await hre.ethers.provider.getBalance(
account.address
);
console.log(`${account.address} has balance ${balance.toString()}`);
}
}
);

npx hardhat accounts prints the list of accounts. npx hardhat balances prints the list of AVAX account balances. As with other yarn scripts you can pass in a --network flag to hardhat tasks.

Accounts​

Prints a list of accounts on the local Avalanche Network Runner network.

npx hardhat accounts --network local
0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC
0x9632a79656af553F58738B0FB750320158495942
0x55ee05dF718f1a5C1441e76190EB1a19eE2C9430
0x4Cf2eD3665F6bFA95cE6A11CFDb7A2EF5FC1C7E4
0x0B891dB1901D4875056896f28B6665083935C7A8
0x01F253bE2EBF0bd64649FA468bF7b95ca933BDe2
0x78A23300E04FB5d5D2820E23cc679738982e1fd5
0x3C7daE394BBf8e9EE1359ad14C1C47003bD06293
0x61e0B3CD93F36847Abbd5d40d6F00a8eC6f3cfFB
0x0Fa8EA536Be85F32724D57A37758761B86416123

Balances​

Prints a list of accounts and their corresponding AVAX balances on the local Avalanche Network Runner network.

npx hardhat balances --network local
0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC has balance 50000000000000000000000000
0x9632a79656af553F58738B0FB750320158495942 has balance 0
0x55ee05dF718f1a5C1441e76190EB1a19eE2C9430 has balance 0
0x4Cf2eD3665F6bFA95cE6A11CFDb7A2EF5FC1C7E4 has balance 0
0x0B891dB1901D4875056896f28B6665083935C7A8 has balance 0
0x01F253bE2EBF0bd64649FA468bF7b95ca933BDe2 has balance 0
0x78A23300E04FB5d5D2820E23cc679738982e1fd5 has balance 0
0x3C7daE394BBf8e9EE1359ad14C1C47003bD06293 has balance 0
0x61e0B3CD93F36847Abbd5d40d6F00a8eC6f3cfFB has balance 0
0x0Fa8EA536Be85F32724D57A37758761B86416123 has balance 0

Notice that the first account is already funded. This is because this address is pre-funded in the local network genesis file.

ERC20 Balances​

task(
"check-erc20-balance",
"Prints out the ERC20 balance of your account"
).setAction(async function (taskArguments, hre) {
const genericErc20Abi = require("./erc20.abi.json");
const tokenContractAddress = "0x...";
const provider = ethers.getDefaultProvider(
"https://api.avax.network/ext/bc/C/rpc"
);
const contract = new ethers.Contract(
tokenContractAddress,
genericErc20Abi,
provider
);
const balance = await contract.balanceOf("0x...");
console.log(`Balance in wei: ${balance}`);
});

This will return the result in wei. If you want to know the exact amount of token with its token name then you need to divide it with its decimal. erc20.abi.json can be found here.

The example uses the C-Chain Public API for the provider. For a local Avalanche network use http://127.0.0.1:9650/ext/bc/C/rpc and for Fuji Testnet use https://api.avax-test.network/ext/bc/C/rpc.

Hardhat Help​

Run yarn hardhat to list Hardhat's version, usage instructions, global options and available tasks.

Typical Avalanche Network Runner Workflow​

Run Avalanche Network Runner​

First confirm you have the latest AvalancheGo built.

cd /path/to/avalanchego
git fetch -p
git checkout master
./scripts/build.sh

(Note that you can also download pre-compiled AvalancheGo binaries rather than building from source.)

Confirm you have Avalanche Network Runner installed by following the steps listed here

Start Avalanche Network Runner and run a script to start a new local network.

Start the Server​

$ cd /path/to/Avalanche-Network-Runner
$ avalanche-network-runner server \
--log-level debug \
--port=":8080" \
--grpc-gateway-port=":8081"

Start a New Avalanche Network with Five Nodes​

# replace execPath with the path to AvalancheGo on your machine
# e.g., ${HOME}/go/src/github.com/ava-labs/avalanchego/build/avalanchego
$ AVALANCHEGO_EXEC_PATH="avalanchego"
$ avalanche-network-runner control start \
--log-level debug \
--endpoint="0.0.0.0:8080" \
--number-of-nodes=5 \
--avalanchego-path ${AVALANCHEGO_EXEC_PATH}

Now you're running a local Avalanche network with 5 nodes.

Fund Accounts​

Transfer 1,000 AVAX from the X-Chain to each of the 10 accounts in hardhat.config.ts with the script fund-cchain-addresses. Funding these accounts is a prerequisite for deploying and interacting with smart contracts.

Note: If you see Error: Invalid JSON RPC response: "API call rejected because chain is not done bootstrapping", you need to wait until network is bootstrapped and ready to use. It should not take too long.

$ cd /path/to/avalanche-smart-contract-quickstart
$ yarn fund-cchain-addresses
yarn run v1.22.4
npx hardhat run scripts/fund-cchain-addresses.js
Exporting 1000 AVAX to each address on the C-Chain...
2b75ae74ScLkWe5GVFTYJoP2EniMywkcZySQUoFGN2EJLiPDgp
Importing AVAX to the C-Chain...
2dyXcQGiCk1ckCX4Fs8nLgL8GJgsM72f9Ga13rX5v9TAguVJYM
✨ Done in 5.03s.

Confirm each of the accounts are funded with 1000 AVAX.

$ yarn balances --network local
yarn run v1.22.4
npx hardhat balances --network local
0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC has balance 50000001000000000000000000
0x9632a79656af553F58738B0FB750320158495942 has balance 1000000000000000000
0x55ee05dF718f1a5C1441e76190EB1a19eE2C9430 has balance 1000000000000000000
0x4Cf2eD3665F6bFA95cE6A11CFDb7A2EF5FC1C7E4 has balance 1000000000000000000
0x0B891dB1901D4875056896f28B6665083935C7A8 has balance 1000000000000000000
0x01F253bE2EBF0bd64649FA468bF7b95ca933BDe2 has balance 1000000000000000000
0x78A23300E04FB5d5D2820E23cc679738982e1fd5 has balance 1000000000000000000
0x3C7daE394BBf8e9EE1359ad14C1C47003bD06293 has balance 1000000000000000000
0x61e0B3CD93F36847Abbd5d40d6F00a8eC6f3cfFB has balance 1000000000000000000
0x0Fa8EA536Be85F32724D57A37758761B86416123 has balance 1000000000000000000
✨ Done in 0.72s.

Send each of the accounts some AVAX from the first account.

$ yarn send-avax-wallet-signer --network local
yarn run v1.22.4
npx hardhat run scripts/sendAvaWalletSigner.ts --network local
Seeding addresses with AVAX
✨ Done in 1.33s.

Confirm that the balances are updated

$ yarn balances --network local
yarn run v1.22.4
npx hardhat balances --network local
0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC has balance 49999999995275000000000000
0x9632a79656af553F58738B0FB750320158495942 has balance 1000010000000000000000
0x55ee05dF718f1a5C1441e76190EB1a19eE2C9430 has balance 1000010000000000000000
0x4Cf2eD3665F6bFA95cE6A11CFDb7A2EF5FC1C7E4 has balance 1000010000000000000000
0x0B891dB1901D4875056896f28B6665083935C7A8 has balance 1000010000000000000000
0x01F253bE2EBF0bd64649FA468bF7b95ca933BDe2 has balance 1000010000000000000000
0x78A23300E04FB5d5D2820E23cc679738982e1fd5 has balance 1000010000000000000000
0x3C7daE394BBf8e9EE1359ad14C1C47003bD06293 has balance 1000010000000000000000
0x61e0B3CD93F36847Abbd5d40d6F00a8eC6f3cfFB has balance 1000010000000000000000
0x0Fa8EA536Be85F32724D57A37758761B86416123 has balance 1000010000000000000000

Note: If you see Error HH108: Cannot connect to the network local. Please make sure your node is running, and check your internet connection and networks config, ensure that you are using a valid Node Port. See which ports the Nodes are using by running the command:

$ cd /path/to/avalanche-network-runner
$ avalanche-network-runner control uris \
--log-level debug \
--endpoint="0.0.0.0:8080"

Compile Smart Contracts​

In package.json there's a compile script.

"compile": "npx hardhat compile",

Run yarn compile to make sure your project compiles.

Compile the smart contract.

$ yarn compile
yarn run v1.22.4
rimraf ./build/
npx hardhat compile
Compiling 1 file with 0.6.4
Compilation finished successfully
✨ Done in 2.13s.

Deploy Smart Contracts​

Hardhat enables deploying to multiple environments. In package.json there is a script for deploying.

Edit the deployment script in scripts/deploy.ts

"deploy": "npx hardhat run scripts/deploy.ts",

You can choose which environment that you want to deploy to by passing in the --network flag with local (for example a local network created with Avalanche Network Runner), fuji, or mainnet for each respective environment. If you don't pass in --network then it will default to the hardhat network. For example, if you want to deploy to Mainnet:

yarn deploy --network mainnet

Deploy the contract to your local network

$ yarn deploy --network local
yarn run v1.22.4
npx hardhat run scripts/deploy.ts --network local
Coin deployed to: 0x17aB05351fC94a1a67Bf3f56DdbB941aE6
✨ Done in 1.28s.

We now have a token deployed at 0x17aB05351fC94a1a67Bf3f56DdbB941aE6.

Interact with Smart Contract​

Hardhat has a developer console to interact with contracts and the network. For more information about Hardhat's console see here. Hardhat console is a NodeJS-REPL, and you can use different tools in it. Ethers is the library that we'll use to interact with our network.

You can open console with:

$ yarn console --network local
yarn run v1.22.11
npx hardhat console --network local
Welcome to Node.js v16.2.0.
Type ".help" for more information.
>

Get the contract instance with factory and contract address to interact with our contract:

> const Coin = await ethers.getContractFactory('ExampleERC20');
undefined
> const coin = await Coin.attach('0x17aB05351fC94a1a67Bf3f56DdbB941aE6')
undefined

The first line retrieves contract factory with ABI & bytecode. The second line retrieves an instance of that contract factory with given contract address. Recall that our contract was already deployed to 0x17aB05351fC94a1a67Bf3f56DdbB941aE6 in the previous step.

Fetch the accounts:

> let accounts = await ethers.provider.listAccounts()
undefined
> accounts
[
'0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC',
'0x9632a79656af553F58738B0FB750320158495942',
'0x55ee05dF718f1a5C1441e76190EB1a19eE2C9430',
'0x4Cf2eD3665F6bFA95cE6A11CFDb7A2EF5FC1C7E4',
'0x0B891dB1901D4875056896f28B6665083935C7A8',
'0x01F253bE2EBF0bd64649FA468bF7b95ca933BDe2',
'0x78A23300E04FB5d5D2820E23cc679738982e1fd5',
'0x3C7daE394BBf8e9EE1359ad14C1C47003bD06293',
'0x61e0B3CD93F36847Abbd5d40d6F00a8eC6f3cfFB',
'0x0Fa8EA536Be85F32724D57A37758761B86416123'
]

This is exactly the same account list as in yarn accounts.

Now we can interact with our ERC-20 contract:

> let value = await coin.balanceOf(accounts[0])
undefined
> value.toString()
'123456789'
> value = await coin.balanceOf(accounts[1])
BigNumber { _hex: '0x00', _isBigNumber: true }
> value.toString()
'0'

account[0] has a balance because account[0] is the default account. The contract is deployed with this account. The constructor of ERC20.sol mints TOTAL_SUPPLY of 123456789 token to the deployer of the contract.

accounts[1] currently has no balance. Send some tokens to accounts[1], which is 0x9632a79656af553F58738B0FB750320158495942.

> let result = await coin.transfer(accounts[1], 100)
undefined
> result
{
hash: '0x35eec91011f9089ba7689479617a90baaf8590395b5c80bb209fa7000e4848a5',
type: 0,
accessList: null,
blockHash: null,
blockNumber: null,
transactionIndex: null,
confirmations: 0,
from: '0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC',
gasPrice: BigNumber { _hex: '0x34630b8a00', _isBigNumber: true },
gasLimit: BigNumber { _hex: '0x8754', _isBigNumber: true },
to: '0x17aB05351fC94a1a67Bf3f56DdbB941aE6c63E25',
value: BigNumber { _hex: '0x00', _isBigNumber: true },
nonce: 3,
data: '0xa9059cbb0000000000000000000000009632a79656af553f58738b0fb7503201584959420000000000000000000000000000000000000000000000000000000000000064',
r: '0xc2b9680771c092a106eadb2887e5bff41fcda166c8e00f36ae79b196bbc53d36',
s: '0x355138cb5e2b9f20c15626638750775cfc9423881db374d732a8549d05ebf601',
v: 86260,
creates: null,
chainId: 43112,
wait: [Function (anonymous)]
}

Note: Since this is a local network, we did not need to wait until transaction is accepted. However for other networks like fuji or mainnet you need to wait until transaction is accepted with: await result.wait().

Now we can ensure that tokens are transferred:

> value = await coin.balanceOf(accounts[0])
BigNumber { _hex: '0x075bccb1', _isBigNumber: true }
> value.toString()
'123456689'
> value = await coin.balanceOf(accounts[1])
BigNumber { _hex: '0x64', _isBigNumber: true }
> value.toString()
'100'

As you might noticed there was no "sender" information in await coin.transfer(accounts[1], 100); this is because ethers uses the first signer as the default signer. In our case this is account[0]. If we want to use another account we need to connect with it first.

> let signer1 = await ethers.provider.getSigner(1)
> let contractAsSigner1 = coin.connect(signer1)

Now we can call the contract with signer1, which is account[1].

> await contractAsSigner1.transfer(accounts[0], 5)
{
hash: '0x807947f1c40bb723ac312739d238b62764ae3c3387c6cdbbb6534501577382dd',
type: 0,
accessList: null,
blockHash: null,
blockNumber: null,
transactionIndex: null,
confirmations: 0,
from: '0x9632a79656af553F58738B0FB750320158495942',
gasPrice: BigNumber { _hex: '0x34630b8a00', _isBigNumber: true },
gasLimit: BigNumber { _hex: '0x8754', _isBigNumber: true },
to: '0x17aB05351fC94a1a67Bf3f56DdbB941aE6c63E25',
value: BigNumber { _hex: '0x00', _isBigNumber: true },
nonce: 2,
data: '0xa9059cbb0000000000000000000000008db97c7cece249c2b98bdc0226cc4c2a57bf52fc0000000000000000000000000000000000000000000000000000000000000005',
r: '0xcbf126dd0b109491d037c5f3af754ef2d0d7d06149082b13d0e27e502d3adc5b',
s: '0x5978521804dd15674147cc6b532b8801c4d3a0e94f41f5d7ffaced14b9262504',
v: 86259,
creates: null,
chainId: 43112,
wait: [Function (anonymous)]
}

Let's check balances now:

> value = await coin.balanceOf(accounts[0])
BigNumber { _hex: '0x075bccb6', _isBigNumber: true }
> value.toString()
'123456694'
> value = await coin.balanceOf(accounts[1])
BigNumber { _hex: '0x5f', _isBigNumber: true }
> value.toString()
'95'

We've successfully transferred 5 tokes from accounts[1] to accounts[0]

Summary​

Now you have the tools you need to launch a local Avalanche network, create a Hardhat project, as well as create, compile, deploy and interact with Solidity contracts.

Join our Discord Server to learn more and ask any questions you may have.

Was this page helpful?