
{"id":43447,"date":"2025-02-11T08:11:54","date_gmt":"2025-02-11T08:11:54","guid":{"rendered":"https:\/\/mycryptomania.com\/?p=43447"},"modified":"2025-02-11T08:11:54","modified_gmt":"2025-02-11T08:11:54","slug":"qa-blockchain-testing-smart-contract-network-performance-with-hardhat","status":"publish","type":"post","link":"https:\/\/mycryptomania.com\/?p=43447","title":{"rendered":"QA Blockchain Testing: Smart Contract &amp; Network Performance with Hardhat"},"content":{"rendered":"<h4>\ud83d\udee0\ufe0f <strong>\u201cTesting Node &amp; Network Behavior in Smart Contracts Using Hardhat with TypeScript &amp;\u00a0Mocha\u201d<\/strong><\/h4>\n<h3>Background knowledge:<\/h3>\n<p>A blockchain network consists of multiple <strong>nodes<\/strong> that communicate with each other. Each node participates in <strong>mining blocks<\/strong> to form a <strong>chain<\/strong>, which requires <strong>gas fees<\/strong>. Each block contains:<\/p>\n<p><strong>Transaction data<\/strong> (e.g., deposit or transfer\u00a0records)<strong>Timestamp<\/strong> (when the block was\u00a0created)<strong>A link to the previous\u00a0block<\/strong><strong>Technical information<\/strong> about the network\u00a0state<\/p>\n<p>For example, if you deposit <strong>10 ETH<\/strong>, the block would\u00a0record:<\/p>\n<p><strong>Your wallet address<\/strong> (who sent\u00a0it)<strong>The contract address<\/strong> (who received\u00a0it)<strong>The amount<\/strong> (10\u00a0ETH)<strong>The transaction timestamp<\/strong><strong>Other technical details<\/strong> about the transaction<\/p>\n<p>In this blog, we will test a <strong>smart contract for token lending<\/strong>, where you can <strong>deposit ETH<\/strong> to <strong>borrow WETH tokens<\/strong>, as shown in the Solidity code\u00a0below:<\/p>\n<h3>Why using\u00a0Hardhat?<\/h3>\n<p><strong>Network Simulation Capabilities<\/strong>\/\/ In Hardhat, we can control block mining<br \/>await network.provider.send(&#8220;evm_mine&#8221;, []);  \/\/ Mine a new block<\/p>\n<p>\/\/ We can control block timestamps<br \/>await network.provider.send(&#8220;evm_increaseTime&#8221;, [3600]);  \/\/ Move time forward 1 hour<\/p>\n<p>\/\/ We can even control gas prices<br \/>await network.provider.send(&#8220;hardhat_setNextBlockBaseFeePerGas&#8221;, [<br \/>    ethers.utils.hexValue(ethers.utils.parseUnits(&#8220;100&#8221;, &#8220;gwei&#8221;))<br \/>]);<\/p>\n<p>You can save and restore the entire blockchain state for test error recovery:<\/p>\n<p>describe(&#8220;Error Recovery Testing&#8221;, () =&gt; {<br \/>  it(&#8220;should recover from failed operations&#8221;, async function() {<br \/>      \/\/ Take snapshot before risky operation<br \/>      const snapshotId = await network.provider.send(&#8220;evm_snapshot&#8221;, []);<\/p>\n<p>      try {<br \/>          \/\/ Attempt potentially failing operation<br \/>          await lendingProtocol.withdraw(ethers.utils.parseEther(&#8220;100&#8221;)); \/\/ More than deposited<br \/>      } catch (error) {<br \/>          \/\/ Revert to clean state<br \/>          await network.provider.send(&#8220;evm_revert&#8221;, [snapshotId]);<\/p>\n<p>          \/\/ Verify state is clean<br \/>          const balance = await lendingProtocol.getUserDeposit(signer.address);<br \/>          expect(balance).to.equal(0);<br \/>      }<br \/>  });<br \/>});<strong>Mainnet Forking<\/strong><\/p>\n<p>Hardhat allows us to create a copy of the real Ethereum mainnet at any point in time for\u00a0testing.<\/p>\n<p>\/\/ We can fork mainnet at a specific block<br \/>await network.provider.request({<br \/>    method: &#8220;hardhat_reset&#8221;,<br \/>    params: [{<br \/>        forking: {<br \/>            jsonRpcUrl: MAINNET_RPC_URL,<br \/>            blockNumber: 15000000  \/\/ Specific block number<br \/>        }<br \/>    }]<br \/>});<\/p>\n<p>\/\/ Now we can interact with real mainnet contracts<br \/>const USDC = await ethers.getContractAt(&#8220;IERC20&#8221;, &#8220;0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48&#8221;);<strong>Debugging Capabilities<\/strong><\/p>\n<p>We can get stack traces for failed transactions<\/p>\n<p>it(&#8220;should maintain consistent performance under sustained load&#8221;, async function() {<br \/>    const testUser = await setupTestUser(0, &#8220;1000&#8221;);<br \/>    const userContract = lendingProtocol.connect(testUser);<br \/>    const depositAmount = ethers.utils.parseEther(&#8220;0.1&#8221;);<br \/>    const iterations = 3;<br \/>    const results = [];<\/p>\n<p>    try {<br \/>        \/\/ Get initial balance<br \/>        const initialBalance = await userContract.getUserDeposit(testUser.address);<br \/>        console.log(&#8220;Initial balance:&#8221;, ethers.utils.formatEther(initialBalance));<br \/>  } catch (error) {<br \/>    console.error(&#8220;Load test failed:&#8221;, error);<br \/>}<\/p>\n<p>You can also use Harthat to debug Smart Contract in the\u00a0console:<\/p>\n<p>npx hardhat console<strong>Test Environment Control<\/strong><\/p>\n<p>With Hardhat, we have extensive control over the test environment.<\/p>\n<p>\/\/ We can manipulate account balances directly<br \/>await network.provider.send(&#8220;hardhat_setBalance&#8221;, [<br \/>    userAddress,<br \/>    ethers.utils.hexValue(ethers.utils.parseEther(&#8220;100&#8221;))<br \/>]);<\/p>\n<p>\/\/ We can impersonate any Ethereum address<br \/>await network.provider.request({<br \/>    method: &#8220;hardhat_impersonateAccount&#8221;,<br \/>    params: [whaleAddress],<br \/>});<\/p>\n<p><strong>Advance Hardhat feature:<\/strong><br \/>We can test how our protocol handles pending transactions and mempool behavior like the code\u00a0below.<\/p>\n<p>describe(&#8220;Transaction Pool Behavior&#8221;, () =&gt; {<br \/>  it(&#8220;should handle multiple pending transactions correctly&#8221;, async function() {<br \/>      \/\/ Enable auto-mining<br \/>      await network.provider.send(&#8220;evm_setAutomine&#8221;, [false]);<\/p>\n<p>      \/\/ Submit multiple transactions that will sit in the mempool<br \/>      const tx1 = lendingProtocol.deposit({ value: ethers.utils.parseEther(&#8220;1&#8221;) });<br \/>      const tx2 = lendingProtocol.deposit({ value: ethers.utils.parseEther(&#8220;2&#8221;) });<br \/>      const tx3 = lendingProtocol.deposit({ value: ethers.utils.parseEther(&#8220;3&#8221;) });<\/p>\n<p>      \/\/ Get pending transactions<br \/>      const pendingTxs = await network.provider.send(&#8220;eth_pendingTransactions&#8221;);<br \/>      console.log(&#8220;Pending transactions:&#8221;, pendingTxs.length);<\/p>\n<p>      \/\/ Mine them all at once<br \/>      await network.provider.send(&#8220;evm_mine&#8221;);<\/p>\n<p>      \/\/ Verify final state<br \/>      const totalDeposits = await lendingProtocol.totalDeposits();<br \/>      expect(totalDeposits).to.equal(ethers.utils.parseEther(&#8220;6&#8221;));<br \/>  });<br \/>});<\/p>\n<p>To run a test script with Hardhat on\u00a0local:<\/p>\n<p>npx hardhat test<\/p>\n<h3>How can the QA team test the smart contract?<\/h3>\n<p><strong>Method 1: Have fully access to the dev code repository<\/strong><\/p>\n<p>\/\/ Need access to contract code and typechain<br \/>import { TestLendingProtocol } from &#8220;..\/..\/typechain\/contracts\/TestLendingProtocol&#8221;;<\/p>\n<p>\/\/ Full testing capabilities with type safety<br \/>describe(&#8220;Full Testing Suite&#8221;, () =&gt; {<br \/>    let lendingProtocol: TestLendingProtocol;<\/p>\n<p>    beforeEach(async () =&gt; {<br \/>        lendingProtocol = await ethers.getContractAt(<br \/>            &#8220;TestLendingProtocol&#8221;,<br \/>            addresses.lendingProtocol,<br \/>            signer<br \/>        );<br \/>    });<br \/>});<\/p>\n<p>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.<\/p>\n<p>npx hardhat compile<\/p>\n<p>This creates:<\/p>\n<p>artifacts\/ folder containing ABIstypechain\/ folder containing TypeScript types<\/p>\n<p><strong>Method 2: Testing Deployed Contracts (No Source Code\u00a0Access)<\/strong><\/p>\n<p>\/\/ QA only has contract address and ABI<br \/>const contractAddress = &#8220;0x123&#8230;&#8221;; \/\/ Deployed contract address<br \/>const abi = [<br \/>    &#8220;function deposit() external payable&#8221;,<br \/>    &#8220;function withdraw(uint256 amount) external&#8221;<br \/>];<\/p>\n<p>describe(&#8220;Limited Testing Suite&#8221;, () =&gt; {<br \/>    it(&#8220;can test basic functionality&#8221;, async () =&gt; {<br \/>        const contract = new ethers.Contract(contractAddress, abi, signer);<\/p>\n<p>        \/\/ Can test basic functions<br \/>        await contract.deposit({ value: ethers.utils.parseEther(&#8220;1&#8221;) });<\/p>\n<p>        \/\/ Can&#8217;t simulate network conditions as effectively<br \/>        \/\/ Limited to actual network behavior<br \/>    });<br \/>});<\/p>\n<p>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\u00a0to.<\/p>\n<p><strong>If you fork Ethereum\u00a0Mainnet:<\/strong><\/p>\n<p>\/\/ CORRECT: Using Mainnet contract address<br \/>const MAINNET_USDC = &#8220;0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48&#8221;;<br \/>const MAINNET_WETH = &#8220;0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2&#8221;;<\/p>\n<p>await network.provider.request({<br \/>    method: &#8220;hardhat_reset&#8221;,<br \/>    params: [{<br \/>        forking: {<br \/>            jsonRpcUrl: MAINNET_RPC_URL,  \/\/ Mainnet RPC URL<br \/>            blockNumber: 15000000<br \/>        }<br \/>    }]<br \/>});<\/p>\n<p>const usdc = new ethers.Contract(MAINNET_USDC, USDC_ABI, signer);<br \/>const weth = new ethers.Contract(MAINNET_WETH, WETH_ABI, signer);<\/p>\n<p><strong>If you fork Testnets (e.g., Sepolia):<\/strong><\/p>\n<p>\/\/ CORRECT: Using Sepolia contract address<br \/>const SEPOLIA_USDC = &#8220;0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238&#8221;; \/\/ Example address<\/p>\n<p>await network.provider.request({<br \/>    method: &#8220;hardhat_reset&#8221;,<br \/>    params: [{<br \/>        forking: {<br \/>            jsonRpcUrl: SEPOLIA_RPC_URL,  \/\/ Sepolia RPC URL<br \/>            blockNumber: 3000000<br \/>        }<br \/>    }]<br \/>});<\/p>\n<p>const usdc = new ethers.Contract(SEPOLIA_USDC, USDC_ABI, signer);<\/p>\n<h3>How can the QA team test smart contracts across different networks?<\/h3>\n<p>You can configure the network configuration in <em>hardhat.config.ts<\/em> like\u00a0this:<\/p>\n<p>When you want to run with a specific network name, use the\u00a0command:<\/p>\n<p>hardhat test &#8211;network mainnetFork<\/p>\n<h4>Pre-requisite:<\/h4>\n<p>You can copy the mainnet URL with an API key from Alchemy or\u00a0Infura.<\/p>\n<p>Alchemy API\u00a0keyInfura API\u00a0key<\/p>\n<p>Get testnet token Sepolia for\u00a0free:<\/p>\n<p><a href=\"https:\/\/console.optimism.io\/faucet\">https:\/\/console.optimism.io\/faucet<\/a><a href=\"https:\/\/cloud.google.com\/application\/web3\/faucet\/ethereum\/sepolia\">https:\/\/cloud.google.com\/application\/web3\/faucet\/ethereum\/sepolia<\/a><a href=\"https:\/\/www.ethereumsepoliafaucet.com\/\">https:\/\/www.ethereumsepoliafaucet.com\/<\/a><a href=\"https:\/\/sepolia-faucet.pk910.de\/\">https:\/\/sepolia-faucet.pk910.de\/<\/a><\/p>\n<h3>How can QA identify what to test on the Defi smart contract?<\/h3>\n<p>First, let\u2019s look at what we know about our lending protocol:<\/p>\n<h4>Step 1: Understand the business requirements and core functions. We can start with functional testing from this\u00a0stage.<\/h4>\n<p>it(&#8220;should allow deposits and withdrawals&#8221;, async function() {<br \/>    await lendingProtocol.deposit({ value: ethers.utils.parseEther(&#8220;1&#8221;) });<br \/>    await lendingProtocol.withdraw(ethers.utils.parseEther(&#8220;1&#8221;));<br \/>});<\/p>\n<h4>Step 2: Understanding financial risk, think about what could go wrong with\u00a0money:<\/h4>\n<p>What if the network is slow and transactions get\u00a0stuck?What if gas prices suddenly spike while processing a\u00a0loan?What if the system gets many requests at\u00a0once?<\/p>\n<p>This leads us to our first network\u00a0tests:<\/p>\n<p>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.<\/p>\n<p><strong>Understand blockchain behavior:<\/strong><\/p>\n<p>Network congestion can cause sudden gas price increasesTransactions with too low gas prices may never\u00a0confirmUsers can set maximum gas price\u00a0limitsTransactions should fail gracefully if gas limits are\u00a0exceeded<\/p>\n<h4>Step 3: Learning from real DeFi incidents<\/h4>\n<p>Many DeFi protocols have had issues in the past.\u00a0Example:<\/p>\n<p>MakerDAO had issues during high network congestion in March\u00a02020Compound had problems with block timing affecting interest calculationsVarious protocols had issues during network\u00a0splits<\/p>\n<p>This history teaches us to\u00a0test:<\/p>\n<h4>Step 4: Understanding protocol requirements<\/h4>\n<p>Any lending protocol needs\u00a0to:<\/p>\n<p>Keep accurate\u00a0balancesCalculate interest correctlyAllow withdrawals when\u00a0promisedProtect user\u00a0funds<\/p>\n<p>This leads to testing time-dependent operations:<\/p>\n<p>Result on HTML\u00a0report:<\/p>\n<p><strong>Purpose of this test\u00a0case:<\/strong><\/p>\n<p>Ensure interest accrues correctly, whether blocks are fast or\u00a0slowVerify calculations are precise and match expected mathematical formulasTest system resilience to different network\u00a0speeds<\/p>\n<p><strong>Blockchain Behavior\u00a0Testing:<\/strong><\/p>\n<p>Block time variations:Ethereum block times aren\u2019t\u00a0constantNetwork congestion affects block\u00a0timingSystem must work correctly regardless<\/p>\n<p>2. Gas price\u00a0impact:<\/p>\n<p>High gas prices during network congestionShouldn\u2019t affect protocol calculationsOnly affects transaction costs<\/p>\n<p>3. Time-Based Calculations:<\/p>\n<p>Interest depends on elapsed\u00a0timeMust handle various timeframes accuratelyPrecision important for financial calculations<\/p>\n<h4>Step 5: Learning about network\u00a0behavior<\/h4>\n<p>As you learn more about Ethereum, you discover:<\/p>\n<p>Blocks can be reorganized (changed)The network can split temporarilyGas prices can change\u00a0rapidlyTransactions can get\u00a0stuck<\/p>\n<p>This knowledge leads to more sophisticated tests:<\/p>\n<h4>Step 6: Understanding user\u00a0impact<\/h4>\n<p>Some might deposit small\u00a0amountsOthers might deposit large\u00a0amountsThey might need their money during network\u00a0problemsThey shouldn\u2019t lose money due to technical issues<\/p>\n<p>This leads to testing different scenarios:<\/p>\n<h3>How to generate an HTML report with\u00a0Mocha?<\/h3>\n<p><strong>Install dependecies:<\/strong><\/p>\n<p>npm install &#8211;save-dev mochawesome mochawesome-merge mochawesome-report-generator mocha-multi-reporters mocha-ctrf-json-reporter<\/p>\n<p>You can configure mocha settings on <em>hardhat.config.ts<\/em> like\u00a0this:<\/p>\n<p>Import mocha to the test\u00a0script:<\/p>\n<p>import &#8220;mocha&#8221;;<br \/>import addContext from &#8216;mochawesome\/addContext&#8217;;<\/p>\n<p>Example using addContext to show test results visible in HTML\u00a0report:<\/p>\n<p>\/\/ @ts-ignore<br \/>addContext(this, {<br \/>    title: &#8216;WETH balance&#8217;,<br \/>    value: ethers.utils.formatEther(balance)<br \/>});<\/p>\n<p>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\u00a0place.<\/p>\n<p><strong>Install Hardhat gas reporter:<\/strong><\/p>\n<p>npm install &#8211;save-dev hardhat-gas-reporter<\/p>\n<p>You can configure gas reporter settings on <em>hardhat.config.ts<\/em> like\u00a0this:<\/p>\n<p>Example some part of Hardhat gas report json format that we need to covert to mocha json format for generate HTML\u00a0later:<\/p>\n<p>A custom script to convert the Hardhat gas report into an \u201cafter\u201d hook inside the Mocha JSON report, as shown in the code\u00a0below:<\/p>\n<p>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\u00a0:<\/p>\n<p>&#8220;scripts&#8221;: {<br \/>  &#8220;test&#8221;: &#8220;hardhat test&#8221;,<br \/>  &#8220;clean:reports&#8221;: &#8220;rm -rf test-reports\/* &amp;&amp; rm -rf gas-reports\/* &amp;&amp; rm -rf ctrf\/*&#8221;,<br \/>  &#8220;test:specific&#8221;: &#8220;npm run clean:reports &amp;&amp; npm run deploy:mainnet-fork &amp;&amp; hardhat test&#8221;,<br \/>  &#8220;deploy:mainnet-fork&#8221;: &#8220;hardhat run scripts\/deploy\/deployAndUpdateAddresses.ts &#8211;network mainnetFork&#8221;,<br \/>  &#8220;test:main_network_fork_report&#8221;: &#8220;npm run clean:reports &amp;&amp; npm run deploy:mainnet-fork &amp;&amp; hardhat test &#8211;network mainnetFork &amp;&amp; npx mochawesome-merge test-reports\/*.json -o test-reports\/merged.json &amp;&amp; node scripts\/utils\/addGasToMergedReport.js &amp;&amp; npx marge test-reports\/merged.json -o test-reports -f report&#8221;<br \/>},<\/p>\n<p>Result on HTML\u00a0report:<\/p>\n<p><em>All the source code, including additional test cases, is available in this\u00a0<\/em><a href=\"https:\/\/github.com\/Thanasornsawan\/E2E_blockchain_testing\"><em>repo<\/em><\/a><\/p>\n<p>Thanks for reading, and I hope you found this article helpful.<br \/>If you found this article helpful, give it a \ud83d\udc4f (or a few!) to help others discover\u00a0it!<\/p>\n<p><a href=\"https:\/\/medium.com\/coinmonks\/qa-blockchain-testing-smart-contract-network-performance-with-hardhat-d01e99e331e7\">QA Blockchain Testing: Smart Contract &amp; Network Performance with Hardhat<\/a> was originally published in <a href=\"https:\/\/medium.com\/coinmonks\">Coinmonks<\/a> on Medium, where people are continuing the conversation by highlighting and responding to this story.<\/p>","protected":false},"excerpt":{"rendered":"<p>\ud83d\udee0\ufe0f \u201cTesting Node &amp; Network Behavior in Smart Contracts Using Hardhat with TypeScript &amp;\u00a0Mocha\u201d 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\u00a0records)Timestamp (when the block was\u00a0created)A [&hellip;]<\/p>\n","protected":false},"author":0,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2],"tags":[],"class_list":["post-43447","post","type-post","status-publish","format-standard","hentry","category-interesting"],"_links":{"self":[{"href":"https:\/\/mycryptomania.com\/index.php?rest_route=\/wp\/v2\/posts\/43447"}],"collection":[{"href":"https:\/\/mycryptomania.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/mycryptomania.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"replies":[{"embeddable":true,"href":"https:\/\/mycryptomania.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=43447"}],"version-history":[{"count":0,"href":"https:\/\/mycryptomania.com\/index.php?rest_route=\/wp\/v2\/posts\/43447\/revisions"}],"wp:attachment":[{"href":"https:\/\/mycryptomania.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=43447"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/mycryptomania.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=43447"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/mycryptomania.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=43447"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}