Hardhat¶
-
Test and Deploy with Hardhat
Master Solidity smart contract development with Hardhat. Learn testing, deployment, and network interaction in one comprehensive tutorial.
Overview¶
Hardhat is a robust development environment for EVM-compatible chains that makes smart contract development more efficient. This guide walks you through the essentials of using Hardhat to create, compile, test, and deploy smart contracts on Asset Hub.
Prerequisites¶
Before getting started, ensure you have:
- Node.js (v16.0.0 or later) and npm installed
- Basic understanding of Solidity programming
- Some WND test tokens to cover transaction fees (easily obtainable from the Polkadot faucet). To learn how to get test tokens, check out the Test Tokens section
- MetaMask installed and connected to Westend Asset Hub. For more detailed instructions on connecting your wallet, see the Connect Your Wallet section
Setting Up Hardhat¶
-
Create a new directory for your project and navigate into it:
-
Initialize a new npm project:
-
Install Hardhat and the required plugins:
To interact with Asset Hub, Hardhat requires the
hardhat-resolc
plugin to compile contracts to PolkaVM bytecode and thehardhat-revive-node
plugin to spawn a local node compatible with PolkaVM. -
Create a Hardhat project:
Select "Create a JavaScript project" when prompted and follow the instructions. After that, your project will be created with three main folders:
contracts
- where your Solidity smart contracts livetest
- contains your test files that validate contract functionalityignition
- deployment modules for safely deploying your contracts to various networks
-
Update your Hardhat configuration file (
hardhat.config.js
) to include the plugins:hardhat.config.jsrequire('@nomicfoundation/hardhat-toolbox'); require('@nomicfoundation/hardhat-network-helpers'); require('@nomicfoundation/hardhat-chai-matchers'); require('@nomicfoundation/hardhat-ethers'); require('hardhat-gas-reporter'); require('@nomicfoundation/hardhat-ignition'); require('hardhat-resolc'); require('hardhat-revive-node'); /** @type import('hardhat/config').HardhatUserConfig */ module.exports = { // Additional configuration will be added later };
Compiling Your Contract¶
The hardhat-resolc
plugin will compile your Solidity contracts for Solidity versions 0.8.0
and higher to be PolkaVM compatible. When compiling your contract using the hardhat-resolc
plugin, there are two ways to configure your compilation process:
- Remix compiler - uses the Remix online compiler backend for simplicity and ease of use
- Binary compiler - uses the resolc binary directly for more control and configuration options
To compile your project, follow these instructions:
-
Modify your Hardhat configuration file to specify which compilation process you will be using:
hardhat.config.jsrequire('@nomicfoundation/hardhat-toolbox'); require('@nomicfoundation/hardhat-network-helpers'); require('@nomicfoundation/hardhat-chai-matchers'); require('@nomicfoundation/hardhat-ethers'); require('hardhat-gas-reporter'); require('@nomicfoundation/hardhat-ignition'); require('hardhat-resolc'); require('hardhat-revive-node'); /** @type import('hardhat/config').HardhatUserConfig */ module.exports = { solidity: '0.8.28', resolc: { version: '1.5.2', compilerSource: 'remix', settings: { optimizer: { enabled: false, runs: 600, }, evmVersion: 'istanbul', }, }, networks: { hardhat: { polkavm: true, }, };
hardhat.config.jsrequire('@nomicfoundation/hardhat-toolbox'); require('@nomicfoundation/hardhat-network-helpers'); require('@nomicfoundation/hardhat-chai-matchers'); require('@nomicfoundation/hardhat-ethers'); require('hardhat-gas-reporter'); require('@nomicfoundation/hardhat-ignition'); require('hardhat-resolc'); require('hardhat-revive-node'); /** @type import('hardhat/config').HardhatUserConfig */ module.exports = { solidity: '0.8.28', resolc: { compilerSource: 'binary', settings: { optimizer: { enabled: true, runs: 400, }, evmVersion: 'istanbul', compilerPath: 'INSERT_PATH_TO_RESOLC_COMPILER', standardJson: true, }, }, networks: { hardhat: { polkavm: true, }, };
For the binary configuration, replace
INSERT_PATH_TO_RESOLC_COMPILER
with the proper path to the binary. For more information about its installation, check the installation section of thepallet-revive
. -
Compile the contract with Hardhat:
-
After successful compilation, you'll see the artifacts generated in the
artifacts-pvm
directory:You should see JSON files containing the contract ABI and bytecode of the contracts you compiled.
Testing Your Contract¶
When testing your contract, be aware that @nomicfoundation/hardhat-toolbox/network-helpers
is not fully compatible with Asset Hub's available RPCs. Specifically, Hardhat-only helpers like time
and loadFixture
may not work due to missing RPC calls in the node. For more details, refer to the Compatibility section in the hardhat-revive
docs.
You should avoid using helpers like time
and loadFixture
when writing tests. For example, for the Lock.sol
contract, you can replace the default test file under tests/Lock.js
with the following logic:
const { expect } = require('chai');
const { ethers } = require('hardhat');
describe('Lock', function () {
let owner, otherAccount;
beforeEach(async function () {
[owner, otherAccount] = await ethers.getSigners();
});
const FAR_FUTURE_TIME = 10 ** 13;
const ONE_GWEI = 1_000_000_000;
it('Should deploy successfully with future unlock time', async function () {
// Deploy with a timestamp far in the future
const Lock = await ethers.getContractFactory('Lock');
const lock = await Lock.deploy(FAR_FUTURE_TIME, { value: ONE_GWEI });
// Verify the contract was deployed successfully
expect(await lock.unlockTime()).to.equal(FAR_FUTURE_TIME);
expect(await lock.owner()).to.equal(owner.address);
expect(await ethers.provider.getBalance(lock.target)).to.equal(ONE_GWEI);
});
it('Should fail if unlock time is not in the future', async function () {
// Use a timestamp in the past
const PAST_TIME = Math.floor(Date.now() / 1000) - 1000;
const Lock = await ethers.getContractFactory('Lock');
// This should be reverted due to the past timestamp
await expect(
Lock.deploy(PAST_TIME, { value: ONE_GWEI })
).to.be.revertedWith('Unlock time should be in the future');
});
it('Should not allow non-owners to withdraw', async function () {
// Deploy with a future timestamp
const Lock = await ethers.getContractFactory('Lock');
const lock = await Lock.deploy(FAR_FUTURE_TIME, { value: ONE_GWEI });
// Try to withdraw as non-owner
await expect(lock.connect(otherAccount).withdraw()).to.be.revertedWith(
"You can't withdraw yet"
);
});
});
To run your test, execute the following command:
Deploying with a Local Node¶
Before deploying to a live network, you can deploy your contract to a local node using the hardhat-revive-node
plugin and Ignition modules:
Contract Size Limitation in Testing Environment
When testing or deploying large contracts in Hardhat's local environment, you may encounter this error:
Error: the initcode size of this transaction is too large
This limitation is established by Hardhat based on Ethereum's default contract size limits. While Hardhat can disable this limitation, technical constraints currently prevent it from being applied to the PolkaVM test environment.
-
First, ensure you have compiled a Substrate node and the ETH RPC adapter from the Polkadot SDK. Checkout the compatible commit from the SDK and build the node and the ETH-RPC from source:
git clone https://github.com/paritytech/polkadot-sdk.git cd polkadot-sdk git checkout c29e72a8628835e34deb6aa7db9a78a2e4eabcee
Now, build the node and the ETH-RPC adapter. Note that this process might take a long time to complete:
-
Update the Hardhat configuration file to add the local node as a target for local deployment:
hardhat.config.jsrequire('@nomicfoundation/hardhat-toolbox'); require('@nomicfoundation/hardhat-network-helpers'); require('@nomicfoundation/hardhat-chai-matchers'); require('@nomicfoundation/hardhat-ethers'); require('hardhat-gas-reporter'); require('@nomicfoundation/hardhat-ignition'); require('hardhat-resolc'); require('hardhat-revive-node'); /** @type import('hardhat/config').HardhatUserConfig */ module.exports = { solidity: '0.8.28', // Remix Compiler resolc: { version: '1.5.2', compilerSource: 'remix', settings: { optimizer: { enabled: false, runs: 600, }, evmVersion: 'istanbul', }, }, networks: { hardhat: { polkavm: true, nodeConfig: { nodeBinaryPath: 'INSERT_PATH_TO_SUBSTRATE_NODE', rpcPort: 8000, dev: true, }, adapterConfig: { adapterBinaryPath: 'INSERT_PATH_TO_ETH_RPC_ADAPTER', dev: true, }, }, localNode: { polkavm: true, url: `http://127.0.0.1:8545`, }, }, };
hardhat.config.jsrequire('@nomicfoundation/hardhat-toolbox'); require('@nomicfoundation/hardhat-network-helpers'); require('@nomicfoundation/hardhat-chai-matchers'); require('@nomicfoundation/hardhat-ethers'); require('hardhat-gas-reporter'); require('@nomicfoundation/hardhat-ignition'); require('hardhat-resolc'); require('hardhat-revive-node'); /** @type import('hardhat/config').HardhatUserConfig */ module.exports = { solidity: '0.8.28', resolc: { compilerSource: 'binary', settings: { optimizer: { enabled: true, runs: 400, }, evmVersion: 'istanbul', compilerPath: 'INSERT_PATH_TO_RESOLC_COMPILER', standardJson: true, }, }, networks: { hardhat: { polkavm: true, nodeConfig: { nodeBinaryPath: 'INSERT_PATH_TO_SUBSTRATE_NODE', rpcPort: 8000, dev: true, }, adapterConfig: { adapterBinaryPath: 'INSERT_PATH_TO_ETH_RPC_ADAPTER', dev: true, }, }, localNode: { polkavm: true, url: `http://127.0.0.1:8545`, }, }, };
Ensure to replace
INSERT_PATH_TO_SUBSTRATE_NODE
andINSERT_PATH_TO_ETH_RPC_ADAPTER
with the proper paths to the compiled binaries. Since you compiled these from source using Rust's Cargo build system, you can find them at:- Substrate node path -
polkadot-sdk/target/release/substrate-node
- ETH-RPC adapter path -
polkadot-sdk/target/release/eth-rpc
For example, if you cloned the polkadot-sdk repository to your home directory, the paths might look like:
- Substrate node path -
-
Modify the Ignition modules, considering that the value of the pallet revive
block.timestamp
is returned in seconds. Check this PR for more information. For example, for the defaultignition/modules/Lock.js
file, the needed modification should be:Modified
ignition/modules/Lock.js
fileLock.js// This setup uses Hardhat Ignition to manage smart contract deployments. // Learn more about it at https://hardhat.org/ignition const { buildModule } = require('@nomicfoundation/hardhat-ignition/modules'); const JAN_1ST_2030 = 18934560000000; const ONE_GWEI = 1_000_000_000n; module.exports = buildModule('LockModule', (m) => { const unlockTime = m.getParameter('unlockTime', JAN_1ST_2030); const lockedAmount = m.getParameter('lockedAmount', ONE_GWEI); const lock = m.contract('Lock', [unlockTime], { value: lockedAmount, }); return { lock }; });
-
Start a local node:
This command will start a local PolkaVM node powered by the
hardhat-revive-node
plugin. -
In a new terminal window, deploy the contract using Ignition:
Replace
INSERT_IGNITION_MODULE_NAME
with the proper name for your contract. You'll see deployment information, including the contract address.
Deploying to a Live Network¶
After testing your contract locally, you can deploy it to a live network. This guide will use Westend Asset Hub as the target network. Here's how to configure and deploy:
-
Fund your deployment account with enough tokens to cover gas fees. In this case, the needed tokens are WND (on Westend). You can use the Polkadot faucet to obtain testing tokens.
-
Export your private key and save it in a
.env
file:Replace
INSERT_PRIVATE_KEY
with your actual private key. For further details on private key exportation, refer to the article How to export an account's private key.Warning
Never reveal your private key. Be sure you add the
.env
file to your .gitignore file. -
Install the
dotenv
package to load the private key into your Hardhat configuration: -
Update your config to load it:
hardhat.config.jsrequire('@nomicfoundation/hardhat-toolbox'); require('@nomicfoundation/hardhat-network-helpers'); require('@nomicfoundation/hardhat-chai-matchers'); require('@nomicfoundation/hardhat-ethers'); require('hardhat-gas-reporter'); require('@nomicfoundation/hardhat-ignition'); require('hardhat-resolc'); require('hardhat-revive-node'); require('dotenv').config(); /** @type import('hardhat/config').HardhatUserConfig */ module.exports = { // The rest remains the same... };
-
Update your Hardhat configuration file with network settings for the Asset Hub network you want to target:
hardhat.config.jsrequire('@nomicfoundation/hardhat-toolbox'); require('@nomicfoundation/hardhat-network-helpers'); require('@nomicfoundation/hardhat-chai-matchers'); require('@nomicfoundation/hardhat-ethers'); require('hardhat-gas-reporter'); require('@nomicfoundation/hardhat-ignition'); require('hardhat-resolc'); require('hardhat-revive-node'); /** @type import('hardhat/config').HardhatUserConfig */ module.exports = { solidity: '0.8.28', // Remix Compiler resolc: { version: '1.5.2', compilerSource: 'remix', settings: { optimizer: { enabled: false, runs: 600, }, evmVersion: 'istanbul', }, }, networks: { hardhat: { polkavm: true, nodeConfig: { nodeBinaryPath: 'INSERT_PATH_TO_SUBSTRATE_NODE', rpcPort: 8000, dev: true, }, adapterConfig: { adapterBinaryPath: 'INSERT_PATH_TO_ETH_RPC_ADAPTER', dev: true, }, }, localNode: { polkavm: true, url: `http://127.0.0.1:8545`, }, westendAssetHub: { polkavm: true, url: 'https://westend-asset-hub-eth-rpc.polkadot.io', accounts: [process.env.PRIVATE_KEY], }, }, };
hardhat.config.jsrequire('@nomicfoundation/hardhat-toolbox'); require('@nomicfoundation/hardhat-network-helpers'); require('@nomicfoundation/hardhat-chai-matchers'); require('@nomicfoundation/hardhat-ethers'); require('hardhat-gas-reporter'); require('@nomicfoundation/hardhat-ignition'); require('hardhat-resolc'); require('hardhat-revive-node'); /** @type import('hardhat/config').HardhatUserConfig */ module.exports = { solidity: '0.8.28', resolc: { compilerSource: 'binary', settings: { optimizer: { enabled: true, runs: 400, }, evmVersion: 'istanbul', compilerPath: 'INSERT_PATH_TO_RESOLC_COMPILER', standardJson: true, }, }, networks: { hardhat: { polkavm: true, nodeConfig: { nodeBinaryPath: 'INSERT_PATH_TO_SUBSTRATE_NODE', rpcPort: 8000, dev: true, }, adapterConfig: { adapterBinaryPath: 'INSERT_PATH_TO_ETH_RPC_ADAPTER', dev: true, }, }, localNode: { polkavm: true, url: `http://127.0.0.1:8545`, }, westendAssetHub: { polkavm: true, url: 'https://westend-asset-hub-eth-rpc.polkadot.io', accounts: [process.env.PRIVATE_KEY], }, }, };
-
Deploy your contract using Ignition:
npx hardhat ignition deploy ./ignition/modules/INSERT_IGNITION_MODULE_NAME.js --network westendAssetHub
Replace
INSERT_IGNITION_MODULE_NAME
with the proper name for your contract. You'll see deployment information, including the contract address.
Interacting with Your Contract¶
Once deployed, you can create a script to interact with your contract. To do so, create a file called scripts/interact.js
and add some logic to interact with the contract.
For example, for the default Lock.sol
contract, you can use the following file that connects to the contract at its address and retrieves the unlockTime
, which represents when funds can be withdrawn. The script converts this timestamp into a readable date and logs it. It then checks the contract's balance and displays it. Finally, it attempts to call the withdrawal function on the contract, but it catches and logs the error message if the withdrawal is not yet allowed (e.g., before unlockTime
).
const hre = require('hardhat');
async function main() {
// Get the contract instance
const Lock = await hre.ethers.getContractFactory('Lock');
const contractAddress = '0x46E0a43DC905a5b8FA9Fc4dA61774abE31a098a5';
const lock = await Lock.attach(contractAddress);
// Read contract state
const unlockTime = await lock.unlockTime();
const unlockTimestamp = Number(unlockTime);
console.log(`Unlock time: ${new Date(unlockTimestamp)}`);
const balance = await hre.ethers.provider.getBalance(lock.target);
console.log(`Contract balance: ${hre.ethers.formatEther(balance)} ETH`);
// Interact with contract (transaction)
try {
const tx = await lock.withdraw();
await tx.wait();
console.log('Withdrawal successful!');
} catch (error) {
console.error(`Error during withdrawal: ${error.message}`);
}
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Run your interaction script:
Where to Go Next¶
Hardhat provides a powerful environment for developing, testing, and deploying smart contracts on Asset Hub. Its plugin architecture allows seamless integration with PolkaVM through the hardhat-resolc
and hardhat-revive-node
plugins.
Explore more about smart contracts through these resources:
-
Guide Smart Contracts on Polkadot
Dive into advanced smart contract concepts.
-
External Hardhat Documentation
Learn more about Hardhat's advanced features and best practices.
-
External OpenZeppelin Contracts
Test your skills by deploying contracts with prebuilt templates.
| Created: April 9, 2025