This article shows how to thoroughly test the DeFi Central ERC20 token presale smart contract using Hardhat and Chai on the Ethereum Sepolia testnet. We’ll cover testing all key functionalities, including token purchases with multiple currencies, claiming, refunds, and withdraw functions. Testing smart contracts on a testnet before mainnet deployment is crucial for ensuring functionality and security. The Sepolia testnet provides an ideal environment to test the smart contract with real network conditions without risking real assets.
Testing on a testnet gives developers a real-world feel for how their contracts will behave. It simulates the actual blockchain environment, so you can see how your code handles real transactions and network conditions. Plus, it lets you test how your contracts interact with other contracts and dApps, uncovering potential problems that might not show up in simpler unit tests.
Hardhat is a powerful Ethereum development framework that simplifies the creation, testing, and deployment of smart contracts. This guide will walk you through setting up Hardhat on Ubuntu Linux.
Hardhat requires Node.js and npm (Node Package Manager). Follow these steps to install the latest LTS version of Node.js:
sudo apt update
sudo apt upgrade
Add the Node.js repository and install:
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt install -y nodejs
Check the versions of Node.js and npm
node -v
npm -v
You should see the installed versions. For example:
v18.x
v8.x
Once Node.js and npm are set up, install Hardhat in a new project directory.
mkdir defi-central-presale
cd defi-central-presale
Run the following command and follow the prompts:
npm init -y
Use npm to install Hardhat:
npm install --save-dev hardhat
Run the Hardhat CLI to confirm installation:
npx hardhat
You should see the following prompt:
After verifying Hardhat, create a project structure.
Run the following command and choose the appropriate options:
npx hardhat
Select:
This will create a basic Hardhat project with the following structure:
To test and deploy smart contracts, install these essential dependencies:
npm install --save-dev @nomicfoundation/hardhat-toolbox
npm install dotenv
npm install --save-dev @nomiclabs/hardhat-ethers ethers
Edit the hardhat.config.js
file to specify network settings, paths, and compiler versions.
Example Configuration for Sepolia:
require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();
module.exports = {
solidity: "0.8.18",
networks: {
sepolia: {
url: process.env.SEPOLIA_RPC_URL, // RPC URL for Sepolia
accounts: [process.env.PRIVATE_KEY], // Private key for deployment
},
},
};
Create a .env
file in the root of your project:
touch .env
Add the following keys to .env
:
SEPOLIA_RPC_URL=https://your-sepolia-rpc-url
PRIVATE_KEY=your-private-key
Run the following command to test the Hardhat setup:
npx hardhat test
You should see the sample test for
Lock.sol
executed successfully.
You’re all set! Use the following commands for common Hardhat tasks:
px hardhat compile
npx hardhat test
npx hardhat run scripts/deploy.js --network sepolia
Chai is a popular assertion library for writing robust tests. It’s included in the Hardhat toolbox, but you can explicitly install and configure it as follows:
npm install chai
Install chai-as-promised
and chai-ethers
for Ethereum smart contract testing:
npm install --save-dev chai-as-promised chai-ethers
Add the following imports to your test files:
const { expect } = require("chai");
const chaiAsPromised = require("chai-as-promised");
chai.use(chaiAsPromised);
Use Chai to write clean and expressive tests. Here’s an example for a simple ERC-20 token:
test/TokenTest.js
):const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Token", function () {
it("Should deploy and mint initial supply", async function () {
const [owner] = await ethers.getSigners();
const Token = await ethers.getContractFactory("Token");
const token = await Token.deploy();
await token.deployed();
expect(await token.balanceOf(owner.address)).to.equal(ethers.utils.parseEther("100000000"));
});
});
npx hardhat test
Expected Output:
Token
✓ Should deploy and mint initial supply (500ms)
Ethereum-Waffle is a framework specifically designed for testing smart contracts with Ethereum. It integrates seamlessly with Hardhat and provides advanced features such as snapshot testing, mocking, and built-in matchers for contract testing.
Here’s how to set it up:
Run the following command to install Ethereum-Waffle as a development dependency:
npm install --save-dev ethereum-waffle
Waffle is already compatible with Hardhat when using the @nomicfoundation/hardhat-toolbox
package. Ensure this package is installed:
npm install --save-dev @nomicfoundation/hardhat-toolbox
The toolbox includes Ethereum-Waffle, so you don’t need additional configuration for integration.
Ethereum-Waffle provides a range of matchers to simplify testing smart contracts. You can use expect
assertions from Chai along with Waffle’s matchers.
test/WaffleTest.js
):const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Token", function () {
let token;
let owner, addr1, addr2;
beforeEach(async function () {
[owner, addr1, addr2] = await ethers.getSigners();
const Token = await ethers.getContractFactory("Token");
token = await Token.deploy();
await token.deployed();
});
it("Should assign the total supply to the owner", async function () {
const ownerBalance = await token.balanceOf(owner.address);
expect(await token.totalSupply()).to.equal(ownerBalance);
});
it("Should transfer tokens between accounts", async function () {
// Transfer 50 tokens from owner to addr1
await token.transfer(addr1.address, 50);
const addr1Balance = await token.balanceOf(addr1.address);
expect(addr1Balance).to.equal(50);
// Transfer 50 tokens from addr1 to addr2
await token.connect(addr1).transfer(addr2.address, 50);
const addr2Balance = await token.balanceOf(addr2.address);
expect(addr2Balance).to.equal(50);
});
});
Ethereum-Waffle extends Chai with matchers specifically designed for smart contract testing. Here are some examples:
Check Events:
await expect(token.transfer(addr1.address, 50)) .to.emit(token, "Transfer") .withArgs(owner.address, addr1.address, 50);
Revert Testing:
await expect(token.transfer(addr1.address, 1000)).to.be.revertedWith("ERC20: transfer amount exceeds balance");
Balance Assertions:
await expect(() => token.transfer(addr1.address, 50)) .to.changeTokenBalances(token, [owner, addr1], [-50, 50]);
Run the tests using Hardhat’s testing command:
npx hardhat test
Example output:
Token
✓ Should assign the total supply to the owner
✓ Should transfer tokens between accounts
2 passing (400ms)
By setting up Ethereum-Waffle, you gain access to powerful testing utilities tailored for blockchain development. Combined with Hardhat and Chai, Waffle ensures your tests are concise, readable, and robust.
This guide explains the testing process for the DeFi Central Presale contract. Each section focuses on critical functionalities, ensuring the contract behaves as expected under various conditions. Solidity code snippets and explanations are provided for clarity.
The test suite is structured into distinct sections for systematic testing of the presale contract:
describe('Presale Contract', async function () {
// Deploy the DeFi Central token and presale smart contract before running tests
before(async function () {
// Deploy the token, presale, and mock USDT, USDC, and DAI contracts to Sepolia testnet
// Approve the presale contract to spend stablecoins on behalf of investors
});
describe("Presale setup", function () {
// Ensure the presale contract has sufficient tokens for distribution
});
// Test purchasing tokens with various payment methods
describe("Buying DeFi Central with USDT", function () {
// Validate scenarios for spending beyond allowance, successful purchases, and timing restrictions
});
describe("Buying DeFi Central with USDC", function () {
// Similar tests as for USDT, adapted for USDC
});
describe("Buying DeFi Central with DAI", function () {
// Similar tests as for USDT, adapted for DAI
});
describe("Buying DeFi Central with ETH", function () {
// Test ETH purchases, timing restrictions, and other edge cases
});
// Test administrative functionalities
describe("Claim functionality", function () {
before(async function () {
// Configure claim time before running tests
});
// Validate token claiming, early investor bonuses, and access control
});
describe("Withdraw functionality", function () {
before(async function () {
// Set the multisig wallet address before testing withdrawals
});
// Test owner withdrawals, unauthorized actions, and timing restrictions
});
describe("Refund functionality", function () {
// Validate the refund process for unmet softcap, unauthorized refunds, and timing restrictions
});
});
The setup phase ensures the contract is initialized correctly with the required funds and settings:
describe("Presale setup", function () {
it("should set up presale correctly", async function () {
expect(await presale.getFundsRaised()).to.equal(0);
expect(await presale.tokensAvailable()).to.equal(presaleSupply);
});
});
This test verifies:
Each payment method (USDT, USDC, DAI, ETH) has its own test suite. Here’s an example for USDT purchases:
describe("Buying DeFi Central with USDT", function () {
it("should not allow investors to spend more USDT than allowed", async function () {
const tokenAmount = ethers.parseUnits("20000000", 18);
await expect(presale.connect(investor1).buyWithUSDT(tokenAmount))
.to.be.revertedWith("Insufficient allowance set for the contract.");
});
it("should allow investors to buy tokens with USDT", async function () {
const tokenAmount = ethers.parseUnits("1500000", 18);
const usdtAmount = await presale.estimatedCoinAmountForTokenAmount(tokenAmount, usdtMockInterface);
// Capture state before purchase
const investmentsBefore = await presale.getInvestments(investor1.address, SEPOLIA_USDT);
const fundsRaisedBefore = await presale.getFundsRaised();
const tokensAvailableBefore = await presale.tokensAvailable();
// Perform purchase
const tx = await presale.connect(investor1).buyWithUSDT(tokenAmount);
await tx.wait();
// Validate state after purchase
const investmentsAfter = await presale.getInvestments(investor1.address, SEPOLIA_USDT);
const fundsRaisedAfter = await presale.getFundsRaised();
const tokensAvailableAfter = await presale.tokensAvailable();
expect(investmentsAfter).to.equal(investmentsBefore + usdtAmount);
expect(fundsRaisedAfter).to.equal(fundsRaisedBefore + usdtAmount);
expect(tokensAvailableAfter).to.equal(tokensAvailableBefore - tokenAmount);
});
it("should not allow token purchases before presale starts", async function () {
const tokenAmount = ethers.parseUnits("1500000", 18);
await expect(presale.connect(investor1).buyWithUSDT(tokenAmount))
.to.be.revertedWith("Invalid time for buying the token.");
});
it("should not allow token purchases after presale ends", async function () {
const tokenAmount = ethers.parseUnits("1500000", 18);
await expect(presale.connect(investor1).buyWithUSDT(tokenAmount))
.to.be.revertedWith("Invalid time for buying the token.");
});
});
This test suite for “Buying DeFi Central with USDT” validates four critical scenarios, ensuring the contract’s functionality and security:
"Insufficient allowance set for the contract."
"Invalid time for buying the token."
"Invalid time for buying the token."
This test suite thoroughly validates the USDT purchase functionality, ensuring:
Similar test suites can be written for purchases using USDC, DAI, and ETH to ensure consistent functionality across all payment methods.
Testing token claims ensures proper distribution and bonus allocation:
describe("Claim functionality", function () {
before(async function () {
const setClaimTimeTx = await presale.connect(owner).setClaimTime(claimTime);
await setClaimTimeTx.wait();
});
it("should revert if tokens are claimed before the claim time", async function () {
await expect(presale.connect(investor1).claim()).to.be.revertedWith("It's not claiming time yet.");
});
it("should distribute tokens and bonuses correctly", async function () {
const initialBalance = await spx.balanceOf(investor1.address);
const tokenAmount = await presale.getTokenAmountForInvestor(investor1.address);
const bonusAmount = await presale.getBonusTokenAmount();
const claimTx = await presale.connect(investor1).claim();
await claimTx.wait();
const finalBalance = await spx.balanceOf(investor1.address);
expect(finalBalance - initialBalance).to.equal(tokenAmount + bonusAmount);
});
});
This Claim Test Suite thoroughly validates the token claiming process for the DeFi Central Presale, covering five essential test cases:
setClaimTime
function."It's not claiming time yet."
"No tokens claim."
setClaimTime
."NotOwner."
This test suite ensures that the token claiming process is reliable and secure:
By covering a range of conditions, this suite guarantees the contract’s behavior aligns with the presale rules and maintains fairness for all participants.
Testing withdrawals ensures only authorized users can perform these actions:
describe("Withdraw functionality", function () {
before(async function () {
const setWalletTx = await presale.connect(owner).setWallet(wallet.address);
await setWalletTx.wait();
});
it("should allow the owner to withdraw funds after the presale ends", async function () {
const initialBalance = await usdtMockInterface.balanceOf(wallet.address);
const presaleBalance = await usdtMockInterface.balanceOf(presale.address);
const withdrawTx = await presale.connect(owner).withdraw();
await withdrawTx.wait();
const finalBalance = await usdtMockInterface.balanceOf(wallet.address);
expect(finalBalance).to.equal(initialBalance + presaleBalance);
});
it("should revert if a non-owner attempts to withdraw", async function () {
await expect(presale.connect(investor1).withdraw()).to.be.revertedWithCustomError(presale, "NotOwner");
});
});
This Withdraw Test Suite rigorously evaluates the withdrawal process for the DeFi Central Presale, focusing on five essential scenarios to ensure security and correctness:
setWallet
function.withdraw
function."NotOwner."
setWallet
function."NotOwner."
withdraw
function while the presale is still active."Cannot withdraw because presale is still in progress."
This test suite ensures the withdrawal mechanism operates securely and as intended:
By validating these scenarios, the suite guarantees the withdrawal process is reliable, secure, and compliant with the presale rules.
Testing refunds ensures compliance with softcap conditions:
describe("Refund functionality", function () {
it("should allow the owner to refund if softcap is not met", async function () {
const initialBalance = await usdtMockInterface.balanceOf(investor1.address);
const refundAmount = await presale.getInvestments(investor1.address, usdtMockInterface);
const refundTx = await presale.connect(owner).refund();
await refundTx.wait();
const finalBalance = await usdtMockInterface.balanceOf(investor1.address);
expect(finalBalance).to.equal(initialBalance + refundAmount);
});
it("should revert if a non-owner attempts a refund", async function () {
await expect(presale.connect(investor1).refund()).to.be.revertedWithCustomError(presale, "NotOwner");
});
});
This Refund Test Suite comprehensively validates the refund process for the DeFi Central Presale, covering three critical scenarios to ensure functionality and security:
refund
function."NotOwner."
refund
function while the presale is still active."Cannot refund because presale is still in progress."
This test suite ensures the refund mechanism operates as expected under all conditions:
By covering these scenarios, this suite validates that the refund process is secure, reliable, and fair to all participants.
before
hooks for setup tasks.This comprehensive test suite ensures the DeFi Central Presale contract is secure, functional, and fair. Each test validates key functionalities, enforcing timing restrictions, access control, and accurate state transitions for a seamless presale experience.