Skip to content
2 changes: 2 additions & 0 deletions programs/drift/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,8 @@ pub enum ErrorCode {
WrongNumberOfConstituents,
#[msg("Oracle too stale for LP AUM update")]
OracleTooStaleForLPAUMUpdate,
#[msg("Insufficient constituent token balance")]
InsufficientConstituentTokenBalance,
}

#[macro_export]
Expand Down
4 changes: 4 additions & 0 deletions programs/drift/src/instructions/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4545,13 +4545,17 @@ pub fn handle_initialize_constituent<'info>(
.resize_with((current_len + 1) as usize, WeightDatum::default);
constituent_target_weights.validate()?;

msg!("initializing constituent {}", lp_pool.constituents);

constituent.spot_market_index = spot_market_index;
constituent.constituent_index = lp_pool.constituents;
constituent.decimals = decimals;
constituent.max_weight_deviation = max_weight_deviation;
constituent.swap_fee_min = swap_fee_min;
constituent.swap_fee_max = swap_fee_max;
constituent.oracle_staleness_threshold = oracle_staleness_threshold;
constituent.pubkey = ctx.accounts.constituent.key();
constituent.mint = ctx.accounts.spot_market_mint.key();
constituent.constituent_index = (constituent_target_weights.weights.len() - 1) as u16;
lp_pool.constituents += 1;

Expand Down
281 changes: 267 additions & 14 deletions programs/drift/src/instructions/lp_pool.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use anchor_lang::{prelude::*, Accounts, Key, Result};
use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface};

use crate::{
error::ErrorCode,
Expand All @@ -12,22 +13,30 @@ use crate::{
state::{
constituent_map::{ConstituentMap, ConstituentSet},
lp_pool::{
AmmConstituentDatum, AmmConstituentMappingFixed, LPPool, WeightValidationFlags,
CONSTITUENT_PDA_SEED,
AmmConstituentDatum, AmmConstituentMappingFixed, Constituent, LPPool,
WeightValidationFlags,
},
oracle::OraclePriceData,
perp_market::{AmmCacheFixed, CacheInfo, AMM_POSITIONS_CACHE},
perp_market_map::MarketSet,
spot_market_map::get_writable_spot_market_set_from_many,
state::State,
user::MarketType,
zero_copy::{AccountZeroCopy, ZeroCopyLoader},
events::LPSwapRecord,
},
validate,
};

use solana_program::sysvar::clock::Clock;

use super::optional_accounts::{load_maps, AccountMaps};
use crate::state::lp_pool::{AMM_MAP_PDA_SEED, CONSTITUENT_TARGET_WEIGHT_PDA_SEED};
use crate::controller::spot_balance::update_spot_market_cumulative_interest;
use crate::controller::token::{receive, send_from_program_vault};
use crate::instructions::constraints::*;
use crate::state::lp_pool::{
AMM_MAP_PDA_SEED, CONSTITUENT_PDA_SEED, CONSTITUENT_TARGET_WEIGHT_PDA_SEED,
};

pub fn handle_update_constituent_target_weights<'c: 'info, 'info>(
ctx: Context<'_, '_, 'c, 'info, UpdateConstituentTargetWeights<'info>>,
Expand Down Expand Up @@ -191,7 +200,6 @@ pub fn handle_update_lp_pool_aum<'c: 'info, 'info>(
};

if oracle_price.is_none() {
msg!("hi");
return Err(ErrorCode::OracleTooStaleForLPAUMUpdate.into());
}

Expand Down Expand Up @@ -226,16 +234,214 @@ pub fn handle_update_lp_pool_aum<'c: 'info, 'info>(
Ok(())
}

#[access_control(
fill_not_paused(&ctx.accounts.state)
)]
pub fn handle_lp_pool_swap<'c: 'info, 'info>(
ctx: Context<'_, '_, 'c, 'info, LPPoolSwap<'info>>,
in_market_index: u16,
out_market_index: u16,
in_amount: u64,
min_out_amount: u64,
) -> Result<()> {
validate!(
in_market_index != out_market_index,
ErrorCode::InvalidSpotMarketAccount,
"In and out spot market indices cannot be the same"
)?;

let slot = Clock::get()?.slot;
let now = Clock::get()?.unix_timestamp;
let state = &ctx.accounts.state;
let lp_pool = &ctx.accounts.lp_pool.load()?;

let mut in_constituent = ctx.accounts.in_constituent.load_mut()?;
let mut out_constituent = ctx.accounts.out_constituent.load_mut()?;

let constituent_target_weights = ctx.accounts.constituent_target_weights.load_zc()?;

let AccountMaps {
perp_market_map: _,
spot_market_map,
mut oracle_map,
} = load_maps(
&mut ctx.remaining_accounts.iter().peekable(),
&MarketSet::new(),
&get_writable_spot_market_set_from_many(vec![in_market_index, out_market_index]),
slot,
Some(state.oracle_guard_rails),
)?;

let mut in_spot_market = spot_market_map.get_ref_mut(&in_market_index)?;
let mut out_spot_market = spot_market_map.get_ref_mut(&out_market_index)?;

let in_oracle_id = in_spot_market.oracle_id();
let out_oracle_id = out_spot_market.oracle_id();

let (in_oracle, in_oracle_validity) = oracle_map.get_price_data_and_validity(
MarketType::Spot,
in_spot_market.market_index,
&in_oracle_id,
in_spot_market.historical_oracle_data.last_oracle_price_twap,
in_spot_market.get_max_confidence_interval_multiplier()?,
)?;
let in_oracle = in_oracle.clone();

let (out_oracle, out_oracle_validity) = oracle_map.get_price_data_and_validity(
MarketType::Spot,
out_spot_market.market_index,
&out_oracle_id,
out_spot_market
.historical_oracle_data
.last_oracle_price_twap,
out_spot_market.get_max_confidence_interval_multiplier()?,
)?;

if !is_oracle_valid_for_action(in_oracle_validity, Some(DriftAction::LpPoolSwap))? {
msg!(
"In oracle data for spot market {} is invalid for lp pool swap.",
in_spot_market.market_index,
);
return Err(ErrorCode::InvalidOracle.into());
}

if !is_oracle_valid_for_action(out_oracle_validity, Some(DriftAction::LpPoolSwap))? {
msg!(
"Out oracle data for spot market {} is invalid for lp pool swap.",
out_spot_market.market_index,
);
return Err(ErrorCode::InvalidOracle.into());
}

update_spot_market_cumulative_interest(&mut in_spot_market, Some(&in_oracle), now)?;
update_spot_market_cumulative_interest(&mut out_spot_market, Some(&out_oracle), now)?;

let in_target_weight =
constituent_target_weights.get_target_weight(in_constituent.constituent_index)?;
let out_target_weight =
constituent_target_weights.get_target_weight(out_constituent.constituent_index)?;

let (in_amount, out_amount, in_fee, out_fee) = lp_pool.get_swap_amount(
&in_oracle,
&out_oracle,
&in_constituent,
&out_constituent,
&in_spot_market,
&out_spot_market,
in_target_weight,
out_target_weight,
in_amount,
)?;
msg!(
"in_amount: {}, out_amount: {}, in_fee: {}, out_fee: {}",
in_amount,
out_amount,
in_fee,
out_fee
);
let out_amount_net_fees = if out_fee > 0 {
out_amount.safe_sub(out_fee.unsigned_abs() as u64)?
} else {
out_amount.safe_add(out_fee.unsigned_abs() as u64)?
};

validate!(
out_amount_net_fees >= min_out_amount,
ErrorCode::SlippageOutsideLimit,
format!(
"Slippage outside limit: out_amount_net_fees({}) < min_out_amount({})",
out_amount_net_fees, min_out_amount
)
.as_str()
)?;

validate!(
out_amount_net_fees <= out_constituent.token_balance,
ErrorCode::InsufficientConstituentTokenBalance,
format!(
"Insufficient out constituent balance: out_amount_net_fees({}) > out_constituent.token_balance({})",
out_amount_net_fees, out_constituent.token_balance
)
.as_str()
)?;

in_constituent.record_swap_fees(in_fee)?;
out_constituent.record_swap_fees(out_fee)?;

emit!(LPSwapRecord {
ts: now,
authority: ctx.accounts.authority.key(),
amount_out: out_amount_net_fees,
amount_in: in_amount,
fee_out: out_fee,
fee_in: in_fee,
out_spot_market_index: out_market_index,
in_spot_market_index: in_market_index,
out_constituent_index: out_constituent.constituent_index,
in_constituent_index: in_constituent.constituent_index,
out_oracle_price: out_oracle.price,
in_oracle_price: in_oracle.price,
mint_out: out_constituent.mint,
mint_in: in_constituent.mint,
});

receive(
&ctx.accounts.token_program,
&ctx.accounts.user_in_token_account,
&ctx.accounts.constituent_in_token_account,
&ctx.accounts.authority,
in_amount,
&Some((*ctx.accounts.in_market_mint).clone()),
)?;

send_from_program_vault(
&ctx.accounts.token_program,
&ctx.accounts.constituent_out_token_account,
&ctx.accounts.user_out_token_account,
&ctx.accounts.drift_signer,
state.signer_nonce,
out_amount_net_fees,
&Some((*ctx.accounts.out_market_mint).clone()),
)?;

ctx.accounts.constituent_in_token_account.reload()?;
ctx.accounts.constituent_out_token_account.reload()?;

in_constituent.sync_token_balance(ctx.accounts.constituent_in_token_account.amount);
out_constituent.sync_token_balance(ctx.accounts.constituent_out_token_account.amount);

Ok(())
}

#[derive(Accounts)]
#[instruction(
lp_pool_name: [u8; 32],
)]
pub struct UpdateLPPoolAum<'info> {
pub struct UpdateConstituentTargetWeights<'info> {
pub state: Box<Account<'info, State>>,
#[account(mut)]
pub keeper: Signer<'info>,
#[account(
seeds = [AMM_MAP_PDA_SEED.as_ref(), lp_pool.key().as_ref()],
bump,
)]
/// CHECK: checked in AmmConstituentMappingZeroCopy checks
pub amm_constituent_mapping: AccountInfo<'info>,
#[account(
mut,
seeds = [CONSTITUENT_TARGET_WEIGHT_PDA_SEED.as_ref(), lp_pool.key().as_ref()],
bump,
)]
/// CHECK: checked in ConstituentTargetWeightsZeroCopy checks
pub constituent_target_weights: AccountInfo<'info>,
#[account(
mut,
seeds = [AMM_POSITIONS_CACHE.as_ref()],
bump,
)]
/// CHECK: checked in ConstituentTargetWeightsZeroCopy checks
pub amm_cache: AccountInfo<'info>,
#[account(
seeds = [b"lp_pool", lp_pool_name.as_ref()],
bump,
)]
Expand All @@ -246,33 +452,80 @@ pub struct UpdateLPPoolAum<'info> {
#[instruction(
lp_pool_name: [u8; 32],
)]
pub struct UpdateConstituentTargetWeights<'info> {
pub struct UpdateLPPoolAum<'info> {
pub state: Box<Account<'info, State>>,
#[account(mut)]
pub keeper: Signer<'info>,
#[account(
seeds = [AMM_MAP_PDA_SEED.as_ref(), lp_pool.key().as_ref()],
mut,
seeds = [b"lp_pool", lp_pool_name.as_ref()],
bump,
)]
/// CHECK: checked in AmmConstituentMappingZeroCopy checks
pub amm_constituent_mapping: AccountInfo<'info>,
pub lp_pool: AccountLoader<'info, LPPool>,
}

/// `in`/`out` is in the program's POV for this swap. So `user_in_token_account` is the user owned token account
/// for the `in` token for this swap.
#[derive(Accounts)]
#[instruction(
in_market_index: u16,
out_market_index: u16,
)]
pub struct LPPoolSwap<'info> {
/// CHECK: forced drift_signer
pub drift_signer: AccountInfo<'info>,
pub state: Box<Account<'info, State>>,
pub lp_pool: AccountLoader<'info, LPPool>,
#[account(
mut,
seeds = [CONSTITUENT_TARGET_WEIGHT_PDA_SEED.as_ref(), lp_pool.key().as_ref()],
bump,
)]
/// CHECK: checked in ConstituentTargetWeightsZeroCopy checks
pub constituent_target_weights: AccountInfo<'info>,

#[account(mut)]
pub constituent_in_token_account: Box<InterfaceAccount<'info, TokenAccount>>,
#[account(mut)]
pub constituent_out_token_account: Box<InterfaceAccount<'info, TokenAccount>>,

#[account(
mut,
seeds = [AMM_POSITIONS_CACHE.as_ref()],
constraint = user_in_token_account.mint.eq(&constituent_in_token_account.mint)
)]
pub user_in_token_account: Box<InterfaceAccount<'info, TokenAccount>>,
#[account(
mut,
constraint = user_out_token_account.mint.eq(&constituent_out_token_account.mint)
)]
pub user_out_token_account: Box<InterfaceAccount<'info, TokenAccount>>,

#[account(
mut,
seeds = [CONSTITUENT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), in_market_index.to_le_bytes().as_ref()],
bump,
constraint = in_constituent.load()?.mint.eq(&constituent_in_token_account.mint)
)]
/// CHECK: checked in ConstituentTargetWeightsZeroCopy checks
pub amm_cache: AccountInfo<'info>,
pub in_constituent: AccountLoader<'info, Constituent>,
#[account(
seeds = [b"lp_pool", lp_pool_name.as_ref()],
mut,
seeds = [CONSTITUENT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), out_market_index.to_le_bytes().as_ref()],
bump,
constraint = out_constituent.load()?.mint.eq(&constituent_out_token_account.mint)
)]
pub lp_pool: AccountLoader<'info, LPPool>,
pub out_constituent: AccountLoader<'info, Constituent>,

#[account(
constraint = in_market_mint.key() == in_constituent.load()?.mint,
)]
pub in_market_mint: Box<InterfaceAccount<'info, Mint>>,
#[account(
constraint = out_market_mint.key() == out_constituent.load()?.mint,
)]
pub out_market_mint: Box<InterfaceAccount<'info, Mint>>,

pub authority: Signer<'info>,

// TODO: in/out token program
pub token_program: Interface<'info, TokenInterface>,
}
1 change: 1 addition & 0 deletions programs/drift/src/instructions/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ use crate::state::fulfillment_params::openbook_v2::OpenbookV2FulfillmentParams;
use crate::state::fulfillment_params::phoenix::PhoenixFulfillmentParams;
use crate::state::fulfillment_params::serum::SerumFulfillmentParams;
use crate::state::high_leverage_mode_config::HighLeverageModeConfig;
use crate::state::lp_pool::{Constituent, LPPool};
use crate::state::margin_calculation::MarginContext;
use crate::state::oracle::StrictOraclePrice;
use crate::state::order_params::{
Expand Down
Loading