使用 Hardhat 和 Chai 对 ERC20 代币预售智能合约进行全面测试
介绍
本文将介绍如何使用 Hardhat 和 Chai 在以太坊 Sepolia 测试网上全面测试SPX ERC20 代币预售智能合约。我们将涵盖所有关键功能的测试,包括使用多种货币购买代币、认领、退款和提现功能。
先决条件
- Hardhat 开发环境
- Chai断言库
- 以太坊华夫饼用于区块链测试
- 对 TypeScript 测试有基本的了解
逐步测试预售智能合约
测试结构
describe('Presale Contract', async function () {
// Before testing, deploy SPX ERC20 token smart contract and Presale smart contract
before(async function () {
// Deploy SPX, Presale, USDT, USDC, DAI token contract to Sepolia testnet and gets contract instance.
// Approve presale contract to spend USDT, USDC, and DAI from investors
})
describe("Presale setup", function () {
//Checks if sufficient tokens are available in presale contract
})
// Test token purchase functions
describe("Buying SPX with USDT", function () {
// It should not allow investors spending USDT more thatn allowance
// It should allow investors buying SPX tokens with USDT
// It should not allow investors buying SPX tokens before presale starts
// It should now allow investors buying SPX tokens after presale ends
})
describe("Buying SPX with USDC", function () {
// It should not allow investors spending USDC more thatn allowance
// It should allow investors buying SPX tokens with USDC
// It should not allow investors buying SPX tokens before presale starts
// It should now allow investors buying SPX tokens after presale ends
})
describe("Buying SPX with DAI", function () {
// It should not allow investors spending DAI more thatn allowance
// It should allow investors buying SPX tokens with DAI
// It should not allow investors buying SPX tokens before presale starts
// It should now allow investors buying SPX tokens after presale ends
})
describe("Buying SPX with ETH", function () {
// It should allow investors buying SPX tokens with ETH
// It should not allow investors buying SPX tokens before presale starts
// It should now allow investors buying SPX tokens after presale ends
})
// Test admin functions
describe("Claim functionality", function () {
before(async function () {
// Set the claim time before each test
})
// It should revert if trying to claim tokens before claim time is set
// It should correctly distribute bonus tokens among multiple early investors
// It should allow investors to claim their tokens
// It should revert if a non-owner tries to set the claim time
})
describe("Withdraw functionality", function () {
before(async function () {
// Set the predefined multisig wallet before each test
})
// It should allow the owner to withdraw balance of contract to wallet after presale ends
// It should revert if non-owner tries to withdraw
// It should revert if a non-owner tries to set the wallet
// It should revert if trying to withdraw before the presale ends
})
describe("Refund Functionality", function () {
// It should allow the owner to refund to investors if softcap is not reached
// It should revert if non-owner tries to refund
// It should revert if trying to refund before the presale ends
})
})
重要关键测试用例
预售准备
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);
});
});
代币购买功能测试
describe("Buying SPX with USDT", function () {
it("should not allow investors spending usdt more than allowance", 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 buying SPX tokens with USDT.", async function () {
const tokenAmount = ethers.parseUnits("1500000", 18);
const usdtAmount = await presale.estimatedCoinAmountForTokenAmount(tokenAmount, usdtMockInterface);
const investmentsforUserBeforeTx1 = await presale.getInvestments(investor1.address, SEPOLIA_USDT);
const investmentsforUserBeforeTx2 = await presale.getInvestments(investor2.address, SEPOLIA_USDT);
const fundsRaisedBeforeTx = await presale.getFundsRaised();
const investorTokenBalanceBeforeTx1 = await presale.getTokenAmountForInvestor(investor1.address);
const investorTokenBalanceBeforeTx2 = await presale.getTokenAmountForInvestor(investor2.address);
const tokensAvailableBeforeTx = await presale.tokensAvailable();
const tx1 = await presale.connect(investor1).buyWithUSDT(tokenAmount);
await tx1.wait();
const tx2 = await presale.connect(investor2).buyWithUSDT(tokenAmount);
await tx2.wait();
const investmentsforUserAfterTx1 = await presale.getInvestments(investor1.address, SEPOLIA_USDT);
const investmentsforUserAfterTx2 = await presale.getInvestments(investor2.address, SEPOLIA_USDT);
const fundsRaisedAfterTx = await presale.getFundsRaised();
const investorTokenBalanceAfterTx1 = await presale.getTokenAmountForInvestor(investor1.address);
const investorTokenBalanceAfterTx2 = await presale.getTokenAmountForInvestor(investor2.address);
const tokensAvailableAfterTx = await presale.tokensAvailable();
expect(investorTokenBalanceAfterTx1).to.equal(investorTokenBalanceBeforeTx1 + tokenAmount);
expect(investorTokenBalanceAfterTx2).to.equal(investorTokenBalanceBeforeTx2 + tokenAmount);
expect(tokensAvailableAfterTx).to.equal(tokensAvailableBeforeTx - BigInt(2) * tokenAmount);
expect(investmentsforUserAfterTx1).to.equal(investmentsforUserBeforeTx1 + usdtAmount);
expect(investmentsforUserAfterTx2).to.equal(investmentsforUserBeforeTx2 + usdtAmount);
expect(fundsRaisedAfterTx).to.equal(fundsRaisedBeforeTx + usdtAmount * BigInt(2) / BigInt(1000000));
});
//Before presale starts
it("should not allow investors buying SPX tokens 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.");
});
//After presale ends
it("should not allow investors buying SPX tokens 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.");
});
});
此测试套件“使用 USDT 购买 SPX”包含四个测试用例,用于验证不同的场景:
测试资金不足:
- 测试购买 20,000,000 个 SPX 代币(价值 1600 USDT),投资者拥有 1000 USDT 的额度。
- 预计交易将因“授权不足”错误而失败。
- 核实津贴检查是否正常工作
测试代币购买是否成功:
- 测试购买 1,500,000 个 SPX 代币(价值 120 USDT)
- 交易前的记录状态:
- 对两位投资者而言,USDT投资都是必要的。
- 筹款总额
- 代币余额
- 可用代币
- 为两位投资者执行购买操作
- 交易后进行验证:
- 代币余额已正确增加。
- 可用代币数量已相应减少
- 准确记录的投资金额
- 筹款金额已正确更新
测试预售时间限制(开始前):
- 在预售开始时间之前尝试购买
- 预期会返回“时间无效”消息
- 验证时序限制是否有效
测试预售时间限制(结束后):
- 预售结束后尝试购买
- 预期会返回“时间无效”消息
- 核实预售结束时间执行情况
此测试套件确保 USDT 购买功能在所有预期情况下都能正常工作。
同样,我们也可以编写 USDC、DAI 和 ETH 购买功能的测试套件。
声明功能测试
describe("Claim functionality", function () {
before(async function () {
// Set the claim time before each test
const setClaimTimeTx = await presale.connect(owner).setClaimTime(claimTime);
await setClaimTimeTx.wait();
});
//Before presale ends
it("should revert if trying to claim tokens before claim time is set", async function () {
await presale.connect(investor1).buyWithUSDT(ethers.parseUnits("1500000", 18));
await expect(presale.connect(investor1).claim(investor1.address)).to.be.revertedWith("It's not claiming time yet.");
});
it("should correctly distribute bonus tokens among multiple early investors", async function () {
expect(await presale.isEarlyInvestors(investor1.address)).to.be.true;
expect(await presale.isEarlyInvestors(investor2.address)).to.be.true;
});
it("should allow investors to claim their tokens", async function () {
const initialBalance = await spx.balanceOf(investor2.address);
const tokenAmount = await presale.getTokenAmountForInvestor(investor2.address);
const bonusTokenAmount = await presale.getBonusTokenAmount();
const claimTx = await presale.connect(investor2).claim(investor2.address);
await claimTx.wait();
const finalBalance = await spx.balanceOf(investor2.address);
expect(finalBalance - initialBalance).to.equal(tokenAmount + bonusTokenAmount);
expect(await presale.getTokenAmountForInvestor(investor2.address)).to.equal(0);
//Second claim
await expect(presale.connect(investor2).claim(investor2.address))
.to.be.revertedWith("No tokens claim.");
});
it("should revert if a non-owner tries to set the claim time", async function () {
await expect(presale.connect(investor1).setClaimTime(claimTime)).to.be.revertedWithCustomError(presale, "NotOwner");
});
});
此认领测试套件通过五个关键测试用例验证代币认领过程:
初始设置:
- 在运行测试之前设置索赔时间
- 使用所有者帐户设置索赔时间
早期索赔预防:
- 测试声称在适当时间之前进行
- 首先用 USDT 购买代币
- 验证索赔尝试因计时错误而失败
早期投资者奖励核实:
- 检查两位投资者的早期投资者身份
- 确认两者均被标记为早期投资者
- 确保奖金发放资格
代币领取流程:
- 记录初始代币余额
- 获得预期代币数量和奖励
- 执行索赔交易
- 核实:
- 最终余额与预期金额相符
- 代币余额重置为零
- 第二次索赔尝试失败
代币领取流程:
- 测试未经授权的索赔时间设置
- 只有所有者才能设置索赔时间
- 检查自定义错误处理
该测试套件确保索赔机制在各种条件下都能正确、安全地运行。
撤回功能测试
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 balance of contract to wallet after presale ends", async function () {
const initialUSDTBalance = await usdtMockInterface.balanceOf(wallet.address);
const initialUSDCBalance = await usdcMockInterface.balanceOf(wallet.address);
const initialDAIBalance = await daiMockInterface.balanceOf(wallet.address);
const usdtAmount = await usdtMockInterface.balanceOf(presaleAddress);
const usdcAmount = await usdcMockInterface.balanceOf(presaleAddress);
const daiAmount = await daiMockInterface.balanceOf(presaleAddress);
const withdrawTx = await presale.connect(owner).withdraw();
await withdrawTx.wait();
const finalUSDTBalance = await usdtMockInterface.balanceOf(wallet.address);
const finalUSDCBalance = await usdcMockInterface.balanceOf(wallet.address);
const finalDAIBalance = await daiMockInterface.balanceOf(wallet.address);
expect(finalUSDTBalance).to.equal(initialUSDTBalance + usdtAmount);
expect(finalUSDCBalance).to.equal(initialUSDCBalance + usdcAmount);
expect(finalDAIBalance).to.equal(initialDAIBalance + daiAmount);
});
it("should revert if non-owner tries to withdraw", async function () {
await expect(presale.connect(investor1).withdraw()).to.be.revertedWithCustomError(presale, "NotOwner");
});
it("should revert if a non-owner tries to set the wallet", async function () {
await expect(presale.connect(investor1).setWallet(wallet)).to.be.revertedWithCustomError(presale, "NotOwner");
});
//Before presale ends
it("should revert if trying to withdraw before the presale ends", async function () {
await expect(presale.connect(owner).withdraw())
.to.be.revertedWith("Cannot withdraw because presale is still in progress.");
})
})
此提现测试套件通过五个关键测试用例验证代币领取流程:
初始设置:
- 测试前设置提现钱包地址
- 使用所有者帐户配置钱包
所有者撤回测试:
- 记录钱包中 USDT、USDC 和 DAI 的初始余额
- 获取所有代币的合约余额
- 执行提款
- 核实最终余额与预期金额是否一致:
- USDT余额已正确增加
- USDC余额已正确增加
- DAI余额已正确增加
防止未经授权的提款:
- 使用非账户进行测试提款
- 验证是否抛出自定义错误“NotOwner”。
钱包设置访问控制:
- 测试未经授权的钱包地址设置
- 验证只有所有者才能设置钱包地址
- 检查自定义错误处理
提前戒断预防:
- 测试在预售结束前撤回
- 验证时序限制消息
- 确保预售期间资金被锁定
该测试套件确保提款机制在各种条件下都能正确、安全地运行。
退款功能测试
describe("Refund Functionality", function () {
it("should allow the owner to refund to investors if softcap is not reached", async function () {
const investor1USDTInitialBalance = await usdtMockInterface.balanceOf(investor1.address);
const investor2USDTInitialBalance = await usdtMockInterface.balanceOf(investor2.address);
const investor1USDCInitialBalance = await usdcMockInterface.balanceOf(investor1.address);
const investor2USDCInitialBalance = await usdcMockInterface.balanceOf(investor2.address);
const investor1DAIInitialBalance = await daiMockInterface.balanceOf(investor1.address);
const investor2DAIInitialBalance = await daiMockInterface.balanceOf(investor2.address);
const investor1USDTAmount = await presale.getInvestments(investor1.address, usdtMockInterface);
const investor2USDTAmount = await presale.getInvestments(investor2.address, usdtMockInterface);
const investor1USDCAmount = await presale.getInvestments(investor1.address, usdcMockInterface);
const investor2USDCAmount = await presale.getInvestments(investor2.address, usdcMockInterface);
const investor1DAIAmount = await presale.getInvestments(investor1.address, daiMockInterface);
const investor2DAIAmount = await presale.getInvestments(investor2.address, daiMockInterface);
await usdtMockInterface.connect(investor1).approve(presaleAddress, investor1USDTAmount);
await usdtMockInterface.connect(investor2).approve(presaleAddress, investor2USDTAmount);
await usdcMockInterface.connect(investor1).approve(presaleAddress, investor1USDCAmount);
await usdcMockInterface.connect(investor2).approve(presaleAddress, investor2USDCAmount);
await daiMockInterface.connect(investor1).approve(presaleAddress, investor1DAIAmount);
await daiMockInterface.connect(investor2).approve(presaleAddress, investor2DAIAmount);
const tx = await presale.connect(owner).refund();
await tx.wait();
const investor1USDTFinalBalance = await usdtMockInterface.balanceOf(investor1.address);
const investor2USDTFinalBalance = await usdtMockInterface.balanceOf(investor2.address);
const investor1USDCFinalBalance = await usdcMockInterface.balanceOf(investor1.address);
const investor2USDCFinalBalance = await usdcMockInterface.balanceOf(investor2.address);
const investor1DAIFinalBalance = await daiMockInterface.balanceOf(investor1.address);
const investor2DAIFinalBalance = await daiMockInterface.balanceOf(investor2.address);
expect(investor1USDTFinalBalance).to.equal(investor1USDTInitialBalance + investor1USDTAmount);
expect(investor2USDTFinalBalance).to.equal(investor2USDTInitialBalance + investor2USDTAmount);
expect(investor1USDCFinalBalance).to.equal(investor1USDCInitialBalance + investor1USDCAmount);
expect(investor2USDCFinalBalance).to.equal(investor2USDCInitialBalance + investor2USDCAmount);
expect(investor1DAIFinalBalance).to.equal(investor1DAIInitialBalance + investor1DAIAmount);
expect(investor2DAIFinalBalance).to.equal(investor2DAIInitialBalance + investor2DAIAmount);
});
it("should revert if non-owner tries to refund", async function () {
await expect(presale.connect(investor1).refund())
.to.be.revertedWithCustomError(presale, "NotOwner");
});
//After presale ends
it("should revert if trying to refund before the presale ends", async function () {
await expect(presale.connect(owner).refund())
.to.be.revertedWith("Cannot refund because presale is still in progress.");
})
});
此退款测试套件通过三个关键测试用例验证代币退款流程:
业主退款测试:
- 记录所有投资者的 USDT、USDC 和 DAI 初始余额
- 获取每位投资者和代币的投资金额。
- 设置所有代币和投资者的审批流程
- 执行退款交易
- 核实最终余额:
- USDT余额已正确返还。
- USDC余额已正确退回
- DAI余额已正确返回。
- 确保每位投资者都能收回全部投资款项。
防止未经授权的退款:
- 使用非所有者账户进行测试可获得退款。
- 验证是否抛出自定义错误“NotOwner”。
- 确保只有所有者才能发起退款。
提前退款预防:
- 预售结束前可退款
- 验证时序限制消息
- 确保在预售期间不会发生退款。
这确保了在各种情况下,所有投资者和代币的退款机制都能正确、安全地运行。
最佳实践
- 在测试设置之前使用钩子
- 测试成功和失败两种情况
- 验证事件和状态变化
- 测试访问控制
- 使用辅助函数进行常见操作
- 保持测试隔离
结论
对于涉及金额巨大的预售合同而言,全面的测试至关重要。这种测试方法能够确保合同在任何情况下都能正确运行,同时保障所有参与者的安全性和公平性。
文章来源:https://dev.to/imcrazysteven/thorough-testing-for-erc20-token-presale-smart-contract-using-hardhat-and-chai-3i4g