|
@@ -0,0 +1,317 @@
|
|
|
+import { expect } from "chai"
|
|
|
+import hre, { ethers } from "hardhat"
|
|
|
+import { StandardMerkleTree } from "@openzeppelin/merkle-tree"
|
|
|
+import { time } from "@nomicfoundation/hardhat-toolbox/network-helpers"
|
|
|
+
|
|
|
+describe("Launchpad", () => {
|
|
|
+ async function deployLaunchpadFixture() {
|
|
|
+ const signers = await hre.ethers.getSigners()
|
|
|
+ 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(signers[0].address, ethers.parseEther("1000000"))
|
|
|
+ await saleTokenContract.approve(launchpadAddress, ethers.MaxUint256)
|
|
|
+ await buyTokenContract.mint(signers[1].address, ethers.parseEther("10000"))
|
|
|
+
|
|
|
+ return {
|
|
|
+ launchpadContract,
|
|
|
+ saleTokenContract,
|
|
|
+ buyTokenContract,
|
|
|
+ owner,
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async function createNativeSaleFixture() {
|
|
|
+ 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 paymentTokens = ethers.ZeroAddress
|
|
|
+ const paymentTokenPrice = ethers.parseEther("1")
|
|
|
+
|
|
|
+ await launchpadContract.createSale(
|
|
|
+ saleTokenAddress,
|
|
|
+ balanceBefore,
|
|
|
+ startTime,
|
|
|
+ endTime,
|
|
|
+ paymentTokens,
|
|
|
+ paymentTokenPrice
|
|
|
+ )
|
|
|
+
|
|
|
+ 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 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 paymentTokens = ethers.ZeroAddress
|
|
|
+ const paymentTokenPrice = ethers.parseEther("1") / 2500n
|
|
|
+ await launchpadContract.createSale(
|
|
|
+ saleTokenAddress,
|
|
|
+ balanceBefore,
|
|
|
+ startTime,
|
|
|
+ endTime,
|
|
|
+ paymentTokens,
|
|
|
+ paymentTokenPrice
|
|
|
+ )
|
|
|
+ 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.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)
|
|
|
+ })
|
|
|
+
|
|
|
+ it("should Allow Owner reset payment info", async () => {
|
|
|
+ const { launchpadContract, saleTokenContract, owner } = await 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 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 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).contributeETH(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, {
|
|
|
+ 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"),
|
|
|
+ })
|
|
|
+ ).to.be.revertedWith("Invalid proof")
|
|
|
+ await expect(
|
|
|
+ launchpadContract.connect(buyer).contributeETH(buyAmount, maxBuyAmount, proof, {
|
|
|
+ value: ethers.parseEther("0.5"),
|
|
|
+ })
|
|
|
+ ).to.be.revertedWith("Not enough ETH sent")
|
|
|
+ await expect(
|
|
|
+ launchpadContract.connect(buyer).contributeETH(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, {
|
|
|
+ value: ethers.parseEther("1"),
|
|
|
+ })
|
|
|
+ ).to.emit(launchpadContract, "Contributed")
|
|
|
+ //make it success twice
|
|
|
+ await expect(
|
|
|
+ launchpadContract.connect(buyer).contributeETH(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).contributeETH(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 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).contributeETH(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)).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 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 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)
|
|
|
+
|
|
|
+ const launchpadAddress = await launchpadContract.getAddress()
|
|
|
+ await expect(
|
|
|
+ launchpadContract.connect(buyer).contributeETH(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 time.increase(60 * 60 + 100) // 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"
|
|
|
+ )
|
|
|
+ expect(await saleTokenContract.balanceOf(launchpadAddress)).to.equal(0)
|
|
|
+ })
|
|
|
+ })
|
|
|
+})
|