use {
crate::{
error::PerpetualsError,
math,
state::{
custody::Custody,
oracle::OraclePrice,
perpetuals::Perpetuals,
pool::{AumCalcMode, Pool},
},
},
anchor_lang::prelude::*,
anchor_spl::token::{Mint, Token, TokenAccount},
solana_program::program_error::ProgramError,
};
#[derive(Accounts)]
#[instruction(params: AddLiquidityParams)]
pub struct AddLiquidity<'info> {
#[account(mut)]
pub owner: Signer<'info>,
#[account(
mut,
constraint = funding_account.mint == custody.mint,
has_one = owner
)]
pub funding_account: Box<Account<'info, TokenAccount>>,
#[account(
mut,
constraint = lp_token_account.mint == lp_token_mint.key(),
has_one = owner
)]
pub lp_token_account: Box<Account<'info, TokenAccount>>,
#[account(
seeds = [b"transfer_authority"],
bump = perpetuals.transfer_authority_bump
)]
pub transfer_authority: AccountInfo<'info>,
#[account(
seeds = [b"perpetuals"],
bump = perpetuals.perpetuals_bump
)]
pub perpetuals: Box<Account<'info, Perpetuals>>,
#[account(
mut,
seeds = [b"pool",
pool.name.as_bytes()],
bump = pool.bump
)]
pub pool: Box<Account<'info, Pool>>,
#[account(
mut,
seeds = [b"custody",
pool.key().as_ref(),
custody.mint.as_ref()],
bump = custody.bump
)]
pub custody: Box<Account<'info, Custody>>,
#[account(
constraint = custody_oracle_account.key() == custody.oracle.oracle_account
)]
pub custody_oracle_account: AccountInfo<'info>,
#[account(
mut,
seeds = [b"custody_token_account",
pool.key().as_ref(),
custody.mint.as_ref()],
bump = custody.token_account_bump
)]
pub custody_token_account: Box<Account<'info, TokenAccount>>,
#[account(
mut,
seeds = [b"lp_token_mint",
pool.key().as_ref()],
bump = pool.lp_token_bump
)]
pub lp_token_mint: Box<Account<'info, Mint>>,
token_program: Program<'info, Token>,
}
#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct AddLiquidityParams {
pub amount_in: u64,
pub min_lp_amount_out: u64,
}
pub fn add_liquidity(ctx: Context<AddLiquidity>, params: &AddLiquidityParams) -> Result<()> {
msg!("Check permissions");
let perpetuals = ctx.accounts.perpetuals.as_mut();
let custody = ctx.accounts.custody.as_mut();
require!(
perpetuals.permissions.allow_add_liquidity && custody.permissions.allow_add_liquidity,
PerpetualsError::InstructionNotAllowed
);
msg!("Validate inputs");
if params.amount_in == 0 {
return Err(ProgramError::InvalidArgument.into());
}
let pool = ctx.accounts.pool.as_mut();
let token_id = pool.get_token_id(&custody.key())?;
let curtime = perpetuals.get_time()?;
let token_price = OraclePrice::new_from_oracle(
custody.oracle.oracle_type,
&ctx.accounts.custody_oracle_account.to_account_info(),
custody.oracle.max_price_error,
custody.oracle.max_price_age_sec,
curtime,
false,
)?;
let token_ema_price = OraclePrice::new_from_oracle(
custody.oracle.oracle_type,
&ctx.accounts.custody_oracle_account.to_account_info(),
custody.oracle.max_price_error,
custody.oracle.max_price_age_sec,
curtime,
custody.pricing.use_ema,
)?;
let min_price = if token_price < token_ema_price {
token_price
} else {
token_ema_price
};
let fee_amount =
pool.get_add_liquidity_fee(token_id, params.amount_in, custody, &token_ema_price)?;
msg!("Collected fee: {}", fee_amount);
msg!("Check pool constraints");
let protocol_fee = Pool::get_fee_amount(custody.fees.protocol_share, fee_amount)?;
let deposit_amount = math::checked_sub(params.amount_in, protocol_fee)?;
require!(
pool.check_token_ratio(token_id, deposit_amount, 0, custody, &token_ema_price)?,
PerpetualsError::TokenRatioOutOfRange
);
msg!("Transfer tokens");
perpetuals.transfer_tokens_from_user(
ctx.accounts.funding_account.to_account_info(),
ctx.accounts.custody_token_account.to_account_info(),
ctx.accounts.owner.to_account_info(),
ctx.accounts.token_program.to_account_info(),
params.amount_in,
)?;
msg!("Compute assets under management");
let pool_amount_usd =
pool.get_assets_under_management_usd(AumCalcMode::Max, ctx.remaining_accounts, curtime)?;
let no_fee_amount = math::checked_sub(params.amount_in, fee_amount)?;
require_gte!(
no_fee_amount,
1u64,
PerpetualsError::InsufficientAmountReturned
);
let token_amount_usd = min_price.get_asset_amount_usd(no_fee_amount, custody.decimals)?;
let lp_amount = if pool_amount_usd == 0 {
token_amount_usd
} else {
math::checked_as_u64(math::checked_div(
math::checked_mul(
token_amount_usd as u128,
ctx.accounts.lp_token_mint.supply as u128,
)?,
pool_amount_usd,
)?)?
};
msg!("LP tokens to mint: {}", lp_amount);
require!(
lp_amount >= params.min_lp_amount_out,
PerpetualsError::MaxPriceSlippage
);
perpetuals.mint_tokens(
ctx.accounts.lp_token_mint.to_account_info(),
ctx.accounts.lp_token_account.to_account_info(),
ctx.accounts.transfer_authority.to_account_info(),
ctx.accounts.token_program.to_account_info(),
lp_amount,
)?;
msg!("Update custody stats");
custody.collected_fees.add_liquidity_usd = custody
.collected_fees
.add_liquidity_usd
.wrapping_add(token_ema_price.get_asset_amount_usd(fee_amount, custody.decimals)?);
custody.volume_stats.add_liquidity_usd = custody
.volume_stats
.add_liquidity_usd
.wrapping_add(token_ema_price.get_asset_amount_usd(params.amount_in, custody.decimals)?);
custody.assets.protocol_fees = math::checked_add(custody.assets.protocol_fees, protocol_fee)?;
custody.assets.owned = math::checked_add(custody.assets.owned, deposit_amount)?;
custody.update_borrow_rate(curtime)?;
msg!("Update pool stats");
custody.exit(&crate::ID)?;
pool.aum_usd =
pool.get_assets_under_management_usd(AumCalcMode::EMA, ctx.remaining_accounts, curtime)?;
Ok(())
}