const anchor = require("@coral-xyz/anchor"); const { assert } = require("chai"); const { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID, Token, } = require("@solana/spl-token"); const { sleep, getTokenAccount, createMint, createTokenAccount, } = require("./utils"); const { token } = require("@coral-xyz/anchor/dist/cjs/utils"); describe("ido-pool", () => { const provider = anchor.AnchorProvider.local(); // Configure the client to use the local cluster. anchor.setProvider(provider); const program = anchor.workspace.IdoPool; // All mints default to 6 decimal places. const watermelonIdoAmount = new anchor.BN(5000000); // These are all of the variables we assume exist in the world already and // are available to the client. let usdcMintAccount = null; let usdcMint = null; let watermelonMintAccount = null; let watermelonMint = null; let idoAuthorityUsdc = null; let idoAuthorityWatermelon = null; it("Initializes the state-of-the-world", async () => { usdcMintAccount = await createMint(provider); watermelonMintAccount = await createMint(provider); usdcMint = usdcMintAccount.publicKey; watermelonMint = watermelonMintAccount.publicKey; idoAuthorityUsdc = await createTokenAccount( provider, usdcMint, provider.wallet.publicKey ); idoAuthorityWatermelon = await createTokenAccount( provider, watermelonMint, provider.wallet.publicKey ); // Mint Watermelon tokens that will be distributed from the IDO pool. await watermelonMintAccount.mintTo( idoAuthorityWatermelon, provider.wallet.publicKey, [], watermelonIdoAmount.toString() ); idoAuthority_watermelon_account = await getTokenAccount( provider, idoAuthorityWatermelon ); assert.isTrue( idoAuthority_watermelon_account.amount.eq(watermelonIdoAmount) ); }); // These are all variables the client will need to create in order to // initialize the IDO pool let idoTimes; let idoName = "test_ido"; it("Initializes the IDO pool", async () => { let bumps = new PoolBumps(); const [idoAccount, idoAccountBump] = await anchor.web3.PublicKey.findProgramAddress( [Buffer.from(idoName)], program.programId ); bumps.idoAccount = idoAccountBump; const [redeemableMint, redeemableMintBump] = await anchor.web3.PublicKey.findProgramAddress( [Buffer.from(idoName), Buffer.from("redeemable_mint")], program.programId ); bumps.redeemableMint = redeemableMintBump; const [poolWatermelon, poolWatermelonBump] = await anchor.web3.PublicKey.findProgramAddress( [Buffer.from(idoName), Buffer.from("pool_watermelon")], program.programId ); bumps.poolWatermelon = poolWatermelonBump; const [poolUsdc, poolUsdcBump] = await anchor.web3.PublicKey.findProgramAddress( [Buffer.from(idoName), Buffer.from("pool_usdc")], program.programId ); bumps.poolUsdc = poolUsdcBump; idoTimes = new IdoTimes(); const nowBn = new anchor.BN(Date.now() / 1000); idoTimes.startIdo = nowBn.add(new anchor.BN(5)); idoTimes.endDeposits = nowBn.add(new anchor.BN(10)); idoTimes.endIdo = nowBn.add(new anchor.BN(15)); idoTimes.endEscrow = nowBn.add(new anchor.BN(16)); await program.rpc.initializePool( idoName, bumps, watermelonIdoAmount, idoTimes, { accounts: { idoAuthority: provider.wallet.publicKey, idoAuthorityWatermelon, idoAccount, watermelonMint, usdcMint, redeemableMint, poolWatermelon, poolUsdc, systemProgram: anchor.web3.SystemProgram.programId, tokenProgram: TOKEN_PROGRAM_ID, }, } ); idoAuthorityWatermelonAccount = await getTokenAccount( provider, idoAuthorityWatermelon ); assert.isTrue(idoAuthorityWatermelonAccount.amount.eq(new anchor.BN(0))); }); // We're going to need to start using the associated program account for creating token accounts // if not in testing, then definitely in production. let userUsdc = null; // 10 usdc const firstDeposit = new anchor.BN(10_000_349); it("Exchanges user USDC for redeemable tokens", async () => { // Wait until the IDO has opened. if (Date.now() < idoTimes.startIdo.toNumber() * 1000) { await sleep(idoTimes.startIdo.toNumber() * 1000 - Date.now() + 2000); } const [idoAccount] = await anchor.web3.PublicKey.findProgramAddress( [Buffer.from(idoName)], program.programId ); const [redeemableMint] = await anchor.web3.PublicKey.findProgramAddress( [Buffer.from(idoName), Buffer.from("redeemable_mint")], program.programId ); const [poolUsdc] = await anchor.web3.PublicKey.findProgramAddress( [Buffer.from(idoName), Buffer.from("pool_usdc")], program.programId ); userUsdc = await Token.getAssociatedTokenAddress( ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID, usdcMint, program.provider.wallet.publicKey ); // Get the instructions to add to the RPC call let createUserUsdcInstr = Token.createAssociatedTokenAccountInstruction( ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID, usdcMint, userUsdc, program.provider.wallet.publicKey, program.provider.wallet.publicKey ); let createUserUsdcTrns = new anchor.web3.Transaction().add( createUserUsdcInstr ); await provider.sendAndConfirm(createUserUsdcTrns); await usdcMintAccount.mintTo( userUsdc, provider.wallet.publicKey, [], firstDeposit.toString() ); // Check if we inited correctly userUsdcAccount = await getTokenAccount(provider, userUsdc); assert.isTrue(userUsdcAccount.amount.eq(firstDeposit)); const [userRedeemable] = await anchor.web3.PublicKey.findProgramAddress( [ provider.wallet.publicKey.toBuffer(), Buffer.from(idoName), Buffer.from("user_redeemable"), ], program.programId ); try { const tx = await program.rpc.exchangeUsdcForRedeemable(firstDeposit, { accounts: { userAuthority: provider.wallet.publicKey, userUsdc, userRedeemable, idoAccount, usdcMint, redeemableMint, watermelonMint, poolUsdc, tokenProgram: TOKEN_PROGRAM_ID, }, instructions: [ program.instruction.initUserRedeemable({ accounts: { userAuthority: provider.wallet.publicKey, userRedeemable, idoAccount, redeemableMint, systemProgram: anchor.web3.SystemProgram.programId, tokenProgram: TOKEN_PROGRAM_ID, }, }), ], }); } catch (err) { console.log("This is the error message", err.toString()); } poolUsdcAccount = await getTokenAccount(provider, poolUsdc); assert.isTrue(poolUsdcAccount.amount.eq(firstDeposit)); userRedeemableAccount = await getTokenAccount(provider, userRedeemable); assert.isTrue(userRedeemableAccount.amount.eq(firstDeposit)); }); // 23 usdc const secondDeposit = new anchor.BN(23_000_672); let totalPoolUsdc, secondUserKeypair, secondUserUsdc; it("Exchanges a second users USDC for redeemable tokens", async () => { const [idoAccount] = await anchor.web3.PublicKey.findProgramAddress( [Buffer.from(idoName)], program.programId ); const [redeemableMint] = await anchor.web3.PublicKey.findProgramAddress( [Buffer.from(idoName), Buffer.from("redeemable_mint")], program.programId ); const [poolUsdc] = await anchor.web3.PublicKey.findProgramAddress( [Buffer.from(idoName), Buffer.from("pool_usdc")], program.programId ); secondUserKeypair = anchor.web3.Keypair.generate(); transferSolInstr = anchor.web3.SystemProgram.transfer({ fromPubkey: provider.wallet.publicKey, lamports: 100_000_000_000, // 100 sol toPubkey: secondUserKeypair.publicKey, }); secondUserUsdc = await Token.getAssociatedTokenAddress( ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID, usdcMint, secondUserKeypair.publicKey ); createSecondUserUsdcInstr = Token.createAssociatedTokenAccountInstruction( ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID, usdcMint, secondUserUsdc, secondUserKeypair.publicKey, provider.wallet.publicKey ); let createSecondUserUsdcTrns = new anchor.web3.Transaction(); createSecondUserUsdcTrns.add(transferSolInstr); createSecondUserUsdcTrns.add(createSecondUserUsdcInstr); await provider.sendAndConfirm(createSecondUserUsdcTrns); await usdcMintAccount.mintTo( secondUserUsdc, provider.wallet.publicKey, [], secondDeposit.toString() ); // Checking the transfer went through secondUserUsdcAccount = await getTokenAccount(provider, secondUserUsdc); assert.isTrue(secondUserUsdcAccount.amount.eq(secondDeposit)); const [secondUserRedeemable] = await anchor.web3.PublicKey.findProgramAddress( [ secondUserKeypair.publicKey.toBuffer(), Buffer.from(idoName), Buffer.from("user_redeemable"), ], program.programId ); await program.rpc.exchangeUsdcForRedeemable(secondDeposit, { accounts: { userAuthority: secondUserKeypair.publicKey, userUsdc: secondUserUsdc, userRedeemable: secondUserRedeemable, idoAccount, usdcMint, redeemableMint, watermelonMint, poolUsdc, tokenProgram: TOKEN_PROGRAM_ID, }, instructions: [ program.instruction.initUserRedeemable({ accounts: { userAuthority: secondUserKeypair.publicKey, userRedeemable: secondUserRedeemable, idoAccount, redeemableMint, systemProgram: anchor.web3.SystemProgram.programId, tokenProgram: TOKEN_PROGRAM_ID, }, }), ], signers: [secondUserKeypair], }); secondUserRedeemableAccount = await getTokenAccount( provider, secondUserRedeemable ); assert.isTrue(secondUserRedeemableAccount.amount.eq(secondDeposit)); totalPoolUsdc = firstDeposit.add(secondDeposit); poolUsdcAccount = await getTokenAccount(provider, poolUsdc); assert.isTrue(poolUsdcAccount.amount.eq(totalPoolUsdc)); }); const firstWithdrawal = new anchor.BN(2_000_000); it("Exchanges user Redeemable tokens for USDC", async () => { const [idoAccount] = await anchor.web3.PublicKey.findProgramAddress( [Buffer.from(idoName)], program.programId ); const [redeemableMint] = await anchor.web3.PublicKey.findProgramAddress( [Buffer.from(idoName), Buffer.from("redeemable_mint")], program.programId ); const [poolUsdc] = await anchor.web3.PublicKey.findProgramAddress( [Buffer.from(idoName), Buffer.from("pool_usdc")], program.programId ); const [userRedeemable] = await anchor.web3.PublicKey.findProgramAddress( [ provider.wallet.publicKey.toBuffer(), Buffer.from(idoName), Buffer.from("user_redeemable"), ], program.programId ); const [escrowUsdc] = await anchor.web3.PublicKey.findProgramAddress( [ provider.wallet.publicKey.toBuffer(), Buffer.from(idoName), Buffer.from("escrow_usdc"), ], program.programId ); await program.rpc.exchangeRedeemableForUsdc(firstWithdrawal, { accounts: { userAuthority: provider.wallet.publicKey, escrowUsdc, userRedeemable, idoAccount, usdcMint, redeemableMint, watermelonMint, poolUsdc, tokenProgram: TOKEN_PROGRAM_ID, }, instructions: [ program.instruction.initEscrowUsdc({ accounts: { userAuthority: provider.wallet.publicKey, escrowUsdc, idoAccount, usdcMint, systemProgram: anchor.web3.SystemProgram.programId, tokenProgram: TOKEN_PROGRAM_ID, }, }), ], }); totalPoolUsdc = totalPoolUsdc.sub(firstWithdrawal); poolUsdcAccount = await getTokenAccount(provider, poolUsdc); assert.isTrue(poolUsdcAccount.amount.eq(totalPoolUsdc)); escrowUsdcAccount = await getTokenAccount(provider, escrowUsdc); assert.isTrue(escrowUsdcAccount.amount.eq(firstWithdrawal)); }); it("Exchanges user Redeemable tokens for watermelon", async () => { // Wait until the IDO has ended. if (Date.now() < idoTimes.endIdo.toNumber() * 1000) { await sleep(idoTimes.endIdo.toNumber() * 1000 - Date.now() + 3000); } const [idoAccount] = await anchor.web3.PublicKey.findProgramAddress( [Buffer.from(idoName)], program.programId ); const [poolWatermelon] = await anchor.web3.PublicKey.findProgramAddress( [Buffer.from(idoName), Buffer.from("pool_watermelon")], program.programId ); const [redeemableMint] = await anchor.web3.PublicKey.findProgramAddress( [Buffer.from(idoName), Buffer.from("redeemable_mint")], program.programId ); const [userRedeemable] = await anchor.web3.PublicKey.findProgramAddress( [ provider.wallet.publicKey.toBuffer(), Buffer.from(idoName), Buffer.from("user_redeemable"), ], program.programId ); let firstUserRedeemable = firstDeposit.sub(firstWithdrawal); // TODO we've been lazy here and not used an ATA as we did with USDC userWatermelon = await createTokenAccount( provider, watermelonMint, provider.wallet.publicKey ); await program.rpc.exchangeRedeemableForWatermelon(firstUserRedeemable, { accounts: { payer: provider.wallet.publicKey, userAuthority: provider.wallet.publicKey, userWatermelon, userRedeemable, idoAccount, watermelonMint, redeemableMint, poolWatermelon, tokenProgram: TOKEN_PROGRAM_ID, }, }); poolWatermelonAccount = await getTokenAccount(provider, poolWatermelon); let redeemedWatermelon = firstUserRedeemable .mul(watermelonIdoAmount) .div(totalPoolUsdc); let remainingWatermelon = watermelonIdoAmount.sub(redeemedWatermelon); assert.isTrue(poolWatermelonAccount.amount.eq(remainingWatermelon)); userWatermelonAccount = await getTokenAccount(provider, userWatermelon); assert.isTrue(userWatermelonAccount.amount.eq(redeemedWatermelon)); }); it("Exchanges second user's Redeemable tokens for watermelon", async () => { const [idoAccount] = await anchor.web3.PublicKey.findProgramAddress( [Buffer.from(idoName)], program.programId ); const [redeemableMint] = await anchor.web3.PublicKey.findProgramAddress( [Buffer.from(idoName), Buffer.from("redeemable_mint")], program.programId ); const [secondUserRedeemable] = await anchor.web3.PublicKey.findProgramAddress( [ secondUserKeypair.publicKey.toBuffer(), Buffer.from(idoName), Buffer.from("user_redeemable"), ], program.programId ); const [poolWatermelon] = await anchor.web3.PublicKey.findProgramAddress( [Buffer.from(idoName), Buffer.from("pool_watermelon")], program.programId ); secondUserWatermelon = await createTokenAccount( provider, watermelonMint, secondUserKeypair.publicKey ); await program.rpc.exchangeRedeemableForWatermelon(secondDeposit, { accounts: { payer: provider.wallet.publicKey, userAuthority: secondUserKeypair.publicKey, userWatermelon: secondUserWatermelon, userRedeemable: secondUserRedeemable, idoAccount, watermelonMint, redeemableMint, poolWatermelon, tokenProgram: TOKEN_PROGRAM_ID, }, }); poolWatermelonAccount = await getTokenAccount(provider, poolWatermelon); assert.isTrue(poolWatermelonAccount.amount.eq(new anchor.BN(0))); }); it("Withdraws total USDC from pool account", async () => { const [idoAccount] = await anchor.web3.PublicKey.findProgramAddress( [Buffer.from(idoName)], program.programId ); const [poolUsdc] = await anchor.web3.PublicKey.findProgramAddress( [Buffer.from(idoName), Buffer.from("pool_usdc")], program.programId ); await program.rpc.withdrawPoolUsdc({ accounts: { idoAuthority: provider.wallet.publicKey, idoAuthorityUsdc, idoAccount, usdcMint, watermelonMint, poolUsdc, tokenProgram: TOKEN_PROGRAM_ID, }, }); poolUsdcAccount = await getTokenAccount(provider, poolUsdc); assert.isTrue(poolUsdcAccount.amount.eq(new anchor.BN(0))); idoAuthorityUsdcAccount = await getTokenAccount(provider, idoAuthorityUsdc); assert.isTrue(idoAuthorityUsdcAccount.amount.eq(totalPoolUsdc)); }); it("Withdraws USDC from the escrow account after waiting period is over", async () => { // Wait until the escrow period is over. if (Date.now() < idoTimes.endEscrow.toNumber() * 1000 + 1000) { await sleep(idoTimes.endEscrow.toNumber() * 1000 - Date.now() + 4000); } const [idoAccount] = await anchor.web3.PublicKey.findProgramAddress( [Buffer.from(idoName)], program.programId ); const [escrowUsdc] = await anchor.web3.PublicKey.findProgramAddress( [ provider.wallet.publicKey.toBuffer(), Buffer.from(idoName), Buffer.from("escrow_usdc"), ], program.programId ); await program.rpc.withdrawFromEscrow(firstWithdrawal, { accounts: { payer: provider.wallet.publicKey, userAuthority: provider.wallet.publicKey, userUsdc, escrowUsdc, idoAccount, usdcMint, tokenProgram: TOKEN_PROGRAM_ID, }, }); userUsdcAccount = await getTokenAccount(provider, userUsdc); assert.isTrue(userUsdcAccount.amount.eq(firstWithdrawal)); }); function PoolBumps() { this.idoAccount; this.redeemableMint; this.poolWatermelon; this.poolUsdc; } function IdoTimes() { this.startIdo; this.endDeposts; this.endIdo; this.endEscrow; } });