tests: Change ido pool to use PDAs (#699)
This commit is contained in:
parent
1c5c6a8aba
commit
d5fe9281a7
|
@ -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 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;
|
||||
let ido_account = &mut ctx.accounts.ido_account;
|
||||
|
||||
// Transfer Watermelon from creator to pool 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();
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
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>,
|
||||
}
|
||||
|
||||
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());
|
||||
#[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> {
|
||||
// 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>,
|
||||
}
|
||||
|
||||
#[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]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
);
|
||||
poolUsdc = await createTokenAccount(provider, usdcMint, poolSigner);
|
||||
bumps.redeemableMint = redeemableMintBump;
|
||||
|
||||
poolAccount = anchor.web3.Keypair.generate();
|
||||
const [poolWatermelon, poolWatermelonBump] = await anchor.web3.PublicKey.findProgramAddress(
|
||||
[Buffer.from(idoName), Buffer.from("pool_watermelon")],
|
||||
program.programId
|
||||
);
|
||||
bumps.poolWatermelon = poolWatermelonBump;
|
||||
|
||||
const [poolUsdc, poolUsdcBump] = await anchor.web3.PublicKey.findProgramAddress(
|
||||
[Buffer.from(idoName), Buffer.from("pool_usdc")],
|
||||
program.programId
|
||||
);
|
||||
bumps.poolUsdc = poolUsdcBump;
|
||||
|
||||
idoTimes = new IdoTimes();
|
||||
const nowBn = new anchor.BN(Date.now() / 1000);
|
||||
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,
|
||||
usdcMint,
|
||||
provider.wallet.publicKey
|
||||
const [idoAccount] = await anchor.web3.PublicKey.findProgramAddress(
|
||||
[Buffer.from(idoName)],
|
||||
program.programId
|
||||
);
|
||||
await usdcMintToken.mintTo(
|
||||
|
||||
const [redeemableMint] = await anchor.web3.PublicKey.findProgramAddress(
|
||||
[Buffer.from(idoName), Buffer.from("redeemable_mint")],
|
||||
program.programId
|
||||
);
|
||||
|
||||
const [poolUsdc] = await anchor.web3.PublicKey.findProgramAddress(
|
||||
[Buffer.from(idoName), Buffer.from("pool_usdc")],
|
||||
program.programId
|
||||
);
|
||||
|
||||
userUsdc = await Token.getAssociatedTokenAddress(
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
TOKEN_PROGRAM_ID,
|
||||
usdcMint,
|
||||
program.provider.wallet.publicKey
|
||||
);
|
||||
// Get the instructions to add to the RPC call
|
||||
let createUserUsdcInstr = Token.createAssociatedTokenAccountInstruction(
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
TOKEN_PROGRAM_ID,
|
||||
usdcMint,
|
||||
userUsdc,
|
||||
program.provider.wallet.publicKey,
|
||||
program.provider.wallet.publicKey,
|
||||
)
|
||||
let createUserUsdcTrns = new anchor.web3.Transaction().add(createUserUsdcInstr);
|
||||
await provider.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;
|
||||
};
|
||||
});
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue