Learn how to write, test and deploy smart contracts to Avalanche's C-Chain with Hardhat.
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 Hardhat.
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.
Edit the ExampleERC20.sol contract in contracts/. ExampleERC20.sol is an Open ZeppelinERC20 contract. ERC20 is a popular smart contract interface. You can also add your own contracts.
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.
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.htmlconst 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.
Note
The port in this tutorial uses 9650. Depending on how you start your local network, it could be different.
Prints a list of accounts and their corresponding AVAX balances on the local Avalanche Network Runner network.
npx hardhat balances --network local0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC has balance 500000000000000000000000000x9632a79656af553F58738B0FB750320158495942 has balance 00x55ee05dF718f1a5C1441e76190EB1a19eE2C9430 has balance 00x4Cf2eD3665F6bFA95cE6A11CFDb7A2EF5FC1C7E4 has balance 00x0B891dB1901D4875056896f28B6665083935C7A8 has balance 00x01F253bE2EBF0bd64649FA468bF7b95ca933BDe2 has balance 00x78A23300E04FB5d5D2820E23cc679738982e1fd5 has balance 00x3C7daE394BBf8e9EE1359ad14C1C47003bD06293 has balance 00x61e0B3CD93F36847Abbd5d40d6F00a8eC6f3cfFB has balance 00x0Fa8EA536Be85F32724D57A37758761B86416123 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.
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.
# replace execPath with the path to AvalancheGo on your machine# e.g., ${HOME}/go/src/github.com/ava-labs/avalanchego/build/avalanchegoAVALANCHEGO_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.
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-quickstartyarn fund-cchain-addresses
Confirm each of the accounts are funded with 1000 AVAX.
yarn balances --network local# outputyarn run v1.22.4npx hardhat balances --network local0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC has balance 500000010000000000000000000x9632a79656af553F58738B0FB750320158495942 has balance 10000000000000000000x55ee05dF718f1a5C1441e76190EB1a19eE2C9430 has balance 10000000000000000000x4Cf2eD3665F6bFA95cE6A11CFDb7A2EF5FC1C7E4 has balance 10000000000000000000x0B891dB1901D4875056896f28B6665083935C7A8 has balance 10000000000000000000x01F253bE2EBF0bd64649FA468bF7b95ca933BDe2 has balance 10000000000000000000x78A23300E04FB5d5D2820E23cc679738982e1fd5 has balance 10000000000000000000x3C7daE394BBf8e9EE1359ad14C1C47003bD06293 has balance 10000000000000000000x61e0B3CD93F36847Abbd5d40d6F00a8eC6f3cfFB has balance 10000000000000000000x0Fa8EA536Be85F32724D57A37758761B86416123 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# outputyarn run v1.22.4npx hardhat run scripts/sendAvaWalletSigner.ts --network localSeeding addresses with AVAX✨ Done in 1.33s.
Confirm that the balances are updated
yarn balances --network local# outputyarn run v1.22.4npx hardhat balances --network local0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC has balance 499999999952750000000000000x9632a79656af553F58738B0FB750320158495942 has balance 10000100000000000000000x55ee05dF718f1a5C1441e76190EB1a19eE2C9430 has balance 10000100000000000000000x4Cf2eD3665F6bFA95cE6A11CFDb7A2EF5FC1C7E4 has balance 10000100000000000000000x0B891dB1901D4875056896f28B6665083935C7A8 has balance 10000100000000000000000x01F253bE2EBF0bd64649FA468bF7b95ca933BDe2 has balance 10000100000000000000000x78A23300E04FB5d5D2820E23cc679738982e1fd5 has balance 10000100000000000000000x3C7daE394BBf8e9EE1359ad14C1C47003bD06293 has balance 10000100000000000000000x61e0B3CD93F36847Abbd5d40d6F00a8eC6f3cfFB has balance 10000100000000000000000x0Fa8EA536Be85F32724D57A37758761B86416123 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# next commandavalanche-network-runner control uris \--log-level debug \--endpoint="0.0.0.0:8080"
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# outputyarn run v1.22.4npx hardhat run scripts/deploy.ts --network localCoin deployed to: 0x17aB05351fC94a1a67Bf3f56DdbB941aE6✨ Done in 1.28s.
We now have a token deployed at 0x17aB05351fC94a1a67Bf3f56DdbB941aE6.
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# outputyarn run v1.22.11npx hardhat console --network localWelcome 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:
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.
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.
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().
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].
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.