|
@@ -1,11 +1,19 @@
|
|
|
import { expect } from "chai"
|
|
|
import hre, { ethers } from "hardhat"
|
|
|
import { StandardMerkleTree } from "@openzeppelin/merkle-tree"
|
|
|
-import { time } from "@nomicfoundation/hardhat-toolbox/network-helpers"
|
|
|
+import { loadFixture, time } from "@nomicfoundation/hardhat-toolbox/network-helpers"
|
|
|
+import { network } from "hardhat"
|
|
|
+let snapshotId: string
|
|
|
|
|
|
describe("Launchpad", () => {
|
|
|
- async function deployLaunchpadFixture() {
|
|
|
+ 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")
|
|
@@ -21,7 +29,7 @@ describe("Launchpad", () => {
|
|
|
|
|
|
const launchpadAddress = await launchpadContract.getAddress()
|
|
|
|
|
|
- await saleTokenContract.mint(signers[0].address, ethers.parseEther("1000000"))
|
|
|
+ await saleTokenContract.mint(owner, ethers.parseEther("1000000"))
|
|
|
await saleTokenContract.approve(launchpadAddress, ethers.MaxUint256)
|
|
|
await buyTokenContract.mint(signers[1].address, ethers.parseEther("10000"))
|
|
|
|
|
@@ -33,23 +41,25 @@ describe("Launchpad", () => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- async function createNativeSaleFixture() {
|
|
|
+ const createNativeSaleFixture = async () => {
|
|
|
const { launchpadContract, saleTokenContract, owner } = await deployLaunchpadFixture()
|
|
|
-
|
|
|
const balanceBefore = await saleTokenContract.balanceOf(owner)
|
|
|
const saleTokenAddress = await saleTokenContract.getAddress()
|
|
|
- const startTime = Math.floor(Date.now() / 1000) + 60 // 1 minute from now
|
|
|
- const endTime = Math.floor(Date.now() / 1000) + 60 * 60 // 1 hour from now
|
|
|
+ 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
|
|
|
+ paymentTokenPrice,
|
|
|
+ 0n // feeBps
|
|
|
)
|
|
|
|
|
|
return {
|
|
@@ -61,34 +71,39 @@ describe("Launchpad", () => {
|
|
|
|
|
|
describe("init function and setters", () => {
|
|
|
it("Should Allow Owner to create a sale use native token", async () => {
|
|
|
- const { launchpadContract, saleTokenContract, owner } = await deployLaunchpadFixture()
|
|
|
+ const { launchpadContract, saleTokenContract, owner } = await loadFixture(deployLaunchpadFixture)
|
|
|
|
|
|
const balanceBefore = await saleTokenContract.balanceOf(owner)
|
|
|
const saleTokenAddress = await saleTokenContract.getAddress()
|
|
|
- const startTime = Math.floor(Date.now() / 1000) + 60 // 1 minute from now
|
|
|
- const endTime = Math.floor(Date.now() / 1000) + 60 * 60 // 1 hour from now
|
|
|
+ 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") / 2500n
|
|
|
+ const paymentTokenPrice = ethers.parseEther("1")
|
|
|
await launchpadContract.createSale(
|
|
|
saleTokenAddress,
|
|
|
balanceBefore,
|
|
|
+ ethers.parseEther("100"), // contributionTarget
|
|
|
startTime,
|
|
|
endTime,
|
|
|
paymentTokens,
|
|
|
- paymentTokenPrice
|
|
|
+ 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.getTotalTokens()).to.equal(balanceBefore)
|
|
|
+ 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.getTokensSold()).to.equal(0n)
|
|
|
+ expect(await launchpadContract.totalContributionAmount()).to.equal(0n)
|
|
|
})
|
|
|
|
|
|
it("should Allow Owner reset payment info", async () => {
|
|
|
- const { launchpadContract, saleTokenContract, owner } = await createNativeSaleFixture()
|
|
|
+ const { launchpadContract, saleTokenContract, owner } = await loadFixture(createNativeSaleFixture)
|
|
|
const paymentTokenBefore = await launchpadContract.getPaymentTokens()
|
|
|
const paymentTokenPriceBefore = await launchpadContract.getPaymentTokenPrice()
|
|
|
expect(await launchpadContract.getPaymentTokens()).to.equal(paymentTokenBefore)
|
|
@@ -103,7 +118,7 @@ describe("Launchpad", () => {
|
|
|
})
|
|
|
|
|
|
it("should Allow Owner set MerkleRoot", async () => {
|
|
|
- const { launchpadContract, owner } = await createNativeSaleFixture()
|
|
|
+ const { launchpadContract, owner } = await loadFixture(createNativeSaleFixture)
|
|
|
const signers = await hre.ethers.getSigners()
|
|
|
|
|
|
const values = [
|
|
@@ -128,7 +143,7 @@ describe("Launchpad", () => {
|
|
|
|
|
|
describe("after init buy function", () => {
|
|
|
it("should Allow user use native token buy token", async () => {
|
|
|
- const { launchpadContract, owner } = await createNativeSaleFixture()
|
|
|
+ const { launchpadContract, owner } = await loadFixture(createNativeSaleFixture)
|
|
|
|
|
|
const signers = await hre.ethers.getSigners()
|
|
|
const values = [
|
|
@@ -156,44 +171,44 @@ describe("Launchpad", () => {
|
|
|
wrongProof = tree.getProof(i)
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
await expect(
|
|
|
- launchpadContract.connect(buyer).contributeETH(buyAmount, maxBuyAmount, proof)
|
|
|
+ launchpadContract.connect(buyer).contributeWithETH(buyAmount, maxBuyAmount, proof)
|
|
|
).to.be.revertedWith("MerkleRoot not initialized")
|
|
|
await launchpadContract.setMerkleRoot(root, { from: owner })
|
|
|
-
|
|
|
- await expect(
|
|
|
- launchpadContract.connect(buyer).contributeETH(buyAmount, maxBuyAmount, proof)
|
|
|
- ).to.be.revertedWith("Sale is not active")
|
|
|
await time.increase(100)
|
|
|
await expect(
|
|
|
- launchpadContract.connect(buyer).contributeETH(buyAmount + maxBuyAmount, maxBuyAmount, proof, {
|
|
|
+ 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).contributeETH(buyAmount, maxBuyAmount, wrongProof, {
|
|
|
- value: ethers.parseEther("0.5"),
|
|
|
+ launchpadContract.connect(buyer).contributeWithETH(buyAmount, maxBuyAmount, wrongProof, {
|
|
|
+ value: ethers.parseEther("1"),
|
|
|
})
|
|
|
).to.be.revertedWith("Invalid proof")
|
|
|
await expect(
|
|
|
- launchpadContract.connect(buyer).contributeETH(buyAmount, maxBuyAmount, proof, {
|
|
|
+ launchpadContract.connect(buyer).contributeWithETH(buyAmount, maxBuyAmount, proof, {
|
|
|
value: ethers.parseEther("0.5"),
|
|
|
})
|
|
|
- ).to.be.revertedWith("Not enough ETH sent")
|
|
|
+ ).to.be.revertedWith("Must send enough ETH")
|
|
|
await expect(
|
|
|
- launchpadContract.connect(buyer).contributeETH(ethers.parseEther("50"), maxBuyAmount, proof, {
|
|
|
+ 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).contributeETH(buyAmount, maxBuyAmount, proof, {
|
|
|
+ launchpadContract.connect(buyer).contributeWithETH(buyAmount, maxBuyAmount, proof, {
|
|
|
value: ethers.parseEther("1"),
|
|
|
})
|
|
|
).to.emit(launchpadContract, "Contributed")
|
|
|
//make it success twice
|
|
|
await expect(
|
|
|
- launchpadContract.connect(buyer).contributeETH(ethers.parseEther("4"), maxBuyAmount, proof, {
|
|
|
+ launchpadContract.connect(buyer).contributeWithETH(ethers.parseEther("4"), maxBuyAmount, proof, {
|
|
|
value: ethers.parseEther("4"),
|
|
|
})
|
|
|
).to.emit(launchpadContract, "Contributed")
|
|
@@ -202,13 +217,13 @@ describe("Launchpad", () => {
|
|
|
)
|
|
|
//failed by exceeding max buy amount
|
|
|
await expect(
|
|
|
- launchpadContract.connect(buyer).contributeETH(ethers.parseEther("10"), maxBuyAmount, proof, {
|
|
|
+ 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 createNativeSaleFixture()
|
|
|
+ const { launchpadContract, saleTokenContract, owner } = await loadFixture(createNativeSaleFixture)
|
|
|
|
|
|
const signers = await hre.ethers.getSigners()
|
|
|
const values = [
|
|
@@ -235,7 +250,7 @@ describe("Launchpad", () => {
|
|
|
await launchpadContract.setMerkleRoot(root, { from: owner })
|
|
|
await time.increase(100)
|
|
|
await expect(
|
|
|
- launchpadContract.connect(buyer).contributeETH(buyAmount, maxBuyAmount, proof, {
|
|
|
+ launchpadContract.connect(buyer).contributeWithETH(buyAmount, maxBuyAmount, proof, {
|
|
|
value: ethers.parseEther("1"),
|
|
|
})
|
|
|
).to.emit(launchpadContract, "Contributed")
|
|
@@ -243,14 +258,14 @@ describe("Launchpad", () => {
|
|
|
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)).to.emit(
|
|
|
+ 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 + 100) // increase time to finish sale
|
|
|
+ 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")
|
|
@@ -258,7 +273,7 @@ describe("Launchpad", () => {
|
|
|
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 createNativeSaleFixture()
|
|
|
+ const { launchpadContract, saleTokenContract, owner } = await loadFixture(createNativeSaleFixture)
|
|
|
|
|
|
const signers = await hre.ethers.getSigners()
|
|
|
const values = [
|
|
@@ -283,35 +298,399 @@ describe("Launchpad", () => {
|
|
|
}
|
|
|
}
|
|
|
await launchpadContract.setMerkleRoot(root, { from: owner })
|
|
|
+ const launchpadAddress = await launchpadContract.getAddress()
|
|
|
+
|
|
|
await time.increase(100)
|
|
|
|
|
|
- const launchpadAddress = await launchpadContract.getAddress()
|
|
|
await expect(
|
|
|
- launchpadContract.connect(buyer).contributeETH(buyAmount, maxBuyAmount, proof, {
|
|
|
+ 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)
|
|
|
+ await launchpadContract.enableClaimTokens(Math.floor(Date.now() / 1000) + 60 * 60 * 8)
|
|
|
|
|
|
- await time.increase(60 * 60 + 100) // increase time to finish sale
|
|
|
+ await time.increase(60 * 60 * 8 + 1) // increase time to finish sale
|
|
|
await launchpadContract.connect(buyer).claimTokens()
|
|
|
- //todo 写不动了 接下来测试 非 owner withdraw token/钱 owner 的 withdraw token/钱
|
|
|
await expect(launchpadContract.connect(signers[2]).withdrawPayments()).to.revertedWith(
|
|
|
"Only owner can call this function"
|
|
|
)
|
|
|
- await expect(launchpadContract.withdrawPayments()).to.emit(launchpadContract, "WithdrawPayments")
|
|
|
- expect(await ethers.provider.getBalance(launchpadAddress)).to.equal(0n)
|
|
|
await expect(launchpadContract.connect(signers[2]).withdrawRemainingTokens()).to.revertedWith(
|
|
|
"Only owner can call this function"
|
|
|
)
|
|
|
- await expect(launchpadContract.withdrawRemainingTokens()).to.emit(
|
|
|
- launchpadContract,
|
|
|
- "WithdrawRemainingTokens"
|
|
|
- )
|
|
|
+
|
|
|
+ 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())
|
|
|
+ }
|
|
|
+ })
|
|
|
})
|
|
|
})
|