anchor/tests/ido-pool/programs/ido-pool/src/lib.rs

412 lines
16 KiB
Rust

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