Selaa lähdekoodia

examples: IDO pool (#220)

Henry-E 4 vuotta sitten
vanhempi
sitoutus
31662b95e8

+ 1 - 0
.travis.yml

@@ -64,6 +64,7 @@ jobs:
       name: Runs the examples 2
       script:
         - pushd examples/chat && yarn && anchor test && popd
+        - pushd examples/ido-pool && yarn && anchor test && popd
         - pushd examples/tutorial/basic-0 && anchor test && popd
         - pushd examples/tutorial/basic-1 && anchor test && popd
         - pushd examples/tutorial/basic-2 && anchor test && popd

+ 2 - 0
examples/ido-pool/Anchor.toml

@@ -0,0 +1,2 @@
+cluster = "localnet"
+wallet = "~/.config/solana/id.json"

+ 4 - 0
examples/ido-pool/Cargo.toml

@@ -0,0 +1,4 @@
+[workspace]
+members = [
+    "programs/*"
+]

+ 12 - 0
examples/ido-pool/migrations/deploy.js

@@ -0,0 +1,12 @@
+// Migrations are an early feature. Currently, they're nothing more than this
+// single deploy script that's invoked from the CLI, injecting a provider
+// configured from the workspace's Anchor.toml.
+
+const anchor = require("@project-serum/anchor");
+
+module.exports = async function (provider) {
+  // Configure client to use the provider.
+  anchor.setProvider(provider);
+
+  // Add your deploy script here.
+}

+ 19 - 0
examples/ido-pool/programs/ido-pool/Cargo.toml

@@ -0,0 +1,19 @@
+[package]
+name = "ido-pool"
+version = "0.1.0"
+description = "Created with Anchor"
+edition = "2018"
+
+[lib]
+crate-type = ["cdylib", "lib"]
+name = "ido_pool"
+
+[features]
+no-entrypoint = []
+no-idl = []
+cpi = ["no-entrypoint"]
+default = []
+
+[dependencies]
+anchor-lang = "0.4.4"
+anchor-spl = "0.4.4"

+ 2 - 0
examples/ido-pool/programs/ido-pool/Xargo.toml

@@ -0,0 +1,2 @@
+[target.bpfel-unknown-unknown.dependencies.std]
+features = []

+ 393 - 0
examples/ido-pool/programs/ido-pool/src/lib.rs

@@ -0,0 +1,393 @@
+//! An IDO pool program implementing the Mango Markets token sale design here:
+//! https://docs.mango.markets/litepaper#token-sale.
+
+use anchor_lang::prelude::*;
+use anchor_lang::solana_program::program_option::COption;
+use anchor_spl::token::{self, Burn, Mint, MintTo, TokenAccount, Transfer};
+
+#[program]
+pub mod ido_pool {
+    use super::*;
+
+    #[access_control(InitializePool::accounts(&ctx, nonce))]
+    pub fn initialize_pool(
+        ctx: Context<InitializePool>,
+        num_ido_tokens: u64,
+        nonce: u8,
+        start_ido_ts: i64,
+        end_deposits_ts: i64,
+        end_ido_ts: i64,
+    ) -> Result<()> {
+        if !(ctx.accounts.clock.unix_timestamp < start_ido_ts
+            && start_ido_ts < end_deposits_ts
+            && end_deposits_ts <= end_ido_ts)
+        {
+            return Err(ErrorCode::InitTime.into());
+        }
+
+        let pool_account = &mut ctx.accounts.pool_account;
+        pool_account.redeemable_mint = *ctx.accounts.redeemable_mint.to_account_info().key;
+        pool_account.pool_watermelon = *ctx.accounts.pool_watermelon.to_account_info().key;
+        pool_account.watermelon_mint = ctx.accounts.pool_watermelon.mint;
+        pool_account.pool_usdc = *ctx.accounts.pool_usdc.to_account_info().key;
+        pool_account.distribution_authority = *ctx.accounts.distribution_authority.key;
+        pool_account.nonce = nonce;
+        pool_account.num_ido_tokens = num_ido_tokens;
+        pool_account.start_ido_ts = start_ido_ts;
+        pool_account.end_deposits_ts = end_deposits_ts;
+        pool_account.end_ido_ts = end_ido_ts;
+
+        msg!(
+            "pool usdc owner: {}, pool signer key: {}",
+            ctx.accounts.pool_usdc.owner,
+            ctx.accounts.pool_signer.key
+        );
+
+        // Transfer Watermelon from creator to pool account.
+        let cpi_accounts = Transfer {
+            from: ctx.accounts.creator_watermelon.to_account_info(),
+            to: ctx.accounts.pool_watermelon.to_account_info(),
+            authority: ctx.accounts.distribution_authority.to_account_info(),
+        };
+        let cpi_program = ctx.accounts.token_program.clone();
+        let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
+        token::transfer(cpi_ctx, num_ido_tokens)?;
+
+        Ok(())
+    }
+
+    #[access_control(unrestricted_phase(&ctx))]
+    pub fn exchange_usdc_for_redeemable(
+        ctx: Context<ExchangeUsdcForRedeemable>,
+        amount: u64,
+    ) -> Result<()> {
+        // While token::transfer will check this, we prefer a verbose err msg.
+        if ctx.accounts.user_usdc.amount < amount {
+            return Err(ErrorCode::LowUsdc.into());
+        }
+
+        // Transfer user's USDC to pool USDC account.
+        let cpi_accounts = Transfer {
+            from: ctx.accounts.user_usdc.to_account_info(),
+            to: ctx.accounts.pool_usdc.to_account_info(),
+            authority: ctx.accounts.user_authority.clone(),
+        };
+        let cpi_program = ctx.accounts.token_program.to_account_info();
+        let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
+        token::transfer(cpi_ctx, amount)?;
+
+        // Mint Redeemable to user Redeemable account.
+        let seeds = &[
+            ctx.accounts.pool_account.watermelon_mint.as_ref(),
+            &[ctx.accounts.pool_account.nonce],
+        ];
+        let signer = &[&seeds[..]];
+        let cpi_accounts = MintTo {
+            mint: ctx.accounts.redeemable_mint.to_account_info(),
+            to: ctx.accounts.user_redeemable.to_account_info(),
+            authority: ctx.accounts.pool_signer.clone(),
+        };
+        let cpi_program = ctx.accounts.token_program.clone();
+        let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer);
+        token::mint_to(cpi_ctx, amount)?;
+
+        Ok(())
+    }
+
+    #[access_control(withdraw_only_phase(&ctx))]
+    pub fn exchange_redeemable_for_usdc(
+        ctx: Context<ExchangeRedeemableForUsdc>,
+        amount: u64,
+    ) -> Result<()> {
+        // While token::burn will check this, we prefer a verbose err msg.
+        if ctx.accounts.user_redeemable.amount < amount {
+            return Err(ErrorCode::LowRedeemable.into());
+        }
+
+        // Burn the user's redeemable tokens.
+        let cpi_accounts = Burn {
+            mint: ctx.accounts.redeemable_mint.to_account_info(),
+            to: ctx.accounts.user_redeemable.to_account_info(),
+            authority: ctx.accounts.user_authority.to_account_info(),
+        };
+        let cpi_program = ctx.accounts.token_program.clone();
+        let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
+        token::burn(cpi_ctx, amount)?;
+
+        // Transfer USDC from pool account to user.
+        let seeds = &[
+            ctx.accounts.pool_account.watermelon_mint.as_ref(),
+            &[ctx.accounts.pool_account.nonce],
+        ];
+        let signer = &[&seeds[..]];
+        let cpi_accounts = Transfer {
+            from: ctx.accounts.pool_usdc.to_account_info(),
+            to: ctx.accounts.user_usdc.to_account_info(),
+            authority: ctx.accounts.pool_signer.to_account_info(),
+        };
+        let cpi_program = ctx.accounts.token_program.clone();
+        let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer);
+        token::transfer(cpi_ctx, amount)?;
+
+        Ok(())
+    }
+
+    #[access_control(ido_over(&ctx.accounts.pool_account, &ctx.accounts.clock))]
+    pub fn exchange_redeemable_for_watermelon(
+        ctx: Context<ExchangeRedeemableForWatermelon>,
+        amount: u64,
+    ) -> Result<()> {
+        // While token::burn will check this, we prefer a verbose err msg.
+        if ctx.accounts.user_redeemable.amount < amount {
+            return Err(ErrorCode::LowRedeemable.into());
+        }
+
+        let watermelon_amount = (amount as u128)
+            .checked_mul(ctx.accounts.pool_watermelon.amount as u128)
+            .unwrap()
+            .checked_div(ctx.accounts.redeemable_mint.supply as u128)
+            .unwrap();
+
+        // Burn the user's redeemable tokens.
+        let cpi_accounts = Burn {
+            mint: ctx.accounts.redeemable_mint.to_account_info(),
+            to: ctx.accounts.user_redeemable.to_account_info(),
+            authority: ctx.accounts.user_authority.to_account_info(),
+        };
+        let cpi_program = ctx.accounts.token_program.clone();
+        let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
+        token::burn(cpi_ctx, amount)?;
+
+        // Transfer Watermelon from pool account to user.
+        let seeds = &[
+            ctx.accounts.pool_account.watermelon_mint.as_ref(),
+            &[ctx.accounts.pool_account.nonce],
+        ];
+        let signer = &[&seeds[..]];
+        let cpi_accounts = Transfer {
+            from: ctx.accounts.pool_watermelon.to_account_info(),
+            to: ctx.accounts.user_watermelon.to_account_info(),
+            authority: ctx.accounts.pool_signer.to_account_info(),
+        };
+        let cpi_program = ctx.accounts.token_program.clone();
+        let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer);
+        token::transfer(cpi_ctx, watermelon_amount as u64)?;
+
+        Ok(())
+    }
+
+    #[access_control(ido_over(&ctx.accounts.pool_account, &ctx.accounts.clock))]
+    pub fn withdraw_pool_usdc(ctx: Context<WithdrawPoolUsdc>) -> Result<()> {
+        // Transfer total USDC from pool account to creator account.
+        let seeds = &[
+            ctx.accounts.pool_account.watermelon_mint.as_ref(),
+            &[ctx.accounts.pool_account.nonce],
+        ];
+        let signer = &[&seeds[..]];
+        let cpi_accounts = Transfer {
+            from: ctx.accounts.pool_usdc.to_account_info(),
+            to: ctx.accounts.creator_usdc.to_account_info(),
+            authority: ctx.accounts.pool_signer.to_account_info(),
+        };
+        let cpi_program = ctx.accounts.token_program.clone();
+        let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer);
+        token::transfer(cpi_ctx, ctx.accounts.pool_usdc.amount)?;
+
+        Ok(())
+    }
+}
+
+#[derive(Accounts)]
+pub struct InitializePool<'info> {
+    #[account(init)]
+    pub pool_account: ProgramAccount<'info, PoolAccount>,
+    pub pool_signer: AccountInfo<'info>,
+    #[account("redeemable_mint.mint_authority == COption::Some(*pool_signer.key)")]
+    pub redeemable_mint: CpiAccount<'info, Mint>,
+    #[account("usdc_mint.decimals == redeemable_mint.decimals")]
+    pub usdc_mint: CpiAccount<'info, Mint>,
+    #[account(mut, "pool_watermelon.owner == *pool_signer.key")]
+    pub pool_watermelon: CpiAccount<'info, TokenAccount>,
+    #[account("pool_usdc.owner == *pool_signer.key")]
+    pub pool_usdc: CpiAccount<'info, TokenAccount>,
+    #[account(signer)]
+    pub distribution_authority: AccountInfo<'info>,
+    #[account(mut, "creator_watermelon.owner == *distribution_authority.key")]
+    pub creator_watermelon: CpiAccount<'info, TokenAccount>,
+    #[account("token_program.key == &token::ID")]
+    pub token_program: AccountInfo<'info>,
+    pub rent: Sysvar<'info, Rent>,
+    pub clock: Sysvar<'info, Clock>,
+}
+
+impl<'info> InitializePool<'info> {
+    fn accounts(ctx: &Context<InitializePool<'info>>, nonce: u8) -> Result<()> {
+        let expected_signer = Pubkey::create_program_address(
+            &[ctx.accounts.pool_watermelon.mint.as_ref(), &[nonce]],
+            ctx.program_id,
+        )
+        .map_err(|_| ErrorCode::InvalidNonce)?;
+        if ctx.accounts.pool_signer.key != &expected_signer {
+            return Err(ErrorCode::InvalidNonce.into());
+        }
+        Ok(())
+    }
+}
+
+#[derive(Accounts)]
+pub struct ExchangeUsdcForRedeemable<'info> {
+    #[account(has_one = redeemable_mint, has_one = pool_usdc)]
+    pub pool_account: ProgramAccount<'info, PoolAccount>,
+    #[account(seeds = [pool_account.watermelon_mint.as_ref(), &[pool_account.nonce]])]
+    pool_signer: AccountInfo<'info>,
+    #[account(
+        mut,
+        "redeemable_mint.mint_authority == COption::Some(*pool_signer.key)"
+    )]
+    pub redeemable_mint: CpiAccount<'info, Mint>,
+    #[account(mut, "pool_usdc.owner == *pool_signer.key")]
+    pub pool_usdc: CpiAccount<'info, TokenAccount>,
+    #[account(signer)]
+    pub user_authority: AccountInfo<'info>,
+    #[account(mut, "user_usdc.owner == *user_authority.key")]
+    pub user_usdc: CpiAccount<'info, TokenAccount>,
+    #[account(mut, "user_redeemable.owner == *user_authority.key")]
+    pub user_redeemable: CpiAccount<'info, TokenAccount>,
+    #[account("token_program.key == &token::ID")]
+    pub token_program: AccountInfo<'info>,
+    pub clock: Sysvar<'info, Clock>,
+}
+
+#[derive(Accounts)]
+pub struct ExchangeRedeemableForUsdc<'info> {
+    #[account(has_one = redeemable_mint, has_one = pool_usdc)]
+    pub pool_account: ProgramAccount<'info, PoolAccount>,
+    #[account(seeds = [pool_account.watermelon_mint.as_ref(), &[pool_account.nonce]])]
+    pool_signer: AccountInfo<'info>,
+    #[account(
+        mut,
+        "redeemable_mint.mint_authority == COption::Some(*pool_signer.key)"
+    )]
+    pub redeemable_mint: CpiAccount<'info, Mint>,
+    #[account(mut, "pool_usdc.owner == *pool_signer.key")]
+    pub pool_usdc: CpiAccount<'info, TokenAccount>,
+    #[account(signer)]
+    pub user_authority: AccountInfo<'info>,
+    #[account(mut, "user_usdc.owner == *user_authority.key")]
+    pub user_usdc: CpiAccount<'info, TokenAccount>,
+    #[account(mut, "user_redeemable.owner == *user_authority.key")]
+    pub user_redeemable: CpiAccount<'info, TokenAccount>,
+    #[account("token_program.key == &token::ID")]
+    pub token_program: AccountInfo<'info>,
+    pub clock: Sysvar<'info, Clock>,
+}
+
+#[derive(Accounts)]
+pub struct ExchangeRedeemableForWatermelon<'info> {
+    #[account(has_one = redeemable_mint, has_one = pool_watermelon)]
+    pub pool_account: ProgramAccount<'info, PoolAccount>,
+    #[account(seeds = [pool_account.watermelon_mint.as_ref(), &[pool_account.nonce]])]
+    pool_signer: AccountInfo<'info>,
+    #[account(
+        mut,
+        "redeemable_mint.mint_authority == COption::Some(*pool_signer.key)"
+    )]
+    pub redeemable_mint: CpiAccount<'info, Mint>,
+    #[account(mut, "pool_watermelon.owner == *pool_signer.key")]
+    pub pool_watermelon: CpiAccount<'info, TokenAccount>,
+    #[account(signer)]
+    pub user_authority: AccountInfo<'info>,
+    #[account(mut, "user_watermelon.owner == *user_authority.key")]
+    pub user_watermelon: CpiAccount<'info, TokenAccount>,
+    #[account(mut, "user_redeemable.owner == *user_authority.key")]
+    pub user_redeemable: CpiAccount<'info, TokenAccount>,
+    #[account("token_program.key == &token::ID")]
+    pub token_program: AccountInfo<'info>,
+    pub clock: Sysvar<'info, Clock>,
+}
+
+#[derive(Accounts)]
+pub struct WithdrawPoolUsdc<'info> {
+    #[account(has_one = pool_usdc, has_one = distribution_authority)]
+    pub pool_account: ProgramAccount<'info, PoolAccount>,
+    #[account(seeds = [pool_account.watermelon_mint.as_ref(), &[pool_account.nonce]])]
+    pub pool_signer: AccountInfo<'info>,
+    #[account(mut, "pool_usdc.owner == *pool_signer.key")]
+    pub pool_usdc: CpiAccount<'info, TokenAccount>,
+    #[account(signer)]
+    pub distribution_authority: AccountInfo<'info>,
+    #[account(mut, "creator_usdc.owner == *distribution_authority.key")]
+    pub creator_usdc: CpiAccount<'info, TokenAccount>,
+    #[account("token_program.key == &token::ID")]
+    pub token_program: AccountInfo<'info>,
+    pub clock: Sysvar<'info, Clock>,
+}
+
+#[account]
+pub struct PoolAccount {
+    pub redeemable_mint: Pubkey,
+    pub pool_watermelon: Pubkey,
+    pub watermelon_mint: Pubkey,
+    pub pool_usdc: Pubkey,
+    pub distribution_authority: Pubkey,
+    pub nonce: u8,
+    pub num_ido_tokens: u64,
+    pub start_ido_ts: i64,
+    pub end_deposits_ts: i64,
+    pub end_ido_ts: i64,
+}
+
+#[error]
+pub enum ErrorCode {
+    #[msg("IDO times are non-sequential")]
+    InitTime,
+    #[msg("IDO has not started")]
+    StartIdoTime,
+    #[msg("Deposits period has ended")]
+    EndDepositsTime,
+    #[msg("IDO has ended")]
+    EndIdoTime,
+    #[msg("IDO has not finished yet")]
+    IdoNotOver,
+    #[msg("Insufficient USDC")]
+    LowUsdc,
+    #[msg("Insufficient redeemable tokens")]
+    LowRedeemable,
+    #[msg("USDC total and redeemable total don't match")]
+    UsdcNotEqRedeem,
+    #[msg("Given nonce is invalid")]
+    InvalidNonce,
+}
+
+// Access control modifiers.
+
+// Asserts the IDO is in the first phase.
+fn unrestricted_phase<'info>(ctx: &Context<ExchangeUsdcForRedeemable<'info>>) -> Result<()> {
+    if !(ctx.accounts.pool_account.start_ido_ts < ctx.accounts.clock.unix_timestamp) {
+        return Err(ErrorCode::StartIdoTime.into());
+    } else if !(ctx.accounts.clock.unix_timestamp < ctx.accounts.pool_account.end_deposits_ts) {
+        return Err(ErrorCode::EndDepositsTime.into());
+    }
+    Ok(())
+}
+
+// Asserts the IDO is in the second phase.
+fn withdraw_only_phase(ctx: &Context<ExchangeRedeemableForUsdc>) -> Result<()> {
+    if !(ctx.accounts.pool_account.start_ido_ts < ctx.accounts.clock.unix_timestamp) {
+        return Err(ErrorCode::StartIdoTime.into());
+    } else if !(ctx.accounts.clock.unix_timestamp < ctx.accounts.pool_account.end_ido_ts) {
+        return Err(ErrorCode::EndIdoTime.into());
+    }
+    Ok(())
+}
+
+// Asserts the IDO sale period has ended, based on the current timestamp.
+fn ido_over<'info>(
+    pool_account: &ProgramAccount<'info, PoolAccount>,
+    clock: &Sysvar<'info, Clock>,
+) -> Result<()> {
+    if !(pool_account.end_ido_ts < clock.unix_timestamp) {
+        return Err(ErrorCode::IdoNotOver.into());
+    }
+    Ok(())
+}

+ 335 - 0
examples/ido-pool/tests/ido-pool.js

@@ -0,0 +1,335 @@
+const anchor = require("@project-serum/anchor");
+const assert = require("assert");
+const {
+  TOKEN_PROGRAM_ID,
+  sleep,
+  getTokenAccount,
+  createMint,
+  createTokenAccount,
+  mintToAccount,
+} = require("./utils");
+
+describe("ido-pool", () => {
+  const provider = anchor.Provider.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 usdcMint = null;
+  let watermelonMint = null;
+  let creatorUsdc = null;
+  let creatorWatermelon = null;
+
+  it("Initializes the state-of-the-world", async () => {
+    usdcMint = await createMint(provider);
+    watermelonMint = await createMint(provider);
+    creatorUsdc = await createTokenAccount(
+      provider,
+      usdcMint,
+      provider.wallet.publicKey
+    );
+    creatorWatermelon = await createTokenAccount(
+      provider,
+      watermelonMint,
+      provider.wallet.publicKey
+    );
+    // Mint Watermelon tokens the will be distributed from the IDO pool.
+    await mintToAccount(
+      provider,
+      watermelonMint,
+      creatorWatermelon,
+      watermelonIdoAmount,
+      provider.wallet.publicKey
+    );
+    creator_watermelon_account = await getTokenAccount(
+      provider,
+      creatorWatermelon
+    );
+    assert.ok(creator_watermelon_account.amount.eq(watermelonIdoAmount));
+  });
+
+  // These are all variables the client will have to create to initialize the
+  // IDO pool
+  let poolSigner = null;
+  let redeemableMint = null;
+  let poolWatermelon = null;
+  let poolUsdc = null;
+  let poolAccount = null;
+
+  let startIdoTs = null;
+  let endDepositsTs = null;
+  let endIdoTs = null;
+
+  it("Initializes the IDO pool", async () => {
+    // We use the watermelon mint address as the seed, could use something else though.
+    const [_poolSigner, nonce] = await anchor.web3.PublicKey.findProgramAddress(
+      [watermelonMint.toBuffer()],
+      program.programId
+    );
+    poolSigner = _poolSigner;
+
+    // Pool doesn't need a Redeemable SPL token account because it only
+    // burns and mints redeemable tokens, it never stores them.
+    redeemableMint = await createMint(provider, poolSigner);
+    poolWatermelon = await createTokenAccount(
+      provider,
+      watermelonMint,
+      poolSigner
+    );
+    poolUsdc = await createTokenAccount(provider, usdcMint, poolSigner);
+
+    poolAccount = new anchor.web3.Account();
+    const nowBn = new anchor.BN(Date.now() / 1000);
+    startIdoTs = nowBn.add(new anchor.BN(5));
+    endDepositsTs = nowBn.add(new anchor.BN(10));
+    endIdoTs = nowBn.add(new anchor.BN(15));
+
+    // Atomically create the new account and initialize it with the program.
+    await program.rpc.initializePool(
+      watermelonIdoAmount,
+      nonce,
+      startIdoTs,
+      endDepositsTs,
+      endIdoTs,
+      {
+        accounts: {
+          poolAccount: poolAccount.publicKey,
+          poolSigner,
+          distributionAuthority: provider.wallet.publicKey,
+          creatorWatermelon,
+          creatorUsdc,
+          redeemableMint,
+          usdcMint,
+          poolWatermelon,
+          poolUsdc,
+          tokenProgram: TOKEN_PROGRAM_ID,
+          rent: anchor.web3.SYSVAR_RENT_PUBKEY,
+          clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
+        },
+        signers: [poolAccount],
+        instructions: [
+          await program.account.poolAccount.createInstruction(poolAccount),
+        ],
+      }
+    );
+
+    creators_watermelon_account = await getTokenAccount(
+      provider,
+      creatorWatermelon
+    );
+    assert.ok(creators_watermelon_account.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;
+  let userRedeemable = 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() < startIdoTs.toNumber() * 1000) {
+      await sleep(startIdoTs.toNumber() * 1000 - Date.now() + 1000);
+    }
+
+    userUsdc = await createTokenAccount(
+      provider,
+      usdcMint,
+      provider.wallet.publicKey
+    );
+    await mintToAccount(
+      provider,
+      usdcMint,
+      userUsdc,
+      firstDeposit,
+      provider.wallet.publicKey
+    );
+    userRedeemable = await createTokenAccount(
+      provider,
+      redeemableMint,
+      provider.wallet.publicKey
+    );
+
+    try {
+      const tx = await program.rpc.exchangeUsdcForRedeemable(firstDeposit, {
+        accounts: {
+          poolAccount: poolAccount.publicKey,
+          poolSigner,
+          redeemableMint,
+          poolUsdc,
+          userAuthority: provider.wallet.publicKey,
+          userUsdc,
+          userRedeemable,
+          tokenProgram: TOKEN_PROGRAM_ID,
+          clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
+        },
+      });
+    } catch (err) {
+      console.log("This is the error message", err.toString());
+    }
+    poolUsdcAccount = await getTokenAccount(provider, poolUsdc);
+    assert.ok(poolUsdcAccount.amount.eq(firstDeposit));
+    userRedeemableAccount = await getTokenAccount(provider, userRedeemable);
+    assert.ok(userRedeemableAccount.amount.eq(firstDeposit));
+  });
+
+  // 23 usdc
+  const secondDeposit = new anchor.BN(23_000_672);
+  let totalPoolUsdc = null;
+
+  it("Exchanges a second users USDC for redeemable tokens", async () => {
+    secondUserUsdc = await createTokenAccount(
+      provider,
+      usdcMint,
+      provider.wallet.publicKey
+    );
+    await mintToAccount(
+      provider,
+      usdcMint,
+      secondUserUsdc,
+      secondDeposit,
+      provider.wallet.publicKey
+    );
+    secondUserRedeemable = await createTokenAccount(
+      provider,
+      redeemableMint,
+      provider.wallet.publicKey
+    );
+
+    await program.rpc.exchangeUsdcForRedeemable(secondDeposit, {
+      accounts: {
+        poolAccount: poolAccount.publicKey,
+        poolSigner,
+        redeemableMint,
+        poolUsdc,
+        userAuthority: provider.wallet.publicKey,
+        userUsdc: secondUserUsdc,
+        userRedeemable: secondUserRedeemable,
+        tokenProgram: TOKEN_PROGRAM_ID,
+        clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
+      },
+    });
+
+    totalPoolUsdc = firstDeposit.add(secondDeposit);
+    poolUsdcAccount = await getTokenAccount(provider, poolUsdc);
+    assert.ok(poolUsdcAccount.amount.eq(totalPoolUsdc));
+    secondUserRedeemableAccount = await getTokenAccount(
+      provider,
+      secondUserRedeemable
+    );
+    assert.ok(secondUserRedeemableAccount.amount.eq(secondDeposit));
+  });
+
+  const firstWithdrawal = new anchor.BN(2_000_000);
+
+  it("Exchanges user Redeemable tokens for USDC", async () => {
+    await program.rpc.exchangeRedeemableForUsdc(firstWithdrawal, {
+      accounts: {
+        poolAccount: poolAccount.publicKey,
+        poolSigner,
+        redeemableMint,
+        poolUsdc,
+        userAuthority: provider.wallet.publicKey,
+        userUsdc,
+        userRedeemable,
+        tokenProgram: TOKEN_PROGRAM_ID,
+        clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
+      },
+    });
+
+    totalPoolUsdc = totalPoolUsdc.sub(firstWithdrawal);
+    poolUsdcAccount = await getTokenAccount(provider, poolUsdc);
+    assert.ok(poolUsdcAccount.amount.eq(totalPoolUsdc));
+    userUsdcAccount = await getTokenAccount(provider, userUsdc);
+    assert.ok(userUsdcAccount.amount.eq(firstWithdrawal));
+  });
+
+  it("Exchanges user Redeemable tokens for watermelon", async () => {
+    // Wait until the IDO has opened.
+    if (Date.now() < endIdoTs.toNumber() * 1000) {
+      await sleep(endIdoTs.toNumber() * 1000 - Date.now() + 2000);
+    }
+    let firstUserRedeemable = firstDeposit.sub(firstWithdrawal);
+    userWatermelon = await createTokenAccount(
+      provider,
+      watermelonMint,
+      provider.wallet.publicKey
+    );
+
+    await program.rpc.exchangeRedeemableForWatermelon(firstUserRedeemable, {
+      accounts: {
+        poolAccount: poolAccount.publicKey,
+        poolSigner,
+        redeemableMint,
+        poolWatermelon,
+        userAuthority: provider.wallet.publicKey,
+        userWatermelon,
+        userRedeemable,
+        tokenProgram: TOKEN_PROGRAM_ID,
+        clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
+      },
+    });
+
+    poolWatermelonAccount = await getTokenAccount(provider, poolWatermelon);
+    let redeemedWatermelon = firstUserRedeemable
+      .mul(watermelonIdoAmount)
+      .div(totalPoolUsdc);
+    let remainingWatermelon = watermelonIdoAmount.sub(redeemedWatermelon);
+    assert.ok(poolWatermelonAccount.amount.eq(remainingWatermelon));
+    userWatermelonAccount = await getTokenAccount(provider, userWatermelon);
+    assert.ok(userWatermelonAccount.amount.eq(redeemedWatermelon));
+  });
+
+  it("Exchanges second users Redeemable tokens for watermelon", async () => {
+    secondUserWatermelon = await createTokenAccount(
+      provider,
+      watermelonMint,
+      provider.wallet.publicKey
+    );
+
+    await program.rpc.exchangeRedeemableForWatermelon(secondDeposit, {
+      accounts: {
+        poolAccount: poolAccount.publicKey,
+        poolSigner,
+        redeemableMint,
+        poolWatermelon,
+        userAuthority: provider.wallet.publicKey,
+        userWatermelon: secondUserWatermelon,
+        userRedeemable: secondUserRedeemable,
+        tokenProgram: TOKEN_PROGRAM_ID,
+        clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
+      },
+    });
+
+    poolWatermelonAccount = await getTokenAccount(provider, poolWatermelon);
+    assert.ok(poolWatermelonAccount.amount.eq(new anchor.BN(0)));
+  });
+
+  it("Withdraws total USDC from pool account", async () => {
+    await program.rpc.withdrawPoolUsdc({
+      accounts: {
+        poolAccount: poolAccount.publicKey,
+        poolSigner,
+        distributionAuthority: provider.wallet.publicKey,
+        creatorUsdc,
+        poolUsdc,
+        tokenProgram: TOKEN_PROGRAM_ID,
+        clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
+      },
+    });
+
+    poolUsdcAccount = await getTokenAccount(provider, poolUsdc);
+    assert.ok(poolUsdcAccount.amount.eq(new anchor.BN(0)));
+    creatorUsdcAccount = await getTokenAccount(provider, creatorUsdc);
+    assert.ok(creatorUsdcAccount.amount.eq(totalPoolUsdc));
+  });
+});

+ 139 - 0
examples/ido-pool/tests/utils/index.js

@@ -0,0 +1,139 @@
+// TODO: use the `@solana/spl-token` package instead of utils here.
+
+const anchor = require("@project-serum/anchor");
+const serumCmn = require("@project-serum/common");
+const TokenInstructions = require("@project-serum/serum").TokenInstructions;
+
+// TODO: remove this constant once @project-serum/serum uses the same version
+//       of @solana/web3.js as anchor (or switch packages).
+const TOKEN_PROGRAM_ID = new anchor.web3.PublicKey(
+  TokenInstructions.TOKEN_PROGRAM_ID.toString()
+);
+
+// Our own sleep function.
+function sleep(ms) {
+  return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+async function getTokenAccount(provider, addr) {
+  return await serumCmn.getTokenAccount(provider, addr);
+}
+
+async function createMint(provider, authority) {
+  if (authority === undefined) {
+    authority = provider.wallet.publicKey;
+  }
+  const mint = new anchor.web3.Account();
+  const instructions = await createMintInstructions(
+    provider,
+    authority,
+    mint.publicKey
+  );
+
+  const tx = new anchor.web3.Transaction();
+  tx.add(...instructions);
+
+  await provider.send(tx, [mint]);
+
+  return mint.publicKey;
+}
+
+async function createMintInstructions(provider, authority, mint) {
+  let instructions = [
+    anchor.web3.SystemProgram.createAccount({
+      fromPubkey: provider.wallet.publicKey,
+      newAccountPubkey: mint,
+      space: 82,
+      lamports: await provider.connection.getMinimumBalanceForRentExemption(82),
+      programId: TOKEN_PROGRAM_ID,
+    }),
+    TokenInstructions.initializeMint({
+      mint,
+      decimals: 6,
+      mintAuthority: authority,
+    }),
+  ];
+  return instructions;
+}
+
+async function createTokenAccount(provider, mint, owner) {
+  const vault = new anchor.web3.Account();
+  const tx = new anchor.web3.Transaction();
+  tx.add(
+    ...(await createTokenAccountInstrs(provider, vault.publicKey, mint, owner))
+  );
+  await provider.send(tx, [vault]);
+  return vault.publicKey;
+}
+
+async function createTokenAccountInstrs(
+  provider,
+  newAccountPubkey,
+  mint,
+  owner,
+  lamports
+) {
+  if (lamports === undefined) {
+    lamports = await provider.connection.getMinimumBalanceForRentExemption(165);
+  }
+  return [
+    anchor.web3.SystemProgram.createAccount({
+      fromPubkey: provider.wallet.publicKey,
+      newAccountPubkey,
+      space: 165,
+      lamports,
+      programId: TOKEN_PROGRAM_ID,
+    }),
+    TokenInstructions.initializeAccount({
+      account: newAccountPubkey,
+      mint,
+      owner,
+    }),
+  ];
+}
+
+async function mintToAccount(
+  provider,
+  mint,
+  destination,
+  amount,
+  mintAuthority
+) {
+  // mint authority is the provider
+  const tx = new anchor.web3.Transaction();
+  tx.add(
+    ...(await createMintToAccountInstrs(
+      mint,
+      destination,
+      amount,
+      mintAuthority
+    ))
+  );
+  await provider.send(tx, []);
+  return;
+}
+
+async function createMintToAccountInstrs(
+  mint,
+  destination,
+  amount,
+  mintAuthority
+) {
+  return [
+    TokenInstructions.mintTo({
+      mint,
+      destination: destination,
+      amount: amount,
+      mintAuthority: mintAuthority,
+    }),
+  ];
+}
+
+module.exports = {
+  TOKEN_PROGRAM_ID,
+  sleep,
+  getTokenAccount,
+  createMint,
+  createTokenAccount,
+  mintToAccount,
+};