Skip to content

Commit cb34da4

Browse files
authored
Wphan/dlp-swap-ixs (#1592)
* add lp_swap ix * rebase * test helpers * swap works * fix swaps, add more cargo tests for fees n swap amt * remove console.logs * address PR comments * merge upstream
1 parent 1c24027 commit cb34da4

File tree

17 files changed

+1607
-286
lines changed

17 files changed

+1607
-286
lines changed

programs/drift/src/error.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,8 @@ pub enum ErrorCode {
651651
WrongNumberOfConstituents,
652652
#[msg("Oracle too stale for LP AUM update")]
653653
OracleTooStaleForLPAUMUpdate,
654+
#[msg("Insufficient constituent token balance")]
655+
InsufficientConstituentTokenBalance,
654656
}
655657

656658
#[macro_export]

programs/drift/src/instructions/admin.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4545,13 +4545,17 @@ pub fn handle_initialize_constituent<'info>(
45454545
.resize_with((current_len + 1) as usize, WeightDatum::default);
45464546
constituent_target_weights.validate()?;
45474547

4548+
msg!("initializing constituent {}", lp_pool.constituents);
4549+
45484550
constituent.spot_market_index = spot_market_index;
4551+
constituent.constituent_index = lp_pool.constituents;
45494552
constituent.decimals = decimals;
45504553
constituent.max_weight_deviation = max_weight_deviation;
45514554
constituent.swap_fee_min = swap_fee_min;
45524555
constituent.swap_fee_max = swap_fee_max;
45534556
constituent.oracle_staleness_threshold = oracle_staleness_threshold;
45544557
constituent.pubkey = ctx.accounts.constituent.key();
4558+
constituent.mint = ctx.accounts.spot_market_mint.key();
45554559
constituent.constituent_index = (constituent_target_weights.weights.len() - 1) as u16;
45564560
lp_pool.constituents += 1;
45574561

programs/drift/src/instructions/lp_pool.rs

Lines changed: 267 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use anchor_lang::{prelude::*, Accounts, Key, Result};
2+
use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface};
23

34
use crate::{
45
error::ErrorCode,
@@ -12,22 +13,30 @@ use crate::{
1213
state::{
1314
constituent_map::{ConstituentMap, ConstituentSet},
1415
lp_pool::{
15-
AmmConstituentDatum, AmmConstituentMappingFixed, LPPool, WeightValidationFlags,
16-
CONSTITUENT_PDA_SEED,
16+
AmmConstituentDatum, AmmConstituentMappingFixed, Constituent, LPPool,
17+
WeightValidationFlags,
1718
},
1819
oracle::OraclePriceData,
1920
perp_market::{AmmCacheFixed, CacheInfo, AMM_POSITIONS_CACHE},
2021
perp_market_map::MarketSet,
22+
spot_market_map::get_writable_spot_market_set_from_many,
2123
state::State,
2224
user::MarketType,
2325
zero_copy::{AccountZeroCopy, ZeroCopyLoader},
26+
events::LPSwapRecord,
2427
},
2528
validate,
2629
};
30+
2731
use solana_program::sysvar::clock::Clock;
2832

2933
use super::optional_accounts::{load_maps, AccountMaps};
30-
use crate::state::lp_pool::{AMM_MAP_PDA_SEED, CONSTITUENT_TARGET_WEIGHT_PDA_SEED};
34+
use crate::controller::spot_balance::update_spot_market_cumulative_interest;
35+
use crate::controller::token::{receive, send_from_program_vault};
36+
use crate::instructions::constraints::*;
37+
use crate::state::lp_pool::{
38+
AMM_MAP_PDA_SEED, CONSTITUENT_PDA_SEED, CONSTITUENT_TARGET_WEIGHT_PDA_SEED,
39+
};
3140

3241
pub fn handle_update_constituent_target_weights<'c: 'info, 'info>(
3342
ctx: Context<'_, '_, 'c, 'info, UpdateConstituentTargetWeights<'info>>,
@@ -191,7 +200,6 @@ pub fn handle_update_lp_pool_aum<'c: 'info, 'info>(
191200
};
192201

193202
if oracle_price.is_none() {
194-
msg!("hi");
195203
return Err(ErrorCode::OracleTooStaleForLPAUMUpdate.into());
196204
}
197205

@@ -226,16 +234,214 @@ pub fn handle_update_lp_pool_aum<'c: 'info, 'info>(
226234
Ok(())
227235
}
228236

237+
#[access_control(
238+
fill_not_paused(&ctx.accounts.state)
239+
)]
240+
pub fn handle_lp_pool_swap<'c: 'info, 'info>(
241+
ctx: Context<'_, '_, 'c, 'info, LPPoolSwap<'info>>,
242+
in_market_index: u16,
243+
out_market_index: u16,
244+
in_amount: u64,
245+
min_out_amount: u64,
246+
) -> Result<()> {
247+
validate!(
248+
in_market_index != out_market_index,
249+
ErrorCode::InvalidSpotMarketAccount,
250+
"In and out spot market indices cannot be the same"
251+
)?;
252+
253+
let slot = Clock::get()?.slot;
254+
let now = Clock::get()?.unix_timestamp;
255+
let state = &ctx.accounts.state;
256+
let lp_pool = &ctx.accounts.lp_pool.load()?;
257+
258+
let mut in_constituent = ctx.accounts.in_constituent.load_mut()?;
259+
let mut out_constituent = ctx.accounts.out_constituent.load_mut()?;
260+
261+
let constituent_target_weights = ctx.accounts.constituent_target_weights.load_zc()?;
262+
263+
let AccountMaps {
264+
perp_market_map: _,
265+
spot_market_map,
266+
mut oracle_map,
267+
} = load_maps(
268+
&mut ctx.remaining_accounts.iter().peekable(),
269+
&MarketSet::new(),
270+
&get_writable_spot_market_set_from_many(vec![in_market_index, out_market_index]),
271+
slot,
272+
Some(state.oracle_guard_rails),
273+
)?;
274+
275+
let mut in_spot_market = spot_market_map.get_ref_mut(&in_market_index)?;
276+
let mut out_spot_market = spot_market_map.get_ref_mut(&out_market_index)?;
277+
278+
let in_oracle_id = in_spot_market.oracle_id();
279+
let out_oracle_id = out_spot_market.oracle_id();
280+
281+
let (in_oracle, in_oracle_validity) = oracle_map.get_price_data_and_validity(
282+
MarketType::Spot,
283+
in_spot_market.market_index,
284+
&in_oracle_id,
285+
in_spot_market.historical_oracle_data.last_oracle_price_twap,
286+
in_spot_market.get_max_confidence_interval_multiplier()?,
287+
)?;
288+
let in_oracle = in_oracle.clone();
289+
290+
let (out_oracle, out_oracle_validity) = oracle_map.get_price_data_and_validity(
291+
MarketType::Spot,
292+
out_spot_market.market_index,
293+
&out_oracle_id,
294+
out_spot_market
295+
.historical_oracle_data
296+
.last_oracle_price_twap,
297+
out_spot_market.get_max_confidence_interval_multiplier()?,
298+
)?;
299+
300+
if !is_oracle_valid_for_action(in_oracle_validity, Some(DriftAction::LpPoolSwap))? {
301+
msg!(
302+
"In oracle data for spot market {} is invalid for lp pool swap.",
303+
in_spot_market.market_index,
304+
);
305+
return Err(ErrorCode::InvalidOracle.into());
306+
}
307+
308+
if !is_oracle_valid_for_action(out_oracle_validity, Some(DriftAction::LpPoolSwap))? {
309+
msg!(
310+
"Out oracle data for spot market {} is invalid for lp pool swap.",
311+
out_spot_market.market_index,
312+
);
313+
return Err(ErrorCode::InvalidOracle.into());
314+
}
315+
316+
update_spot_market_cumulative_interest(&mut in_spot_market, Some(&in_oracle), now)?;
317+
update_spot_market_cumulative_interest(&mut out_spot_market, Some(&out_oracle), now)?;
318+
319+
let in_target_weight =
320+
constituent_target_weights.get_target_weight(in_constituent.constituent_index)?;
321+
let out_target_weight =
322+
constituent_target_weights.get_target_weight(out_constituent.constituent_index)?;
323+
324+
let (in_amount, out_amount, in_fee, out_fee) = lp_pool.get_swap_amount(
325+
&in_oracle,
326+
&out_oracle,
327+
&in_constituent,
328+
&out_constituent,
329+
&in_spot_market,
330+
&out_spot_market,
331+
in_target_weight,
332+
out_target_weight,
333+
in_amount,
334+
)?;
335+
msg!(
336+
"in_amount: {}, out_amount: {}, in_fee: {}, out_fee: {}",
337+
in_amount,
338+
out_amount,
339+
in_fee,
340+
out_fee
341+
);
342+
let out_amount_net_fees = if out_fee > 0 {
343+
out_amount.safe_sub(out_fee.unsigned_abs() as u64)?
344+
} else {
345+
out_amount.safe_add(out_fee.unsigned_abs() as u64)?
346+
};
347+
348+
validate!(
349+
out_amount_net_fees >= min_out_amount,
350+
ErrorCode::SlippageOutsideLimit,
351+
format!(
352+
"Slippage outside limit: out_amount_net_fees({}) < min_out_amount({})",
353+
out_amount_net_fees, min_out_amount
354+
)
355+
.as_str()
356+
)?;
357+
358+
validate!(
359+
out_amount_net_fees <= out_constituent.token_balance,
360+
ErrorCode::InsufficientConstituentTokenBalance,
361+
format!(
362+
"Insufficient out constituent balance: out_amount_net_fees({}) > out_constituent.token_balance({})",
363+
out_amount_net_fees, out_constituent.token_balance
364+
)
365+
.as_str()
366+
)?;
367+
368+
in_constituent.record_swap_fees(in_fee)?;
369+
out_constituent.record_swap_fees(out_fee)?;
370+
371+
emit!(LPSwapRecord {
372+
ts: now,
373+
authority: ctx.accounts.authority.key(),
374+
amount_out: out_amount_net_fees,
375+
amount_in: in_amount,
376+
fee_out: out_fee,
377+
fee_in: in_fee,
378+
out_spot_market_index: out_market_index,
379+
in_spot_market_index: in_market_index,
380+
out_constituent_index: out_constituent.constituent_index,
381+
in_constituent_index: in_constituent.constituent_index,
382+
out_oracle_price: out_oracle.price,
383+
in_oracle_price: in_oracle.price,
384+
mint_out: out_constituent.mint,
385+
mint_in: in_constituent.mint,
386+
});
387+
388+
receive(
389+
&ctx.accounts.token_program,
390+
&ctx.accounts.user_in_token_account,
391+
&ctx.accounts.constituent_in_token_account,
392+
&ctx.accounts.authority,
393+
in_amount,
394+
&Some((*ctx.accounts.in_market_mint).clone()),
395+
)?;
396+
397+
send_from_program_vault(
398+
&ctx.accounts.token_program,
399+
&ctx.accounts.constituent_out_token_account,
400+
&ctx.accounts.user_out_token_account,
401+
&ctx.accounts.drift_signer,
402+
state.signer_nonce,
403+
out_amount_net_fees,
404+
&Some((*ctx.accounts.out_market_mint).clone()),
405+
)?;
406+
407+
ctx.accounts.constituent_in_token_account.reload()?;
408+
ctx.accounts.constituent_out_token_account.reload()?;
409+
410+
in_constituent.sync_token_balance(ctx.accounts.constituent_in_token_account.amount);
411+
out_constituent.sync_token_balance(ctx.accounts.constituent_out_token_account.amount);
412+
413+
Ok(())
414+
}
415+
229416
#[derive(Accounts)]
230417
#[instruction(
231418
lp_pool_name: [u8; 32],
232419
)]
233-
pub struct UpdateLPPoolAum<'info> {
420+
pub struct UpdateConstituentTargetWeights<'info> {
234421
pub state: Box<Account<'info, State>>,
235422
#[account(mut)]
236423
pub keeper: Signer<'info>,
424+
#[account(
425+
seeds = [AMM_MAP_PDA_SEED.as_ref(), lp_pool.key().as_ref()],
426+
bump,
427+
)]
428+
/// CHECK: checked in AmmConstituentMappingZeroCopy checks
429+
pub amm_constituent_mapping: AccountInfo<'info>,
430+
#[account(
431+
mut,
432+
seeds = [CONSTITUENT_TARGET_WEIGHT_PDA_SEED.as_ref(), lp_pool.key().as_ref()],
433+
bump,
434+
)]
435+
/// CHECK: checked in ConstituentTargetWeightsZeroCopy checks
436+
pub constituent_target_weights: AccountInfo<'info>,
237437
#[account(
238438
mut,
439+
seeds = [AMM_POSITIONS_CACHE.as_ref()],
440+
bump,
441+
)]
442+
/// CHECK: checked in ConstituentTargetWeightsZeroCopy checks
443+
pub amm_cache: AccountInfo<'info>,
444+
#[account(
239445
seeds = [b"lp_pool", lp_pool_name.as_ref()],
240446
bump,
241447
)]
@@ -246,33 +452,80 @@ pub struct UpdateLPPoolAum<'info> {
246452
#[instruction(
247453
lp_pool_name: [u8; 32],
248454
)]
249-
pub struct UpdateConstituentTargetWeights<'info> {
455+
pub struct UpdateLPPoolAum<'info> {
250456
pub state: Box<Account<'info, State>>,
251457
#[account(mut)]
252458
pub keeper: Signer<'info>,
253459
#[account(
254-
seeds = [AMM_MAP_PDA_SEED.as_ref(), lp_pool.key().as_ref()],
460+
mut,
461+
seeds = [b"lp_pool", lp_pool_name.as_ref()],
255462
bump,
256463
)]
257-
/// CHECK: checked in AmmConstituentMappingZeroCopy checks
258-
pub amm_constituent_mapping: AccountInfo<'info>,
464+
pub lp_pool: AccountLoader<'info, LPPool>,
465+
}
466+
467+
/// `in`/`out` is in the program's POV for this swap. So `user_in_token_account` is the user owned token account
468+
/// for the `in` token for this swap.
469+
#[derive(Accounts)]
470+
#[instruction(
471+
in_market_index: u16,
472+
out_market_index: u16,
473+
)]
474+
pub struct LPPoolSwap<'info> {
475+
/// CHECK: forced drift_signer
476+
pub drift_signer: AccountInfo<'info>,
477+
pub state: Box<Account<'info, State>>,
478+
pub lp_pool: AccountLoader<'info, LPPool>,
259479
#[account(
260480
mut,
261481
seeds = [CONSTITUENT_TARGET_WEIGHT_PDA_SEED.as_ref(), lp_pool.key().as_ref()],
262482
bump,
263483
)]
264484
/// CHECK: checked in ConstituentTargetWeightsZeroCopy checks
265485
pub constituent_target_weights: AccountInfo<'info>,
486+
487+
#[account(mut)]
488+
pub constituent_in_token_account: Box<InterfaceAccount<'info, TokenAccount>>,
489+
#[account(mut)]
490+
pub constituent_out_token_account: Box<InterfaceAccount<'info, TokenAccount>>,
491+
266492
#[account(
267493
mut,
268-
seeds = [AMM_POSITIONS_CACHE.as_ref()],
494+
constraint = user_in_token_account.mint.eq(&constituent_in_token_account.mint)
495+
)]
496+
pub user_in_token_account: Box<InterfaceAccount<'info, TokenAccount>>,
497+
#[account(
498+
mut,
499+
constraint = user_out_token_account.mint.eq(&constituent_out_token_account.mint)
500+
)]
501+
pub user_out_token_account: Box<InterfaceAccount<'info, TokenAccount>>,
502+
503+
#[account(
504+
mut,
505+
seeds = [CONSTITUENT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), in_market_index.to_le_bytes().as_ref()],
269506
bump,
507+
constraint = in_constituent.load()?.mint.eq(&constituent_in_token_account.mint)
270508
)]
271-
/// CHECK: checked in ConstituentTargetWeightsZeroCopy checks
272-
pub amm_cache: AccountInfo<'info>,
509+
pub in_constituent: AccountLoader<'info, Constituent>,
273510
#[account(
274-
seeds = [b"lp_pool", lp_pool_name.as_ref()],
511+
mut,
512+
seeds = [CONSTITUENT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), out_market_index.to_le_bytes().as_ref()],
275513
bump,
514+
constraint = out_constituent.load()?.mint.eq(&constituent_out_token_account.mint)
276515
)]
277-
pub lp_pool: AccountLoader<'info, LPPool>,
516+
pub out_constituent: AccountLoader<'info, Constituent>,
517+
518+
#[account(
519+
constraint = in_market_mint.key() == in_constituent.load()?.mint,
520+
)]
521+
pub in_market_mint: Box<InterfaceAccount<'info, Mint>>,
522+
#[account(
523+
constraint = out_market_mint.key() == out_constituent.load()?.mint,
524+
)]
525+
pub out_market_mint: Box<InterfaceAccount<'info, Mint>>,
526+
527+
pub authority: Signer<'info>,
528+
529+
// TODO: in/out token program
530+
pub token_program: Interface<'info, TokenInterface>,
278531
}

programs/drift/src/instructions/user.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ use crate::state::fulfillment_params::openbook_v2::OpenbookV2FulfillmentParams;
6767
use crate::state::fulfillment_params::phoenix::PhoenixFulfillmentParams;
6868
use crate::state::fulfillment_params::serum::SerumFulfillmentParams;
6969
use crate::state::high_leverage_mode_config::HighLeverageModeConfig;
70+
use crate::state::lp_pool::{Constituent, LPPool};
7071
use crate::state::margin_calculation::MarginContext;
7172
use crate::state::oracle::StrictOraclePrice;
7273
use crate::state::order_params::{

0 commit comments

Comments
 (0)