🛠️ “Testing Node & Network Behavior in Smart Contracts Using Hardhat with TypeScript & Mocha”

Background knowledge:

A blockchain network consists of multiple nodes that communicate with each other. Each node participates in mining blocks to form a chain, which requires gas fees. Each block contains:

Transaction data (e.g., deposit or transfer records)Timestamp (when the block was created)A link to the previous blockTechnical information about the network state

For example, if you deposit 10 ETH, the block would record:

Your wallet address (who sent it)The contract address (who received it)The amount (10 ETH)The transaction timestampOther technical details about the transaction

In this blog, we will test a smart contract for token lending, where you can deposit ETH to borrow WETH tokens, as shown in the Solidity code below:

Why using Hardhat?

Network Simulation Capabilities// In Hardhat, we can control block mining
await network.provider.send(“evm_mine”, []); // Mine a new block

// We can control block timestamps
await network.provider.send(“evm_increaseTime”, [3600]); // Move time forward 1 hour

// We can even control gas prices
await network.provider.send(“hardhat_setNextBlockBaseFeePerGas”, [
ethers.utils.hexValue(ethers.utils.parseUnits(“100”, “gwei”))
]);

You can save and restore the entire blockchain state for test error recovery:

describe(“Error Recovery Testing”, () => {
it(“should recover from failed operations”, async function() {
// Take snapshot before risky operation
const snapshotId = await network.provider.send(“evm_snapshot”, []);

try {
// Attempt potentially failing operation
await lendingProtocol.withdraw(ethers.utils.parseEther(“100”)); // More than deposited
} catch (error) {
// Revert to clean state
await network.provider.send(“evm_revert”, [snapshotId]);

// Verify state is clean
const balance = await lendingProtocol.getUserDeposit(signer.address);
expect(balance).to.equal(0);
}
});
});Mainnet Forking

Hardhat allows us to create a copy of the real Ethereum mainnet at any point in time for testing.

// We can fork mainnet at a specific block
await network.provider.request({
method: “hardhat_reset”,
params: [{
forking: {
jsonRpcUrl: MAINNET_RPC_URL,
blockNumber: 15000000 // Specific block number
}
}]
});

// Now we can interact with real mainnet contracts
const USDC = await ethers.getContractAt(“IERC20”, “0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48”);Debugging Capabilities

We can get stack traces for failed transactions

it(“should maintain consistent performance under sustained load”, async function() {
const testUser = await setupTestUser(0, “1000”);
const userContract = lendingProtocol.connect(testUser);
const depositAmount = ethers.utils.parseEther(“0.1”);
const iterations = 3;
const results = [];

try {
// Get initial balance
const initialBalance = await userContract.getUserDeposit(testUser.address);
console.log(“Initial balance:”, ethers.utils.formatEther(initialBalance));
} catch (error) {
console.error(“Load test failed:”, error);
}

You can also use Harthat to debug Smart Contract in the console:

npx hardhat consoleTest Environment Control

With Hardhat, we have extensive control over the test environment.

// We can manipulate account balances directly
await network.provider.send(“hardhat_setBalance”, [
userAddress,
ethers.utils.hexValue(ethers.utils.parseEther(“100”))
]);

// We can impersonate any Ethereum address
await network.provider.request({
method: “hardhat_impersonateAccount”,
params: [whaleAddress],
});

Advance Hardhat feature:
We can test how our protocol handles pending transactions and mempool behavior like the code below.

describe(“Transaction Pool Behavior”, () => {
it(“should handle multiple pending transactions correctly”, async function() {
// Enable auto-mining
await network.provider.send(“evm_setAutomine”, [false]);

// Submit multiple transactions that will sit in the mempool
const tx1 = lendingProtocol.deposit({ value: ethers.utils.parseEther(“1”) });
const tx2 = lendingProtocol.deposit({ value: ethers.utils.parseEther(“2”) });
const tx3 = lendingProtocol.deposit({ value: ethers.utils.parseEther(“3”) });

// Get pending transactions
const pendingTxs = await network.provider.send(“eth_pendingTransactions”);
console.log(“Pending transactions:”, pendingTxs.length);

// Mine them all at once
await network.provider.send(“evm_mine”);

// Verify final state
const totalDeposits = await lendingProtocol.totalDeposits();
expect(totalDeposits).to.equal(ethers.utils.parseEther(“6”));
});
});

To run a test script with Hardhat on local:

npx hardhat test

How can the QA team test the smart contract?

Method 1: Have fully access to the dev code repository

// Need access to contract code and typechain
import { TestLendingProtocol } from “../../typechain/contracts/TestLendingProtocol”;

// Full testing capabilities with type safety
describe(“Full Testing Suite”, () => {
let lendingProtocol: TestLendingProtocol;

beforeEach(async () => {
lendingProtocol = await ethers.getContractAt(
“TestLendingProtocol”,
addresses.lendingProtocol,
signer
);
});
});

Before importing from the typechain folder, you might need to run this command to compile the Solidity file to generate ABI (Application Binary Interface) and TypeChain types.

npx hardhat compile

This creates:

artifacts/ folder containing ABIstypechain/ folder containing TypeScript types

Method 2: Testing Deployed Contracts (No Source Code Access)

// QA only has contract address and ABI
const contractAddress = “0x123…”; // Deployed contract address
const abi = [
“function deposit() external payable”,
“function withdraw(uint256 amount) external”
];

describe(“Limited Testing Suite”, () => {
it(“can test basic functionality”, async () => {
const contract = new ethers.Contract(contractAddress, abi, signer);

// Can test basic functions
await contract.deposit({ value: ethers.utils.parseEther(“1”) });

// Can’t simulate network conditions as effectively
// Limited to actual network behavior
});
});

About testing deployed contracts, you can still use Hardhat to fork the Ethereum Mainnet or a Testnet, depending on which network the developer has deployed to.

If you fork Ethereum Mainnet:

// CORRECT: Using Mainnet contract address
const MAINNET_USDC = “0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48”;
const MAINNET_WETH = “0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2”;

await network.provider.request({
method: “hardhat_reset”,
params: [{
forking: {
jsonRpcUrl: MAINNET_RPC_URL, // Mainnet RPC URL
blockNumber: 15000000
}
}]
});

const usdc = new ethers.Contract(MAINNET_USDC, USDC_ABI, signer);
const weth = new ethers.Contract(MAINNET_WETH, WETH_ABI, signer);

If you fork Testnets (e.g., Sepolia):

// CORRECT: Using Sepolia contract address
const SEPOLIA_USDC = “0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238”; // Example address

await network.provider.request({
method: “hardhat_reset”,
params: [{
forking: {
jsonRpcUrl: SEPOLIA_RPC_URL, // Sepolia RPC URL
blockNumber: 3000000
}
}]
});

const usdc = new ethers.Contract(SEPOLIA_USDC, USDC_ABI, signer);

How can the QA team test smart contracts across different networks?

You can configure the network configuration in hardhat.config.ts like this:

When you want to run with a specific network name, use the command:

hardhat test –network mainnetFork

Pre-requisite:

You can copy the mainnet URL with an API key from Alchemy or Infura.

Alchemy API keyInfura API key

Get testnet token Sepolia for free:

https://console.optimism.io/faucethttps://cloud.google.com/application/web3/faucet/ethereum/sepoliahttps://www.ethereumsepoliafaucet.com/https://sepolia-faucet.pk910.de/

How can QA identify what to test on the Defi smart contract?

First, let’s look at what we know about our lending protocol:

Step 1: Understand the business requirements and core functions. We can start with functional testing from this stage.

it(“should allow deposits and withdrawals”, async function() {
await lendingProtocol.deposit({ value: ethers.utils.parseEther(“1”) });
await lendingProtocol.withdraw(ethers.utils.parseEther(“1”));
});

Step 2: Understanding financial risk, think about what could go wrong with money:

What if the network is slow and transactions get stuck?What if gas prices suddenly spike while processing a loan?What if the system gets many requests at once?

This leads us to our first network tests:

In this test case, we simulate extreme network conditions and attempt transactions with gas price limits. The result should be no state changes occurring to protect users from overpaying for transactions.

Understand blockchain behavior:

Network congestion can cause sudden gas price increasesTransactions with too low gas prices may never confirmUsers can set maximum gas price limitsTransactions should fail gracefully if gas limits are exceeded

Step 3: Learning from real DeFi incidents

Many DeFi protocols have had issues in the past. Example:

MakerDAO had issues during high network congestion in March 2020Compound had problems with block timing affecting interest calculationsVarious protocols had issues during network splits

This history teaches us to test:

Step 4: Understanding protocol requirements

Any lending protocol needs to:

Keep accurate balancesCalculate interest correctlyAllow withdrawals when promisedProtect user funds

This leads to testing time-dependent operations:

Result on HTML report:

Purpose of this test case:

Ensure interest accrues correctly, whether blocks are fast or slowVerify calculations are precise and match expected mathematical formulasTest system resilience to different network speeds

Blockchain Behavior Testing:

Block time variations:Ethereum block times aren’t constantNetwork congestion affects block timingSystem must work correctly regardless

2. Gas price impact:

High gas prices during network congestionShouldn’t affect protocol calculationsOnly affects transaction costs

3. Time-Based Calculations:

Interest depends on elapsed timeMust handle various timeframes accuratelyPrecision important for financial calculations

Step 5: Learning about network behavior

As you learn more about Ethereum, you discover:

Blocks can be reorganized (changed)The network can split temporarilyGas prices can change rapidlyTransactions can get stuck

This knowledge leads to more sophisticated tests:

Step 6: Understanding user impact

Some might deposit small amountsOthers might deposit large amountsThey might need their money during network problemsThey shouldn’t lose money due to technical issues

This leads to testing different scenarios:

How to generate an HTML report with Mocha?

Install dependecies:

npm install –save-dev mochawesome mochawesome-merge mochawesome-report-generator mocha-multi-reporters mocha-ctrf-json-reporter

You can configure mocha settings on hardhat.config.ts like this:

Import mocha to the test script:

import “mocha”;
import addContext from ‘mochawesome/addContext’;

Example using addContext to show test results visible in HTML report:

// @ts-ignore
addContext(this, {
title: ‘WETH balance’,
value: ethers.utils.formatEther(balance)
});

Additionally, we might consider creating a custom script to display total gas usage from the Hardhat gas reporter plugin inside an HTML report. This would help optimize the contract while making test results easily visible in one place.

Install Hardhat gas reporter:

npm install –save-dev hardhat-gas-reporter

You can configure gas reporter settings on hardhat.config.ts like this:

Example some part of Hardhat gas report json format that we need to covert to mocha json format for generate HTML later:

A custom script to convert the Hardhat gas report into an “after” hook inside the Mocha JSON report, as shown in the code below:

Here is the command to run Hardhat test cases along with the Mocha report and a custom script for the Hardhat Gas Reporter to generate an HTML report in package.json :

“scripts”: {
“test”: “hardhat test”,
“clean:reports”: “rm -rf test-reports/* && rm -rf gas-reports/* && rm -rf ctrf/*”,
“test:specific”: “npm run clean:reports && npm run deploy:mainnet-fork && hardhat test”,
“deploy:mainnet-fork”: “hardhat run scripts/deploy/deployAndUpdateAddresses.ts –network mainnetFork”,
“test:main_network_fork_report”: “npm run clean:reports && npm run deploy:mainnet-fork && hardhat test –network mainnetFork && npx mochawesome-merge test-reports/*.json -o test-reports/merged.json && node scripts/utils/addGasToMergedReport.js && npx marge test-reports/merged.json -o test-reports -f report”
},

Result on HTML report:

All the source code, including additional test cases, is available in this repo

Thanks for reading, and I hope you found this article helpful.
If you found this article helpful, give it a 👏 (or a few!) to help others discover it!

QA Blockchain Testing: Smart Contract & Network Performance with Hardhat was originally published in Coinmonks on Medium, where people are continuing the conversation by highlighting and responding to this story.

By

Leave a Reply

Your email address will not be published. Required fields are marked *