examples: IDO pool (#220)
This commit is contained in:
parent
2bd84f23b5
commit
31662b95e8
|
@ -64,6 +64,7 @@ jobs:
|
|||
name: Runs the examples 2
|
||||
script:
|
||||
- pushd examples/chat && yarn && anchor test && popd
|
||||
- pushd examples/ido-pool && yarn && anchor test && popd
|
||||
- pushd examples/tutorial/basic-0 && anchor test && popd
|
||||
- pushd examples/tutorial/basic-1 && anchor test && popd
|
||||
- pushd examples/tutorial/basic-2 && anchor test && popd
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
cluster = "localnet"
|
||||
wallet = "~/.config/solana/id.json"
|
|
@ -0,0 +1,4 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"programs/*"
|
||||
]
|
|
@ -0,0 +1,12 @@
|
|||
// Migrations are an early feature. Currently, they're nothing more than this
|
||||
// single deploy script that's invoked from the CLI, injecting a provider
|
||||
// configured from the workspace's Anchor.toml.
|
||||
|
||||
const anchor = require("@project-serum/anchor");
|
||||
|
||||
module.exports = async function (provider) {
|
||||
// Configure client to use the provider.
|
||||
anchor.setProvider(provider);
|
||||
|
||||
// Add your deploy script here.
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "ido-pool"
|
||||
version = "0.1.0"
|
||||
description = "Created with Anchor"
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
name = "ido_pool"
|
||||
|
||||
[features]
|
||||
no-entrypoint = []
|
||||
no-idl = []
|
||||
cpi = ["no-entrypoint"]
|
||||
default = []
|
||||
|
||||
[dependencies]
|
||||
anchor-lang = "0.4.4"
|
||||
anchor-spl = "0.4.4"
|
|
@ -0,0 +1,2 @@
|
|||
[target.bpfel-unknown-unknown.dependencies.std]
|
||||
features = []
|
|
@ -0,0 +1,393 @@
|
|||
//! An IDO pool program implementing the Mango Markets token sale design here:
|
||||
//! https://docs.mango.markets/litepaper#token-sale.
|
||||
|
||||
use anchor_lang::prelude::*;
|
||||
use anchor_lang::solana_program::program_option::COption;
|
||||
use anchor_spl::token::{self, Burn, Mint, MintTo, TokenAccount, Transfer};
|
||||
|
||||
#[program]
|
||||
pub mod ido_pool {
|
||||
use super::*;
|
||||
|
||||
#[access_control(InitializePool::accounts(&ctx, nonce))]
|
||||
pub fn initialize_pool(
|
||||
ctx: Context<InitializePool>,
|
||||
num_ido_tokens: u64,
|
||||
nonce: u8,
|
||||
start_ido_ts: i64,
|
||||
end_deposits_ts: i64,
|
||||
end_ido_ts: i64,
|
||||
) -> Result<()> {
|
||||
if !(ctx.accounts.clock.unix_timestamp < start_ido_ts
|
||||
&& start_ido_ts < end_deposits_ts
|
||||
&& end_deposits_ts <= end_ido_ts)
|
||||
{
|
||||
return Err(ErrorCode::InitTime.into());
|
||||
}
|
||||
|
||||
let pool_account = &mut ctx.accounts.pool_account;
|
||||
pool_account.redeemable_mint = *ctx.accounts.redeemable_mint.to_account_info().key;
|
||||
pool_account.pool_watermelon = *ctx.accounts.pool_watermelon.to_account_info().key;
|
||||
pool_account.watermelon_mint = ctx.accounts.pool_watermelon.mint;
|
||||
pool_account.pool_usdc = *ctx.accounts.pool_usdc.to_account_info().key;
|
||||
pool_account.distribution_authority = *ctx.accounts.distribution_authority.key;
|
||||
pool_account.nonce = nonce;
|
||||
pool_account.num_ido_tokens = num_ido_tokens;
|
||||
pool_account.start_ido_ts = start_ido_ts;
|
||||
pool_account.end_deposits_ts = end_deposits_ts;
|
||||
pool_account.end_ido_ts = end_ido_ts;
|
||||
|
||||
msg!(
|
||||
"pool usdc owner: {}, pool signer key: {}",
|
||||
ctx.accounts.pool_usdc.owner,
|
||||
ctx.accounts.pool_signer.key
|
||||
);
|
||||
|
||||
// Transfer Watermelon from creator to pool account.
|
||||
let cpi_accounts = Transfer {
|
||||
from: ctx.accounts.creator_watermelon.to_account_info(),
|
||||
to: ctx.accounts.pool_watermelon.to_account_info(),
|
||||
authority: ctx.accounts.distribution_authority.to_account_info(),
|
||||
};
|
||||
let cpi_program = ctx.accounts.token_program.clone();
|
||||
let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
|
||||
token::transfer(cpi_ctx, num_ido_tokens)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[access_control(unrestricted_phase(&ctx))]
|
||||
pub fn exchange_usdc_for_redeemable(
|
||||
ctx: Context<ExchangeUsdcForRedeemable>,
|
||||
amount: u64,
|
||||
) -> Result<()> {
|
||||
// While token::transfer will check this, we prefer a verbose err msg.
|
||||
if ctx.accounts.user_usdc.amount < amount {
|
||||
return Err(ErrorCode::LowUsdc.into());
|
||||
}
|
||||
|
||||
// Transfer user's USDC to pool USDC account.
|
||||
let cpi_accounts = Transfer {
|
||||
from: ctx.accounts.user_usdc.to_account_info(),
|
||||
to: ctx.accounts.pool_usdc.to_account_info(),
|
||||
authority: ctx.accounts.user_authority.clone(),
|
||||
};
|
||||
let cpi_program = ctx.accounts.token_program.to_account_info();
|
||||
let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
|
||||
token::transfer(cpi_ctx, amount)?;
|
||||
|
||||
// Mint Redeemable to user Redeemable account.
|
||||
let seeds = &[
|
||||
ctx.accounts.pool_account.watermelon_mint.as_ref(),
|
||||
&[ctx.accounts.pool_account.nonce],
|
||||
];
|
||||
let signer = &[&seeds[..]];
|
||||
let cpi_accounts = MintTo {
|
||||
mint: ctx.accounts.redeemable_mint.to_account_info(),
|
||||
to: ctx.accounts.user_redeemable.to_account_info(),
|
||||
authority: ctx.accounts.pool_signer.clone(),
|
||||
};
|
||||
let cpi_program = ctx.accounts.token_program.clone();
|
||||
let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer);
|
||||
token::mint_to(cpi_ctx, amount)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[access_control(withdraw_only_phase(&ctx))]
|
||||
pub fn exchange_redeemable_for_usdc(
|
||||
ctx: Context<ExchangeRedeemableForUsdc>,
|
||||
amount: u64,
|
||||
) -> Result<()> {
|
||||
// While token::burn will check this, we prefer a verbose err msg.
|
||||
if ctx.accounts.user_redeemable.amount < amount {
|
||||
return Err(ErrorCode::LowRedeemable.into());
|
||||
}
|
||||
|
||||
// Burn the user's redeemable tokens.
|
||||
let cpi_accounts = Burn {
|
||||
mint: ctx.accounts.redeemable_mint.to_account_info(),
|
||||
to: ctx.accounts.user_redeemable.to_account_info(),
|
||||
authority: ctx.accounts.user_authority.to_account_info(),
|
||||
};
|
||||
let cpi_program = ctx.accounts.token_program.clone();
|
||||
let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
|
||||
token::burn(cpi_ctx, amount)?;
|
||||
|
||||
// Transfer USDC from pool account to user.
|
||||
let seeds = &[
|
||||
ctx.accounts.pool_account.watermelon_mint.as_ref(),
|
||||
&[ctx.accounts.pool_account.nonce],
|
||||
];
|
||||
let signer = &[&seeds[..]];
|
||||
let cpi_accounts = Transfer {
|
||||
from: ctx.accounts.pool_usdc.to_account_info(),
|
||||
to: ctx.accounts.user_usdc.to_account_info(),
|
||||
authority: ctx.accounts.pool_signer.to_account_info(),
|
||||
};
|
||||
let cpi_program = ctx.accounts.token_program.clone();
|
||||
let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer);
|
||||
token::transfer(cpi_ctx, amount)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[access_control(ido_over(&ctx.accounts.pool_account, &ctx.accounts.clock))]
|
||||
pub fn exchange_redeemable_for_watermelon(
|
||||
ctx: Context<ExchangeRedeemableForWatermelon>,
|
||||
amount: u64,
|
||||
) -> Result<()> {
|
||||
// While token::burn will check this, we prefer a verbose err msg.
|
||||
if ctx.accounts.user_redeemable.amount < amount {
|
||||
return Err(ErrorCode::LowRedeemable.into());
|
||||
}
|
||||
|
||||
let watermelon_amount = (amount as u128)
|
||||
.checked_mul(ctx.accounts.pool_watermelon.amount as u128)
|
||||
.unwrap()
|
||||
.checked_div(ctx.accounts.redeemable_mint.supply as u128)
|
||||
.unwrap();
|
||||
|
||||
// Burn the user's redeemable tokens.
|
||||
let cpi_accounts = Burn {
|
||||
mint: ctx.accounts.redeemable_mint.to_account_info(),
|
||||
to: ctx.accounts.user_redeemable.to_account_info(),
|
||||
authority: ctx.accounts.user_authority.to_account_info(),
|
||||
};
|
||||
let cpi_program = ctx.accounts.token_program.clone();
|
||||
let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
|
||||
token::burn(cpi_ctx, amount)?;
|
||||
|
||||
// Transfer Watermelon from pool account to user.
|
||||
let seeds = &[
|
||||
ctx.accounts.pool_account.watermelon_mint.as_ref(),
|
||||
&[ctx.accounts.pool_account.nonce],
|
||||
];
|
||||
let signer = &[&seeds[..]];
|
||||
let cpi_accounts = Transfer {
|
||||
from: ctx.accounts.pool_watermelon.to_account_info(),
|
||||
to: ctx.accounts.user_watermelon.to_account_info(),
|
||||
authority: ctx.accounts.pool_signer.to_account_info(),
|
||||
};
|
||||
let cpi_program = ctx.accounts.token_program.clone();
|
||||
let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer);
|
||||
token::transfer(cpi_ctx, watermelon_amount as u64)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[access_control(ido_over(&ctx.accounts.pool_account, &ctx.accounts.clock))]
|
||||
pub fn withdraw_pool_usdc(ctx: Context<WithdrawPoolUsdc>) -> Result<()> {
|
||||
// Transfer total USDC from pool account to creator account.
|
||||
let seeds = &[
|
||||
ctx.accounts.pool_account.watermelon_mint.as_ref(),
|
||||
&[ctx.accounts.pool_account.nonce],
|
||||
];
|
||||
let signer = &[&seeds[..]];
|
||||
let cpi_accounts = Transfer {
|
||||
from: ctx.accounts.pool_usdc.to_account_info(),
|
||||
to: ctx.accounts.creator_usdc.to_account_info(),
|
||||
authority: ctx.accounts.pool_signer.to_account_info(),
|
||||
};
|
||||
let cpi_program = ctx.accounts.token_program.clone();
|
||||
let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer);
|
||||
token::transfer(cpi_ctx, ctx.accounts.pool_usdc.amount)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct InitializePool<'info> {
|
||||
#[account(init)]
|
||||
pub pool_account: ProgramAccount<'info, PoolAccount>,
|
||||
pub pool_signer: AccountInfo<'info>,
|
||||
#[account("redeemable_mint.mint_authority == COption::Some(*pool_signer.key)")]
|
||||
pub redeemable_mint: CpiAccount<'info, Mint>,
|
||||
#[account("usdc_mint.decimals == redeemable_mint.decimals")]
|
||||
pub usdc_mint: CpiAccount<'info, Mint>,
|
||||
#[account(mut, "pool_watermelon.owner == *pool_signer.key")]
|
||||
pub pool_watermelon: CpiAccount<'info, TokenAccount>,
|
||||
#[account("pool_usdc.owner == *pool_signer.key")]
|
||||
pub pool_usdc: CpiAccount<'info, TokenAccount>,
|
||||
#[account(signer)]
|
||||
pub distribution_authority: AccountInfo<'info>,
|
||||
#[account(mut, "creator_watermelon.owner == *distribution_authority.key")]
|
||||
pub creator_watermelon: CpiAccount<'info, TokenAccount>,
|
||||
#[account("token_program.key == &token::ID")]
|
||||
pub token_program: AccountInfo<'info>,
|
||||
pub rent: Sysvar<'info, Rent>,
|
||||
pub clock: Sysvar<'info, Clock>,
|
||||
}
|
||||
|
||||
impl<'info> InitializePool<'info> {
|
||||
fn accounts(ctx: &Context<InitializePool<'info>>, nonce: u8) -> Result<()> {
|
||||
let expected_signer = Pubkey::create_program_address(
|
||||
&[ctx.accounts.pool_watermelon.mint.as_ref(), &[nonce]],
|
||||
ctx.program_id,
|
||||
)
|
||||
.map_err(|_| ErrorCode::InvalidNonce)?;
|
||||
if ctx.accounts.pool_signer.key != &expected_signer {
|
||||
return Err(ErrorCode::InvalidNonce.into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct ExchangeUsdcForRedeemable<'info> {
|
||||
#[account(has_one = redeemable_mint, has_one = pool_usdc)]
|
||||
pub pool_account: ProgramAccount<'info, PoolAccount>,
|
||||
#[account(seeds = [pool_account.watermelon_mint.as_ref(), &[pool_account.nonce]])]
|
||||
pool_signer: AccountInfo<'info>,
|
||||
#[account(
|
||||
mut,
|
||||
"redeemable_mint.mint_authority == COption::Some(*pool_signer.key)"
|
||||
)]
|
||||
pub redeemable_mint: CpiAccount<'info, Mint>,
|
||||
#[account(mut, "pool_usdc.owner == *pool_signer.key")]
|
||||
pub pool_usdc: CpiAccount<'info, TokenAccount>,
|
||||
#[account(signer)]
|
||||
pub user_authority: AccountInfo<'info>,
|
||||
#[account(mut, "user_usdc.owner == *user_authority.key")]
|
||||
pub user_usdc: CpiAccount<'info, TokenAccount>,
|
||||
#[account(mut, "user_redeemable.owner == *user_authority.key")]
|
||||
pub user_redeemable: CpiAccount<'info, TokenAccount>,
|
||||
#[account("token_program.key == &token::ID")]
|
||||
pub token_program: AccountInfo<'info>,
|
||||
pub clock: Sysvar<'info, Clock>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct ExchangeRedeemableForUsdc<'info> {
|
||||
#[account(has_one = redeemable_mint, has_one = pool_usdc)]
|
||||
pub pool_account: ProgramAccount<'info, PoolAccount>,
|
||||
#[account(seeds = [pool_account.watermelon_mint.as_ref(), &[pool_account.nonce]])]
|
||||
pool_signer: AccountInfo<'info>,
|
||||
#[account(
|
||||
mut,
|
||||
"redeemable_mint.mint_authority == COption::Some(*pool_signer.key)"
|
||||
)]
|
||||
pub redeemable_mint: CpiAccount<'info, Mint>,
|
||||
#[account(mut, "pool_usdc.owner == *pool_signer.key")]
|
||||
pub pool_usdc: CpiAccount<'info, TokenAccount>,
|
||||
#[account(signer)]
|
||||
pub user_authority: AccountInfo<'info>,
|
||||
#[account(mut, "user_usdc.owner == *user_authority.key")]
|
||||
pub user_usdc: CpiAccount<'info, TokenAccount>,
|
||||
#[account(mut, "user_redeemable.owner == *user_authority.key")]
|
||||
pub user_redeemable: CpiAccount<'info, TokenAccount>,
|
||||
#[account("token_program.key == &token::ID")]
|
||||
pub token_program: AccountInfo<'info>,
|
||||
pub clock: Sysvar<'info, Clock>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct ExchangeRedeemableForWatermelon<'info> {
|
||||
#[account(has_one = redeemable_mint, has_one = pool_watermelon)]
|
||||
pub pool_account: ProgramAccount<'info, PoolAccount>,
|
||||
#[account(seeds = [pool_account.watermelon_mint.as_ref(), &[pool_account.nonce]])]
|
||||
pool_signer: AccountInfo<'info>,
|
||||
#[account(
|
||||
mut,
|
||||
"redeemable_mint.mint_authority == COption::Some(*pool_signer.key)"
|
||||
)]
|
||||
pub redeemable_mint: CpiAccount<'info, Mint>,
|
||||
#[account(mut, "pool_watermelon.owner == *pool_signer.key")]
|
||||
pub pool_watermelon: CpiAccount<'info, TokenAccount>,
|
||||
#[account(signer)]
|
||||
pub user_authority: AccountInfo<'info>,
|
||||
#[account(mut, "user_watermelon.owner == *user_authority.key")]
|
||||
pub user_watermelon: CpiAccount<'info, TokenAccount>,
|
||||
#[account(mut, "user_redeemable.owner == *user_authority.key")]
|
||||
pub user_redeemable: CpiAccount<'info, TokenAccount>,
|
||||
#[account("token_program.key == &token::ID")]
|
||||
pub token_program: AccountInfo<'info>,
|
||||
pub clock: Sysvar<'info, Clock>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct WithdrawPoolUsdc<'info> {
|
||||
#[account(has_one = pool_usdc, has_one = distribution_authority)]
|
||||
pub pool_account: ProgramAccount<'info, PoolAccount>,
|
||||
#[account(seeds = [pool_account.watermelon_mint.as_ref(), &[pool_account.nonce]])]
|
||||
pub pool_signer: AccountInfo<'info>,
|
||||
#[account(mut, "pool_usdc.owner == *pool_signer.key")]
|
||||
pub pool_usdc: CpiAccount<'info, TokenAccount>,
|
||||
#[account(signer)]
|
||||
pub distribution_authority: AccountInfo<'info>,
|
||||
#[account(mut, "creator_usdc.owner == *distribution_authority.key")]
|
||||
pub creator_usdc: CpiAccount<'info, TokenAccount>,
|
||||
#[account("token_program.key == &token::ID")]
|
||||
pub token_program: AccountInfo<'info>,
|
||||
pub clock: Sysvar<'info, Clock>,
|
||||
}
|
||||
|
||||
#[account]
|
||||
pub struct PoolAccount {
|
||||
pub redeemable_mint: Pubkey,
|
||||
pub pool_watermelon: Pubkey,
|
||||
pub watermelon_mint: Pubkey,
|
||||
pub pool_usdc: Pubkey,
|
||||
pub distribution_authority: Pubkey,
|
||||
pub nonce: u8,
|
||||
pub num_ido_tokens: u64,
|
||||
pub start_ido_ts: i64,
|
||||
pub end_deposits_ts: i64,
|
||||
pub end_ido_ts: i64,
|
||||
}
|
||||
|
||||
#[error]
|
||||
pub enum ErrorCode {
|
||||
#[msg("IDO times are non-sequential")]
|
||||
InitTime,
|
||||
#[msg("IDO has not started")]
|
||||
StartIdoTime,
|
||||
#[msg("Deposits period has ended")]
|
||||
EndDepositsTime,
|
||||
#[msg("IDO has ended")]
|
||||
EndIdoTime,
|
||||
#[msg("IDO has not finished yet")]
|
||||
IdoNotOver,
|
||||
#[msg("Insufficient USDC")]
|
||||
LowUsdc,
|
||||
#[msg("Insufficient redeemable tokens")]
|
||||
LowRedeemable,
|
||||
#[msg("USDC total and redeemable total don't match")]
|
||||
UsdcNotEqRedeem,
|
||||
#[msg("Given nonce is invalid")]
|
||||
InvalidNonce,
|
||||
}
|
||||
|
||||
// Access control modifiers.
|
||||
|
||||
// Asserts the IDO is in the first phase.
|
||||
fn unrestricted_phase<'info>(ctx: &Context<ExchangeUsdcForRedeemable<'info>>) -> Result<()> {
|
||||
if !(ctx.accounts.pool_account.start_ido_ts < ctx.accounts.clock.unix_timestamp) {
|
||||
return Err(ErrorCode::StartIdoTime.into());
|
||||
} else if !(ctx.accounts.clock.unix_timestamp < ctx.accounts.pool_account.end_deposits_ts) {
|
||||
return Err(ErrorCode::EndDepositsTime.into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Asserts the IDO is in the second phase.
|
||||
fn withdraw_only_phase(ctx: &Context<ExchangeRedeemableForUsdc>) -> Result<()> {
|
||||
if !(ctx.accounts.pool_account.start_ido_ts < ctx.accounts.clock.unix_timestamp) {
|
||||
return Err(ErrorCode::StartIdoTime.into());
|
||||
} else if !(ctx.accounts.clock.unix_timestamp < ctx.accounts.pool_account.end_ido_ts) {
|
||||
return Err(ErrorCode::EndIdoTime.into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Asserts the IDO sale period has ended, based on the current timestamp.
|
||||
fn ido_over<'info>(
|
||||
pool_account: &ProgramAccount<'info, PoolAccount>,
|
||||
clock: &Sysvar<'info, Clock>,
|
||||
) -> Result<()> {
|
||||
if !(pool_account.end_ido_ts < clock.unix_timestamp) {
|
||||
return Err(ErrorCode::IdoNotOver.into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
|
@ -0,0 +1,335 @@
|
|||
const anchor = require("@project-serum/anchor");
|
||||
const assert = require("assert");
|
||||
const {
|
||||
TOKEN_PROGRAM_ID,
|
||||
sleep,
|
||||
getTokenAccount,
|
||||
createMint,
|
||||
createTokenAccount,
|
||||
mintToAccount,
|
||||
} = require("./utils");
|
||||
|
||||
describe("ido-pool", () => {
|
||||
const provider = anchor.Provider.local();
|
||||
|
||||
// Configure the client to use the local cluster.
|
||||
anchor.setProvider(provider);
|
||||
|
||||
const program = anchor.workspace.IdoPool;
|
||||
|
||||
// All mints default to 6 decimal places.
|
||||
const watermelonIdoAmount = new anchor.BN(5000000);
|
||||
|
||||
// These are all of the variables we assume exist in the world already and
|
||||
// are available to the client.
|
||||
let usdcMint = null;
|
||||
let watermelonMint = null;
|
||||
let creatorUsdc = null;
|
||||
let creatorWatermelon = null;
|
||||
|
||||
it("Initializes the state-of-the-world", async () => {
|
||||
usdcMint = await createMint(provider);
|
||||
watermelonMint = await createMint(provider);
|
||||
creatorUsdc = await createTokenAccount(
|
||||
provider,
|
||||
usdcMint,
|
||||
provider.wallet.publicKey
|
||||
);
|
||||
creatorWatermelon = await createTokenAccount(
|
||||
provider,
|
||||
watermelonMint,
|
||||
provider.wallet.publicKey
|
||||
);
|
||||
// Mint Watermelon tokens the will be distributed from the IDO pool.
|
||||
await mintToAccount(
|
||||
provider,
|
||||
watermelonMint,
|
||||
creatorWatermelon,
|
||||
watermelonIdoAmount,
|
||||
provider.wallet.publicKey
|
||||
);
|
||||
creator_watermelon_account = await getTokenAccount(
|
||||
provider,
|
||||
creatorWatermelon
|
||||
);
|
||||
assert.ok(creator_watermelon_account.amount.eq(watermelonIdoAmount));
|
||||
});
|
||||
|
||||
// These are all variables the client will have to create to initialize the
|
||||
// IDO pool
|
||||
let poolSigner = null;
|
||||
let redeemableMint = null;
|
||||
let poolWatermelon = null;
|
||||
let poolUsdc = null;
|
||||
let poolAccount = null;
|
||||
|
||||
let startIdoTs = null;
|
||||
let endDepositsTs = null;
|
||||
let endIdoTs = null;
|
||||
|
||||
it("Initializes the IDO pool", async () => {
|
||||
// We use the watermelon mint address as the seed, could use something else though.
|
||||
const [_poolSigner, nonce] = await anchor.web3.PublicKey.findProgramAddress(
|
||||
[watermelonMint.toBuffer()],
|
||||
program.programId
|
||||
);
|
||||
poolSigner = _poolSigner;
|
||||
|
||||
// Pool doesn't need a Redeemable SPL token account because it only
|
||||
// burns and mints redeemable tokens, it never stores them.
|
||||
redeemableMint = await createMint(provider, poolSigner);
|
||||
poolWatermelon = await createTokenAccount(
|
||||
provider,
|
||||
watermelonMint,
|
||||
poolSigner
|
||||
);
|
||||
poolUsdc = await createTokenAccount(provider, usdcMint, poolSigner);
|
||||
|
||||
poolAccount = new anchor.web3.Account();
|
||||
const nowBn = new anchor.BN(Date.now() / 1000);
|
||||
startIdoTs = nowBn.add(new anchor.BN(5));
|
||||
endDepositsTs = nowBn.add(new anchor.BN(10));
|
||||
endIdoTs = nowBn.add(new anchor.BN(15));
|
||||
|
||||
// Atomically create the new account and initialize it with the program.
|
||||
await program.rpc.initializePool(
|
||||
watermelonIdoAmount,
|
||||
nonce,
|
||||
startIdoTs,
|
||||
endDepositsTs,
|
||||
endIdoTs,
|
||||
{
|
||||
accounts: {
|
||||
poolAccount: poolAccount.publicKey,
|
||||
poolSigner,
|
||||
distributionAuthority: provider.wallet.publicKey,
|
||||
creatorWatermelon,
|
||||
creatorUsdc,
|
||||
redeemableMint,
|
||||
usdcMint,
|
||||
poolWatermelon,
|
||||
poolUsdc,
|
||||
tokenProgram: TOKEN_PROGRAM_ID,
|
||||
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
|
||||
clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
|
||||
},
|
||||
signers: [poolAccount],
|
||||
instructions: [
|
||||
await program.account.poolAccount.createInstruction(poolAccount),
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
creators_watermelon_account = await getTokenAccount(
|
||||
provider,
|
||||
creatorWatermelon
|
||||
);
|
||||
assert.ok(creators_watermelon_account.amount.eq(new anchor.BN(0)));
|
||||
});
|
||||
|
||||
// We're going to need to start using the associated program account for creating token accounts
|
||||
// if not in testing, then definitely in production.
|
||||
|
||||
let userUsdc = null;
|
||||
let userRedeemable = null;
|
||||
// 10 usdc
|
||||
const firstDeposit = new anchor.BN(10_000_349);
|
||||
|
||||
it("Exchanges user USDC for redeemable tokens", async () => {
|
||||
// Wait until the IDO has opened.
|
||||
if (Date.now() < startIdoTs.toNumber() * 1000) {
|
||||
await sleep(startIdoTs.toNumber() * 1000 - Date.now() + 1000);
|
||||
}
|
||||
|
||||
userUsdc = await createTokenAccount(
|
||||
provider,
|
||||
usdcMint,
|
||||
provider.wallet.publicKey
|
||||
);
|
||||
await mintToAccount(
|
||||
provider,
|
||||
usdcMint,
|
||||
userUsdc,
|
||||
firstDeposit,
|
||||
provider.wallet.publicKey
|
||||
);
|
||||
userRedeemable = await createTokenAccount(
|
||||
provider,
|
||||
redeemableMint,
|
||||
provider.wallet.publicKey
|
||||
);
|
||||
|
||||
try {
|
||||
const tx = await program.rpc.exchangeUsdcForRedeemable(firstDeposit, {
|
||||
accounts: {
|
||||
poolAccount: poolAccount.publicKey,
|
||||
poolSigner,
|
||||
redeemableMint,
|
||||
poolUsdc,
|
||||
userAuthority: provider.wallet.publicKey,
|
||||
userUsdc,
|
||||
userRedeemable,
|
||||
tokenProgram: TOKEN_PROGRAM_ID,
|
||||
clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.log("This is the error message", err.toString());
|
||||
}
|
||||
poolUsdcAccount = await getTokenAccount(provider, poolUsdc);
|
||||
assert.ok(poolUsdcAccount.amount.eq(firstDeposit));
|
||||
userRedeemableAccount = await getTokenAccount(provider, userRedeemable);
|
||||
assert.ok(userRedeemableAccount.amount.eq(firstDeposit));
|
||||
});
|
||||
|
||||
// 23 usdc
|
||||
const secondDeposit = new anchor.BN(23_000_672);
|
||||
let totalPoolUsdc = null;
|
||||
|
||||
it("Exchanges a second users USDC for redeemable tokens", async () => {
|
||||
secondUserUsdc = await createTokenAccount(
|
||||
provider,
|
||||
usdcMint,
|
||||
provider.wallet.publicKey
|
||||
);
|
||||
await mintToAccount(
|
||||
provider,
|
||||
usdcMint,
|
||||
secondUserUsdc,
|
||||
secondDeposit,
|
||||
provider.wallet.publicKey
|
||||
);
|
||||
secondUserRedeemable = await createTokenAccount(
|
||||
provider,
|
||||
redeemableMint,
|
||||
provider.wallet.publicKey
|
||||
);
|
||||
|
||||
await program.rpc.exchangeUsdcForRedeemable(secondDeposit, {
|
||||
accounts: {
|
||||
poolAccount: poolAccount.publicKey,
|
||||
poolSigner,
|
||||
redeemableMint,
|
||||
poolUsdc,
|
||||
userAuthority: provider.wallet.publicKey,
|
||||
userUsdc: secondUserUsdc,
|
||||
userRedeemable: secondUserRedeemable,
|
||||
tokenProgram: TOKEN_PROGRAM_ID,
|
||||
clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
|
||||
},
|
||||
});
|
||||
|
||||
totalPoolUsdc = firstDeposit.add(secondDeposit);
|
||||
poolUsdcAccount = await getTokenAccount(provider, poolUsdc);
|
||||
assert.ok(poolUsdcAccount.amount.eq(totalPoolUsdc));
|
||||
secondUserRedeemableAccount = await getTokenAccount(
|
||||
provider,
|
||||
secondUserRedeemable
|
||||
);
|
||||
assert.ok(secondUserRedeemableAccount.amount.eq(secondDeposit));
|
||||
});
|
||||
|
||||
const firstWithdrawal = new anchor.BN(2_000_000);
|
||||
|
||||
it("Exchanges user Redeemable tokens for USDC", async () => {
|
||||
await program.rpc.exchangeRedeemableForUsdc(firstWithdrawal, {
|
||||
accounts: {
|
||||
poolAccount: poolAccount.publicKey,
|
||||
poolSigner,
|
||||
redeemableMint,
|
||||
poolUsdc,
|
||||
userAuthority: provider.wallet.publicKey,
|
||||
userUsdc,
|
||||
userRedeemable,
|
||||
tokenProgram: TOKEN_PROGRAM_ID,
|
||||
clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
|
||||
},
|
||||
});
|
||||
|
||||
totalPoolUsdc = totalPoolUsdc.sub(firstWithdrawal);
|
||||
poolUsdcAccount = await getTokenAccount(provider, poolUsdc);
|
||||
assert.ok(poolUsdcAccount.amount.eq(totalPoolUsdc));
|
||||
userUsdcAccount = await getTokenAccount(provider, userUsdc);
|
||||
assert.ok(userUsdcAccount.amount.eq(firstWithdrawal));
|
||||
});
|
||||
|
||||
it("Exchanges user Redeemable tokens for watermelon", async () => {
|
||||
// Wait until the IDO has opened.
|
||||
if (Date.now() < endIdoTs.toNumber() * 1000) {
|
||||
await sleep(endIdoTs.toNumber() * 1000 - Date.now() + 2000);
|
||||
}
|
||||
let firstUserRedeemable = firstDeposit.sub(firstWithdrawal);
|
||||
userWatermelon = await createTokenAccount(
|
||||
provider,
|
||||
watermelonMint,
|
||||
provider.wallet.publicKey
|
||||
);
|
||||
|
||||
await program.rpc.exchangeRedeemableForWatermelon(firstUserRedeemable, {
|
||||
accounts: {
|
||||
poolAccount: poolAccount.publicKey,
|
||||
poolSigner,
|
||||
redeemableMint,
|
||||
poolWatermelon,
|
||||
userAuthority: provider.wallet.publicKey,
|
||||
userWatermelon,
|
||||
userRedeemable,
|
||||
tokenProgram: TOKEN_PROGRAM_ID,
|
||||
clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
|
||||
},
|
||||
});
|
||||
|
||||
poolWatermelonAccount = await getTokenAccount(provider, poolWatermelon);
|
||||
let redeemedWatermelon = firstUserRedeemable
|
||||
.mul(watermelonIdoAmount)
|
||||
.div(totalPoolUsdc);
|
||||
let remainingWatermelon = watermelonIdoAmount.sub(redeemedWatermelon);
|
||||
assert.ok(poolWatermelonAccount.amount.eq(remainingWatermelon));
|
||||
userWatermelonAccount = await getTokenAccount(provider, userWatermelon);
|
||||
assert.ok(userWatermelonAccount.amount.eq(redeemedWatermelon));
|
||||
});
|
||||
|
||||
it("Exchanges second users Redeemable tokens for watermelon", async () => {
|
||||
secondUserWatermelon = await createTokenAccount(
|
||||
provider,
|
||||
watermelonMint,
|
||||
provider.wallet.publicKey
|
||||
);
|
||||
|
||||
await program.rpc.exchangeRedeemableForWatermelon(secondDeposit, {
|
||||
accounts: {
|
||||
poolAccount: poolAccount.publicKey,
|
||||
poolSigner,
|
||||
redeemableMint,
|
||||
poolWatermelon,
|
||||
userAuthority: provider.wallet.publicKey,
|
||||
userWatermelon: secondUserWatermelon,
|
||||
userRedeemable: secondUserRedeemable,
|
||||
tokenProgram: TOKEN_PROGRAM_ID,
|
||||
clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
|
||||
},
|
||||
});
|
||||
|
||||
poolWatermelonAccount = await getTokenAccount(provider, poolWatermelon);
|
||||
assert.ok(poolWatermelonAccount.amount.eq(new anchor.BN(0)));
|
||||
});
|
||||
|
||||
it("Withdraws total USDC from pool account", async () => {
|
||||
await program.rpc.withdrawPoolUsdc({
|
||||
accounts: {
|
||||
poolAccount: poolAccount.publicKey,
|
||||
poolSigner,
|
||||
distributionAuthority: provider.wallet.publicKey,
|
||||
creatorUsdc,
|
||||
poolUsdc,
|
||||
tokenProgram: TOKEN_PROGRAM_ID,
|
||||
clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
|
||||
},
|
||||
});
|
||||
|
||||
poolUsdcAccount = await getTokenAccount(provider, poolUsdc);
|
||||
assert.ok(poolUsdcAccount.amount.eq(new anchor.BN(0)));
|
||||
creatorUsdcAccount = await getTokenAccount(provider, creatorUsdc);
|
||||
assert.ok(creatorUsdcAccount.amount.eq(totalPoolUsdc));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,139 @@
|
|||
// TODO: use the `@solana/spl-token` package instead of utils here.
|
||||
|
||||
const anchor = require("@project-serum/anchor");
|
||||
const serumCmn = require("@project-serum/common");
|
||||
const TokenInstructions = require("@project-serum/serum").TokenInstructions;
|
||||
|
||||
// TODO: remove this constant once @project-serum/serum uses the same version
|
||||
// of @solana/web3.js as anchor (or switch packages).
|
||||
const TOKEN_PROGRAM_ID = new anchor.web3.PublicKey(
|
||||
TokenInstructions.TOKEN_PROGRAM_ID.toString()
|
||||
);
|
||||
|
||||
// Our own sleep function.
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function getTokenAccount(provider, addr) {
|
||||
return await serumCmn.getTokenAccount(provider, addr);
|
||||
}
|
||||
|
||||
async function createMint(provider, authority) {
|
||||
if (authority === undefined) {
|
||||
authority = provider.wallet.publicKey;
|
||||
}
|
||||
const mint = new anchor.web3.Account();
|
||||
const instructions = await createMintInstructions(
|
||||
provider,
|
||||
authority,
|
||||
mint.publicKey
|
||||
);
|
||||
|
||||
const tx = new anchor.web3.Transaction();
|
||||
tx.add(...instructions);
|
||||
|
||||
await provider.send(tx, [mint]);
|
||||
|
||||
return mint.publicKey;
|
||||
}
|
||||
|
||||
async function createMintInstructions(provider, authority, mint) {
|
||||
let instructions = [
|
||||
anchor.web3.SystemProgram.createAccount({
|
||||
fromPubkey: provider.wallet.publicKey,
|
||||
newAccountPubkey: mint,
|
||||
space: 82,
|
||||
lamports: await provider.connection.getMinimumBalanceForRentExemption(82),
|
||||
programId: TOKEN_PROGRAM_ID,
|
||||
}),
|
||||
TokenInstructions.initializeMint({
|
||||
mint,
|
||||
decimals: 6,
|
||||
mintAuthority: authority,
|
||||
}),
|
||||
];
|
||||
return instructions;
|
||||
}
|
||||
|
||||
async function createTokenAccount(provider, mint, owner) {
|
||||
const vault = new anchor.web3.Account();
|
||||
const tx = new anchor.web3.Transaction();
|
||||
tx.add(
|
||||
...(await createTokenAccountInstrs(provider, vault.publicKey, mint, owner))
|
||||
);
|
||||
await provider.send(tx, [vault]);
|
||||
return vault.publicKey;
|
||||
}
|
||||
|
||||
async function createTokenAccountInstrs(
|
||||
provider,
|
||||
newAccountPubkey,
|
||||
mint,
|
||||
owner,
|
||||
lamports
|
||||
) {
|
||||
if (lamports === undefined) {
|
||||
lamports = await provider.connection.getMinimumBalanceForRentExemption(165);
|
||||
}
|
||||
return [
|
||||
anchor.web3.SystemProgram.createAccount({
|
||||
fromPubkey: provider.wallet.publicKey,
|
||||
newAccountPubkey,
|
||||
space: 165,
|
||||
lamports,
|
||||
programId: TOKEN_PROGRAM_ID,
|
||||
}),
|
||||
TokenInstructions.initializeAccount({
|
||||
account: newAccountPubkey,
|
||||
mint,
|
||||
owner,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
async function mintToAccount(
|
||||
provider,
|
||||
mint,
|
||||
destination,
|
||||
amount,
|
||||
mintAuthority
|
||||
) {
|
||||
// mint authority is the provider
|
||||
const tx = new anchor.web3.Transaction();
|
||||
tx.add(
|
||||
...(await createMintToAccountInstrs(
|
||||
mint,
|
||||
destination,
|
||||
amount,
|
||||
mintAuthority
|
||||
))
|
||||
);
|
||||
await provider.send(tx, []);
|
||||
return;
|
||||
}
|
||||
|
||||
async function createMintToAccountInstrs(
|
||||
mint,
|
||||
destination,
|
||||
amount,
|
||||
mintAuthority
|
||||
) {
|
||||
return [
|
||||
TokenInstructions.mintTo({
|
||||
mint,
|
||||
destination: destination,
|
||||
amount: amount,
|
||||
mintAuthority: mintAuthority,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
TOKEN_PROGRAM_ID,
|
||||
sleep,
|
||||
getTokenAccount,
|
||||
createMint,
|
||||
createTokenAccount,
|
||||
mintToAccount,
|
||||
};
|
Loading…
Reference in New Issue