소스 검색

tests: Change ido pool to use PDAs (#699)

Henry-E 4 년 전
부모
커밋
d5fe9281a7
3개의 변경된 파일844개의 추가작업 그리고 336개의 파일을 삭제
  1. 466 208
      tests/ido-pool/programs/ido-pool/src/lib.rs
  2. 377 128
      tests/ido-pool/tests/ido-pool.js
  3. 1 0
      tests/ido-pool/tests/utils/index.js

+ 466 - 208
tests/ido-pool/programs/ido-pool/src/lib.rs

@@ -1,59 +1,74 @@
 //! An IDO pool program implementing the Mango Markets token sale design here:
 //! https://docs.mango.markets/litepaper#token-sale.
+// #![warn(clippy::all)]
 
 use anchor_lang::prelude::*;
-use anchor_lang::solana_program::program_option::COption;
-use anchor_spl::token::{self, Burn, Mint, MintTo, TokenAccount, Transfer};
+use anchor_spl::token::{self, Burn, CloseAccount, Mint, MintTo, Token, TokenAccount, Transfer};
+
+use std::ops::Deref;
 
 declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
 
+const DECIMALS: u8 = 6;
+
 #[program]
 pub mod ido_pool {
     use super::*;
 
-    #[access_control(InitializePool::accounts(&ctx, nonce) future_start_time(&ctx, start_ido_ts))]
+    #[access_control(validate_ido_times(ido_times))]
     pub fn initialize_pool(
         ctx: Context<InitializePool>,
+        ido_name: String,
+        bumps: PoolBumps,
         num_ido_tokens: u64,
-        nonce: u8,
-        start_ido_ts: i64,
-        end_deposits_ts: i64,
-        end_ido_ts: i64,
-    ) -> Result<()> {
-        if !(start_ido_ts < end_deposits_ts && end_deposits_ts < end_ido_ts) {
-            return Err(ErrorCode::SeqTimes.into());
-        }
+        ido_times: IdoTimes,
+    ) -> ProgramResult {
+        msg!("INITIALIZE POOL");
+
+        let ido_account = &mut ctx.accounts.ido_account;
+
+        let name_bytes = ido_name.as_bytes();
+        let mut name_data = [b' '; 10];
+        name_data[..name_bytes.len()].copy_from_slice(name_bytes);
+
+        ido_account.ido_name = name_data;
+        ido_account.bumps = bumps;
+        ido_account.ido_authority = ctx.accounts.ido_authority.key();
+
+        ido_account.usdc_mint = ctx.accounts.usdc_mint.key();
+        ido_account.redeemable_mint = ctx.accounts.redeemable_mint.key();
+        ido_account.watermelon_mint = ctx.accounts.watermelon_mint.key();
+        ido_account.pool_usdc = ctx.accounts.pool_usdc.key();
+        ido_account.pool_watermelon = ctx.accounts.pool_watermelon.key();
 
-        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;
-
-        // Transfer Watermelon from creator to pool account.
+        ido_account.num_ido_tokens = num_ido_tokens;
+        ido_account.ido_times = ido_times;
+
+        // Transfer Watermelon from ido_authority to pool account.
         let cpi_accounts = Transfer {
-            from: ctx.accounts.creator_watermelon.to_account_info(),
+            from: ctx.accounts.ido_authority_watermelon.to_account_info(),
             to: ctx.accounts.pool_watermelon.to_account_info(),
-            authority: ctx.accounts.distribution_authority.to_account_info(),
+            authority: ctx.accounts.ido_authority.to_account_info(),
         };
-        let cpi_program = ctx.accounts.token_program.clone();
+        let cpi_program = ctx.accounts.token_program.to_account_info();
         let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
         token::transfer(cpi_ctx, num_ido_tokens)?;
 
         Ok(())
     }
 
-    #[access_control(unrestricted_phase(&ctx))]
+    #[access_control(unrestricted_phase(&ctx.accounts.ido_account))]
+    pub fn init_user_redeemable(ctx: Context<InitUserRedeemable>) -> ProgramResult {
+        msg!("INIT USER REDEEMABLE");
+        Ok(())
+    }
+
+    #[access_control(unrestricted_phase(&ctx.accounts.ido_account))]
     pub fn exchange_usdc_for_redeemable(
         ctx: Context<ExchangeUsdcForRedeemable>,
         amount: u64,
-    ) -> Result<()> {
+    ) -> ProgramResult {
+        msg!("EXCHANGE USDC FOR REDEEMABLE");
         // While token::transfer will check this, we prefer a verbose err msg.
         if ctx.accounts.user_usdc.amount < amount {
             return Err(ErrorCode::LowUsdc.into());
@@ -63,73 +78,84 @@ pub mod ido_pool {
         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(),
+            authority: ctx.accounts.user_authority.to_account_info(),
         };
         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 ido_name = ctx.accounts.ido_account.ido_name.as_ref();
         let seeds = &[
-            ctx.accounts.pool_account.watermelon_mint.as_ref(),
-            &[ctx.accounts.pool_account.nonce],
+            ido_name.trim_ascii_whitespace(),
+            &[ctx.accounts.ido_account.bumps.ido_account],
         ];
         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(),
+            authority: ctx.accounts.ido_account.to_account_info(),
         };
-        let cpi_program = ctx.accounts.token_program.clone();
+        let cpi_program = ctx.accounts.token_program.to_account_info();
         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))]
+    #[access_control(withdraw_phase(&ctx.accounts.ido_account))]
+    pub fn init_escrow_usdc(ctx: Context<InitEscrowUsdc>) -> ProgramResult {
+        msg!("INIT ESCROW USDC");
+        Ok(())
+    }
+
+    #[access_control(withdraw_phase(&ctx.accounts.ido_account))]
     pub fn exchange_redeemable_for_usdc(
         ctx: Context<ExchangeRedeemableForUsdc>,
         amount: u64,
-    ) -> Result<()> {
+    ) -> ProgramResult {
+        msg!("EXCHANGE REDEEMABLE FOR USDC");
         // 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 ido_name = ctx.accounts.ido_account.ido_name.as_ref();
+        let seeds = &[
+            ido_name.trim_ascii_whitespace(),
+            &[ctx.accounts.ido_account.bumps.ido_account],
+        ];
+        let signer = &[&seeds[..]];
+
         // 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(),
+            authority: ctx.accounts.ido_account.to_account_info(),
         };
-        let cpi_program = ctx.accounts.token_program.clone();
-        let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
+        let cpi_program = ctx.accounts.token_program.to_account_info();
+        let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer);
         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[..]];
+        // Transfer USDC from pool account to the user's escrow account.
         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(),
+            to: ctx.accounts.escrow_usdc.to_account_info(),
+            authority: ctx.accounts.ido_account.to_account_info(),
         };
-        let cpi_program = ctx.accounts.token_program.clone();
+        let cpi_program = ctx.accounts.token_program.to_account_info();
         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))]
+    #[access_control(ido_over(&ctx.accounts.ido_account))]
     pub fn exchange_redeemable_for_watermelon(
         ctx: Context<ExchangeRedeemableForWatermelon>,
         amount: u64,
-    ) -> Result<()> {
+    ) -> ProgramResult {
+        msg!("EXCHANGE REDEEMABLE FOR WATERMELON");
         // While token::burn will check this, we prefer a verbose err msg.
         if ctx.accounts.user_redeemable.amount < amount {
             return Err(ErrorCode::LowRedeemable.into());
@@ -142,207 +168,404 @@ pub mod ido_pool {
             .checked_div(ctx.accounts.redeemable_mint.supply as u128)
             .unwrap();
 
+        let ido_name = ctx.accounts.ido_account.ido_name.as_ref();
+        let seeds = &[
+            ido_name.trim_ascii_whitespace(),
+            &[ctx.accounts.ido_account.bumps.ido_account],
+        ];
+        let signer = &[&seeds[..]];
+
         // 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(),
+            authority: ctx.accounts.ido_account.to_account_info(),
         };
-        let cpi_program = ctx.accounts.token_program.clone();
-        let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
+        let cpi_program = ctx.accounts.token_program.to_account_info();
+        let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer);
         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(),
+            authority: ctx.accounts.ido_account.to_account_info(),
         };
-        let cpi_program = ctx.accounts.token_program.clone();
+        let cpi_program = ctx.accounts.token_program.to_account_info();
         let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer);
         token::transfer(cpi_ctx, watermelon_amount as u64)?;
 
+        // Send rent back to user if account is empty
+        ctx.accounts.user_redeemable.reload()?;
+        if ctx.accounts.user_redeemable.amount == 0 {
+            let cpi_accounts = CloseAccount {
+                account: ctx.accounts.user_redeemable.to_account_info(),
+                destination: ctx.accounts.user_authority.clone(),
+                authority: ctx.accounts.ido_account.to_account_info(),
+            };
+            let cpi_program = ctx.accounts.token_program.to_account_info();
+            let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer);
+            token::close_account(cpi_ctx)?;
+        }
+
         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.
+    #[access_control(ido_over(&ctx.accounts.ido_account))]
+    pub fn withdraw_pool_usdc(ctx: Context<WithdrawPoolUsdc>) -> ProgramResult {
+        msg!("WITHDRAW POOL USDC");
+        // Transfer total USDC from pool account to ido_authority account.
+        let ido_name = ctx.accounts.ido_account.ido_name.as_ref();
         let seeds = &[
-            ctx.accounts.pool_account.watermelon_mint.as_ref(),
-            &[ctx.accounts.pool_account.nonce],
+            ido_name.trim_ascii_whitespace(),
+            &[ctx.accounts.ido_account.bumps.ido_account],
         ];
         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(),
+            to: ctx.accounts.ido_authority_usdc.to_account_info(),
+            authority: ctx.accounts.ido_account.to_account_info(),
         };
-        let cpi_program = ctx.accounts.token_program.clone();
+        let cpi_program = ctx.accounts.token_program.to_account_info();
         let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer);
         token::transfer(cpi_ctx, ctx.accounts.pool_usdc.amount)?;
 
         Ok(())
     }
+
+    #[access_control(escrow_over(&ctx.accounts.ido_account))]
+    pub fn withdraw_from_escrow(ctx: Context<WithdrawFromEscrow>, amount: u64) -> ProgramResult {
+        msg!("WITHDRAW FROM ESCROW");
+        // While token::transfer will check this, we prefer a verbose err msg.
+        if ctx.accounts.escrow_usdc.amount < amount {
+            return Err(ErrorCode::LowUsdc.into());
+        }
+
+        let ido_name = ctx.accounts.ido_account.ido_name.as_ref();
+        let seeds = &[
+            ido_name.trim_ascii_whitespace(),
+            &[ctx.accounts.ido_account.bumps.ido_account],
+        ];
+        let signer = &[&seeds[..]];
+
+        // Transfer USDC from user's escrow account to user's USDC account.
+        let cpi_accounts = Transfer {
+            from: ctx.accounts.escrow_usdc.to_account_info(),
+            to: ctx.accounts.user_usdc.to_account_info(),
+            authority: ctx.accounts.ido_account.to_account_info(),
+        };
+        let cpi_program = ctx.accounts.token_program.to_account_info();
+        let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer);
+        token::transfer(cpi_ctx, amount)?;
+
+        // Send rent back to user if account is empty
+        ctx.accounts.escrow_usdc.reload()?;
+        if ctx.accounts.escrow_usdc.amount == 0 {
+            let cpi_accounts = CloseAccount {
+                account: ctx.accounts.escrow_usdc.to_account_info(),
+                destination: ctx.accounts.user_authority.clone(),
+                authority: ctx.accounts.ido_account.to_account_info(),
+            };
+            let cpi_program = ctx.accounts.token_program.to_account_info();
+            let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer);
+            token::close_account(cpi_ctx)?;
+        }
+
+        Ok(())
+    }
 }
 
 #[derive(Accounts)]
+#[instruction(ido_name: String, bumps: PoolBumps)]
 pub struct InitializePool<'info> {
-    #[account(zero)]
-    pub pool_account: Box<Account<'info, PoolAccount>>,
-    pub pool_signer: AccountInfo<'info>,
-    #[account(
-        constraint = redeemable_mint.mint_authority == COption::Some(*pool_signer.key),
-        constraint = redeemable_mint.supply == 0
-    )]
-    pub redeemable_mint: Account<'info, Mint>,
-    #[account(constraint = usdc_mint.decimals == redeemable_mint.decimals)]
-    pub usdc_mint: Account<'info, Mint>,
-    #[account(mut, constraint = pool_watermelon.owner == *pool_signer.key)]
-    pub pool_watermelon: Account<'info, TokenAccount>,
-    #[account(constraint = pool_usdc.owner == *pool_signer.key)]
-    pub pool_usdc: Account<'info, TokenAccount>,
-    #[account(signer)]
-    pub distribution_authority: AccountInfo<'info>,
-    #[account(mut, constraint = creator_watermelon.owner == *distribution_authority.key)]
-    pub creator_watermelon: Account<'info, TokenAccount>,
-    #[account(constraint = token_program.key == &token::ID)]
-    pub token_program: AccountInfo<'info>,
-    pub clock: Sysvar<'info, Clock>,
+    // IDO Authority accounts
+    #[account(mut)]
+    pub ido_authority: Signer<'info>,
+    // Watermelon Doesn't have to be an ATA because it could be DAO controlled
+    #[account(mut,
+        constraint = ido_authority_watermelon.owner == ido_authority.key(),
+        constraint = ido_authority_watermelon.mint == watermelon_mint.key())]
+    pub ido_authority_watermelon: Box<Account<'info, TokenAccount>>,
+    // IDO Accounts
+    #[account(init,
+        seeds = [ido_name.as_bytes()],
+        bump = bumps.ido_account,
+        payer = ido_authority)]
+    pub ido_account: Box<Account<'info, IdoAccount>>,
+    // TODO Confirm USDC mint address on mainnet or leave open as an option for other stables
+    #[account(constraint = usdc_mint.decimals == DECIMALS)]
+    pub usdc_mint: Box<Account<'info, Mint>>,
+    #[account(init,
+        mint::decimals = DECIMALS,
+        mint::authority = ido_account,
+        seeds = [ido_name.as_bytes(), b"redeemable_mint".as_ref()],
+        bump = bumps.redeemable_mint,
+        payer = ido_authority)]
+    pub redeemable_mint: Box<Account<'info, Mint>>,
+    #[account(constraint = watermelon_mint.key() == ido_authority_watermelon.mint)]
+    pub watermelon_mint: Box<Account<'info, Mint>>,
+    #[account(init,
+        token::mint = watermelon_mint,
+        token::authority = ido_account,
+        seeds = [ido_name.as_bytes(), b"pool_watermelon"],
+        bump = bumps.pool_watermelon,
+        payer = ido_authority)]
+    pub pool_watermelon: Box<Account<'info, TokenAccount>>,
+    #[account(init,
+        token::mint = usdc_mint,
+        token::authority = ido_account,
+        seeds = [ido_name.as_bytes(), b"pool_usdc"],
+        bump = bumps.pool_usdc,
+        payer = ido_authority)]
+    pub pool_usdc: Box<Account<'info, TokenAccount>>,
+    // Programs and Sysvars
+    pub system_program: Program<'info, System>,
+    pub token_program: Program<'info, Token>,
+    pub rent: Sysvar<'info, Rent>,
 }
 
-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 InitUserRedeemable<'info> {
+    // User Accounts
+    #[account(mut)]
+    pub user_authority: Signer<'info>,
+    #[account(init,
+        token::mint = redeemable_mint,
+        token::authority = ido_account,
+        seeds = [user_authority.key().as_ref(),
+            ido_account.ido_name.as_ref().trim_ascii_whitespace(),
+            b"user_redeemable"],
+        bump,
+        payer = user_authority)]
+    pub user_redeemable: Box<Account<'info, TokenAccount>>,
+    // IDO Accounts
+    #[account(seeds = [ido_account.ido_name.as_ref().trim_ascii_whitespace()],
+        bump = ido_account.bumps.ido_account)]
+    pub ido_account: Box<Account<'info, IdoAccount>>,
+    #[account(seeds = [ido_account.ido_name.as_ref().trim_ascii_whitespace(), b"redeemable_mint"],
+        bump = ido_account.bumps.redeemable_mint)]
+    pub redeemable_mint: Box<Account<'info, Mint>>,
+    // Programs and Sysvars
+    pub system_program: Program<'info, System>,
+    pub token_program: Program<'info, Token>,
+    pub rent: Sysvar<'info, Rent>,
 }
 
 #[derive(Accounts)]
 pub struct ExchangeUsdcForRedeemable<'info> {
-    #[account(has_one = redeemable_mint, has_one = pool_usdc)]
-    pub pool_account: Account<'info, PoolAccount>,
-    #[account(
-        seeds = [pool_account.watermelon_mint.as_ref()],
-        bump = pool_account.nonce,
-    )]
-    pool_signer: AccountInfo<'info>,
-    #[account(
-        mut,
-        constraint = redeemable_mint.mint_authority == COption::Some(*pool_signer.key)
-    )]
-    pub redeemable_mint: Account<'info, Mint>,
-    #[account(mut, constraint = pool_usdc.owner == *pool_signer.key)]
-    pub pool_usdc: Account<'info, TokenAccount>,
-    #[account(signer)]
-    pub user_authority: AccountInfo<'info>,
-    #[account(mut, constraint = user_usdc.owner == *user_authority.key)]
-    pub user_usdc: Account<'info, TokenAccount>,
-    #[account(mut, constraint = user_redeemable.owner == *user_authority.key)]
-    pub user_redeemable: Account<'info, TokenAccount>,
-    #[account(constraint = token_program.key == &token::ID)]
-    pub token_program: AccountInfo<'info>,
-    pub clock: Sysvar<'info, Clock>,
+    // User Accounts
+    pub user_authority: Signer<'info>,
+    // TODO replace these with the ATA constraints when possible
+    #[account(mut,
+        constraint = user_usdc.owner == user_authority.key(),
+        constraint = user_usdc.mint == usdc_mint.key())]
+    pub user_usdc: Box<Account<'info, TokenAccount>>,
+    #[account(mut,
+        seeds = [user_authority.key().as_ref(),
+            ido_account.ido_name.as_ref().trim_ascii_whitespace(),
+            b"user_redeemable"],
+        bump)]
+    pub user_redeemable: Box<Account<'info, TokenAccount>>,
+    // IDO Accounts
+    #[account(seeds = [ido_account.ido_name.as_ref().trim_ascii_whitespace()],
+        bump = ido_account.bumps.ido_account,
+        has_one = usdc_mint)]
+    pub ido_account: Box<Account<'info, IdoAccount>>,
+    pub usdc_mint: Box<Account<'info, Mint>>,
+    #[account(mut,
+        seeds = [ido_account.ido_name.as_ref().trim_ascii_whitespace(), b"redeemable_mint"],
+        bump = ido_account.bumps.redeemable_mint)]
+    pub redeemable_mint: Box<Account<'info, Mint>>,
+    #[account(mut,
+        seeds = [ido_account.ido_name.as_ref().trim_ascii_whitespace(), b"pool_usdc"],
+        bump = ido_account.bumps.pool_usdc)]
+    pub pool_usdc: Box<Account<'info, TokenAccount>>,
+    // Programs and Sysvars
+    pub token_program: Program<'info, Token>,
+}
+
+#[derive(Accounts)]
+pub struct InitEscrowUsdc<'info> {
+    // User Accounts
+    #[account(mut)]
+    pub user_authority: Signer<'info>,
+    #[account(init,
+        token::mint = usdc_mint,
+        token::authority = ido_account,
+        seeds =  [user_authority.key().as_ref(),
+            ido_account.ido_name.as_ref().trim_ascii_whitespace(),
+            b"escrow_usdc"],
+        bump,
+        payer = user_authority)]
+    pub escrow_usdc: Box<Account<'info, TokenAccount>>,
+    #[account(seeds = [ido_account.ido_name.as_ref().trim_ascii_whitespace()],
+        bump = ido_account.bumps.ido_account,
+        has_one = usdc_mint)]
+    pub ido_account: Box<Account<'info, IdoAccount>>,
+    pub usdc_mint: Box<Account<'info, Mint>>,
+    // Programs and Sysvars
+    pub system_program: Program<'info, System>,
+    pub token_program: Program<'info, Token>,
+    pub rent: Sysvar<'info, Rent>,
 }
 
 #[derive(Accounts)]
 pub struct ExchangeRedeemableForUsdc<'info> {
-    #[account(has_one = redeemable_mint, has_one = pool_usdc)]
-    pub pool_account: Account<'info, PoolAccount>,
-    #[account(
-        seeds = [pool_account.watermelon_mint.as_ref()],
-        bump = pool_account.nonce,
-    )]
-    pool_signer: AccountInfo<'info>,
-    #[account(
-        mut,
-        constraint = redeemable_mint.mint_authority == COption::Some(*pool_signer.key)
-    )]
-    pub redeemable_mint: Account<'info, Mint>,
-    #[account(mut, constraint = pool_usdc.owner == *pool_signer.key)]
-    pub pool_usdc: Account<'info, TokenAccount>,
-    #[account(signer)]
-    pub user_authority: AccountInfo<'info>,
-    #[account(mut, constraint = user_usdc.owner == *user_authority.key)]
-    pub user_usdc: Account<'info, TokenAccount>,
-    #[account(mut, constraint = user_redeemable.owner == *user_authority.key)]
-    pub user_redeemable: Account<'info, TokenAccount>,
-    #[account(constraint = token_program.key == &token::ID)]
-    pub token_program: AccountInfo<'info>,
-    pub clock: Sysvar<'info, Clock>,
+    // User Accounts
+    pub user_authority: Signer<'info>,
+    #[account(mut,
+        seeds = [user_authority.key().as_ref(),
+            ido_account.ido_name.as_ref().trim_ascii_whitespace(),
+            b"escrow_usdc"],
+        bump)]
+    pub escrow_usdc: Box<Account<'info, TokenAccount>>,
+    #[account(mut,
+        seeds = [user_authority.key().as_ref(),
+            ido_account.ido_name.as_ref().trim_ascii_whitespace(),
+            b"user_redeemable"],
+        bump)]
+    pub user_redeemable: Box<Account<'info, TokenAccount>>,
+    // IDO Accounts
+    #[account(seeds = [ido_account.ido_name.as_ref().trim_ascii_whitespace()],
+        bump = ido_account.bumps.ido_account,
+        has_one = usdc_mint)]
+    pub ido_account: Box<Account<'info, IdoAccount>>,
+    pub usdc_mint: Box<Account<'info, Mint>>,
+    pub watermelon_mint: Box<Account<'info, Mint>>,
+    #[account(mut,
+        seeds = [ido_account.ido_name.as_ref().trim_ascii_whitespace(), b"redeemable_mint"],
+        bump = ido_account.bumps.redeemable_mint)]
+    pub redeemable_mint: Box<Account<'info, Mint>>,
+    #[account(mut,
+        seeds = [ido_account.ido_name.as_ref().trim_ascii_whitespace(), b"pool_usdc"],
+        bump = ido_account.bumps.pool_usdc)]
+    pub pool_usdc: Box<Account<'info, TokenAccount>>,
+    // Programs and Sysvars
+    pub token_program: Program<'info, Token>,
 }
 
 #[derive(Accounts)]
 pub struct ExchangeRedeemableForWatermelon<'info> {
-    #[account(has_one = redeemable_mint, has_one = pool_watermelon)]
-    pub pool_account: Account<'info, PoolAccount>,
-    #[account(
-        seeds = [pool_account.watermelon_mint.as_ref()],
-        bump = pool_account.nonce,
-    )]
-    pool_signer: AccountInfo<'info>,
-    #[account(
-        mut,
-        constraint = redeemable_mint.mint_authority == COption::Some(*pool_signer.key)
-    )]
-    pub redeemable_mint: Account<'info, Mint>,
-    #[account(mut, constraint = pool_watermelon.owner == *pool_signer.key)]
-    pub pool_watermelon: Account<'info, TokenAccount>,
-    #[account(signer)]
+    // User does not have to sign, this allows anyone to redeem on their behalf
+    // and prevents forgotten / leftover redeemable tokens in the IDO pool.
+    pub payer: Signer<'info>,
+    // User Accounts
+    #[account(mut)] // Sol rent from empty redeemable account is refunded to the user
     pub user_authority: AccountInfo<'info>,
-    #[account(mut, constraint = user_watermelon.owner == *user_authority.key)]
-    pub user_watermelon: Account<'info, TokenAccount>,
-    #[account(mut, constraint = user_redeemable.owner == *user_authority.key)]
-    pub user_redeemable: Account<'info, TokenAccount>,
-    #[account(constraint = token_program.key == &token::ID)]
-    pub token_program: AccountInfo<'info>,
-    pub clock: Sysvar<'info, Clock>,
+    // TODO replace with ATA constraints
+    #[account(mut,
+        constraint = user_watermelon.owner == user_authority.key(),
+        constraint = user_watermelon.mint == watermelon_mint.key())]
+    pub user_watermelon: Box<Account<'info, TokenAccount>>,
+    #[account(mut,
+        seeds = [user_authority.key().as_ref(),
+            ido_account.ido_name.as_ref().trim_ascii_whitespace(),
+            b"user_redeemable"],
+        bump)]
+    pub user_redeemable: Box<Account<'info, TokenAccount>>,
+    // IDO Accounts
+    #[account(seeds = [ido_account.ido_name.as_ref().trim_ascii_whitespace()],
+        bump = ido_account.bumps.ido_account,
+        has_one = watermelon_mint)]
+    pub ido_account: Box<Account<'info, IdoAccount>>,
+    pub watermelon_mint: Box<Account<'info, Mint>>,
+    #[account(mut,
+        seeds = [ido_account.ido_name.as_ref().trim_ascii_whitespace(), b"redeemable_mint"],
+        bump = ido_account.bumps.redeemable_mint)]
+    pub redeemable_mint: Box<Account<'info, Mint>>,
+    #[account(mut,
+        seeds = [ido_account.ido_name.as_ref().trim_ascii_whitespace(), b"pool_watermelon"],
+        bump = ido_account.bumps.pool_usdc)]
+    pub pool_watermelon: Box<Account<'info, TokenAccount>>,
+    // Programs and Sysvars
+    pub token_program: Program<'info, Token>,
 }
 
 #[derive(Accounts)]
 pub struct WithdrawPoolUsdc<'info> {
-    #[account(has_one = pool_usdc, has_one = distribution_authority)]
-    pub pool_account: Account<'info, PoolAccount>,
-    #[account(
-        seeds = [pool_account.watermelon_mint.as_ref()],
-        bump = pool_account.nonce,
-    )]
-    pub pool_signer: AccountInfo<'info>,
-    #[account(mut, constraint = pool_usdc.owner == *pool_signer.key)]
-    pub pool_usdc: Account<'info, TokenAccount>,
-    #[account(signer)]
-    pub distribution_authority: AccountInfo<'info>,
-    #[account(mut, constraint = creator_usdc.owner == *distribution_authority.key)]
-    pub creator_usdc: Account<'info, TokenAccount>,
-    #[account(constraint = token_program.key == &token::ID)]
-    pub token_program: AccountInfo<'info>,
-    pub clock: Sysvar<'info, Clock>,
+    // IDO Authority Accounts
+    pub ido_authority: Signer<'info>,
+    // Doesn't need to be an ATA because it might be a DAO account
+    #[account(mut,
+        constraint = ido_authority_usdc.owner == ido_authority.key(),
+        constraint = ido_authority_usdc.mint == usdc_mint.key())]
+    pub ido_authority_usdc: Box<Account<'info, TokenAccount>>,
+    // IDO Accounts
+    #[account(seeds = [ido_account.ido_name.as_ref().trim_ascii_whitespace()],
+        bump = ido_account.bumps.ido_account,
+        has_one = ido_authority,
+        has_one = usdc_mint,
+        has_one = watermelon_mint)]
+    pub ido_account: Box<Account<'info, IdoAccount>>,
+    pub usdc_mint: Box<Account<'info, Mint>>,
+    pub watermelon_mint: Box<Account<'info, Mint>>,
+    #[account(mut,
+        seeds = [ido_account.ido_name.as_ref().trim_ascii_whitespace(), b"pool_usdc"],
+        bump = ido_account.bumps.pool_usdc)]
+    pub pool_usdc: Box<Account<'info, TokenAccount>>,
+    // Program and Sysvars
+    pub token_program: Program<'info, Token>,
+}
+
+#[derive(Accounts)]
+pub struct WithdrawFromEscrow<'info> {
+    // User does not have to sign, this allows anyone to redeem on their behalf
+    // and prevents forgotten / leftover USDC in the IDO pool.
+    pub payer: Signer<'info>,
+    // User Accounts
+    #[account(mut)]
+    pub user_authority: AccountInfo<'info>,
+    #[account(mut,
+        constraint = user_usdc.owner == user_authority.key(),
+        constraint = user_usdc.mint == usdc_mint.key())]
+    pub user_usdc: Box<Account<'info, TokenAccount>>,
+    #[account(mut,
+        seeds = [user_authority.key().as_ref(),
+            ido_account.ido_name.as_ref().trim_ascii_whitespace(),
+            b"escrow_usdc"],
+        bump)]
+    pub escrow_usdc: Box<Account<'info, TokenAccount>>,
+    // IDO Accounts
+    #[account(seeds = [ido_account.ido_name.as_ref().trim_ascii_whitespace()],
+        bump = ido_account.bumps.ido_account,
+        has_one = usdc_mint)]
+    pub ido_account: Box<Account<'info, IdoAccount>>,
+    pub usdc_mint: Box<Account<'info, Mint>>,
+    // Programs and Sysvars
+    pub token_program: Program<'info, Token>,
 }
 
 #[account]
-pub struct PoolAccount {
+#[derive(Default)]
+pub struct IdoAccount {
+    pub ido_name: [u8; 10], // Setting an arbitrary max of ten characters in the ido name.
+    pub bumps: PoolBumps,
+    pub ido_authority: Pubkey,
+
+    pub usdc_mint: Pubkey,
     pub redeemable_mint: Pubkey,
-    pub pool_watermelon: Pubkey,
     pub watermelon_mint: Pubkey,
     pub pool_usdc: Pubkey,
-    pub distribution_authority: Pubkey,
-    pub nonce: u8,
+    pub pool_watermelon: Pubkey,
+
     pub num_ido_tokens: u64,
-    pub start_ido_ts: i64,
-    pub end_deposits_ts: i64,
-    pub end_ido_ts: i64,
+    pub ido_times: IdoTimes,
+}
+
+#[derive(AnchorSerialize, AnchorDeserialize, Default, Clone, Copy)]
+pub struct IdoTimes {
+    pub start_ido: i64,
+    pub end_deposits: i64,
+    pub end_ido: i64,
+    pub end_escrow: i64,
+}
+
+#[derive(AnchorSerialize, AnchorDeserialize, Default, Clone)]
+pub struct PoolBumps {
+    pub ido_account: u8,
+    pub redeemable_mint: u8,
+    pub pool_watermelon: u8,
+    pub pool_usdc: u8,
 }
 
 #[error]
@@ -359,6 +582,8 @@ pub enum ErrorCode {
     EndIdoTime,
     #[msg("IDO has not finished yet")]
     IdoNotOver,
+    #[msg("Escrow period has not finished yet")]
+    EscrowNotOver,
     #[msg("Insufficient USDC")]
     LowUsdc,
     #[msg("Insufficient redeemable tokens")]
@@ -372,40 +597,73 @@ pub enum ErrorCode {
 // Access control modifiers.
 
 // Asserts the IDO starts in the future.
-fn future_start_time<'info>(ctx: &Context<InitializePool<'info>>, start_ido_ts: i64) -> Result<()> {
-    if !(ctx.accounts.clock.unix_timestamp < start_ido_ts) {
+fn validate_ido_times(ido_times: IdoTimes) -> ProgramResult {
+    let clock = Clock::get()?;
+    if ido_times.start_ido <= clock.unix_timestamp {
         return Err(ErrorCode::IdoFuture.into());
     }
+    if !(ido_times.start_ido < ido_times.end_deposits
+        && ido_times.end_deposits < ido_times.end_ido
+        && ido_times.end_ido < ido_times.end_escrow)
+    {
+        return Err(ErrorCode::SeqTimes.into());
+    }
     Ok(())
 }
 
-// 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) {
+// Asserts the IDO is still accepting deposits.
+fn unrestricted_phase(ido_account: &IdoAccount) -> ProgramResult {
+    let clock = Clock::get()?;
+    if clock.unix_timestamp <= ido_account.ido_times.start_ido {
         return Err(ErrorCode::StartIdoTime.into());
-    } else if !(ctx.accounts.clock.unix_timestamp < ctx.accounts.pool_account.end_deposits_ts) {
+    } else if ido_account.ido_times.end_deposits <= clock.unix_timestamp {
         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) {
+// Asserts the IDO has started but not yet finished.
+fn withdraw_phase(ido_account: &IdoAccount) -> ProgramResult {
+    let clock = Clock::get()?;
+    if clock.unix_timestamp <= ido_account.ido_times.start_ido {
         return Err(ErrorCode::StartIdoTime.into());
-    } else if !(ctx.accounts.clock.unix_timestamp < ctx.accounts.pool_account.end_ido_ts) {
+    } else if ido_account.ido_times.end_ido <= clock.unix_timestamp {
         return Err(ErrorCode::EndIdoTime.into());
     }
     Ok(())
 }
 
-// Asserts the IDO sale period has ended, based on the current timestamp.
-fn ido_over<'info>(
-    pool_account: &Account<'info, PoolAccount>,
-    clock: &Sysvar<'info, Clock>,
-) -> Result<()> {
-    if !(pool_account.end_ido_ts < clock.unix_timestamp) {
+// Asserts the IDO sale period has ended.
+fn ido_over(ido_account: &IdoAccount) -> ProgramResult {
+    let clock = Clock::get()?;
+    if clock.unix_timestamp <= ido_account.ido_times.end_ido {
         return Err(ErrorCode::IdoNotOver.into());
     }
     Ok(())
 }
+
+fn escrow_over(ido_account: &IdoAccount) -> ProgramResult {
+    let clock = Clock::get()?;
+    if clock.unix_timestamp <= ido_account.ido_times.end_escrow {
+        return Err(ErrorCode::EscrowNotOver.into());
+    }
+    Ok(())
+}
+
+/// Trait to allow trimming ascii whitespace from a &[u8].
+pub trait TrimAsciiWhitespace {
+    /// Trim ascii whitespace (based on `is_ascii_whitespace()`) from the
+    /// start and end of a slice.
+    fn trim_ascii_whitespace(&self) -> &[u8];
+}
+
+impl<T: Deref<Target = [u8]>> TrimAsciiWhitespace for T {
+    fn trim_ascii_whitespace(&self) -> &[u8] {
+        let from = match self.iter().position(|x| !x.is_ascii_whitespace()) {
+            Some(i) => i,
+            None => return &self[0..0],
+        };
+        let to = self.iter().rposition(|x| !x.is_ascii_whitespace()).unwrap();
+        &self[from..=to]
+    }
+}

+ 377 - 128
tests/ido-pool/tests/ido-pool.js

@@ -1,12 +1,17 @@
 const anchor = require("@project-serum/anchor");
 const assert = require("assert");
 const {
+  ASSOCIATED_TOKEN_PROGRAM_ID,
   TOKEN_PROGRAM_ID,
+  Token,
+} = require("@solana/spl-token");
+const {
   sleep,
   getTokenAccount,
   createMint,
   createTokenAccount,
 } = require("./utils");
+const { token } = require("@project-serum/anchor/dist/cjs/utils");
 
 describe("ido-pool", () => {
   const provider = anchor.Provider.local();
@@ -21,159 +26,199 @@ describe("ido-pool", () => {
 
   // These are all of the variables we assume exist in the world already and
   // are available to the client.
-  let usdcMintToken = null;
+  let usdcMintAccount = null;
   let usdcMint = null;
-  let watermelonMintToken = null;
+  let watermelonMintAccount = null;
   let watermelonMint = null;
-  let creatorUsdc = null;
-  let creatorWatermelon = null;
+  let idoAuthorityUsdc = null;
+  let idoAuthorityWatermelon = null;
 
   it("Initializes the state-of-the-world", async () => {
-    usdcMintToken = await createMint(provider);
-    watermelonMintToken = await createMint(provider);
-    usdcMint = usdcMintToken.publicKey;
-    watermelonMint = watermelonMintToken.publicKey;
-    creatorUsdc = await createTokenAccount(
+    usdcMintAccount = await createMint(provider);
+    watermelonMintAccount = await createMint(provider);
+    usdcMint = usdcMintAccount.publicKey;
+    watermelonMint = watermelonMintAccount.publicKey;
+    idoAuthorityUsdc = await createTokenAccount(
       provider,
       usdcMint,
       provider.wallet.publicKey
     );
-    creatorWatermelon = await createTokenAccount(
+    idoAuthorityWatermelon = await createTokenAccount(
       provider,
       watermelonMint,
       provider.wallet.publicKey
     );
-    // Mint Watermelon tokens the will be distributed from the IDO pool.
-    await watermelonMintToken.mintTo(
-      creatorWatermelon,
+    // Mint Watermelon tokens that will be distributed from the IDO pool.
+    await watermelonMintAccount.mintTo(
+      idoAuthorityWatermelon,
       provider.wallet.publicKey,
       [],
       watermelonIdoAmount.toString(),
     );
-    creator_watermelon_account = await getTokenAccount(
+    idoAuthority_watermelon_account = await getTokenAccount(
       provider,
-      creatorWatermelon
+      idoAuthorityWatermelon
     );
-    assert.ok(creator_watermelon_account.amount.eq(watermelonIdoAmount));
+    assert.ok(idoAuthority_watermelon_account.amount.eq(watermelonIdoAmount));
   });
 
-  // These are all variables the client will have to create to initialize the
-  // IDO pool
-  let poolSigner = null;
-  let redeemableMintToken = null;
-  let redeemableMint = null;
-  let poolWatermelon = null;
-  let poolUsdc = null;
-  let poolAccount = null;
-
-  let startIdoTs = null;
-  let endDepositsTs = null;
-  let endIdoTs = null;
+  // 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 () => {
-    // We use the watermelon mint address as the seed, could use something else though.
-    const [_poolSigner, nonce] = await anchor.web3.PublicKey.findProgramAddress(
-      [watermelonMint.toBuffer()],
+    let bumps = new PoolBumps();
+
+    const [idoAccount, idoAccountBump] = await anchor.web3.PublicKey.findProgramAddress(
+      [Buffer.from(idoName)],
       program.programId
     );
-    poolSigner = _poolSigner;
+    bumps.idoAccount = idoAccountBump;
 
-    // Pool doesn't need a Redeemable SPL token account because it only
-    // burns and mints redeemable tokens, it never stores them.
-    redeemableMintToken = await createMint(provider, poolSigner);
-    redeemableMint = redeemableMintToken.publicKey;
-    poolWatermelon = await createTokenAccount(
-      provider,
-      watermelonMint,
-      poolSigner
+    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
     );
-    poolUsdc = await createTokenAccount(provider, usdcMint, poolSigner);
+    bumps.poolUsdc = poolUsdcBump;
 
-    poolAccount = anchor.web3.Keypair.generate();
+    idoTimes = new IdoTimes();
     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));
+    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));
 
-    // Atomically create the new account and initialize it with the program.
     await program.rpc.initializePool(
+      idoName,
+      bumps,
       watermelonIdoAmount,
-      nonce,
-      startIdoTs,
-      endDepositsTs,
-      endIdoTs,
+      idoTimes,
       {
         accounts: {
-          poolAccount: poolAccount.publicKey,
-          poolSigner,
-          distributionAuthority: provider.wallet.publicKey,
-          creatorWatermelon,
-          redeemableMint,
+          idoAuthority: provider.wallet.publicKey,
+          idoAuthorityWatermelon,
+          idoAccount,
+          watermelonMint,
           usdcMint,
+          redeemableMint,
           poolWatermelon,
           poolUsdc,
+          systemProgram: anchor.web3.SystemProgram.programId,
           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(
+    idoAuthorityWatermelonAccount = await getTokenAccount(
       provider,
-      creatorWatermelon
+      idoAuthorityWatermelon
     );
-    assert.ok(creators_watermelon_account.amount.eq(new anchor.BN(0)));
+    assert.ok(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;
-  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);
+    if (Date.now() < idoTimes.startIdo.toNumber() * 1000) {
+      await sleep(idoTimes.startIdo.toNumber() * 1000 - Date.now() + 2000);
     }
 
-    userUsdc = await createTokenAccount(
-      provider,
+    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,
-      provider.wallet.publicKey
+      program.provider.wallet.publicKey
     );
-    await usdcMintToken.mintTo(
+    // 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.send(createUserUsdcTrns);
+    await usdcMintAccount.mintTo(
       userUsdc,
       provider.wallet.publicKey,
       [],
       firstDeposit.toString(),
     );
-    userRedeemable = await createTokenAccount(
-      provider,
-      redeemableMint,
-      provider.wallet.publicKey
+
+    // Check if we inited correctly
+    userUsdcAccount = await getTokenAccount(provider, userUsdc);
+    assert.ok(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: {
-          poolAccount: poolAccount.publicKey,
-          poolSigner,
-          redeemableMint,
-          poolUsdc,
           userAuthority: provider.wallet.publicKey,
           userUsdc,
           userRedeemable,
+          idoAccount,
+          usdcMint,
+          redeemableMint,
+          watermelonMint,
+          poolUsdc,
           tokenProgram: TOKEN_PROGRAM_ID,
-          clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
         },
+        instructions: [
+          program.instruction.initUserRedeemable({
+            accounts: {
+              userAuthority: provider.wallet.publicKey,
+              userRedeemable,
+              idoAccount,
+              redeemableMint,
+              systemProgram: anchor.web3.SystemProgram.programId,
+              tokenProgram: TOKEN_PROGRAM_ID,
+              rent: anchor.web3.SYSVAR_RENT_PUBKEY,
+            }
+          })
+        ]
       });
     } catch (err) {
       console.log("This is the error message", err.toString());
@@ -186,80 +231,203 @@ describe("ido-pool", () => {
 
   // 23 usdc
   const secondDeposit = new anchor.BN(23_000_672);
-  let totalPoolUsdc = null;
+  let totalPoolUsdc, secondUserKeypair, secondUserUsdc;
 
   it("Exchanges a second users USDC for redeemable tokens", async () => {
-    secondUserUsdc = await createTokenAccount(
-      provider,
+    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
     );
-    await usdcMintToken.mintTo(
+    let createSecondUserUsdcTrns = new anchor.web3.Transaction();
+    createSecondUserUsdcTrns.add(transferSolInstr);
+    createSecondUserUsdcTrns.add(createSecondUserUsdcInstr);
+    await provider.send(createSecondUserUsdcTrns);
+    await usdcMintAccount.mintTo(
       secondUserUsdc,
       provider.wallet.publicKey,
       [],
       secondDeposit.toString(),
-    );
-    secondUserRedeemable = await createTokenAccount(
-      provider,
-      redeemableMint,
-      provider.wallet.publicKey
+    )
+
+    // Checking the transfer went through
+    secondUserUsdcAccount = await getTokenAccount(provider, secondUserUsdc);
+    assert.ok(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: {
-        poolAccount: poolAccount.publicKey,
-        poolSigner,
-        redeemableMint,
-        poolUsdc,
-        userAuthority: provider.wallet.publicKey,
+        userAuthority: secondUserKeypair.publicKey,
         userUsdc: secondUserUsdc,
         userRedeemable: secondUserRedeemable,
+        idoAccount,
+        usdcMint,
+        redeemableMint,
+        watermelonMint,
+        poolUsdc,
         tokenProgram: TOKEN_PROGRAM_ID,
-        clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
       },
+      instructions: [
+        program.instruction.initUserRedeemable({
+          accounts: {
+            userAuthority: secondUserKeypair.publicKey,
+            userRedeemable: secondUserRedeemable,
+            idoAccount,
+            redeemableMint,
+            systemProgram: anchor.web3.SystemProgram.programId,
+            tokenProgram: TOKEN_PROGRAM_ID,
+            rent: anchor.web3.SYSVAR_RENT_PUBKEY,
+          },
+        })
+      ],
+      signers: [secondUserKeypair]
     });
 
-    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));
+
+    totalPoolUsdc = firstDeposit.add(secondDeposit);
+    poolUsdcAccount = await getTokenAccount(provider, poolUsdc);
+    assert.ok(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: {
-        poolAccount: poolAccount.publicKey,
-        poolSigner,
-        redeemableMint,
-        poolUsdc,
         userAuthority: provider.wallet.publicKey,
-        userUsdc,
+        escrowUsdc,
         userRedeemable,
+        idoAccount,
+        usdcMint,
+        redeemableMint,
+        watermelonMint,
+        poolUsdc,
         tokenProgram: TOKEN_PROGRAM_ID,
-        clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
       },
+      instructions: [
+        program.instruction.initEscrowUsdc({
+          accounts: {
+            userAuthority: provider.wallet.publicKey,
+            escrowUsdc,
+            idoAccount,
+            usdcMint,
+            systemProgram: anchor.web3.SystemProgram.programId,
+            tokenProgram: TOKEN_PROGRAM_ID,
+            rent: anchor.web3.SYSVAR_RENT_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));
+    escrowUsdcAccount = await getTokenAccount(provider, escrowUsdc);
+    assert.ok(escrowUsdcAccount.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);
+    // 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,
@@ -268,15 +436,15 @@ describe("ido-pool", () => {
 
     await program.rpc.exchangeRedeemableForWatermelon(firstUserRedeemable, {
       accounts: {
-        poolAccount: poolAccount.publicKey,
-        poolSigner,
-        redeemableMint,
-        poolWatermelon,
+        payer: provider.wallet.publicKey,
         userAuthority: provider.wallet.publicKey,
         userWatermelon,
         userRedeemable,
+        idoAccount,
+        watermelonMint,
+        redeemableMint,
+        poolWatermelon,
         tokenProgram: TOKEN_PROGRAM_ID,
-        clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
       },
     });
 
@@ -290,24 +458,46 @@ describe("ido-pool", () => {
     assert.ok(userWatermelonAccount.amount.eq(redeemedWatermelon));
   });
 
-  it("Exchanges second users Redeemable tokens for watermelon", async () => {
+  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,
-      provider.wallet.publicKey
+      secondUserKeypair.publicKey
     );
 
     await program.rpc.exchangeRedeemableForWatermelon(secondDeposit, {
       accounts: {
-        poolAccount: poolAccount.publicKey,
-        poolSigner,
-        redeemableMint,
-        poolWatermelon,
-        userAuthority: provider.wallet.publicKey,
+        payer: provider.wallet.publicKey,
+        userAuthority: secondUserKeypair.publicKey,
         userWatermelon: secondUserWatermelon,
         userRedeemable: secondUserRedeemable,
+        idoAccount,
+        watermelonMint,
+        redeemableMint,
+        poolWatermelon,
         tokenProgram: TOKEN_PROGRAM_ID,
-        clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
       },
     });
 
@@ -316,21 +506,80 @@ describe("ido-pool", () => {
   });
 
   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: {
-        poolAccount: poolAccount.publicKey,
-        poolSigner,
-        distributionAuthority: provider.wallet.publicKey,
-        creatorUsdc,
+        idoAuthority: provider.wallet.publicKey,
+        idoAuthorityUsdc,
+        idoAccount,
+        usdcMint,
+        watermelonMint,
         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));
+    idoAuthorityUsdcAccount = await getTokenAccount(provider, idoAuthorityUsdc);
+    assert.ok(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.ok(userUsdcAccount.amount.eq(firstWithdrawal));
+  });
+
+
+  function PoolBumps() {
+    this.idoAccount;
+    this.redeemableMint;
+    this.poolWatermelon;
+    this.poolUsdc;
+  };
+
+  function IdoTimes() {
+    this.startIdo;
+    this.endDeposts;
+    this.endIdo;
+    this.endEscrow;
+  };
 });

+ 1 - 0
tests/ido-pool/tests/utils/index.js

@@ -11,6 +11,7 @@ const TOKEN_PROGRAM_ID = new anchor.web3.PublicKey(
 
 // Our own sleep function.
 function sleep(ms) {
+  console.log("Sleeping for", ms / 1000, "seconds");
   return new Promise((resolve) => setTimeout(resolve, ms));
 }