examples: IDO pool (#220)

This commit is contained in:
Henry-E 2021-04-23 19:09:28 +01:00 committed by GitHub
parent 2bd84f23b5
commit 31662b95e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 907 additions and 0 deletions

View File

@ -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

View File

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

View File

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

View File

@ -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.
}

View File

@ -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"

View File

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

View File

@ -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(())
}

View File

@ -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));
});
});

View File

@ -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,
};