import { expect } from "chai" import hre, { ethers } from "hardhat" import { StandardMerkleTree } from "@openzeppelin/merkle-tree" import { loadFixture, time } from "@nomicfoundation/hardhat-toolbox/network-helpers" import { network } from "hardhat" let snapshotId: string describe("Launchpad", () => { const deployLaunchpadFixture = async () => { const signers = await hre.ethers.getSigners() // for (const signer of signers) { // await hre.network.provider.send("hardhat_setBalance", [ // signer.address, // "0x3635C9ADC5DEA00000", // 1000 ETH // ]) // } const owner = signers[0].address const Launchpad = await hre.ethers.getContractFactory("Launchpad") const Erc20Token = await hre.ethers.getContractFactory("BasicERC20") const launchpadContract = await Launchpad.deploy({ from: owner, }) const saleTokenContract = await Erc20Token.deploy("TestToken", "TT", owner, { from: owner, }) const buyTokenContract = await Erc20Token.deploy("Usdt", "USDT", owner, { from: owner, }) const launchpadAddress = await launchpadContract.getAddress() await saleTokenContract.mint(owner, ethers.parseEther("1000000")) await saleTokenContract.approve(launchpadAddress, ethers.MaxUint256) await buyTokenContract.mint(signers[1].address, ethers.parseEther("10000")) return { launchpadContract, saleTokenContract, buyTokenContract, owner, } } const createNativeSaleFixture = async () => { const { launchpadContract, saleTokenContract, owner } = await deployLaunchpadFixture() const balanceBefore = await saleTokenContract.balanceOf(owner) const saleTokenAddress = await saleTokenContract.getAddress() const now = Math.floor(Date.now() / 1000) const startTime = now + 60 const endTime = now + 60 * 60 * 8 const paymentTokens = ethers.ZeroAddress const paymentTokenPrice = ethers.parseEther("1") await launchpadContract.createSale( saleTokenAddress, balanceBefore, ethers.parseEther("100"), // contributionTarget startTime, endTime, paymentTokens, paymentTokenPrice, 0n // feeBps ) return { launchpadContract, saleTokenContract, owner, } } describe("init function and setters", () => { it("Should Allow Owner to create a sale use native token", async () => { const { launchpadContract, saleTokenContract, owner } = await loadFixture(deployLaunchpadFixture) const balanceBefore = await saleTokenContract.balanceOf(owner) const saleTokenAddress = await saleTokenContract.getAddress() const now = Math.floor(Date.now() / 1000) let startTime = now + 60 // 1 minute from now const endTime = now + 60 * 60 * 8 // 1 hour from now const paymentTokens = ethers.ZeroAddress const paymentTokenPrice = ethers.parseEther("1") await launchpadContract.createSale( saleTokenAddress, balanceBefore, ethers.parseEther("100"), // contributionTarget startTime, endTime, paymentTokens, paymentTokenPrice, 0n // feeBps ) startTime = Number(await launchpadContract.getStartTime()) await time.increaseTo(startTime + 1) expect(await saleTokenContract.balanceOf(owner)).to.equal(0) expect(await launchpadContract.getSaleToken()).to.equal(saleTokenAddress) expect(await launchpadContract.getTotalTokensForSale()).to.equal(balanceBefore) expect(await launchpadContract.getStartTime()).to.equal(startTime) expect(await launchpadContract.getEndTime()).to.equal(endTime) expect(await launchpadContract.getPaymentTokens()).to.equal(ethers.ZeroAddress) expect(await launchpadContract.getPaymentTokenPrice()).to.equal(paymentTokenPrice) expect(await launchpadContract.totalContributionAmount()).to.equal(0n) }) it("should Allow Owner reset payment info", async () => { const { launchpadContract, saleTokenContract, owner } = await loadFixture(createNativeSaleFixture) const paymentTokenBefore = await launchpadContract.getPaymentTokens() const paymentTokenPriceBefore = await launchpadContract.getPaymentTokenPrice() expect(await launchpadContract.getPaymentTokens()).to.equal(paymentTokenBefore) expect(await launchpadContract.getPaymentTokenPrice()).to.equal(paymentTokenPriceBefore) const newPaymentToken = await saleTokenContract.getAddress() const newPaymentTokenPrice = ethers.parseEther("1") await launchpadContract.setSalePayment(newPaymentToken, newPaymentTokenPrice, { from: owner, }) expect(await launchpadContract.getPaymentTokens()).to.equal(newPaymentToken) expect(await launchpadContract.getPaymentTokenPrice()).to.equal(newPaymentTokenPrice) }) it("should Allow Owner set MerkleRoot", async () => { const { launchpadContract, owner } = await loadFixture(createNativeSaleFixture) const signers = await hre.ethers.getSigners() const values = [ [signers[0].address, ethers.parseEther("50")], [signers[1].address, ethers.parseEther("150")], [signers[2].address, ethers.parseEther("100")], ] const tree = StandardMerkleTree.of(values, ["address", "uint256"]) const root = tree.root await launchpadContract.setMerkleRoot(root, { from: owner }) expect(await launchpadContract.merkleRoot()).to.equal(tree.root) }) it("should Allow Owner transfer Owner", async () => { const { launchpadContract, owner } = await createNativeSaleFixture() const signers = await hre.ethers.getSigners() const newOwner = signers[1].address await launchpadContract.setOwner(newOwner, { from: owner }) expect(await launchpadContract.owner()).to.equal(newOwner) }) }) describe("after init buy function", () => { it("should Allow user use native token buy token", async () => { const { launchpadContract, owner } = await loadFixture(createNativeSaleFixture) const signers = await hre.ethers.getSigners() const values = [ [signers[0].address, ethers.parseEther("50")], [signers[1].address, ethers.parseEther("10")], [signers[2].address, ethers.parseEther("100")], [signers[3].address, ethers.parseEther("200")], [signers[4].address, ethers.parseEther("300")], ] const tree = StandardMerkleTree.of(values, ["address", "uint256"]) const root = tree.root const buyAmount = ethers.parseEther("1") const buyer = signers[1] let maxBuyAmount = 0n let proof = [] as string[] let wrongProof = [] as string[] for (const [i, v] of tree.entries()) { if (v[0] === buyer.address) { proof = tree.getProof(i) if (typeof v[1] === "bigint") { maxBuyAmount = v[1] } } if (v[0] === signers[2].address) { wrongProof = tree.getProof(i) } } await expect( launchpadContract.connect(buyer).contributeWithETH(buyAmount, maxBuyAmount, proof) ).to.be.revertedWith("MerkleRoot not initialized") await launchpadContract.setMerkleRoot(root, { from: owner }) await time.increase(100) await expect( launchpadContract.connect(buyer).contributeWithETH(buyAmount, maxBuyAmount, proof) ).to.be.revertedWith("Must send enough ETH") await expect( launchpadContract.connect(buyer).contributeWithETH(buyAmount + maxBuyAmount, maxBuyAmount, proof, { value: ethers.parseEther("0.5"), }) ).to.be.revertedWith("Buy amount exceeds max buy amount") await expect( launchpadContract.connect(buyer).contributeWithETH(buyAmount, maxBuyAmount, wrongProof, { value: ethers.parseEther("1"), }) ).to.be.revertedWith("Invalid proof") await expect( launchpadContract.connect(buyer).contributeWithETH(buyAmount, maxBuyAmount, proof, { value: ethers.parseEther("0.5"), }) ).to.be.revertedWith("Must send enough ETH") await expect( launchpadContract.connect(buyer).contributeWithETH(ethers.parseEther("50"), maxBuyAmount, proof, { value: ethers.parseEther("50"), }) ).to.revertedWith("Buy amount exceeds max buy amount") //success once await expect( launchpadContract.connect(buyer).contributeWithETH(buyAmount, maxBuyAmount, proof, { value: ethers.parseEther("1"), }) ).to.emit(launchpadContract, "Contributed") //make it success twice await expect( launchpadContract.connect(buyer).contributeWithETH(ethers.parseEther("4"), maxBuyAmount, proof, { value: ethers.parseEther("4"), }) ).to.emit(launchpadContract, "Contributed") expect(await launchpadContract.getClaimableTokens(buyer.address)).to.equal( buyAmount + ethers.parseEther("4") ) //failed by exceeding max buy amount await expect( launchpadContract.connect(buyer).contributeWithETH(ethers.parseEther("10"), maxBuyAmount, proof, { value: ethers.parseEther("10"), }) ).to.be.revertedWith("Buy amount exceeds max buy amount") }) it("should Allow user claim token after finished sale", async () => { const { launchpadContract, saleTokenContract, owner } = await loadFixture(createNativeSaleFixture) const signers = await hre.ethers.getSigners() const values = [ [signers[0].address, ethers.parseEther("50")], [signers[1].address, ethers.parseEther("10")], [signers[2].address, ethers.parseEther("100")], [signers[3].address, ethers.parseEther("200")], [signers[4].address, ethers.parseEther("300")], ] const tree = StandardMerkleTree.of(values, ["address", "uint256"]) const root = tree.root const buyAmount = ethers.parseEther("1") const buyer = signers[1] let maxBuyAmount = 0n let proof = [] as string[] for (const [i, v] of tree.entries()) { if (v[0] === buyer.address) { proof = tree.getProof(i) if (typeof v[1] === "bigint") { maxBuyAmount = v[1] } } } await launchpadContract.setMerkleRoot(root, { from: owner }) await time.increase(100) await expect( launchpadContract.connect(buyer).contributeWithETH(buyAmount, maxBuyAmount, proof, { value: ethers.parseEther("1"), }) ).to.emit(launchpadContract, "Contributed") expect(await launchpadContract.getClaimableTokens(buyer.address)).to.equal(buyAmount) await expect(launchpadContract.connect(buyer).claimTokens()).to.be.revertedWith( "Claiming tokens is not enabled" ) expect(await launchpadContract.enableClaimTokens(Math.floor(Date.now() / 1000) + 60 * 60 * 8)).to.emit( launchpadContract, "EnableClaimToken" ) await expect(launchpadContract.connect(buyer).claimTokens()).to.be.revertedWith( "Claiming tokens not started yet" ) await time.increase(60 * 60 * 8 + 1) // increase time to finish sale await expect(launchpadContract.connect(signers[2]).claimTokens()).to.be.revertedWith("No tokens to claim") //Transfer token await expect(launchpadContract.connect(buyer).claimTokens()).to.emit(saleTokenContract, "Transfer") //Balance check expect(await saleTokenContract.balanceOf(buyer.address)).to.equal(buyAmount) }) it("should Allow owner claim contribution token after finished sale", async () => { const { launchpadContract, saleTokenContract, owner } = await loadFixture(createNativeSaleFixture) const signers = await hre.ethers.getSigners() const values = [ [signers[0].address, ethers.parseEther("50")], [signers[1].address, ethers.parseEther("10")], [signers[2].address, ethers.parseEther("100")], [signers[3].address, ethers.parseEther("200")], [signers[4].address, ethers.parseEther("300")], ] const tree = StandardMerkleTree.of(values, ["address", "uint256"]) const root = tree.root const buyAmount = ethers.parseEther("1") const buyer = signers[1] let maxBuyAmount = 0n let proof = [] as string[] for (const [i, v] of tree.entries()) { if (v[0] === buyer.address) { proof = tree.getProof(i) if (typeof v[1] === "bigint") { maxBuyAmount = v[1] } } } await launchpadContract.setMerkleRoot(root, { from: owner }) const launchpadAddress = await launchpadContract.getAddress() await time.increase(100) await expect( launchpadContract.connect(buyer).contributeWithETH(buyAmount, maxBuyAmount, proof, { value: ethers.parseEther("1"), }) ).to.emit(launchpadContract, "Contributed") await expect(launchpadContract.withdrawPayments()).to.be.revertedWith("Sale is still active") await launchpadContract.enableClaimTokens(Math.floor(Date.now() / 1000) + 60 * 60 * 8) await time.increase(60 * 60 * 8 + 1) // increase time to finish sale await launchpadContract.connect(buyer).claimTokens() await expect(launchpadContract.connect(signers[2]).withdrawPayments()).to.revertedWith( "Only owner can call this function" ) await expect(launchpadContract.connect(signers[2]).withdrawRemainingTokens()).to.revertedWith( "Only owner can call this function" ) await launchpadContract.withdrawPayments() await launchpadContract.withdrawRemainingTokens() expect(await ethers.provider.getBalance(launchpadAddress)).to.equal(0n) expect(await saleTokenContract.balanceOf(launchpadAddress)).to.equal(0) }) it("should allow user to buy and claim with ERC20 payment token", async () => { const { launchpadContract, saleTokenContract, buyTokenContract, owner } = await loadFixture(deployLaunchpadFixture) const signers = await hre.ethers.getSigners() const buyer = signers[1] // 创建ERC20众筹 const saleTokenAddress = await saleTokenContract.getAddress() const buyTokenAddress = await buyTokenContract.getAddress() const now = Math.floor(Date.now() / 1000) const startTime = now + 100 const endTime = now + 60 * 60 * 8 const paymentTokenPrice = ethers.parseEther("1") const totalTokens = await saleTokenContract.balanceOf(owner) await launchpadContract.createSale( saleTokenAddress, totalTokens, totalTokens, // contributionTarget startTime, endTime, buyTokenAddress, paymentTokenPrice, 0n // feeBps ) await time.increase(100) // Merkle tree const values = [[buyer.address, ethers.parseEther("100")]] const tree = StandardMerkleTree.of(values, ["address", "uint256"]) const proof = tree.getProof(0) await launchpadContract.setMerkleRoot(tree.root, { from: owner }) await time.increase(100) // approve await buyTokenContract .connect(buyer) .approve(await launchpadContract.getAddress(), ethers.parseEther("100")) // buy await expect( launchpadContract .connect(buyer) .contributeWithERC20(ethers.parseEther("10"), ethers.parseEther("100"), proof) ).to.emit(launchpadContract, "Contributed") // enable claim await launchpadContract.enableClaimTokens(Math.floor(Date.now() / 1000) + 60 * 60 * 8) await time.increase(60 * 60 * 8 + 1) // claim await expect(launchpadContract.connect(buyer).claimTokens()).to.emit(launchpadContract, "TokensClaimed") expect(await saleTokenContract.balanceOf(buyer.address)).to.equal(ethers.parseEther("10")) }) it("should refund excess and distribute tokens proportionally when oversubscribed", async () => { const { launchpadContract, saleTokenContract, owner } = await loadFixture(deployLaunchpadFixture) const signers = await hre.ethers.getSigners() const tokenBalanceBefore = await saleTokenContract.balanceOf(owner) const saleTokenAddress = await saleTokenContract.getAddress() const now = Math.floor(Date.now() / 1000) const startTime = now + 60 const endTime = now + 60 * 60 * 8 const paymentTokens = ethers.ZeroAddress const paymentTokenPrice = ethers.parseEther("1") await launchpadContract.createSale( saleTokenAddress, tokenBalanceBefore, ethers.parseEther("100"), // contributionTarget startTime, endTime, paymentTokens, paymentTokenPrice, 0n // feeBps ) // 设置白名单 const values = [ [signers[1].address, ethers.parseEther("100")], [signers[2].address, ethers.parseEther("100")], ] const tree = StandardMerkleTree.of(values, ["address", "uint256"]) await launchpadContract.setMerkleRoot(tree.root, { from: owner }) // 两人都买100,目标额度为100,实际总额200 const proof1 = tree.getProof(0) const proof2 = tree.getProof(1) await time.increase(100) await launchpadContract .connect(signers[1]) .contributeWithETH(ethers.parseEther("100"), ethers.parseEther("100"), proof1, { value: ethers.parseEther("100"), }) await launchpadContract .connect(signers[2]) .contributeWithETH(ethers.parseEther("100"), ethers.parseEther("100"), proof2, { value: ethers.parseEther("100"), }) await launchpadContract.enableClaimTokens(Math.floor(Date.now() / 1000) + 60 * 60 * 8) await time.increase(3600 * 8) // 领取 const balanceBefore = await ethers.provider.getBalance(signers[1].address) await launchpadContract.connect(signers[1]).claimTokens() // 检查领取的token数量为50(按比例分配 const balanceAfter = await ethers.provider.getBalance(signers[1].address) expect(Number(ethers.formatEther(balanceAfter - balanceBefore))).to.closeTo(50, 0.001) // 断言退款金额接近50 // ...可进一步断言 }) it("should deduct fee from user contribution when claiming tokens", async () => { const { launchpadContract, saleTokenContract, owner } = await loadFixture(deployLaunchpadFixture) const tokenBalanceBefore = await saleTokenContract.balanceOf(owner) const saleTokenAddress = await saleTokenContract.getAddress() const now = Math.floor(Date.now() / 1000) const startTime = now + 60 const endTime = now + 60 * 60 * 8 const paymentTokens = ethers.ZeroAddress const paymentTokenPrice = ethers.parseEther("1") await launchpadContract.createSale( saleTokenAddress, tokenBalanceBefore, ethers.parseEther("100"), // contributionTarget startTime, endTime, paymentTokens, paymentTokenPrice, 0n // feeBps ) const signers = await hre.ethers.getSigners() const values = [[signers[1].address, ethers.parseEther("10")]] const tree = StandardMerkleTree.of(values, ["address", "uint256"]) await launchpadContract.setMerkleRoot(tree.root, { from: owner }) await launchpadContract.setFeeBps(500) // 5% fee await time.increase(100) await launchpadContract .connect(signers[1]) .contributeWithETH(ethers.parseEther("10"), ethers.parseEther("10"), tree.getProof(0), { value: ethers.parseEther("10"), }) await launchpadContract.enableClaimTokens(Math.floor(Date.now() / 1000) + 60 * 60 * 8) await time.increase(60 * 60 * 8 + 1) const before = await saleTokenContract.balanceOf(signers[1].address) await launchpadContract.connect(signers[1]).claimTokens() const after = await saleTokenContract.balanceOf(signers[1].address) // 断言实际到账 < 10 expect(after - before).to.equal(ethers.parseEther("9.5")) // 5% fee }) it("should not allow user to claim tokens twice", async () => { const { launchpadContract, saleTokenContract, owner } = await loadFixture(deployLaunchpadFixture) const tokenBalanceBefore = await saleTokenContract.balanceOf(owner) const saleTokenAddress = await saleTokenContract.getAddress() const now = Math.floor(Date.now() / 1000) const startTime = now + 60 const endTime = now + 60 * 60 * 8 const paymentTokens = ethers.ZeroAddress const paymentTokenPrice = ethers.parseEther("1") await launchpadContract.createSale( saleTokenAddress, tokenBalanceBefore, ethers.parseEther("100"), // contributionTarget startTime, endTime, paymentTokens, paymentTokenPrice, 0n // feeBps ) const signers = await hre.ethers.getSigners() const values = [[signers[1].address, ethers.parseEther("10")]] const tree = StandardMerkleTree.of(values, ["address", "uint256"]) await launchpadContract.setMerkleRoot(tree.root, { from: owner }) await time.increase(100) await launchpadContract .connect(signers[1]) .contributeWithETH(ethers.parseEther("10"), ethers.parseEther("10"), tree.getProof(0), { value: ethers.parseEther("10"), }) await launchpadContract.enableClaimTokens(Math.floor(Date.now() / 1000) + 60 * 60 * 8) await time.increase(60 * 60 * 8) await launchpadContract.connect(signers[1]).claimTokens() await expect(launchpadContract.connect(signers[1]).claimTokens()).to.be.revertedWith("No tokens to claim") }) it("should revert claim if contract has insufficient token balance", async () => { const { launchpadContract, saleTokenContract, owner } = await loadFixture(deployLaunchpadFixture) const tokenBalanceBefore = await saleTokenContract.balanceOf(owner) const saleTokenAddress = await saleTokenContract.getAddress() const now = Math.floor(Date.now() / 1000) const startTime = now + 60 const endTime = now + 60 * 60 * 8 const paymentTokens = ethers.ZeroAddress const paymentTokenPrice = ethers.parseEther("1") await launchpadContract.createSale( saleTokenAddress, tokenBalanceBefore, ethers.parseEther("100"), // contributionTarget startTime, endTime, paymentTokens, paymentTokenPrice, 0n // feeBps ) const signers = await hre.ethers.getSigners() const values = [[signers[1].address, ethers.parseEther("10")]] const tree = StandardMerkleTree.of(values, ["address", "uint256"]) await launchpadContract.setMerkleRoot(tree.root, { from: owner }) await time.increase(100) await launchpadContract .connect(signers[1]) .contributeWithETH(ethers.parseEther("10"), ethers.parseEther("10"), tree.getProof(0), { value: ethers.parseEther("10"), }) await launchpadContract.enableClaimTokens(Math.floor(Date.now() / 1000) + 60 * 60 * 8) await time.increase(60 * 60 * 8) // owner 提前提走所有token await launchpadContract.withdrawRemainingTokens() await expect(launchpadContract.connect(signers[1]).claimTokens()).to.be.reverted // 断言revert }) it("should revert claim if contract has insufficient ETH balance for refund", async () => { const { launchpadContract, saleTokenContract, owner } = await loadFixture(deployLaunchpadFixture) const tokenBalanceBefore = await saleTokenContract.balanceOf(owner) const saleTokenAddress = await saleTokenContract.getAddress() const now = Math.floor(Date.now() / 1000) const startTime = now + 60 const endTime = now + 60 * 60 * 8 const paymentTokens = ethers.ZeroAddress const paymentTokenPrice = ethers.parseEther("1") await launchpadContract.createSale( saleTokenAddress, tokenBalanceBefore, ethers.parseEther("100"), // contributionTarget startTime, endTime, paymentTokens, paymentTokenPrice, 0n // feeBps ) const signers = await hre.ethers.getSigners() // 设置白名单 const values = [ [signers[1].address, ethers.parseEther("100")], [signers[2].address, ethers.parseEther("100")], ] const tree = StandardMerkleTree.of(values, ["address", "uint256"]) await launchpadContract.setMerkleRoot(tree.root, { from: owner }) await time.increase(100) // 两人都买100,目标额度为100,实际总额200,claim时应部分退款 const proof1 = tree.getProof(0) const proof2 = tree.getProof(1) await launchpadContract .connect(signers[1]) .contributeWithETH(ethers.parseEther("100"), ethers.parseEther("100"), proof1, { value: ethers.parseEther("100"), }) await launchpadContract .connect(signers[2]) .contributeWithETH(ethers.parseEther("100"), ethers.parseEther("100"), proof2, { value: ethers.parseEther("100"), }) await launchpadContract.enableClaimTokens(Math.floor(Date.now() / 1000) + 60 * 60 * 8) await time.increase(60 * 60 * 8) // owner 提前提走所有ETH await launchpadContract.withdrawPayments() await expect(launchpadContract.connect(signers[1]).claimTokens()).to.be.reverted }) it("should revert claim if contract has insufficient ERC20 balance for refund", async () => { const { launchpadContract, saleTokenContract, buyTokenContract, owner } = await loadFixture(deployLaunchpadFixture) const signers = await hre.ethers.getSigners() const buyer1 = signers[1] const buyer2 = signers[2] await buyTokenContract.mint(buyer1.address, ethers.parseEther("1000000000")) await buyTokenContract.mint(buyer2.address, ethers.parseEther("1000000000")) // 创建ERC20众筹 const saleTokenAddress = await saleTokenContract.getAddress() const buyTokenAddress = await buyTokenContract.getAddress() const now = Math.floor(Date.now() / 1000) const startTime = now + 60 const endTime = now + 60 * 60 * 8 const paymentTokenPrice = ethers.parseEther("1") const totalTokens = await saleTokenContract.balanceOf(owner) await launchpadContract.createSale( saleTokenAddress, totalTokens, totalTokens, // contributionTarget startTime, endTime, buyTokenAddress, paymentTokenPrice, 0n // feeBps ) const blockInfo = await ethers.provider.getBlock("latest") await time.increaseTo(blockInfo!.timestamp + 1) // Merkle tree const values = [ [buyer1.address, ethers.parseEther("100")], [buyer2.address, ethers.parseEther("100")], ] const tree = StandardMerkleTree.of(values, ["address", "uint256"]) await launchpadContract.setMerkleRoot(tree.root, { from: owner }) await time.increase(100) // approve await buyTokenContract .connect(buyer1) .approve(await launchpadContract.getAddress(), ethers.parseEther("100")) await buyTokenContract .connect(buyer2) .approve(await launchpadContract.getAddress(), ethers.parseEther("100")) // buy await launchpadContract .connect(buyer1) .contributeWithERC20(ethers.parseEther("100"), ethers.parseEther("100"), tree.getProof(0)) await launchpadContract .connect(buyer2) .contributeWithERC20(ethers.parseEther("100"), ethers.parseEther("100"), tree.getProof(1)) await launchpadContract.enableClaimTokens(Math.floor(Date.now() / 1000) + 60 * 60 * 8) await time.increase(3600 * 8) // owner 提前提走所有ERC20 await launchpadContract.withdrawPayments() await launchpadContract.connect(buyer1).claimTokens() //可能是个 bug 点,在 claim alive 的时候 是否允许 claim token await launchpadContract.withdrawRemainingTokens() await expect(launchpadContract.connect(buyer2).claimTokens()).to.be.reverted }) it("should allow multiple users to claim tokens concurrently without interference", async () => { const { launchpadContract, saleTokenContract, owner } = await loadFixture(deployLaunchpadFixture) const tokenBalanceBefore = await saleTokenContract.balanceOf(owner) const saleTokenAddress = await saleTokenContract.getAddress() const now = Math.floor(Date.now() / 1000) const startTime = now + 60 const endTime = now + 60 * 60 * 8 const paymentTokens = ethers.ZeroAddress const paymentTokenPrice = ethers.parseEther("1") await launchpadContract.createSale( saleTokenAddress, tokenBalanceBefore, ethers.parseEther("100"), // contributionTarget startTime, endTime, paymentTokens, paymentTokenPrice, 0n // feeBps ) const signers = await hre.ethers.getSigners() const values = [ [signers[1].address, ethers.parseEther("10")], [signers[2].address, ethers.parseEther("10")], [signers[3].address, ethers.parseEther("10")], ] const tree = StandardMerkleTree.of(values, ["address", "uint256"]) await launchpadContract.setMerkleRoot(tree.root, { from: owner }) await time.increase(100) for (let i = 1; i <= 3; i++) { await launchpadContract .connect(signers[i]) .contributeWithETH(values[i - 1][1].toString(), values[i - 1][1].toString(), tree.getProof(i - 1), { value: values[i - 1][1].toString(), }) } await launchpadContract.enableClaimTokens(Math.floor(Date.now() / 1000) + 60 * 60 * 8) await time.increase(3600 * 8) // 多用户并发claim await Promise.all([ launchpadContract.connect(signers[1]).claimTokens(), launchpadContract.connect(signers[2]).claimTokens(), launchpadContract.connect(signers[3]).claimTokens(), ]) // 检查每个用户都正确领取 for (let i = 1; i <= 3; i++) { expect(await saleTokenContract.balanceOf(signers[i].address)).to.equal(values[i - 1][1].toString()) } }) }) })