diff --git a/programs/drift/src/controller/builder.rs b/programs/drift/src/controller/builder.rs new file mode 100644 index 000000000..01a3ad6e8 --- /dev/null +++ b/programs/drift/src/controller/builder.rs @@ -0,0 +1,175 @@ +use anchor_lang::prelude::*; + +use crate::controller::spot_balance; +use crate::math::safe_math::SafeMath; +use crate::math::spot_balance::get_token_amount; +use crate::state::builder::{BuilderEscrowZeroCopyMut, BuilderOrder, BuilderOrderBitFlag}; +use crate::state::builder_map::BuilderMap; +use crate::state::events::BuilderSettleRecord; +use crate::state::perp_market_map::PerpMarketMap; +use crate::state::spot_market::SpotBalance; +use crate::state::spot_market_map::SpotMarketMap; +use crate::state::user::MarketType; + +/// Runs through the user's BuilderEscrow account and sweeps any accrued fees to the corresponding +/// builders and referrer. +pub fn sweep_completed_builder_fees_for_market<'a>( + market_index: u16, + builder_escrow: &mut BuilderEscrowZeroCopyMut, + perp_market_map: &PerpMarketMap<'a>, + spot_market_map: &SpotMarketMap<'a>, + builder_map: BuilderMap<'a>, + now_ts: i64, +) -> crate::error::DriftResult<()> { + let perp_market = &mut perp_market_map.get_ref_mut(&market_index)?; + let quote_spot_market = &mut spot_market_map.get_quote_spot_market_mut()?; + + spot_balance::update_spot_market_cumulative_interest(quote_spot_market, None, now_ts)?; + + let orders_len = builder_escrow.orders_len(); + for i in 0..orders_len { + let ( + is_completed, + is_referral_order, + order_market_type, + order_market_index, + fees_accrued, + builder_idx, + ) = { + let ord_ro = match builder_escrow.get_order(i) { + Ok(o) => o, + Err(_) => { + continue; + } + }; + ( + ord_ro.is_completed(), + ord_ro.is_referral_order(), + ord_ro.market_type, + ord_ro.market_index, + ord_ro.fees_accrued, + ord_ro.builder_idx, + ) + }; + + if is_referral_order { + if fees_accrued == 0 { + continue; + } + } else if !(is_completed + && order_market_type == MarketType::Perp + && order_market_index == market_index + && fees_accrued > 0) + { + continue; + } + + let pnl_pool_token_amount = get_token_amount( + perp_market.pnl_pool.scaled_balance, + quote_spot_market, + perp_market.pnl_pool.balance_type(), + )?; + + // TODO: should we add buffer on pnl pool? + if pnl_pool_token_amount < fees_accrued as u128 { + msg!( + "market {} PNL pool has insufficient balance to sweep fees for builder", + market_index + ); + break; + } + + if is_referral_order { + let referrer_authority = if let Some(referrer_authority) = builder_escrow.get_referrer() + { + referrer_authority + } else { + continue; + }; + + let referrer_user = builder_map.get_user_ref_mut(&referrer_authority); + let referrer_builder = builder_map.get_builder_account_mut(&referrer_authority); + + if referrer_user.is_ok() && referrer_builder.is_ok() { + let mut referrer_user = referrer_user.unwrap(); + let mut referrer_builder = referrer_builder.unwrap(); + + spot_balance::transfer_spot_balances( + fees_accrued as i128, + quote_spot_market, + &mut perp_market.pnl_pool, + referrer_user.get_quote_spot_position_mut(), + )?; + + referrer_builder.total_referrer_rewards = referrer_builder + .total_referrer_rewards + .safe_add(fees_accrued as u64)?; + + emit!(BuilderSettleRecord { + ts: now_ts, + builder: None, + referrer: Some(referrer_authority), + fee_settled: fees_accrued as u64, + market_index: order_market_index, + market_type: order_market_type, + builder_total_referrer_rewards: referrer_builder.total_referrer_rewards, + builder_total_builder_rewards: referrer_builder.total_builder_rewards, + builder_sub_account_id: referrer_user.sub_account_id, + }); + + // zero out the order + if let Ok(builder_order) = builder_escrow.get_order_mut(i) { + builder_order.fees_accrued = 0; + } + } + } else { + let builder_authority = match builder_escrow + .get_approved_builder_mut(builder_idx) + .map(|builder| builder.authority) + { + Ok(auth) => auth, + Err(_) => { + continue; + } + }; + + let builder_user = builder_map.get_user_ref_mut(&builder_authority); + let builder = builder_map.get_builder_account_mut(&builder_authority); + + if builder_user.is_ok() && builder.is_ok() { + let mut builder_user = builder_user.unwrap(); + let mut builder_revenue_share = builder.unwrap(); + + spot_balance::transfer_spot_balances( + fees_accrued as i128, + quote_spot_market, + &mut perp_market.pnl_pool, + builder_user.get_quote_spot_position_mut(), + )?; + + builder_revenue_share.total_builder_rewards = builder_revenue_share + .total_builder_rewards + .safe_add(fees_accrued as u64)?; + + emit!(BuilderSettleRecord { + ts: now_ts, + builder: Some(builder_authority), + referrer: None, + fee_settled: fees_accrued as u64, + market_index: order_market_index, + market_type: order_market_type, + builder_total_referrer_rewards: builder_revenue_share.total_referrer_rewards, + builder_total_builder_rewards: builder_revenue_share.total_builder_rewards, + builder_sub_account_id: builder_user.sub_account_id, + }); + + // remove order + if let Ok(builder_order) = builder_escrow.get_order_mut(i) { + *builder_order = BuilderOrder::default(); + } + } + } + } + + Ok(()) +} diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 057f4b816..1813c2c03 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -699,6 +699,8 @@ pub fn liquidate_perp( maker_existing_quote_entry_amount: maker_existing_quote_entry_amount, maker_existing_base_asset_amount: maker_existing_base_asset_amount, trigger_price: None, + builder_idx: None, + builder_fee: None, }; emit!(fill_record); @@ -1083,6 +1085,7 @@ pub fn liquidate_perp_with_fill( clock, order_params, PlaceOrderOptions::default().explanation(OrderActionExplanation::Liquidation), + &mut None, )?; drop(user); @@ -1103,6 +1106,7 @@ pub fn liquidate_perp_with_fill( None, clock, FillMode::Liquidation, + &mut None, )?; let mut user = load_mut!(user_loader)?; diff --git a/programs/drift/src/controller/mod.rs b/programs/drift/src/controller/mod.rs index ecd9a3a6a..614aaa032 100644 --- a/programs/drift/src/controller/mod.rs +++ b/programs/drift/src/controller/mod.rs @@ -1,4 +1,5 @@ pub mod amm; +pub mod builder; pub mod funding; pub mod insurance; pub mod liquidation; diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index 84c2010c6..312153376 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -4,6 +4,7 @@ use std::ops::{Deref, DerefMut}; use std::u64; use crate::msg; +use crate::state::builder::{BuilderEscrowZeroCopyMut, BuilderOrder, BuilderOrderBitFlag}; use crate::state::high_leverage_mode_config::HighLeverageModeConfig; use anchor_lang::prelude::*; @@ -74,7 +75,8 @@ use crate::state::state::FeeStructure; use crate::state::state::*; use crate::state::traits::Size; use crate::state::user::{ - AssetType, Order, OrderBitFlag, OrderStatus, OrderTriggerCondition, OrderType, UserStats, + AssetType, Order, OrderBitFlag, OrderStatus, OrderTriggerCondition, OrderType, ReferrerStatus, + UserStats, }; use crate::state::user::{MarketType, User}; use crate::state::user_map::{UserMap, UserStatsMap}; @@ -108,6 +110,7 @@ pub fn place_perp_order( clock: &Clock, mut params: OrderParams, mut options: PlaceOrderOptions, + builder_order: &mut Option<&mut BuilderOrder>, ) -> DriftResult { let now = clock.unix_timestamp; let slot: u64 = clock.slot; @@ -303,6 +306,10 @@ pub fn place_perp_order( OrderBitFlag::NewTriggerReduceOnly, ); + if builder_order.is_some() { + bit_flags = set_order_bit_flag(bit_flags, true, OrderBitFlag::HasBuilder); + } + let new_order = Order { status: OrderStatus::Open, order_type: params.order_type, @@ -443,6 +450,8 @@ pub fn place_perp_order( None, None, None, + None, + None, )?; emit_stack::<_, { OrderActionRecord::SIZE }>(order_action_record)?; @@ -723,6 +732,8 @@ pub fn cancel_order( None, None, None, + None, + None, )?; emit_stack::<_, { OrderActionRecord::SIZE }>(order_action_record)?; } @@ -849,6 +860,7 @@ pub fn modify_order( clock, order_params, PlaceOrderOptions::default(), + &mut None, )?; } else { place_spot_order( @@ -973,6 +985,7 @@ pub fn fill_perp_order( jit_maker_order_id: Option, clock: &Clock, fill_mode: FillMode, + builder_escrow: &mut Option<&mut BuilderEscrowZeroCopyMut>, ) -> DriftResult<(u64, u64)> { let now = clock.unix_timestamp; let slot = clock.slot; @@ -988,6 +1001,26 @@ pub fn fill_perp_order( .position(|order| order.order_id == order_id && order.status == OrderStatus::Open) .ok_or_else(print_error!(ErrorCode::OrderDoesNotExist))?; + let (mut builder_order, mut builder_order_for_referrer) = + if let Some(escrow) = builder_escrow.as_mut() { + let user_idx = escrow.find_order_index(user.sub_account_id, order_id); + let ref_idx = if escrow.has_referrer() { + escrow.find_or_create_referral_index() + } else { + None + }; + let (order_opt, ref_opt) = escrow.get_two_orders_mut_by_indices(user_idx, ref_idx)?; + (order_opt, ref_opt) + } else { + validate!( + !state.builder_referral_enabled() + || !ReferrerStatus::has_builder_referral(user_stats.referrer_status), + ErrorCode::BuilderEscrowMissing, + "BuilderEscrow account must be included when for referred user" + )?; + (None, None) + }; + let ( order_status, market_index, @@ -1320,6 +1353,8 @@ pub fn fill_perp_order( amm_availability, fill_mode, oracle_stale_for_margin, + &mut builder_order, + &mut builder_order_for_referrer, )?; if base_asset_amount != 0 { @@ -1754,6 +1789,8 @@ fn fulfill_perp_order( amm_availability: AMMAvailability, fill_mode: FillMode, oracle_stale_for_margin: bool, + builder_order: &mut Option<&mut BuilderOrder>, + builder_order_for_referrer: &mut Option<&mut BuilderOrder>, ) -> DriftResult<(u64, u64)> { let market_index = user.orders[user_order_index].market_index; @@ -1853,6 +1890,8 @@ fn fulfill_perp_order( *maker_price, AMMLiquiditySplit::Shared, fill_mode.is_liquidation(), + builder_order, + builder_order_for_referrer, )?; (fill_base_asset_amount, fill_quote_asset_amount) @@ -1898,6 +1937,8 @@ fn fulfill_perp_order( oracle_map, fill_mode.is_liquidation(), None, + builder_order, + builder_order_for_referrer, )?; if maker_fill_base_asset_amount != 0 { @@ -2142,6 +2183,8 @@ pub fn fulfill_perp_order_with_amm( override_fill_price: Option, liquidity_split: AMMLiquiditySplit, is_liquidation: bool, + builder_order: &mut Option<&mut BuilderOrder>, + builder_order_for_referrer: &mut Option<&mut BuilderOrder>, ) -> DriftResult<(u64, u64)> { let position_index = get_position_index(&user.perp_positions, market.market_index)?; let existing_base_asset_amount = user.perp_positions[position_index].base_asset_amount; @@ -2253,6 +2296,7 @@ pub fn fulfill_perp_order_with_amm( referrer_reward, fee_to_market_for_lp, maker_rebate, + builder_fee, } = fees::calculate_fee_for_fulfillment_with_amm( user_stats, quote_asset_amount, @@ -2266,8 +2310,13 @@ pub fn fulfill_perp_order_with_amm( order_post_only, market.fee_adjustment, user.is_high_leverage_mode(MarginRequirementType::Initial), + builder_order.as_ref().map(|o| o.fee_bps), )?; + if let Some(ref mut builder_info) = builder_order { + builder_info.fees_accrued = builder_info.fees_accrued.safe_add(builder_fee)?; + } + let user_position_delta = get_position_delta_for_fill(base_asset_amount, quote_asset_amount, order_direction)?; @@ -2315,7 +2364,13 @@ pub fn fulfill_perp_order_with_amm( user_stats.increment_total_rebate(maker_rebate)?; user_stats.increment_total_referee_discount(referee_discount)?; - if let (Some(referrer), Some(referrer_stats)) = (referrer.as_mut(), referrer_stats.as_mut()) { + if let Some(builder_order_for_referrer) = builder_order_for_referrer.as_mut() { + builder_order_for_referrer.fees_accrued = builder_order_for_referrer + .fees_accrued + .safe_add(referrer_reward)?; + } else if let (Some(referrer), Some(referrer_stats)) = + (referrer.as_mut(), referrer_stats.as_mut()) + { if let Ok(referrer_position) = referrer.force_get_perp_position_mut(market.market_index) { if referrer_reward > 0 { update_quote_asset_amount(referrer_position, market, referrer_reward.cast()?)?; @@ -2330,7 +2385,7 @@ pub fn fulfill_perp_order_with_amm( controller::position::update_quote_asset_and_break_even_amount( &mut user.perp_positions[position_index], market, - -user_fee.cast()?, + -(user_fee.safe_add(builder_fee)?).cast()?, )?; } @@ -2374,6 +2429,7 @@ pub fn fulfill_perp_order_with_amm( &mut user.orders[order_index], base_asset_amount, quote_asset_amount, + builder_order, )?; decrease_open_bids_and_asks( @@ -2455,6 +2511,8 @@ pub fn fulfill_perp_order_with_amm( maker_existing_quote_entry_amount, maker_existing_base_asset_amount, None, + builder_order.as_ref().map(|o| o.builder_idx), + builder_order.as_ref().map(|_| builder_fee), )?; emit_stack::<_, { OrderActionRecord::SIZE }>(order_action_record)?; @@ -2524,6 +2582,8 @@ pub fn fulfill_perp_order_with_match( oracle_map: &mut OracleMap, is_liquidation: bool, amm_lp_allowed_to_jit_make: Option, + builder_order: &mut Option<&mut BuilderOrder>, + builder_order_for_referrer: &mut Option<&mut BuilderOrder>, ) -> DriftResult<(u64, u64, u64)> { if !are_orders_same_market_but_different_sides( &maker.orders[maker_order_index], @@ -2638,6 +2698,8 @@ pub fn fulfill_perp_order_with_match( Some(maker_price), // match the makers price amm_liquidity_split, is_liquidation, + builder_order, + builder_order_for_referrer, )?; total_base_asset_amount = base_asset_amount_filled_by_amm; @@ -2746,6 +2808,7 @@ pub fn fulfill_perp_order_with_match( filler_reward, referrer_reward, referee_discount, + builder_fee, .. } = fees::calculate_fee_for_fulfillment_with_match( taker_stats, @@ -2760,8 +2823,13 @@ pub fn fulfill_perp_order_with_match( &MarketType::Perp, market.fee_adjustment, taker.is_high_leverage_mode(MarginRequirementType::Initial), + builder_order.as_ref().map(|o| o.fee_bps), )?; + if let Some(ref mut builder_order) = builder_order { + builder_order.fees_accrued = builder_order.fees_accrued.safe_add(builder_fee)?; + } + // Increment the markets house's total fee variables market.amm.total_fee = market.amm.total_fee.safe_add(fee_to_market.cast()?)?; market.amm.total_exchange_fee = market @@ -2780,7 +2848,7 @@ pub fn fulfill_perp_order_with_match( controller::position::update_quote_asset_and_break_even_amount( &mut taker.perp_positions[taker_position_index], market, - -taker_fee.cast()?, + -(taker_fee.safe_add(builder_fee)?).cast()?, )?; taker_stats.increment_total_fees(taker_fee)?; @@ -2819,7 +2887,13 @@ pub fn fulfill_perp_order_with_match( filler.update_last_active_slot(slot); } - if let (Some(referrer), Some(referrer_stats)) = (referrer.as_mut(), referrer_stats.as_mut()) { + if let Some(builder_order_for_referrer) = builder_order_for_referrer.as_mut() { + builder_order_for_referrer.fees_accrued = builder_order_for_referrer + .fees_accrued + .safe_add(referrer_reward)?; + } else if let (Some(referrer), Some(referrer_stats)) = + (referrer.as_mut(), referrer_stats.as_mut()) + { if let Ok(referrer_position) = referrer.force_get_perp_position_mut(market.market_index) { if referrer_reward > 0 { update_quote_asset_amount(referrer_position, market, referrer_reward.cast()?)?; @@ -2832,6 +2906,7 @@ pub fn fulfill_perp_order_with_match( &mut taker.orders[taker_order_index], base_asset_amount_fulfilled_by_maker, quote_asset_amount, + builder_order, )?; decrease_open_bids_and_asks( @@ -2845,6 +2920,7 @@ pub fn fulfill_perp_order_with_match( &mut maker.orders[maker_order_index], base_asset_amount_fulfilled_by_maker, quote_asset_amount, + &mut None, )?; decrease_open_bids_and_asks( @@ -2904,6 +2980,8 @@ pub fn fulfill_perp_order_with_match( maker_existing_quote_entry_amount, maker_existing_base_asset_amount, None, + builder_order.as_ref().map(|o| o.builder_idx), + builder_order.as_ref().map(|_| builder_fee), )?; emit_stack::<_, { OrderActionRecord::SIZE }>(order_action_record)?; @@ -2932,6 +3010,7 @@ pub fn update_order_after_fill( order: &mut Order, base_asset_amount: u64, quote_asset_amount: u64, + builder_order: &mut Option<&mut BuilderOrder>, ) -> DriftResult { order.base_asset_amount_filled = order.base_asset_amount_filled.safe_add(base_asset_amount)?; @@ -2941,6 +3020,10 @@ pub fn update_order_after_fill( if order.get_base_asset_amount_unfilled(None)? == 0 { order.status = OrderStatus::Filled; + + if let Some(builder_order) = builder_order { + builder_order.add_bit_flag(BuilderOrderBitFlag::Completed); + } } Ok(()) @@ -3131,6 +3214,8 @@ pub fn trigger_order( None, None, Some(trigger_price), + None, + None, )?; emit!(order_action_record); @@ -3455,6 +3540,7 @@ pub fn burn_user_lp_shares_for_risk_reduction( clock, params, PlaceOrderOptions::default().explanation(OrderActionExplanation::DeriskLp), + &mut None, )?; } @@ -3817,6 +3903,8 @@ pub fn place_spot_order( None, None, None, + None, + None, )?; emit_stack::<_, { OrderActionRecord::SIZE }>(order_action_record)?; @@ -4892,6 +4980,7 @@ pub fn fulfill_spot_order_with_match( &MarketType::Spot, base_market.fee_adjustment, false, + None, )?; // Update taker state @@ -4924,6 +5013,7 @@ pub fn fulfill_spot_order_with_match( &mut taker.orders[taker_order_index], base_asset_amount, quote_asset_amount, + &mut None, )?; let taker_order_direction = taker.orders[taker_order_index].direction; @@ -4970,6 +5060,7 @@ pub fn fulfill_spot_order_with_match( &mut maker.orders[maker_order_index], base_asset_amount, quote_asset_amount, + &mut None, )?; let maker_order_direction = maker.orders[maker_order_index].direction; @@ -5059,6 +5150,8 @@ pub fn fulfill_spot_order_with_match( None, None, None, + None, + None, )?; emit_stack::<_, { OrderActionRecord::SIZE }>(order_action_record)?; @@ -5261,6 +5354,7 @@ pub fn fulfill_spot_order_with_external_market( &mut taker.orders[taker_order_index], base_asset_amount_filled, quote_asset_amount_filled, + &mut None, )?; let taker_order_direction = taker.orders[taker_order_index].direction; @@ -5333,6 +5427,8 @@ pub fn fulfill_spot_order_with_external_market( None, None, None, + None, + None, )?; emit_stack::<_, { OrderActionRecord::SIZE }>(order_action_record)?; @@ -5537,6 +5633,8 @@ pub fn trigger_spot_order( None, None, Some(oracle_price.unsigned_abs()), + None, + None, )?; emit!(order_action_record); diff --git a/programs/drift/src/controller/orders/amm_jit_tests.rs b/programs/drift/src/controller/orders/amm_jit_tests.rs index 8f79f5dcd..d22cde7ac 100644 --- a/programs/drift/src/controller/orders/amm_jit_tests.rs +++ b/programs/drift/src/controller/orders/amm_jit_tests.rs @@ -301,6 +301,8 @@ pub mod amm_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + &mut None, ) .unwrap(); @@ -491,6 +493,8 @@ pub mod amm_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + &mut None, ) .unwrap(); @@ -689,6 +693,8 @@ pub mod amm_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + &mut None, ) .unwrap(); @@ -886,6 +892,8 @@ pub mod amm_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + &mut None, ) .unwrap(); @@ -1085,6 +1093,8 @@ pub mod amm_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + &mut None, ) .unwrap(); @@ -1293,6 +1303,8 @@ pub mod amm_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + &mut None, ) .unwrap(); @@ -1499,6 +1511,8 @@ pub mod amm_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + &mut None, ) .unwrap(); @@ -1728,6 +1742,8 @@ pub mod amm_jit { crate::state::perp_market::AMMAvailability::Unavailable, FillMode::Fill, false, + &mut None, + &mut None, ) .unwrap(); @@ -1928,6 +1944,8 @@ pub mod amm_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + &mut None, ) .unwrap(); @@ -2116,6 +2134,8 @@ pub mod amm_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + &mut None, ) .unwrap(); @@ -2316,6 +2336,8 @@ pub mod amm_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + &mut None, ) .unwrap(); @@ -2567,6 +2589,8 @@ pub mod amm_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + &mut None, ) .unwrap(); @@ -2851,6 +2875,8 @@ pub mod amm_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + &mut None, ) .unwrap(); @@ -3080,6 +3106,8 @@ pub mod amm_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + &mut None, ) .unwrap(); diff --git a/programs/drift/src/controller/orders/amm_lp_jit_tests.rs b/programs/drift/src/controller/orders/amm_lp_jit_tests.rs index 2ef6a02bb..07668cc56 100644 --- a/programs/drift/src/controller/orders/amm_lp_jit_tests.rs +++ b/programs/drift/src/controller/orders/amm_lp_jit_tests.rs @@ -655,6 +655,8 @@ pub mod amm_lp_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + &mut None, ) .unwrap(); @@ -858,6 +860,8 @@ pub mod amm_lp_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + &mut None, ) .unwrap(); @@ -1068,6 +1072,8 @@ pub mod amm_lp_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + &mut None, ) .unwrap(); @@ -1285,6 +1291,8 @@ pub mod amm_lp_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + &mut None, ) .unwrap(); @@ -1505,6 +1513,8 @@ pub mod amm_lp_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + &mut None, ) .unwrap(); @@ -1707,6 +1717,8 @@ pub mod amm_lp_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + &mut None, ) .unwrap(); @@ -1918,6 +1930,8 @@ pub mod amm_lp_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + &mut None, ) .unwrap(); @@ -2121,6 +2135,8 @@ pub mod amm_lp_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + &mut None, ) .unwrap(); @@ -2312,6 +2328,8 @@ pub mod amm_lp_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + &mut None, ) .unwrap(); @@ -2515,6 +2533,8 @@ pub mod amm_lp_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + &mut None, ) .unwrap(); @@ -2766,6 +2786,8 @@ pub mod amm_lp_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + &mut None, ) .unwrap(); @@ -3050,6 +3072,8 @@ pub mod amm_lp_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + &mut None, ) .unwrap(); @@ -3282,6 +3306,8 @@ pub mod amm_lp_jit { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + &mut None, ) .unwrap(); diff --git a/programs/drift/src/controller/orders/fuel_tests.rs b/programs/drift/src/controller/orders/fuel_tests.rs index f29b54add..6eaf86ccf 100644 --- a/programs/drift/src/controller/orders/fuel_tests.rs +++ b/programs/drift/src/controller/orders/fuel_tests.rs @@ -245,6 +245,8 @@ pub mod fuel_scoring { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + &mut None, ) .unwrap(); diff --git a/programs/drift/src/controller/orders/tests.rs b/programs/drift/src/controller/orders/tests.rs index 96038e98c..9870fb861 100644 --- a/programs/drift/src/controller/orders/tests.rs +++ b/programs/drift/src/controller/orders/tests.rs @@ -264,6 +264,7 @@ pub mod fill_order_protected_maker { None, &clock, FillMode::Fill, + &mut None, ) .unwrap(); @@ -373,6 +374,7 @@ pub mod fill_order_protected_maker { None, &clock, FillMode::Fill, + &mut None, ) .unwrap(); @@ -487,6 +489,8 @@ pub mod fulfill_order_with_maker_order { &mut get_oracle_map(), false, None, + &mut None, + &mut None, ) .unwrap(); @@ -611,6 +615,8 @@ pub mod fulfill_order_with_maker_order { &mut get_oracle_map(), false, None, + &mut None, + &mut None, ) .unwrap(); @@ -735,6 +741,8 @@ pub mod fulfill_order_with_maker_order { &mut get_oracle_map(), false, None, + &mut None, + &mut None, ) .unwrap(); @@ -859,6 +867,8 @@ pub mod fulfill_order_with_maker_order { &mut get_oracle_map(), false, None, + &mut None, + &mut None, ) .unwrap(); @@ -982,6 +992,8 @@ pub mod fulfill_order_with_maker_order { &mut get_oracle_map(), false, None, + &mut None, + &mut None, ) .unwrap(); @@ -1072,6 +1084,8 @@ pub mod fulfill_order_with_maker_order { &mut get_oracle_map(), false, None, + &mut None, + &mut None, ) .unwrap(); @@ -1163,6 +1177,8 @@ pub mod fulfill_order_with_maker_order { &mut get_oracle_map(), false, None, + &mut None, + &mut None, ) .unwrap(); @@ -1254,6 +1270,8 @@ pub mod fulfill_order_with_maker_order { &mut get_oracle_map(), false, None, + &mut None, + &mut None, ) .unwrap(); @@ -1345,6 +1363,8 @@ pub mod fulfill_order_with_maker_order { &mut get_oracle_map(), false, None, + &mut None, + &mut None, ) .unwrap(); @@ -1456,6 +1476,8 @@ pub mod fulfill_order_with_maker_order { &mut get_oracle_map(), false, None, + &mut None, + &mut None, ) .unwrap(); @@ -1572,6 +1594,8 @@ pub mod fulfill_order_with_maker_order { &mut get_oracle_map(), false, None, + &mut None, + &mut None, ) .unwrap(); @@ -1693,6 +1717,8 @@ pub mod fulfill_order_with_maker_order { &mut get_oracle_map(), false, None, + &mut None, + &mut None, ) .unwrap(); @@ -1815,6 +1841,8 @@ pub mod fulfill_order_with_maker_order { &mut get_oracle_map(), false, None, + &mut None, + &mut None, ) .unwrap(); @@ -1961,6 +1989,8 @@ pub mod fulfill_order_with_maker_order { &mut get_oracle_map(), false, None, + &mut None, + &mut None, ) .unwrap(); @@ -2082,6 +2112,8 @@ pub mod fulfill_order_with_maker_order { &mut get_oracle_map(), false, None, + &mut None, + &mut None, ) .unwrap(); @@ -2213,6 +2245,8 @@ pub mod fulfill_order_with_maker_order { &mut oracle_map, false, None, + &mut None, + &mut None, ) .unwrap(); @@ -2365,6 +2399,8 @@ pub mod fulfill_order_with_maker_order { &mut oracle_map, false, None, + &mut None, + &mut None, ) .unwrap(); @@ -2515,6 +2551,8 @@ pub mod fulfill_order_with_maker_order { &mut oracle_map, false, None, + &mut None, + &mut None, ) .unwrap(); @@ -2666,6 +2704,8 @@ pub mod fulfill_order_with_maker_order { &mut oracle_map, false, None, + &mut None, + &mut None, ) .unwrap(); @@ -2798,6 +2838,8 @@ pub mod fulfill_order_with_maker_order { &mut get_oracle_map(), false, None, + &mut None, + &mut None, ) .unwrap(); @@ -2929,6 +2971,8 @@ pub mod fulfill_order_with_maker_order { &mut get_oracle_map(), false, None, + &mut None, + &mut None, ) .unwrap(); @@ -3317,6 +3361,8 @@ pub mod fulfill_order { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + &mut None, ) .unwrap(); @@ -3561,6 +3607,8 @@ pub mod fulfill_order { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + &mut None, ) .unwrap(); @@ -3751,6 +3799,8 @@ pub mod fulfill_order { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + &mut None, ) .unwrap(); @@ -3957,6 +4007,8 @@ pub mod fulfill_order { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + &mut None, ) .unwrap(); @@ -4123,6 +4175,8 @@ pub mod fulfill_order { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + &mut None, ) .unwrap(); @@ -4321,6 +4375,8 @@ pub mod fulfill_order { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + &mut None, ); assert!(result.is_ok()); @@ -4508,6 +4564,8 @@ pub mod fulfill_order { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + &mut None, ); assert_eq!(result, Err(ErrorCode::InsufficientCollateral)); @@ -4648,6 +4706,8 @@ pub mod fulfill_order { crate::state::perp_market::AMMAvailability::Immediate, FillMode::Fill, false, + &mut None, + &mut None, ) .unwrap(); @@ -4815,6 +4875,8 @@ pub mod fulfill_order { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + &mut None, ) .unwrap(); @@ -4991,6 +5053,7 @@ pub mod fulfill_order { None, &clock, FillMode::Fill, + &mut None, ) .unwrap(); @@ -5016,6 +5079,7 @@ pub mod fulfill_order { None, &clock, FillMode::Fill, + &mut None, ) .unwrap(); @@ -5165,6 +5229,8 @@ pub mod fulfill_order { // slot, // false, // true, + // &mut None, + // &mut None, // ) // .unwrap(); // @@ -5398,6 +5464,8 @@ pub mod fulfill_order { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + &mut None, ) .unwrap(); @@ -5642,6 +5710,8 @@ pub mod fulfill_order { crate::state::perp_market::AMMAvailability::AfterMinDuration, FillMode::Fill, false, + &mut None, + &mut None, ) .unwrap(); @@ -5899,6 +5969,7 @@ pub mod fill_order { None, &clock, FillMode::Fill, + &mut None, ) .unwrap(); @@ -6101,6 +6172,7 @@ pub mod fill_order { None, &clock, FillMode::Fill, + &mut None, ) .unwrap(); @@ -6230,6 +6302,7 @@ pub mod fill_order { None, &clock, FillMode::Fill, + &mut None, ) .unwrap(); @@ -6392,6 +6465,7 @@ pub mod fill_order { None, &clock, FillMode::Fill, + &mut None, ); assert_eq!(err, Err(ErrorCode::MaxOpenInterest)); diff --git a/programs/drift/src/error.rs b/programs/drift/src/error.rs index 61dee9f5f..d38065943 100644 --- a/programs/drift/src/error.rs +++ b/programs/drift/src/error.rs @@ -639,6 +639,28 @@ pub enum ErrorCode { InvalidIfRebalanceConfig, #[msg("Invalid If Rebalance Swap")] InvalidIfRebalanceSwap, + #[msg("Invalid Builder resize")] + InvalidBuilderResize, + #[msg("Invalid builder approval")] + InvalidBuilderApproval, + #[msg("Could not deserialize builder escrow")] + CouldNotDeserializeBuilderEscrow, + #[msg("Builder has been revoked")] + BuilderRevoked, + #[msg("Builder fee is greater than max fee bps")] + InvalidBuilderFee, + #[msg("BuilderEscrow has too many active orders")] + BuilderEscrowOrdersAccountFull, + #[msg("BuilderEscrow missing")] + BuilderEscrowMissing, + #[msg("Builder missing")] + BuilderMissing, + #[msg("Invalid BuilderAccount")] + InvalidBuilderAccount, + #[msg("Cannot revoke builder with open orders")] + CannotRevokeBuilderWithOpenOrders, + #[msg("Unable to load builder account")] + UnableToLoadBuilderAccount, } #[macro_export] diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index a019e58a5..778ff0364 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -4976,6 +4976,29 @@ pub fn handle_update_feature_bit_flags_median_trigger_price( Ok(()) } +pub fn handle_update_feature_bit_flags_builder_referral( + ctx: Context, + enable: bool, +) -> Result<()> { + let state = &mut ctx.accounts.state; + if enable { + validate!( + ctx.accounts.admin.key().eq(&state.admin), + ErrorCode::DefaultError, + "Only state admin can enable feature bit flags" + )?; + + msg!("Setting 4th bit to 1, enabling builder referral"); + state.feature_bit_flags = + state.feature_bit_flags | (FeatureBitFlags::BuilderReferral as u8); + } else { + msg!("Setting 4th bit to 0, disabling builder referral"); + state.feature_bit_flags = + state.feature_bit_flags & !(FeatureBitFlags::BuilderReferral as u8); + } + Ok(()) +} + #[derive(Accounts)] pub struct Initialize<'info> { #[account(mut)] diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index c34b8dda8..b0a891335 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -23,6 +23,7 @@ use crate::error::ErrorCode; use crate::ids::admin_hot_wallet; use crate::ids::{jupiter_mainnet_3, jupiter_mainnet_4, jupiter_mainnet_6, serum_program}; use crate::instructions::constraints::*; +use crate::instructions::optional_accounts::get_builder_escrow_account; use crate::instructions::optional_accounts::{load_maps, AccountMaps}; use crate::math::casting::Cast; use crate::math::constants::QUOTE_SPOT_MARKET_INDEX; @@ -32,6 +33,10 @@ use crate::math::position::calculate_base_asset_value_and_pnl_with_oracle_price; use crate::math::safe_math::SafeMath; use crate::math::spot_withdraw::validate_spot_market_vault_amount; use crate::optional_accounts::{get_token_mint, update_prelaunch_oracle}; +use crate::state::builder::BuilderEscrowZeroCopyMut; +use crate::state::builder::BuilderOrder; +use crate::state::builder::BuilderOrderBitFlag; +use crate::state::builder_map::load_builder_map; use crate::state::events::{DeleteUserRecord, OrderActionExplanation, SignedMsgOrderRecord}; use crate::state::fill_mode::FillMode; use crate::state::fulfillment_params::drift::MatchFulfillmentParams; @@ -124,7 +129,7 @@ fn fill_order<'c: 'info, 'info>( let clock = &Clock::get()?; let state = &ctx.accounts.state; - let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); + let mut remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); let AccountMaps { perp_market_map, spot_market_map, @@ -140,6 +145,8 @@ fn fill_order<'c: 'info, 'info>( let (makers_and_referrer, makers_and_referrer_stats) = load_user_maps(remaining_accounts_iter, true)?; + let mut builder_escrow = get_builder_escrow_account(&mut remaining_accounts_iter)?; + controller::repeg::update_amm( market_index, &perp_market_map, @@ -163,6 +170,7 @@ fn fill_order<'c: 'info, 'info>( None, clock, FillMode::Fill, + &mut builder_escrow.as_mut(), )?; Ok(()) @@ -627,6 +635,7 @@ pub fn handle_place_signed_msg_taker_order<'c: 'info, 'info>( )?; let high_leverage_mode_config = get_high_leverage_mode_config(&mut remaining_accounts)?; + let builder_escrow = get_builder_escrow_account(&mut remaining_accounts)?; let taker_key = ctx.accounts.user.key(); let mut taker = load_mut!(ctx.accounts.user)?; @@ -642,6 +651,7 @@ pub fn handle_place_signed_msg_taker_order<'c: 'info, 'info>( &spot_market_map, &mut oracle_map, high_leverage_mode_config, + builder_escrow, state, is_delegate_signer, )?; @@ -658,6 +668,7 @@ pub fn place_signed_msg_taker_order<'c: 'info, 'info>( spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, high_leverage_mode_config: Option>, + builder_escrow: Option>, state: &State, is_delegate_signer: bool, ) -> Result<()> { @@ -686,6 +697,39 @@ pub fn place_signed_msg_taker_order<'c: 'info, 'info>( is_delegate_signer, )?; + let mut builder_escrow_zc: Option> = None; + let mut builder_fee_bps: Option = None; + if verified_message_and_signature.builder_idx.is_some() + && verified_message_and_signature.builder_fee.is_some() + { + if let Some(mut builder_escrow) = builder_escrow { + let builder_idx = verified_message_and_signature.builder_idx.unwrap(); + let builder_fee = verified_message_and_signature.builder_fee.unwrap(); + + validate!( + builder_escrow.fixed.authority == taker.authority, + ErrorCode::InvalidUserAccount, + "BuilderEscrow account must be owned by taker", + )?; + + let builder = builder_escrow.get_approved_builder_mut(builder_idx)?; + + if builder.is_revoked() { + return Err(ErrorCode::BuilderRevoked.into()); + } + + if builder_fee > builder.max_fee_bps { + return Err(ErrorCode::InvalidBuilderFee.into()); + } + + builder_fee_bps = Some(builder_fee); + builder_escrow_zc = Some(builder_escrow); + } else { + msg!("RevenueEscrow account must be provided if builder fields are present in OrderParams"); + return Err(ErrorCode::InvalidSignedMsgOrderParam.into()); + } + } + if is_delegate_signer { validate!( verified_message_and_signature.delegate_signed_taker_pubkey == Some(taker_key), @@ -790,6 +834,22 @@ pub fn place_signed_msg_taker_order<'c: 'info, 'info>( ..OrderParams::default() }; + let mut builder_order = if let Some(ref mut builder_escrow_zc) = builder_escrow_zc { + let new_order_id = taker_order_id_to_use - 1; + builder_escrow_zc.add_order(BuilderOrder::new( + verified_message_and_signature.builder_idx.unwrap(), + taker.sub_account_id, + new_order_id, + builder_fee_bps.unwrap(), + MarketType::Perp, + market_index, + BuilderOrderBitFlag::Open as u8, + ))?; + builder_escrow_zc.find_order(taker.sub_account_id, new_order_id) + } else { + None + }; + controller::orders::place_perp_order( state, taker, @@ -805,6 +865,7 @@ pub fn place_signed_msg_taker_order<'c: 'info, 'info>( existing_position_direction_override: Some(matching_taker_order_params.direction), ..PlaceOrderOptions::default() }, + &mut builder_order, )?; } @@ -827,6 +888,22 @@ pub fn place_signed_msg_taker_order<'c: 'info, 'info>( ..OrderParams::default() }; + let mut builder_order = if let Some(ref mut builder_escrow_zc) = builder_escrow_zc { + let new_order_id = taker_order_id_to_use - 1; + builder_escrow_zc.add_order(BuilderOrder::new( + verified_message_and_signature.builder_idx.unwrap(), + taker.sub_account_id, + new_order_id, + builder_fee_bps.unwrap(), + MarketType::Perp, + market_index, + BuilderOrderBitFlag::Open as u8, + ))?; + builder_escrow_zc.find_order(taker.sub_account_id, new_order_id) + } else { + None + }; + controller::orders::place_perp_order( state, taker, @@ -842,11 +919,28 @@ pub fn place_signed_msg_taker_order<'c: 'info, 'info>( existing_position_direction_override: Some(matching_taker_order_params.direction), ..PlaceOrderOptions::default() }, + &mut builder_order, )?; } signed_msg_order_id.order_id = taker_order_id_to_use; signed_msg_account.add_signed_msg_order_id(signed_msg_order_id)?; + let mut builder_order = if let Some(ref mut builder_escrow_zc) = builder_escrow_zc { + let new_order_id = taker_order_id_to_use; + builder_escrow_zc.add_order(BuilderOrder::new( + verified_message_and_signature.builder_idx.unwrap(), + taker.sub_account_id, + new_order_id, + builder_fee_bps.unwrap(), + MarketType::Perp, + market_index, + BuilderOrderBitFlag::Open as u8, + ))?; + builder_escrow_zc.find_order(taker.sub_account_id, new_order_id) + } else { + None + }; + controller::orders::place_perp_order( state, taker, @@ -862,6 +956,7 @@ pub fn place_signed_msg_taker_order<'c: 'info, 'info>( signed_msg_taker_order_slot: Some(order_slot), ..PlaceOrderOptions::default() }, + &mut builder_order, )?; let order_params_hash = @@ -877,6 +972,10 @@ pub fn place_signed_msg_taker_order<'c: 'info, 'info>( ts: clock.unix_timestamp, }); + if let Some(ref mut builder_escrow_zc) = builder_escrow_zc { + builder_escrow_zc.mark_missing_orders_completed(taker)?; + }; + Ok(()) } @@ -899,12 +998,14 @@ pub fn handle_settle_pnl<'c: 'info, 'info>( "user have pool_id 0" )?; + let mut remaining_accounts = ctx.remaining_accounts.iter().peekable(); + let AccountMaps { perp_market_map, spot_market_map, mut oracle_map, } = load_maps( - &mut ctx.remaining_accounts.iter().peekable(), + &mut remaining_accounts, &get_writable_perp_market_set(market_index), &get_writable_spot_market_set(QUOTE_SPOT_MARKET_INDEX), clock.slot, @@ -955,6 +1056,23 @@ pub fn handle_settle_pnl<'c: 'info, 'info>( .map(|_| ErrorCode::InvalidOracleForSettlePnl)?; } + let mut builder_escrow = get_builder_escrow_account(&mut remaining_accounts)?; + if let Some(ref mut builder_escrow_zc) = builder_escrow { + builder_escrow_zc.mark_missing_orders_completed(user)?; + if let Ok(builder_map) = load_builder_map(&mut remaining_accounts) { + controller::builder::sweep_completed_builder_fees_for_market( + market_index, + builder_escrow_zc, + &perp_market_map, + &spot_market_map, + builder_map, + clock.unix_timestamp, + )?; + } else { + msg!("Builder Users not provided, but RevenueEscrow was provided"); + } + } + let spot_market = spot_market_map.get_quote_spot_market()?; validate_spot_market_vault_amount(&spot_market, ctx.accounts.spot_market_vault.amount)?; @@ -975,18 +1093,22 @@ pub fn handle_settle_multiple_pnls<'c: 'info, 'info>( let user_key = ctx.accounts.user.key(); let user = &mut load_mut!(ctx.accounts.user)?; + let mut remaining_accounts = ctx.remaining_accounts.iter().peekable(); + let AccountMaps { perp_market_map, spot_market_map, mut oracle_map, } = load_maps( - &mut ctx.remaining_accounts.iter().peekable(), + &mut remaining_accounts, &get_writable_perp_market_set_from_vec(&market_indexes), &get_writable_spot_market_set(QUOTE_SPOT_MARKET_INDEX), clock.slot, Some(state.oracle_guard_rails), )?; + let mut builder_escrow = get_builder_escrow_account(&mut remaining_accounts)?; + let meets_margin_requirement = meets_settle_pnl_maintenance_margin_requirement( user, &perp_market_map, @@ -1038,6 +1160,21 @@ pub fn handle_settle_multiple_pnls<'c: 'info, 'info>( ) .map(|_| ErrorCode::InvalidOracleForSettlePnl)?; } + + if let Some(ref mut builder_escrow_zc) = builder_escrow { + if let Ok(builder_map) = load_builder_map(&mut remaining_accounts) { + controller::builder::sweep_completed_builder_fees_for_market( + *market_index, + builder_escrow_zc, + &perp_market_map, + &spot_market_map, + builder_map, + clock.unix_timestamp, + )?; + } else { + msg!("Builder Users not provided, but RevenueEscrow was provided"); + } + } } let spot_market = spot_market_map.get_quote_spot_market()?; diff --git a/programs/drift/src/instructions/optional_accounts.rs b/programs/drift/src/instructions/optional_accounts.rs index 7abe5c33d..f33749a1a 100644 --- a/programs/drift/src/instructions/optional_accounts.rs +++ b/programs/drift/src/instructions/optional_accounts.rs @@ -1,4 +1,5 @@ use crate::error::{DriftResult, ErrorCode}; +use crate::state::builder::{BuilderEscrow, BuilderEscrowLoader, BuilderEscrowZeroCopyMut}; use crate::state::high_leverage_mode_config::HighLeverageModeConfig; use std::cell::RefMut; use std::convert::TryFrom; @@ -273,3 +274,33 @@ pub fn get_high_leverage_mode_config<'a>( Ok(Some(high_leverage_mode_config)) } + +pub fn get_builder_escrow_account<'a>( + account_info_iter: &mut Peekable>>, +) -> DriftResult>> { + let account_info = account_info_iter.peek(); + if account_info.is_none() { + return Ok(None); + } + + let account_info = account_info.safe_unwrap()?; + + // Check size and discriminator without borrowing + if account_info.data_len() < 80 { + return Ok(None); + } + + let discriminator: [u8; 8] = BuilderEscrow::discriminator(); + let borrowed_data = account_info.data.borrow(); + let account_discriminator = array_ref![&borrowed_data, 0, 8]; + if account_discriminator != &discriminator { + return Ok(None); + } + + let account_info = account_info_iter.next().safe_unwrap()?; + + drop(borrowed_data); + let builder_escrow: BuilderEscrowZeroCopyMut<'a> = account_info.load_zc_mut()?; + + Ok(Some(builder_escrow)) +} diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 3f11a30c1..aba5726b3 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -26,6 +26,7 @@ use crate::ids::{ serum_program, }; use crate::instructions::constraints::*; +use crate::instructions::optional_accounts::get_builder_escrow_account; use crate::instructions::optional_accounts::{ get_referrer_and_referrer_stats, get_whitelist_token, load_maps, AccountMaps, }; @@ -54,6 +55,12 @@ use crate::optional_accounts::{get_token_interface, get_token_mint}; use crate::print_error; use crate::safe_decrement; use crate::safe_increment; +use crate::state::builder::Builder; +use crate::state::builder::BuilderEscrow; +use crate::state::builder::BuilderInfo; +use crate::state::builder::BuilderOrder; +use crate::state::builder::BUILDER_ESCROW_PDA_SEED; +use crate::state::builder::BUILDER_PDA_SEED; use crate::state::events::emit_stack; use crate::state::events::OrderAction; use crate::state::events::OrderActionRecord; @@ -493,6 +500,140 @@ pub fn handle_reset_fuel_season<'c: 'info, 'info>( Ok(()) } +pub fn handle_initialize_builder<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, InitializeBuilder<'info>>, +) -> Result<()> { + let mut builder = ctx + .accounts + .builder + .load_init() + .or(Err(ErrorCode::UnableToLoadAccountLoader))?; + builder.authority = ctx.accounts.authority.key(); + builder.total_referrer_rewards = 0; + builder.total_builder_rewards = 0; + builder.padding = [0; 18]; + Ok(()) +} + +pub fn handle_initialize_builder_escrow<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, InitializeBuilderEscrow<'info>>, + num_orders: u16, +) -> Result<()> { + let builder_escrow = &mut ctx.accounts.builder_escrow; + builder_escrow.authority = ctx.accounts.authority.key(); + builder_escrow + .orders + .resize_with(num_orders as usize, BuilderOrder::default); + + let state = &mut ctx.accounts.state; + if state.builder_referral_enabled() { + let mut user_stats = ctx.accounts.user_stats.load_mut()?; + builder_escrow.referrer = user_stats.referrer; + user_stats.update_builder_referral_status(); + } + + builder_escrow.validate()?; + Ok(()) +} + +pub fn handle_migrate_referrer<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, MigrateReferrer<'info>>, +) -> Result<()> { + let state = &mut ctx.accounts.state; + if !state.builder_referral_enabled() { + if state.admin != ctx.accounts.payer.key() { + msg!("Only admin can migrate referrer until builder referral feature is enabled"); + return Err(anchor_lang::error::ErrorCode::ConstraintSigner.into()); + } + } + + let builder_escrow = &mut ctx.accounts.builder_escrow; + builder_escrow.referrer = ctx.accounts.user_stats.load()?.referrer; + + builder_escrow.validate()?; + Ok(()) +} + +pub fn handle_resize_builder_escrow_orders<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ResizeBuilderEscrowOrders<'info>>, + num_orders: u16, +) -> Result<()> { + let builder_escrow = &mut ctx.accounts.builder_escrow; + validate!( + num_orders as usize >= builder_escrow.orders.len(), + ErrorCode::InvalidBuilderResize, + "Invalid shrinking resize for revenue share escrow" + )?; + + builder_escrow + .orders + .resize_with(num_orders as usize, BuilderOrder::default); + builder_escrow.validate()?; + Ok(()) +} + +pub fn handle_change_approved_builder<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ChangeApprovedBuilder<'info>>, + builder: Pubkey, + max_fee_bps: u16, + add: bool, +) -> Result<()> { + let existing_builder_index = ctx + .accounts + .builder_escrow + .approved_builders + .iter() + .position(|b| b.authority == builder); + if let Some(index) = existing_builder_index { + if add { + msg!( + "Updated builder: {} with max fee bps: {} -> {}", + builder, + ctx.accounts.builder_escrow.approved_builders[index].max_fee_bps, + max_fee_bps + ); + ctx.accounts.builder_escrow.approved_builders[index].max_fee_bps = max_fee_bps; + } else { + if ctx + .accounts + .builder_escrow + .orders + .iter() + .any(|o| (o.builder_idx == index as u8) && (!o.is_available())) + { + msg!("Builder has open orders, must cancel orders and settle_pnl before revoking"); + return Err(ErrorCode::CannotRevokeBuilderWithOpenOrders.into()); + } + msg!( + "Revoking builder: {}, max fee bps: {} -> 0", + builder, + ctx.accounts.builder_escrow.approved_builders[index].max_fee_bps, + ); + ctx.accounts.builder_escrow.approved_builders[index].max_fee_bps = 0; + } + } else { + if add { + ctx.accounts + .builder_escrow + .approved_builders + .push(BuilderInfo { + authority: builder, + max_fee_bps, + ..BuilderInfo::default() + }); + msg!( + "Added builder: {} with max fee bps: {}", + builder, + max_fee_bps + ); + } else { + msg!("Tried to revoke builder: {}, but it was not found", builder); + } + } + + Ok(()) +} + #[access_control( deposit_not_paused(&ctx.accounts.state) )] @@ -1889,6 +2030,8 @@ pub fn handle_transfer_perp_position<'c: 'info, 'info>( maker_existing_quote_entry_amount: from_existing_quote_entry_amount, maker_existing_base_asset_amount: from_existing_base_asset_amount, trigger_price: None, + builder_idx: None, + builder_fee: None, }; emit_stack::<_, { OrderActionRecord::SIZE }>(fill_record)?; @@ -1940,6 +2083,7 @@ pub fn handle_place_perp_order<'c: 'info, 'info>( clock, params, PlaceOrderOptions::default(), + &mut None, )?; Ok(()) @@ -2242,6 +2386,7 @@ pub fn handle_place_orders<'c: 'info, 'info>( clock, *params, options, + &mut None, )?; } else { controller::orders::place_spot_order( @@ -2322,6 +2467,7 @@ pub fn handle_place_and_take_perp_order<'c: 'info, 'info>( &clock, params, PlaceOrderOptions::default(), + &mut None, )?; drop(user); @@ -2347,6 +2493,7 @@ pub fn handle_place_and_take_perp_order<'c: 'info, 'info>( is_immediate_or_cancel || optional_params.is_some(), auction_duration_percentage, ), + &mut None, )?; let order_unfilled = load!(ctx.accounts.user)? @@ -2436,6 +2583,7 @@ pub fn handle_place_and_make_perp_order<'c: 'info, 'info>( clock, params, PlaceOrderOptions::default(), + &mut None, )?; let (order_id, authority) = (user.get_last_order_id(), user.authority); @@ -2447,6 +2595,8 @@ pub fn handle_place_and_make_perp_order<'c: 'info, 'info>( makers_and_referrer.insert(ctx.accounts.user.key(), ctx.accounts.user.clone())?; makers_and_referrer_stats.insert(authority, ctx.accounts.user_stats.clone())?; + let mut builder_escrow = get_builder_escrow_account(remaining_accounts_iter)?; + controller::orders::fill_perp_order( taker_order_id, state, @@ -2462,6 +2612,7 @@ pub fn handle_place_and_make_perp_order<'c: 'info, 'info>( Some(order_id), clock, FillMode::PlaceAndMake, + &mut builder_escrow.as_mut(), )?; let order_exists = load!(ctx.accounts.user)? @@ -2537,6 +2688,7 @@ pub fn handle_place_and_make_signed_msg_perp_order<'c: 'info, 'info>( clock, params, PlaceOrderOptions::default(), + &mut None, )?; let (order_id, authority) = (user.get_last_order_id(), user.authority); @@ -2548,6 +2700,8 @@ pub fn handle_place_and_make_signed_msg_perp_order<'c: 'info, 'info>( makers_and_referrer.insert(ctx.accounts.user.key(), ctx.accounts.user.clone())?; makers_and_referrer_stats.insert(authority, ctx.accounts.user_stats.clone())?; + let mut builder_escrow = get_builder_escrow_account(remaining_accounts_iter)?; + let taker_signed_msg_account = ctx.accounts.taker_signed_msg_user_orders.load()?; let taker_order_id = taker_signed_msg_account .iter() @@ -2570,6 +2724,7 @@ pub fn handle_place_and_make_signed_msg_perp_order<'c: 'info, 'info>( Some(order_id), clock, FillMode::PlaceAndMake, + &mut builder_escrow.as_mut(), )?; let order_exists = load!(ctx.accounts.user)? @@ -4784,3 +4939,105 @@ pub struct UpdateUserProtectedMakerMode<'info> { #[account(mut)] pub protected_maker_mode_config: AccountLoader<'info, ProtectedMakerModeConfig>, } + +#[derive(Accounts)] +#[instruction()] +pub struct InitializeBuilder<'info> { + #[account( + init, + seeds = [BUILDER_PDA_SEED.as_ref(), authority.key().as_ref()], + space = Builder::space(), + bump, + payer = payer + )] + pub builder: AccountLoader<'info, Builder>, + /// CHECK: The builder and/or referrer authority, beneficiary of builder/ref fees + pub authority: Signer<'info>, + #[account(mut)] + pub payer: Signer<'info>, + pub rent: Sysvar<'info, Rent>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +#[instruction(num_orders: u16)] +pub struct InitializeBuilderEscrow<'info> { + #[account( + init, + seeds = [BUILDER_ESCROW_PDA_SEED.as_ref(), authority.key().as_ref()], + space = BuilderEscrow::space(num_orders as usize, 1), + bump, + payer = payer + )] + pub builder_escrow: Box>, + /// CHECK: The auth owning this account, payer of builder/ref fees + pub authority: Signer<'info>, + #[account( + mut, + has_one = authority + )] + pub user_stats: AccountLoader<'info, UserStats>, + pub state: Box>, + #[account(mut)] + pub payer: Signer<'info>, + pub rent: Sysvar<'info, Rent>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +pub struct MigrateReferrer<'info> { + #[account( + mut, + seeds = [BUILDER_ESCROW_PDA_SEED.as_ref(), authority.key().as_ref()], + bump, + )] + pub builder_escrow: Box>, + /// CHECK: The auth owning this account, payer of builder/ref fees + pub authority: AccountInfo<'info>, + #[account( + has_one = authority + )] + pub user_stats: AccountLoader<'info, UserStats>, + pub state: Box>, + pub payer: Signer<'info>, +} + +#[derive(Accounts)] +#[instruction(num_orders: u16)] +pub struct ResizeBuilderEscrowOrders<'info> { + #[account( + mut, + seeds = [BUILDER_ESCROW_PDA_SEED.as_ref(), authority.key().as_ref()], + bump, + realloc = BuilderEscrow::space(num_orders as usize, builder_escrow.approved_builders.len()), + realloc::payer = payer, + realloc::zero = false, + has_one = authority + )] + pub builder_escrow: Box>, + /// CHECK: The owner of BuilderEscrow + pub authority: AccountInfo<'info>, + #[account(mut)] + pub payer: Signer<'info>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +#[instruction(builder: Pubkey, max_fee_bps: u16, add: bool)] +pub struct ChangeApprovedBuilder<'info> { + #[account( + mut, + seeds = [BUILDER_ESCROW_PDA_SEED.as_ref(), authority.key().as_ref()], + bump, + // revoking a builder does not remove the slot to avoid unintended reuse + realloc = BuilderEscrow::space(builder_escrow.orders.len(), if add { builder_escrow.approved_builders.len() + 1 } else { builder_escrow.approved_builders.len() }), + realloc::payer = payer, + realloc::zero = false, + has_one = authority + )] + pub builder_escrow: Box>, + pub authority: Signer<'info>, + #[account(mut)] + pub payer: Signer<'info>, + pub system_program: Program<'info, System>, +} diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index 92229f7e6..b9d888d61 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -1834,6 +1834,48 @@ pub mod drift { ) -> Result<()> { handle_update_feature_bit_flags_median_trigger_price(ctx, enable) } + + pub fn update_feature_bit_flags_builder_referral( + ctx: Context, + enable: bool, + ) -> Result<()> { + handle_update_feature_bit_flags_builder_referral(ctx, enable) + } + + pub fn initialize_builder<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, InitializeBuilder<'info>>, + ) -> Result<()> { + handle_initialize_builder(ctx) + } + + pub fn initialize_builder_escrow<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, InitializeBuilderEscrow<'info>>, + num_orders: u16, + ) -> Result<()> { + handle_initialize_builder_escrow(ctx, num_orders) + } + + pub fn migrate_referrer<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, MigrateReferrer<'info>>, + ) -> Result<()> { + handle_migrate_referrer(ctx) + } + + pub fn resize_builder_escrow_orders<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ResizeBuilderEscrowOrders<'info>>, + num_orders: u16, + ) -> Result<()> { + handle_resize_builder_escrow_orders(ctx, num_orders) + } + + pub fn change_approved_builder<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ChangeApprovedBuilder<'info>>, + builder: Pubkey, + max_fee_bps: u16, + add: bool, + ) -> Result<()> { + handle_change_approved_builder(ctx, builder, max_fee_bps, add) + } } #[cfg(not(feature = "no-entrypoint"))] diff --git a/programs/drift/src/math/fees.rs b/programs/drift/src/math/fees.rs index da3cd3a35..7569bc6bf 100644 --- a/programs/drift/src/math/fees.rs +++ b/programs/drift/src/math/fees.rs @@ -1,5 +1,6 @@ use std::cmp::{max, min}; +use anchor_lang::prelude::Pubkey; use num_integer::Roots; use crate::error::DriftResult; @@ -30,6 +31,7 @@ pub struct FillFees { pub filler_reward: u64, pub referrer_reward: u64, pub referee_discount: u64, + pub builder_fee: u64, } pub fn calculate_fee_for_fulfillment_with_amm( @@ -45,6 +47,7 @@ pub fn calculate_fee_for_fulfillment_with_amm( is_post_only: bool, fee_adjustment: i16, user_high_leverage_mode: bool, + builder_fee_bps: Option, ) -> DriftResult { let fee_tier = determine_user_fee_tier( user_stats, @@ -92,6 +95,7 @@ pub fn calculate_fee_for_fulfillment_with_amm( filler_reward, referrer_reward: 0, referee_discount: 0, + builder_fee: 0, }) } else { let mut fee = calculate_taker_fee(quote_asset_amount, &fee_tier, fee_adjustment)?; @@ -131,15 +135,25 @@ pub fn calculate_fee_for_fulfillment_with_amm( let fee_to_market_for_lp = fee_to_market.safe_sub(quote_asset_amount_surplus)?; + let builder_fee = if let Some(builder_fee_bps) = builder_fee_bps { + quote_asset_amount + .safe_mul(builder_fee_bps.cast()?)? + .safe_div(10000)? + } else { + 0 + }; + let user_fee = fee.safe_add(builder_fee)?; + // must be non-negative Ok(FillFees { - user_fee: fee, + user_fee, maker_rebate: 0, fee_to_market, fee_to_market_for_lp, filler_reward, referrer_reward, referee_discount, + builder_fee, }) } } @@ -286,6 +300,7 @@ pub fn calculate_fee_for_fulfillment_with_match( market_type: &MarketType, fee_adjustment: i16, user_high_leverage_mode: bool, + builder_fee_bps: Option, ) -> DriftResult { let taker_fee_tier = determine_user_fee_tier( taker_stats, @@ -337,14 +352,24 @@ pub fn calculate_fee_for_fulfillment_with_match( .safe_sub(maker_rebate)? .cast::()?; + let builder_fee = if let Some(builder_fee_bps) = builder_fee_bps { + quote_asset_amount + .safe_mul(builder_fee_bps.cast()?)? + .safe_div(10000)? + } else { + 0 + }; + let user_fee = taker_fee.safe_add(builder_fee)?; + Ok(FillFees { - user_fee: taker_fee, + user_fee, maker_rebate, fee_to_market, filler_reward, referrer_reward, fee_to_market_for_lp: 0, referee_discount, + builder_fee, }) } diff --git a/programs/drift/src/math/fees/tests.rs b/programs/drift/src/math/fees/tests.rs index 82188b62b..296f3bfce 100644 --- a/programs/drift/src/math/fees/tests.rs +++ b/programs/drift/src/math/fees/tests.rs @@ -31,6 +31,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, 0, false, + None, ) .unwrap(); @@ -75,6 +76,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, 0, false, + None, ) .unwrap(); @@ -118,6 +120,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, 0, false, + None, ) .unwrap(); @@ -161,6 +164,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, 0, false, + None, ) .unwrap(); @@ -202,6 +206,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, 0, false, + None, ) .unwrap(); @@ -240,6 +245,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, -50, false, + None, ) .unwrap(); @@ -271,6 +277,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, 50, false, + None, ) .unwrap(); @@ -303,6 +310,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, -50, false, + None, ) .unwrap(); @@ -335,6 +343,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, -50, false, + None, ) .unwrap(); @@ -373,6 +382,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, -100, false, + None, ) .unwrap(); @@ -404,6 +414,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, -100, false, + None, ) .unwrap(); @@ -436,6 +447,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, -100, true, + None, ) .unwrap(); @@ -468,6 +480,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, -100, false, + None, ) .unwrap(); @@ -500,6 +513,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, -100, false, + None, ) .unwrap(); @@ -538,6 +552,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, -50, true, + None, ) .unwrap(); @@ -583,6 +598,7 @@ mod calculate_fee_for_order_fulfill_against_amm { false, 0, false, + None, ) .unwrap(); @@ -620,6 +636,7 @@ mod calculate_fee_for_order_fulfill_against_amm { false, -50, false, + None, ) .unwrap(); @@ -649,6 +666,7 @@ mod calculate_fee_for_order_fulfill_against_amm { false, 50, false, + None, ) .unwrap(); @@ -679,6 +697,7 @@ mod calculate_fee_for_order_fulfill_against_amm { false, -50, false, + None, ) .unwrap(); @@ -709,6 +728,7 @@ mod calculate_fee_for_order_fulfill_against_amm { false, -50, false, + None, ) .unwrap(); @@ -746,6 +766,7 @@ mod calculate_fee_for_order_fulfill_against_amm { false, -50, true, + None, ) .unwrap(); diff --git a/programs/drift/src/state/builder.rs b/programs/drift/src/state/builder.rs new file mode 100644 index 000000000..1b115a84e --- /dev/null +++ b/programs/drift/src/state/builder.rs @@ -0,0 +1,576 @@ +use std::cell::{Ref, RefMut}; + +use anchor_lang::prelude::Pubkey; +use anchor_lang::*; +use anchor_lang::{account, zero_copy}; +use borsh::{BorshDeserialize, BorshSerialize}; +use prelude::AccountInfo; + +use crate::error::{DriftResult, ErrorCode}; +use crate::math::casting::Cast; +use crate::math::orders::set_order_bit_flag; +use crate::math::safe_unwrap::SafeUnwrap; +use crate::state::user::{MarketType, OrderStatus, User}; +use crate::validate; +use crate::{msg, ID}; + +pub const BUILDER_PDA_SEED: &str = "BUILD"; +pub const BUILDER_ESCROW_PDA_SEED: &str = "BUILD_ESCROW"; + +#[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq, Debug, Eq, Default)] +pub enum BuilderOrderBitFlag { + #[default] + Init = 0b00000000, + Open = 0b00000001, + Completed = 0b00000010, + Referral = 0b00000100, +} + +#[account(zero_copy(unsafe))] +#[derive(Eq, PartialEq, Debug, Default)] +pub struct Builder { + /// the owner of this account, a builder or referrer + pub authority: Pubkey, + pub total_referrer_rewards: u64, + pub total_builder_rewards: u64, + pub padding: [u8; 18], +} + +impl Builder { + pub fn space() -> usize { + 8 + 32 + 8 + 8 + 18 + } +} + +#[zero_copy] +#[derive(Default, Eq, PartialEq, Debug, BorshDeserialize, BorshSerialize)] +pub struct BuilderOrder { + /// the index of the BuilderEscrow.approved_builders list, that this order's fee will settle to. Ignored + /// if bit_flag = Referral. + pub builder_idx: u8, + pub padding0: [u8; 7], + /// fees accrued so far for this order slot. This is not exclusively fees from this order_id + /// and may include fees from other orders in the same market. This may be swept to the + /// builder's SpotPosition during settle_pnl. + pub fees_accrued: u64, + /// the order_id of the current active order in this slot. It only carries meaning while bit_flag = Open + pub order_id: u32, + pub fee_bps: u16, + pub market_index: u16, + + /// bitflags that describe the state of the order. + /// [`BuilderOrderBitFlag::Init`]: this order slot is available for use. + /// [`BuilderOrderBitFlag::Open`]: this order slot is occupied, `order_id` is the `sub_account_id`'s active order. + /// [`BuilderOrderBitFlag::Completed`]: this order has been filled or canceled, and is waiting to be settled into. + /// the builder's account order_id and sub_account_id are no longer relevant, it may be merged with other orders. + /// [`BuilderOrderBitFlag::Referral`]: this order stores referral rewards waiting to be settled. If it is set, no + /// other bitflag should be set. + pub bit_flags: u8, + + pub market_type: MarketType, + + /// the subaccount_id of the user who created this order. It only carries meaning while bit_flag = Open + pub sub_account_id: u16, + pub padding: [u8; 12], +} + +impl BuilderOrder { + pub fn new( + builder_idx: u8, + sub_account_id: u16, + order_id: u32, + fee_bps: u16, + market_type: MarketType, + market_index: u16, + bit_flags: u8, + ) -> Self { + Self { + builder_idx, + padding0: [0; 7], + order_id, + fee_bps, + market_type, + market_index, + fees_accrued: 0, + bit_flags, + sub_account_id, + padding: [0; 12], + } + } + + pub fn space() -> usize { + std::mem::size_of::() + } + + pub fn add_bit_flag(&mut self, flag: BuilderOrderBitFlag) { + self.bit_flags |= flag as u8; + } + + pub fn is_bit_flag_set(&self, flag: BuilderOrderBitFlag) -> bool { + (self.bit_flags & flag as u8) != 0 + } + + // An order is Open after it is created, the slot is considered occupied + // and it is waiting to become `Completed` (filled or canceled). + pub fn is_open(&self) -> bool { + self.is_bit_flag_set(BuilderOrderBitFlag::Open) + } + + // An order is Completed after it is filled or canceled. It is waiting to be settled + // into the builder's account + pub fn is_completed(&self) -> bool { + self.is_bit_flag_set(BuilderOrderBitFlag::Completed) + } + + /// An order slot is available (can be written to) if it is neither Completed nor Open. + pub fn is_available(&self) -> bool { + !self.is_completed() && !self.is_open() && !self.is_referral_order() + } + + pub fn is_referral_order(&self) -> bool { + self.is_bit_flag_set(BuilderOrderBitFlag::Referral) + } + + /// Checks if the order can be merged with another order. Merged orders track cumulative fees accrued + /// and are settled together, making more efficient use of the orders list. + pub fn is_mergeable(&self, other: &BuilderOrder) -> bool { + !self.is_referral_order() + && !other.is_referral_order() + && other.is_completed() + && other.market_index == self.market_index + && other.market_type == self.market_type + && other.builder_idx == self.builder_idx + } + + /// Merges `other` into `self`. The orders must be mergeable. + pub fn merge(mut self, other: &BuilderOrder) -> DriftResult { + validate!( + self.is_mergeable(other), + ErrorCode::DefaultError, + "Orders are not mergeable" + )?; + self.fees_accrued = self + .fees_accrued + .checked_add(other.fees_accrued) + .ok_or(ErrorCode::MathError)?; + Ok(self) + } +} + +#[zero_copy] +#[derive(Default, Eq, PartialEq, Debug, BorshDeserialize, BorshSerialize)] +#[repr(C)] +pub struct BuilderInfo { + // pub padding0: u32, + pub authority: Pubkey, // builder authority + // pub padding: u64, // force alignment to 8 bytes + pub max_fee_bps: u16, + pub padding2: [u8; 2], +} + +impl BuilderInfo { + pub fn space() -> usize { + std::mem::size_of::() + } + + pub fn is_revoked(&self) -> bool { + self.max_fee_bps == 0 + } +} + +#[account] +#[derive(Eq, PartialEq, Debug, Default)] +#[repr(C)] +pub struct BuilderEscrow { + /// the owner of this account, a user + pub authority: Pubkey, + pub referrer: Pubkey, + pub padding0: u32, // align with orders 4 bytes len prefix + pub orders: Vec, + pub padding1: u32, // align with approved_builders 4 bytes len prefix + pub approved_builders: Vec, +} + +impl BuilderEscrow { + pub fn space(num_orders: usize, num_builders: usize) -> usize { + 8 + // discriminator + std::mem::size_of::() + // fixed header + 4 + // orders Vec length prefix + 4 + // padding0 + num_orders * std::mem::size_of::() + // orders data + 4 + // approved_builders Vec length prefix + 4 + // padding1 + num_builders * std::mem::size_of::() // builders data + } + + pub fn validate(&self) -> DriftResult<()> { + validate!( + self.orders.len() <= 128 && self.approved_builders.len() <= 128, + ErrorCode::DefaultError, + "BuilderEscrow orders and approved_builders len must be between 1 and 128" + )?; + Ok(()) + } +} + +#[zero_copy] +#[derive(Default, Eq, PartialEq, Debug, BorshDeserialize, BorshSerialize)] +pub struct BuilderEscrowFixed { + pub authority: Pubkey, + pub referrer: Pubkey, +} + +pub struct BuilderEscrowZeroCopy<'a> { + pub fixed: Ref<'a, BuilderEscrowFixed>, + pub data: Ref<'a, [u8]>, +} + +impl<'a> BuilderEscrowZeroCopy<'a> { + pub fn orders_len(&self) -> u32 { + let length_bytes = &self.data[4..8]; + u32::from_le_bytes([ + length_bytes[0], + length_bytes[1], + length_bytes[2], + length_bytes[3], + ]) + } + pub fn approved_builders_len(&self) -> u32 { + let orders_data_size = self.orders_len() as usize * std::mem::size_of::(); + let offset = 4 + // BuilderEscrow.padding0 + 4 + // vec len + orders_data_size + 4; // BuilderEscrow.padding1 + let length_bytes = &self.data[offset..offset + 4]; + u32::from_le_bytes([ + length_bytes[0], + length_bytes[1], + length_bytes[2], + length_bytes[3], + ]) + } + + pub fn get_order(&self, index: u32) -> DriftResult<&BuilderOrder> { + validate!( + index < self.orders_len(), + ErrorCode::DefaultError, + "Order index out of bounds" + )?; + let size = std::mem::size_of::(); + let start = 4 + // BuilderEscrow.padding0 + 4 + // vec len + index as usize * size; // orders data + Ok(bytemuck::from_bytes(&self.data[start..start + size])) + } + + pub fn get_approved_builder(&self, index: u32) -> DriftResult<&BuilderInfo> { + validate!( + index < self.approved_builders_len(), + ErrorCode::DefaultError, + "Builder index out of bounds" + )?; + let size = std::mem::size_of::(); + let offset = 4 + 4 + // Skip orders Vec length prefix + padding0 + self.orders_len() as usize * std::mem::size_of::() + // orders data + 4; // Skip approved_builders Vec length prefix + padding1 + let start = offset + index as usize * size; + Ok(bytemuck::from_bytes(&self.data[start..start + size])) + } + + pub fn iter_orders(&self) -> impl Iterator> + '_ { + (0..self.orders_len()).map(move |i| self.get_order(i)) + } + + pub fn iter_approved_builders(&self) -> impl Iterator> + '_ { + (0..self.approved_builders_len()).map(move |i| self.get_approved_builder(i)) + } +} + +pub struct BuilderEscrowZeroCopyMut<'a> { + pub fixed: RefMut<'a, BuilderEscrowFixed>, + pub data: RefMut<'a, [u8]>, +} + +impl<'a> BuilderEscrowZeroCopyMut<'a> { + pub fn has_referrer(&self) -> bool { + self.fixed.referrer != Pubkey::default() + } + + pub fn get_referrer(&self) -> Option { + if self.has_referrer() { + Some(self.fixed.referrer) + } else { + None + } + } + + pub fn orders_len(&self) -> u32 { + // skip BuilderEscrow.padding0 + let length_bytes = &self.data[4..8]; + u32::from_le_bytes([ + length_bytes[0], + length_bytes[1], + length_bytes[2], + length_bytes[3], + ]) + } + pub fn approved_builders_len(&self) -> u32 { + // Calculate offset to the approved_builders Vec length + let orders_data_size = self.orders_len() as usize * std::mem::size_of::(); + let offset = 4 + // BuilderEscrow.padding0 + 4 + // vec len + orders_data_size + + 4; // BuilderEscrow.padding1 + let length_bytes = &self.data[offset..offset + 4]; + u32::from_le_bytes([ + length_bytes[0], + length_bytes[1], + length_bytes[2], + length_bytes[3], + ]) + } + + pub fn get_order_mut(&mut self, index: u32) -> DriftResult<&mut BuilderOrder> { + validate!( + index < self.orders_len(), + ErrorCode::DefaultError, + "Order index out of bounds" + )?; + let size = std::mem::size_of::(); + let start = 4 + // BuilderEscrow.padding0 + 4 + // vec len + index as usize * size; + Ok(bytemuck::from_bytes_mut( + &mut self.data[start..(start + size)], + )) + } + + /// Returns the index of an order for a given sub_account_id and order_id, if present. + pub fn find_order_index(&self, sub_account_id: u16, order_id: u32) -> Option { + for i in 0..self.orders_len() { + if let Ok(existing_order) = self.get_order(i) { + if existing_order.order_id == order_id + && existing_order.sub_account_id == sub_account_id + { + return Some(i); + } + } + } + None + } + + /// Returns the index for the referral order, creating one if necessary. + pub fn find_or_create_referral_index(&mut self) -> Option { + // look for an existing referral order + for i in 0..self.orders_len() { + if let Ok(existing_order) = self.get_order(i) { + if existing_order.is_referral_order() { + return Some(i); + } + } + } + + // try to create a referral order in an available order slot + match self.add_order(BuilderOrder::new( + 0, + 0, + 0, + 0, + MarketType::Spot, + 0, + BuilderOrderBitFlag::Referral as u8, + )) { + Ok(idx) => Some(idx), + Err(_) => { + msg!("Failed to add referral order, BuilderEscrow is full"); + None + } + } + } + + /// Returns two distinct mutable references to orders by indices in one borrow of `self`. + /// Either index may be None. If both Some, they must be distinct. + pub fn get_two_orders_mut_by_indices( + &mut self, + a: Option, + b: Option, + ) -> DriftResult<(Option<&mut BuilderOrder>, Option<&mut BuilderOrder>)> { + match (a, b) { + (None, None) => Ok((None, None)), + (Some(i), None) => Ok((Some(self.get_order_mut(i)?), None)), + (None, Some(j)) => Ok((None, Some(self.get_order_mut(j)?))), + (Some(i), Some(j)) => { + validate!(i != j, ErrorCode::DefaultError, "indices must be distinct")?; + + let size = core::mem::size_of::(); + let base_offset = 4 + 4; // padding0 + vec len + let start_i = base_offset + i as usize * size; + let start_j = base_offset + j as usize * size; + + if start_i < start_j { + let (left, right) = self.data.split_at_mut(start_j); + let order_i: &mut BuilderOrder = + bytemuck::from_bytes_mut(&mut left[start_i..start_i + size]); + let order_j: &mut BuilderOrder = bytemuck::from_bytes_mut(&mut right[0..size]); + Ok((Some(order_i), Some(order_j))) + } else { + let (left, right) = self.data.split_at_mut(start_i); + let order_j: &mut BuilderOrder = + bytemuck::from_bytes_mut(&mut left[start_j..start_j + size]); + let order_i: &mut BuilderOrder = bytemuck::from_bytes_mut(&mut right[0..size]); + Ok((Some(order_i), Some(order_j))) + } + } + } + } + + pub fn get_order(&self, index: u32) -> DriftResult<&BuilderOrder> { + validate!( + index < self.orders_len(), + ErrorCode::DefaultError, + "Order index out of bounds" + )?; + let size = std::mem::size_of::(); + let start = 4 + // BuilderEscrow.padding0 + 4 + // vec len + index as usize * size; // orders data + Ok(bytemuck::from_bytes(&self.data[start..start + size])) + } + + pub fn get_approved_builder_mut(&mut self, index: u8) -> DriftResult<&mut BuilderInfo> { + validate!( + index < self.approved_builders_len().cast::()?, + ErrorCode::DefaultError, + "Builder index out of bounds, index: {}, orderslen: {}, builderslen: {}", + index, + self.orders_len(), + self.approved_builders_len() + )?; + let size = std::mem::size_of::(); + let offset = 4 + // BuilderEscrow.padding0 + 4 + // vec len + self.orders_len() as usize * std::mem::size_of::() + // orders data + 4 + // BuilderEscrow.padding1 + 4; // vec len + let start = offset + index as usize * size; + Ok(bytemuck::from_bytes_mut( + &mut self.data[start..start + size], + )) + } + + pub fn add_order(&mut self, order: BuilderOrder) -> DriftResult { + for i in 0..self.orders_len() { + let existing_order = self.get_order_mut(i)?; + if existing_order.is_mergeable(&order) { + *existing_order = existing_order.merge(&order)?; + return Ok(i); + } else if existing_order.is_available() { + *existing_order = order; + return Ok(i); + } + } + + Err(ErrorCode::BuilderEscrowOrdersAccountFull.into()) + } + + pub fn find_order(&mut self, sub_account_id: u16, order_id: u32) -> Option<&mut BuilderOrder> { + for i in 0..self.orders_len() { + if let Ok(existing_order) = self.get_order(i) { + if existing_order.order_id == order_id + && existing_order.sub_account_id == sub_account_id + { + return self.get_order_mut(i).ok(); + } + } + } + None + } + + /// Marks any [`BuilderOrder`]s as Complete if there is no longer a corresponding + /// open order in the user's account. This is used to lazily reconcile state when + /// in place_order and settle_pnl instead of requiring explicit updates on cancels. + pub fn mark_missing_orders_completed(&mut self, user: &User) -> DriftResult<()> { + for i in 0..self.orders_len() { + if let Ok(builder_order) = self.get_order_mut(i) { + if builder_order.is_referral_order() { + continue; + } + if builder_order.is_open() && !builder_order.is_completed() { + let still_open = user.orders.iter().any(|o| { + o.order_id == builder_order.order_id + && user.sub_account_id == builder_order.sub_account_id + && o.status == OrderStatus::Open + }); + if !still_open { + if builder_order.fees_accrued > 0 { + builder_order.add_bit_flag(BuilderOrderBitFlag::Completed); + } else { + // order had no fees accrued, we can just clear out the slot + *builder_order = BuilderOrder::default(); + } + } + } + } + } + + Ok(()) + } +} + +pub trait BuilderEscrowLoader<'a> { + fn load_zc(&self) -> DriftResult; + fn load_zc_mut(&self) -> DriftResult; +} + +impl<'a> BuilderEscrowLoader<'a> for AccountInfo<'a> { + fn load_zc(&self) -> DriftResult { + let owner = self.owner; + + validate!( + owner == &ID, + ErrorCode::DefaultError, + "invalid BuilderEscrow owner", + )?; + + let data = self.try_borrow_data().safe_unwrap()?; + + let (discriminator, data) = Ref::map_split(data, |d| d.split_at(8)); + validate!( + *discriminator == BuilderEscrow::discriminator(), + ErrorCode::DefaultError, + "invalid signed_msg user orders discriminator", + )?; + + let hdr_size = std::mem::size_of::(); + let (fixed, data) = Ref::map_split(data, |d| d.split_at(hdr_size)); + Ok(BuilderEscrowZeroCopy { + fixed: Ref::map(fixed, |b| bytemuck::from_bytes(b)), + data, + }) + } + + fn load_zc_mut(&self) -> DriftResult { + let owner = self.owner; + + validate!( + owner == &ID, + ErrorCode::DefaultError, + "invalid BuilderEscrow owner", + )?; + + let data = self.try_borrow_mut_data().safe_unwrap()?; + + let (discriminator, data) = RefMut::map_split(data, |d| d.split_at_mut(8)); + validate!( + *discriminator == BuilderEscrow::discriminator(), + ErrorCode::DefaultError, + "invalid signed_msg user orders discriminator", + )?; + + let hdr_size = std::mem::size_of::(); + let (fixed, data) = RefMut::map_split(data, |d| d.split_at_mut(hdr_size)); + Ok(BuilderEscrowZeroCopyMut { + fixed: RefMut::map(fixed, |b| bytemuck::from_bytes_mut(b)), + data, + }) + } +} diff --git a/programs/drift/src/state/builder_map.rs b/programs/drift/src/state/builder_map.rs new file mode 100644 index 000000000..425f126e4 --- /dev/null +++ b/programs/drift/src/state/builder_map.rs @@ -0,0 +1,206 @@ +use crate::error::{DriftResult, ErrorCode}; +use crate::math::safe_unwrap::SafeUnwrap; +use crate::msg; +use crate::state::builder::Builder; +use crate::state::traits::Size; +use crate::state::user::User; +use crate::validate; +use anchor_lang::prelude::AccountLoader; +use anchor_lang::Discriminator; +use arrayref::array_ref; +use solana_program::account_info::AccountInfo; +use solana_program::pubkey::Pubkey; +use std::cell::{Ref, RefMut}; +use std::collections::BTreeMap; +use std::iter::Peekable; +use std::panic::Location; +use std::slice::Iter; + +pub struct BuilderEntry<'a> { + pub user: Option>, + pub builder: Option>, +} + +impl<'a> Default for BuilderEntry<'a> { + fn default() -> Self { + Self { + user: None, + builder: None, + } + } +} + +pub struct BuilderMap<'a>(pub BTreeMap>); + +impl<'a> BuilderMap<'a> { + pub fn empty() -> Self { + BuilderMap(BTreeMap::new()) + } + + pub fn insert_user( + &mut self, + authority: Pubkey, + user_loader: AccountLoader<'a, User>, + ) -> DriftResult { + let entry = self.0.entry(authority).or_default(); + validate!( + entry.user.is_none(), + ErrorCode::DefaultError, + "Duplicate User for authority {:?}", + authority + )?; + entry.user = Some(user_loader); + Ok(()) + } + + pub fn insert_builder( + &mut self, + authority: Pubkey, + builder_loader: AccountLoader<'a, Builder>, + ) -> DriftResult { + let entry = self.0.entry(authority).or_default(); + validate!( + entry.builder.is_none(), + ErrorCode::DefaultError, + "Duplicate Builder for authority {:?}", + authority + )?; + entry.builder = Some(builder_loader); + Ok(()) + } + + #[track_caller] + #[inline(always)] + pub fn get_user_ref_mut(&self, authority: &Pubkey) -> DriftResult> { + let loader = match self.0.get(authority).and_then(|e| e.user.as_ref()) { + Some(loader) => loader, + None => { + let caller = Location::caller(); + msg!( + "Could not find user for authority {} at {}:{}", + authority, + caller.file(), + caller.line() + ); + return Err(ErrorCode::UserNotFound); + } + }; + + match loader.load_mut() { + Ok(user) => Ok(user), + Err(e) => { + let caller = Location::caller(); + msg!("{:?}", e); + msg!( + "Could not load user for authority {} at {}:{}", + authority, + caller.file(), + caller.line() + ); + Err(ErrorCode::UnableToLoadUserAccount) + } + } + } + + #[track_caller] + #[inline(always)] + pub fn get_builder_account_mut(&self, authority: &Pubkey) -> DriftResult> { + let loader = match self.0.get(authority).and_then(|e| e.builder.as_ref()) { + Some(loader) => loader, + None => { + let caller = Location::caller(); + msg!( + "Could not find builder for authority {} at {}:{}", + authority, + caller.file(), + caller.line() + ); + return Err(ErrorCode::UnableToLoadBuilderAccount); + } + }; + + match loader.load_mut() { + Ok(builder) => Ok(builder), + Err(e) => { + let caller = Location::caller(); + msg!("{:?}", e); + msg!( + "Could not load builder for authority {} at {}:{}", + authority, + caller.file(), + caller.line() + ); + Err(ErrorCode::UnableToLoadBuilderAccount) + } + } + } +} + +pub fn load_builder_map<'a: 'b, 'b>( + account_info_iter: &mut Peekable>>, +) -> DriftResult> { + let mut builder_map = BuilderMap::empty(); + + let user_discriminator: [u8; 8] = User::discriminator(); + let builder_discriminator: [u8; 8] = Builder::discriminator(); + + while let Some(account_info) = account_info_iter.peek() { + let data = account_info + .try_borrow_data() + .or(Err(ErrorCode::DefaultError))?; + + if data.len() < 8 { + break; + } + + let account_discriminator = array_ref![data, 0, 8]; + + if account_discriminator == &user_discriminator { + let user_account_info = account_info_iter.next().safe_unwrap()?; + let is_writable = user_account_info.is_writable; + if !is_writable { + return Err(ErrorCode::UserWrongMutability); + } + + // Extract authority from User account data (after discriminator) + let data = user_account_info + .try_borrow_data() + .or(Err(ErrorCode::CouldNotLoadUserData))?; + let expected_data_len = User::SIZE; + if data.len() < expected_data_len { + return Err(ErrorCode::CouldNotLoadUserData); + } + let authority_slice = array_ref![data, 8, 32]; + let authority = Pubkey::from(*authority_slice); + + let user_account_loader: AccountLoader = + AccountLoader::try_from(user_account_info) + .or(Err(ErrorCode::InvalidUserAccount))?; + + builder_map.insert_user(authority, user_account_loader)?; + continue; + } + + if account_discriminator == &builder_discriminator { + let builder_account_info = account_info_iter.next().safe_unwrap()?; + let is_writable = builder_account_info.is_writable; + if !is_writable { + return Err(ErrorCode::DefaultError); + } + + let authority_slice = array_ref![data, 8, 32]; + let authority = Pubkey::from(*authority_slice); + + let builder_account_loader: AccountLoader = + AccountLoader::try_from(builder_account_info) + .or(Err(ErrorCode::InvalidBuilderAccount))?; + + builder_map.insert_builder(authority, builder_account_loader)?; + continue; + } + + break; + } + + Ok(builder_map) +} diff --git a/programs/drift/src/state/events.rs b/programs/drift/src/state/events.rs index f4806fa5b..74cc673da 100644 --- a/programs/drift/src/state/events.rs +++ b/programs/drift/src/state/events.rs @@ -256,10 +256,15 @@ pub struct OrderActionRecord { pub maker_existing_base_asset_amount: Option, /// precision: PRICE_PRECISION pub trigger_price: Option, + + /// the idx of the builder in the taker's [`BuilderEscrow`] account + pub builder_idx: Option, + /// precision: QUOTE_PRECISION builder fee paid by the taker + pub builder_fee: Option, } impl Size for OrderActionRecord { - const SIZE: usize = 464; + const SIZE: usize = 480; } pub fn get_order_action_record( @@ -288,6 +293,8 @@ pub fn get_order_action_record( maker_existing_quote_entry_amount: Option, maker_existing_base_asset_amount: Option, trigger_price: Option, + builder_idx: Option, + builder_fee: Option, ) -> DriftResult { Ok(OrderActionRecord { ts, @@ -341,6 +348,8 @@ pub fn get_order_action_record( maker_existing_quote_entry_amount, maker_existing_base_asset_amount, trigger_price, + builder_idx, + builder_fee, }) } @@ -737,3 +746,20 @@ pub fn emit_buffers( Ok(()) } + +#[event] +pub struct BuilderSettleRecord { + pub ts: i64, + pub builder: Option, + pub referrer: Option, + pub fee_settled: u64, + pub market_index: u16, + pub market_type: MarketType, + pub builder_sub_account_id: u16, + pub builder_total_referrer_rewards: u64, + pub builder_total_builder_rewards: u64, +} + +impl Size for BuilderSettleRecord { + const SIZE: usize = 140; +} diff --git a/programs/drift/src/state/mod.rs b/programs/drift/src/state/mod.rs index a9c972475..16cbf0031 100644 --- a/programs/drift/src/state/mod.rs +++ b/programs/drift/src/state/mod.rs @@ -1,3 +1,5 @@ +pub mod builder; +pub mod builder_map; pub mod events; pub mod fill_mode; pub mod fulfillment; diff --git a/programs/drift/src/state/order_params.rs b/programs/drift/src/state/order_params.rs index 48bc7399d..5b5282c89 100644 --- a/programs/drift/src/state/order_params.rs +++ b/programs/drift/src/state/order_params.rs @@ -11,6 +11,7 @@ use crate::{ PERCENTAGE_PRECISION_U64, PRICE_PRECISION_I64, }; use anchor_lang::prelude::*; +use anchor_lang::Discriminator; use borsh::{BorshDeserialize, BorshSerialize}; use std::ops::Div; @@ -866,6 +867,26 @@ pub struct SignedMsgOrderParamsMessage { pub stop_loss_order_params: Option, } +impl Discriminator for SignedMsgOrderParamsMessage { + const DISCRIMINATOR: [u8; 8] = [200, 213, 166, 94, 34, 52, 245, 93]; +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default, Eq, PartialEq, Debug)] +pub struct SignedMsgOrderParamsWithBuilderMessage { + pub signed_msg_order_params: OrderParams, + pub sub_account_id: u16, + pub slot: u64, + pub uuid: [u8; 8], + pub take_profit_order_params: Option, + pub stop_loss_order_params: Option, + pub builder_idx: Option, + pub builder_fee: Option, +} + +impl Discriminator for SignedMsgOrderParamsWithBuilderMessage { + const DISCRIMINATOR: [u8; 8] = [157, 106, 150, 102, 56, 204, 43, 146]; +} + #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default, Eq, PartialEq, Debug)] pub struct SignedMsgOrderParamsDelegateMessage { pub signed_msg_order_params: OrderParams, @@ -876,6 +897,26 @@ pub struct SignedMsgOrderParamsDelegateMessage { pub stop_loss_order_params: Option, } +impl Discriminator for SignedMsgOrderParamsDelegateMessage { + const DISCRIMINATOR: [u8; 8] = [66, 101, 102, 56, 199, 37, 158, 35]; +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default, Eq, PartialEq, Debug)] +pub struct SignedMsgOrderParamsDelegateWithBuilderMessage { + pub signed_msg_order_params: OrderParams, + pub taker_pubkey: Pubkey, + pub slot: u64, + pub uuid: [u8; 8], + pub take_profit_order_params: Option, + pub stop_loss_order_params: Option, + pub builder_idx: Option, + pub builder_fee: Option, +} + +impl Discriminator for SignedMsgOrderParamsDelegateWithBuilderMessage { + const DISCRIMINATOR: [u8; 8] = [249, 154, 6, 118, 193, 105, 18, 151]; +} + #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default, Eq, PartialEq, Debug)] pub struct SignedMsgTriggerOrderParams { pub trigger_price: u64, diff --git a/programs/drift/src/state/state.rs b/programs/drift/src/state/state.rs index 9e8b0961d..be2ceab8f 100644 --- a/programs/drift/src/state/state.rs +++ b/programs/drift/src/state/state.rs @@ -120,12 +120,17 @@ impl State { pub fn use_median_trigger_price(&self) -> bool { (self.feature_bit_flags & (FeatureBitFlags::MedianTriggerPrice as u8)) > 0 } + + pub fn builder_referral_enabled(&self) -> bool { + (self.feature_bit_flags & (FeatureBitFlags::BuilderReferral as u8)) > 0 + } } #[derive(Clone, Copy, PartialEq, Debug, Eq)] pub enum FeatureBitFlags { MmOracleUpdate = 0b00000001, MedianTriggerPrice = 0b00000010, + BuilderReferral = 0b00000100, } impl Size for State { diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index 4bcb6fb2f..215110cd9 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -32,6 +32,7 @@ use crate::{safe_increment, SPOT_WEIGHT_PRECISION}; use crate::{validate, MAX_PREDICTION_MARKET_PRICE}; use anchor_lang::prelude::*; use borsh::{BorshDeserialize, BorshSerialize}; +use bytemuck::{Pod, Zeroable}; use std::cmp::max; use std::fmt; use std::ops::Neg; @@ -1686,12 +1687,16 @@ impl fmt::Display for MarketType { } } +unsafe impl Zeroable for MarketType {} +unsafe impl Pod for MarketType {} + #[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq, Debug, Eq)] pub enum OrderBitFlag { SignedMessage = 0b00000001, OracleTriggerMarket = 0b00000010, SafeTriggerOrder = 0b00000100, NewTriggerReduceOnly = 0b00001000, + HasBuilder = 0b00010000, } #[account(zero_copy(unsafe))] @@ -1768,6 +1773,7 @@ pub struct UserStats { pub enum ReferrerStatus { IsReferrer = 0b00000001, IsReferred = 0b00000010, + BuilderReferral = 0b00000100, } impl ReferrerStatus { @@ -1778,6 +1784,10 @@ impl ReferrerStatus { pub fn is_referred(status: u8) -> bool { status & ReferrerStatus::IsReferred as u8 != 0 } + + pub fn has_builder_referral(status: u8) -> bool { + status & ReferrerStatus::BuilderReferral as u8 != 0 + } } impl Size for UserStats { @@ -1984,6 +1994,14 @@ impl UserStats { } } + pub fn update_builder_referral_status(&mut self) { + if !self.referrer.eq(&Pubkey::default()) { + self.referrer_status |= ReferrerStatus::BuilderReferral as u8; + } else { + self.referrer_status &= !(ReferrerStatus::BuilderReferral as u8); + } + } + pub fn update_fuel_overflow_status(&mut self, has_overflow: bool) { if has_overflow { self.fuel_overflow_status |= FuelOverflowStatus::Exists as u8; diff --git a/programs/drift/src/validation/sig_verification.rs b/programs/drift/src/validation/sig_verification.rs index 3349c7d5d..6e3bf5763 100644 --- a/programs/drift/src/validation/sig_verification.rs +++ b/programs/drift/src/validation/sig_verification.rs @@ -1,9 +1,11 @@ use crate::error::ErrorCode; use crate::state::order_params::{ - OrderParams, SignedMsgOrderParamsDelegateMessage, SignedMsgOrderParamsMessage, - SignedMsgTriggerOrderParams, + OrderParams, SignedMsgOrderParamsDelegateMessage, + SignedMsgOrderParamsDelegateWithBuilderMessage, SignedMsgOrderParamsMessage, + SignedMsgOrderParamsWithBuilderMessage, SignedMsgTriggerOrderParams, }; use anchor_lang::prelude::*; +use anchor_lang::Discriminator; use bytemuck::try_cast_slice; use bytemuck::{Pod, Zeroable}; use byteorder::ByteOrder; @@ -53,6 +55,8 @@ pub struct VerifiedMessage { pub uuid: [u8; 8], pub take_profit_order_params: Option, pub stop_loss_order_params: Option, + pub builder_idx: Option, + pub builder_fee: Option, pub signature: [u8; 64], } @@ -232,44 +236,115 @@ pub fn verify_and_decode_ed25519_msg( let payload = hex::decode(payload).map_err(|_| SignatureVerificationError::InvalidMessageHex)?; - if is_delegate_signer { - let deserialized = SignedMsgOrderParamsDelegateMessage::deserialize( - &mut &payload[8..], // 8 byte manual discriminator - ) - .map_err(|_| { - msg!("Invalid message encoding for is_delegate_signer = true"); - SignatureVerificationError::InvalidMessageDataSize - })?; + match payload[0..8].try_into().unwrap() { + SignedMsgOrderParamsMessage::DISCRIMINATOR => { + if is_delegate_signer { + msg!("Invalid delegate message encoding for with is_delegate_signer = true"); + return Err(SignatureVerificationError::InvalidMessageDataSize.into()); + } - return Ok(VerifiedMessage { - signed_msg_order_params: deserialized.signed_msg_order_params, - sub_account_id: None, - delegate_signed_taker_pubkey: Some(deserialized.taker_pubkey), - slot: deserialized.slot, - uuid: deserialized.uuid, - take_profit_order_params: deserialized.take_profit_order_params, - stop_loss_order_params: deserialized.stop_loss_order_params, - signature: *signature, - }); - } else { - let deserialized = SignedMsgOrderParamsMessage::deserialize( - &mut &payload[8..], // 8 byte manual discriminator - ) - .map_err(|_| { - msg!("Invalid delegate message encoding for with is_delegate_signer = false"); - SignatureVerificationError::InvalidMessageDataSize - })?; + let deserialized = SignedMsgOrderParamsMessage::deserialize(&mut &payload[8..]) + .map_err(|_| { + msg!("Invalid message encoding for SignedMsgOrderParamsMessage with is_delegate_signer = false"); + SignatureVerificationError::InvalidMessageDataSize + })?; + + Ok(VerifiedMessage { + signed_msg_order_params: deserialized.signed_msg_order_params, + sub_account_id: Some(deserialized.sub_account_id), + delegate_signed_taker_pubkey: None, + slot: deserialized.slot, + uuid: deserialized.uuid, + take_profit_order_params: deserialized.take_profit_order_params, + stop_loss_order_params: deserialized.stop_loss_order_params, + signature: *signature, + builder_idx: None, + builder_fee: None, + }) + } + SignedMsgOrderParamsDelegateMessage::DISCRIMINATOR => { + if !is_delegate_signer { + msg!("Invalid delegate message encoding for with is_delegate_signer = false"); + return Err(SignatureVerificationError::InvalidMessageDataSize.into()); + } + + let deserialized = SignedMsgOrderParamsDelegateMessage::deserialize(&mut &payload[8..]) + .map_err(|_| { + msg!("Invalid message encoding for with is_delegate_signer = true"); + SignatureVerificationError::InvalidMessageDataSize + })?; + + Ok(VerifiedMessage { + signed_msg_order_params: deserialized.signed_msg_order_params, + sub_account_id: None, + delegate_signed_taker_pubkey: Some(deserialized.taker_pubkey), + slot: deserialized.slot, + uuid: deserialized.uuid, + take_profit_order_params: deserialized.take_profit_order_params, + stop_loss_order_params: deserialized.stop_loss_order_params, + signature: *signature, + builder_idx: None, + builder_fee: None, + }) + } + SignedMsgOrderParamsWithBuilderMessage::DISCRIMINATOR => { + if is_delegate_signer { + msg!("Invalid delegate message encoding for with is_delegate_signer = true"); + return Err(SignatureVerificationError::InvalidMessageDataSize.into()); + } + + let deserialized = SignedMsgOrderParamsWithBuilderMessage::deserialize(&mut &payload[8..]) + .map_err(|_| { + msg!("Invalid message encoding for SignedMsgOrderParamsWithBuilderMessage with is_delegate_signer = false and builder"); + SignatureVerificationError::InvalidMessageDataSize + })?; + + Ok(VerifiedMessage { + signed_msg_order_params: deserialized.signed_msg_order_params, + sub_account_id: Some(deserialized.sub_account_id), + delegate_signed_taker_pubkey: None, + slot: deserialized.slot, + uuid: deserialized.uuid, + take_profit_order_params: deserialized.take_profit_order_params, + stop_loss_order_params: deserialized.stop_loss_order_params, + signature: *signature, + builder_idx: deserialized.builder_idx, + builder_fee: deserialized.builder_fee, + }) + } + SignedMsgOrderParamsDelegateWithBuilderMessage::DISCRIMINATOR => { + if !is_delegate_signer { + msg!("Invalid delegate message encoding for with is_delegate_signer = false"); + return Err(SignatureVerificationError::InvalidMessageDataSize.into()); + } + + let deserialized = SignedMsgOrderParamsDelegateWithBuilderMessage::deserialize(&mut &payload[8..]) + .map_err(|_| { + msg!("Invalid message encoding for SignedMsgOrderParamsDelegateWithBuilderMessage with is_delegate_signer = true and builder"); + SignatureVerificationError::InvalidMessageDataSize + })?; - return Ok(VerifiedMessage { - signed_msg_order_params: deserialized.signed_msg_order_params, - sub_account_id: Some(deserialized.sub_account_id), - delegate_signed_taker_pubkey: None, - slot: deserialized.slot, - uuid: deserialized.uuid, - take_profit_order_params: deserialized.take_profit_order_params, - stop_loss_order_params: deserialized.stop_loss_order_params, - signature: *signature, - }); + Ok(VerifiedMessage { + signed_msg_order_params: deserialized.signed_msg_order_params, + sub_account_id: None, + delegate_signed_taker_pubkey: Some(deserialized.taker_pubkey), + slot: deserialized.slot, + uuid: deserialized.uuid, + take_profit_order_params: deserialized.take_profit_order_params, + stop_loss_order_params: deserialized.stop_loss_order_params, + signature: *signature, + builder_idx: deserialized.builder_idx, + builder_fee: deserialized.builder_fee, + }) + } + _ => { + msg!( + "Invalid message length: {}, is_delegate_signer: {}", + payload.len(), + is_delegate_signer + ); + Err(SignatureVerificationError::InvalidMessageDataSize.into()) + } } } diff --git a/sdk/src/addresses/pda.ts b/sdk/src/addresses/pda.ts index 2dd95c417..1097853d4 100644 --- a/sdk/src/addresses/pda.ts +++ b/sdk/src/addresses/pda.ts @@ -394,3 +394,29 @@ export function getIfRebalanceConfigPublicKey( programId )[0]; } + +export function getBuilderAccountPublicKey( + programId: PublicKey, + authority: PublicKey +): PublicKey { + return PublicKey.findProgramAddressSync( + [ + Buffer.from(anchor.utils.bytes.utf8.encode('BUILD')), + authority.toBuffer(), + ], + programId + )[0]; +} + +export function getBuilderEscrowAccountPublicKey( + programId: PublicKey, + authority: PublicKey +): PublicKey { + return PublicKey.findProgramAddressSync( + [ + Buffer.from(anchor.utils.bytes.utf8.encode('BUILD_ESCROW')), + authority.toBuffer(), + ], + programId + )[0]; +} diff --git a/sdk/src/adminClient.ts b/sdk/src/adminClient.ts index 352e051bf..fdb6c011e 100644 --- a/sdk/src/adminClient.ts +++ b/sdk/src/adminClient.ts @@ -4649,4 +4649,34 @@ export class AdminClient extends DriftClient { } ); } + + public async updateFeatureBitFlagsBuilderReferral( + enable: boolean + ): Promise { + const updateFeatureBitFlagsBuilderReferralIx = + await this.getUpdateFeatureBitFlagsBuilderReferralIx(enable); + + const tx = await this.buildTransaction( + updateFeatureBitFlagsBuilderReferralIx + ); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getUpdateFeatureBitFlagsBuilderReferralIx( + enable: boolean + ): Promise { + return await this.program.instruction.updateFeatureBitFlagsBuilderReferral( + enable, + { + accounts: { + admin: this.useHotWalletAdmin + ? this.wallet.publicKey + : this.getStateAccount().admin, + state: await this.getStatePublicKey(), + }, + } + ); + } } diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index fa5e44157..71f3915f3 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -65,6 +65,8 @@ import { SignedMsgOrderParamsDelegateMessage, TokenProgramFlag, PostOnlyParams, + SignedMsgOrderParamsWithBuilderMessage, + SignedMsgOrderParamsDelegateWithBuilderMessage, } from './types'; import driftIDL from './idl/drift.json'; @@ -113,6 +115,8 @@ import { getUserStatsAccountPublicKey, getSignedMsgWsDelegatesAccountPublicKey, getIfRebalanceConfigPublicKey, + getBuilderAccountPublicKey, + getBuilderEscrowAccountPublicKey, } from './addresses/pda'; import { DataAndSlot, @@ -194,6 +198,9 @@ import { getOracleId } from './oracles/oracleId'; import { SignedMsgOrderParams } from './types'; import { sha256 } from '@noble/hashes/sha256'; import { getOracleConfidenceFromMMOracleData } from './oracles/utils'; +import { hasBuilder } from './math/orders'; +import { BuilderEscrowMap } from './userMap/builderEscrowMap'; +import { isBuilderOrderAvailable } from './math/builder'; type RemainingAccountParams = { userAccounts: UserAccount[]; @@ -1226,6 +1233,172 @@ export class DriftClient { return ix; } + public async initializeBuilder( + authority: PublicKey, + txParams?: TxParams + ): Promise { + const ix = await this.getInitializeBuilderIx(authority); + const tx = await this.buildTransaction([ix], txParams); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async getInitializeBuilderIx( + authority: PublicKey + ): Promise { + const builder = getBuilderAccountPublicKey( + this.program.programId, + authority + ); + return this.program.instruction.initializeBuilder({ + accounts: { + builder, + authority, + payer: this.wallet.publicKey, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + systemProgram: anchor.web3.SystemProgram.programId, + }, + }); + } + + public async initializeBuilderEscrow( + authority: PublicKey, + numOrders: number, + txParams?: TxParams + ): Promise { + const ix = await this.getInitializeBuilderEscrowIx(authority, numOrders); + const tx = await this.buildTransaction([ix], txParams); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async getInitializeBuilderEscrowIx( + authority: PublicKey, + numOrders: number + ): Promise { + const builderEscrow = getBuilderEscrowAccountPublicKey( + this.program.programId, + authority + ); + return this.program.instruction.initializeBuilderEscrow(numOrders, { + accounts: { + builderEscrow, + authority, + payer: this.wallet.publicKey, + userStats: getUserStatsAccountPublicKey( + this.program.programId, + authority + ), + state: await this.getStatePublicKey(), + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + systemProgram: anchor.web3.SystemProgram.programId, + }, + }); + } + + public async migrateReferrer( + authority: PublicKey, + txParams?: TxParams + ): Promise { + const ix = await this.getMigrateReferrerIx(authority); + const tx = await this.buildTransaction([ix], txParams); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async getMigrateReferrerIx( + authority: PublicKey + ): Promise { + const builderEscrow = getBuilderEscrowAccountPublicKey( + this.program.programId, + authority + ); + return this.program.instruction.migrateReferrer({ + accounts: { + builderEscrow, + authority, + userStats: getUserStatsAccountPublicKey( + this.program.programId, + authority + ), + state: await this.getStatePublicKey(), + payer: this.wallet.publicKey, + }, + }); + } + + public async resizeBuilderEscrowOrders( + authority: PublicKey, + numOrders: number, + txParams?: TxParams + ): Promise { + const ix = await this.getResizeBuilderEscrowOrdersIx(authority, numOrders); + const tx = await this.buildTransaction([ix], txParams); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async getResizeBuilderEscrowOrdersIx( + authority: PublicKey, + numOrders: number + ): Promise { + const builderEscrow = getBuilderEscrowAccountPublicKey( + this.program.programId, + authority + ); + return this.program.instruction.resizeBuilderEscrowOrders(numOrders, { + accounts: { + builderEscrow, + authority, + payer: this.wallet.publicKey, + systemProgram: anchor.web3.SystemProgram.programId, + }, + }); + } + + public async changeApprovedBuilder( + authority: PublicKey, + builder: PublicKey, + maxFeeBps: number, + add: boolean, + txParams?: TxParams + ): Promise { + const ix = await this.getChangeApprovedBuilderIx( + authority, + builder, + maxFeeBps, + add + ); + const tx = await this.buildTransaction([ix], txParams); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async getChangeApprovedBuilderIx( + authority: PublicKey, + builder: PublicKey, + maxFeeBps: number, + add: boolean + ): Promise { + const builderEscrow = getBuilderEscrowAccountPublicKey( + this.program.programId, + authority + ); + return this.program.instruction.changeApprovedBuilder( + builder, + maxFeeBps, + add, + { + accounts: { + builderEscrow, + authority, + payer: this.wallet.publicKey, + systemProgram: anchor.web3.SystemProgram.programId, + }, + } + ); + } + public async addSignedMsgWsDelegate( authority: PublicKey, delegate: PublicKey, @@ -1884,6 +2057,20 @@ export class DriftClient { writableSpotMarketIndexes, }); + for (const order of userAccount.orders) { + if (hasBuilder(order)) { + remainingAccounts.push({ + pubkey: getBuilderEscrowAccountPublicKey( + this.program.programId, + userAccount.authority + ), + isWritable: true, + isSigner: false, + }); + break; + } + } + const tokenPrograms = new Set(); for (const spotPosition of userAccount.spotPositions) { if (isSpotPositionAvailable(spotPosition)) { @@ -2385,6 +2572,35 @@ export class DriftClient { } } + addBuilderToRemainingAccounts( + builders: PublicKey[], + remainingAccounts: AccountMeta[] + ): void { + for (const builder of builders) { + // Add User account for the builder + const builderUserAccount = getUserAccountPublicKeySync( + this.program.programId, + builder, + 0 // subAccountId 0 for builder user account + ); + remainingAccounts.push({ + pubkey: builderUserAccount, + isSigner: false, + isWritable: true, + }); + + const builderAccount = getBuilderAccountPublicKey( + this.program.programId, + builder + ); + remainingAccounts.push({ + pubkey: builderAccount, + isSigner: false, + isWritable: true, + }); + } + } + getRemainingAccountMapsForUsers(userAccounts: UserAccount[]): { oracleAccountMap: Map; spotMarketAccountMap: Map; @@ -4604,7 +4820,8 @@ export class DriftClient { referrerInfo?: ReferrerInfo, txParams?: TxParams, fillerSubAccountId?: number, - fillerAuthority?: PublicKey + fillerAuthority?: PublicKey, + hasBuilderFee?: boolean ): Promise { const { txSig } = await this.sendTransaction( await this.buildTransaction( @@ -4616,7 +4833,8 @@ export class DriftClient { referrerInfo, fillerSubAccountId, undefined, - fillerAuthority + fillerAuthority, + hasBuilderFee ), txParams ), @@ -4634,7 +4852,8 @@ export class DriftClient { referrerInfo?: ReferrerInfo, fillerSubAccountId?: number, isSignedMsg?: boolean, - fillerAuthority?: PublicKey + fillerAuthority?: PublicKey, + hasBuilderFee?: boolean ): Promise { const userStatsPublicKey = getUserStatsAccountPublicKey( this.program.programId, @@ -4716,6 +4935,36 @@ export class DriftClient { } } + let withBuilder = false; + if (hasBuilderFee) { + withBuilder = true; + } else { + // figure out if we need builder account or not + if (order && !isSignedMsg) { + const userOrder = userAccount.orders.find( + (o) => o.orderId === order.orderId + ); + if (userOrder) { + withBuilder = hasBuilder(userOrder); + } + } else if (isSignedMsg) { + // Order hasn't been placed yet, we cant tell if it has a builder or not. + // Include it optimistically + withBuilder = true; + } + } + + if (withBuilder) { + remainingAccounts.push({ + pubkey: getBuilderEscrowAccountPublicKey( + this.program.programId, + userAccount.authority + ), + isWritable: true, + isSigner: false, + }); + } + const orderId = isSignedMsg ? null : order.orderId; return await this.program.instruction.fillPerpOrder(orderId, null, { accounts: { @@ -6356,12 +6605,16 @@ export class DriftClient { public signSignedMsgOrderParamsMessage( orderParamsMessage: | SignedMsgOrderParamsMessage - | SignedMsgOrderParamsDelegateMessage, - delegateSigner?: boolean + | SignedMsgOrderParamsDelegateMessage + | SignedMsgOrderParamsWithBuilderMessage + | SignedMsgOrderParamsDelegateWithBuilderMessage, + delegateSigner?: boolean, + withBuilder?: boolean ): SignedMsgOrderParams { const borshBuf = this.encodeSignedMsgOrderParamsMessage( orderParamsMessage, - delegateSigner + delegateSigner, + withBuilder ); const orderParams = Buffer.from(borshBuf.toString('hex')); return { @@ -6376,26 +6629,60 @@ export class DriftClient { public encodeSignedMsgOrderParamsMessage( orderParamsMessage: | SignedMsgOrderParamsMessage - | SignedMsgOrderParamsDelegateMessage, - delegateSigner?: boolean + | SignedMsgOrderParamsDelegateMessage + | SignedMsgOrderParamsWithBuilderMessage + | SignedMsgOrderParamsDelegateWithBuilderMessage, + delegateSigner?: boolean, + withBuilder?: boolean ): Buffer { - const anchorIxName = delegateSigner - ? 'global' + ':' + 'SignedMsgOrderParamsDelegateMessage' - : 'global' + ':' + 'SignedMsgOrderParamsMessage'; + const paramKeys = Object.keys(orderParamsMessage); + const hasBuilderOrderParams = + paramKeys.includes('builder') || paramKeys.includes('builderFee'); + if (withBuilder && !hasBuilderOrderParams) { + throw new Error( + 'Builder order params are required when withBuilder is true' + ); + } + if (!withBuilder && hasBuilderOrderParams) { + throw new Error( + 'Builder order params are not allowed when withBuilder is false' + ); + } + + let anchorIxName = 'global:'; + let messageBuffer: Buffer = null; + if (delegateSigner) { + if (withBuilder) { + anchorIxName += 'SignedMsgOrderParamsDelegateWithBuilderMessage'; + messageBuffer = this.program.coder.types.encode( + 'SignedMsgOrderParamsDelegateWithBuilderMessage', + orderParamsMessage as SignedMsgOrderParamsDelegateWithBuilderMessage + ); + } else { + anchorIxName += 'SignedMsgOrderParamsDelegateMessage'; + messageBuffer = this.program.coder.types.encode( + 'SignedMsgOrderParamsDelegateMessage', + orderParamsMessage as SignedMsgOrderParamsDelegateMessage + ); + } + } else { + if (withBuilder) { + anchorIxName += 'SignedMsgOrderParamsWithBuilderMessage'; + messageBuffer = this.program.coder.types.encode( + 'SignedMsgOrderParamsWithBuilderMessage', + orderParamsMessage as SignedMsgOrderParamsWithBuilderMessage + ); + } else { + anchorIxName += 'SignedMsgOrderParamsMessage'; + messageBuffer = this.program.coder.types.encode( + 'SignedMsgOrderParamsMessage', + orderParamsMessage as SignedMsgOrderParamsMessage + ); + } + } + const prefix = Buffer.from(sha256(anchorIxName).slice(0, 8)); - const buf = Buffer.concat([ - prefix, - delegateSigner - ? this.program.coder.types.encode( - 'SignedMsgOrderParamsDelegateMessage', - orderParamsMessage as SignedMsgOrderParamsDelegateMessage - ) - : this.program.coder.types.encode( - 'SignedMsgOrderParamsMessage', - orderParamsMessage as SignedMsgOrderParamsMessage - ), - ]); - return buf; + return Buffer.concat([prefix, messageBuffer]); } /* @@ -6403,15 +6690,69 @@ export class DriftClient { */ public decodeSignedMsgOrderParamsMessage( encodedMessage: Buffer, - delegateSigner?: boolean - ): SignedMsgOrderParamsMessage | SignedMsgOrderParamsDelegateMessage { - const decodeStr = delegateSigner - ? 'SignedMsgOrderParamsDelegateMessage' - : 'SignedMsgOrderParamsMessage'; - return this.program.coder.types.decode( - decodeStr, - encodedMessage.slice(8) // assumes discriminator - ); + _delegateSigner?: boolean + ): { + signedMessage: + | SignedMsgOrderParamsMessage + | SignedMsgOrderParamsDelegateMessage + | SignedMsgOrderParamsWithBuilderMessage + | SignedMsgOrderParamsDelegateWithBuilderMessage; + isDelegateSigner: boolean; + withBuilder: boolean; + } { + let isDelegateSigner = false; + let withBuilder = false; + + const msgDiscr = Buffer.from(Array.from(encodedMessage).slice(0, 8)); + let decodeStr = null; + if ( + msgDiscr.equals( + Buffer.from(sha256('global:SignedMsgOrderParamsMessage').slice(0, 8)) + ) + ) { + decodeStr = 'SignedMsgOrderParamsMessage'; + } else if ( + msgDiscr.equals( + Buffer.from( + sha256('global:SignedMsgOrderParamsDelegateMessage').slice(0, 8) + ) + ) + ) { + decodeStr = 'SignedMsgOrderParamsDelegateMessage'; + isDelegateSigner = true; + } else if ( + msgDiscr.equals( + Buffer.from( + sha256('global:SignedMsgOrderParamsWithBuilderMessage').slice(0, 8) + ) + ) + ) { + decodeStr = 'SignedMsgOrderParamsWithBuilderMessage'; + withBuilder = true; + } else if ( + msgDiscr.equals( + Buffer.from( + sha256('global:SignedMsgOrderParamsDelegateWithBuilderMessage').slice( + 0, + 8 + ) + ) + ) + ) { + decodeStr = 'SignedMsgOrderParamsDelegateWithBuilderMessage'; + isDelegateSigner = true; + withBuilder = true; + } else { + throw new Error('Invalid signed msg order params message'); + } + return { + signedMessage: this.program.coder.types.decode( + decodeStr, + encodedMessage.slice(8) + ), + isDelegateSigner, + withBuilder, + }; } public signMessage( @@ -6475,20 +6816,25 @@ export class DriftClient { signedSignedMsgOrderParams.orderParams.toString(), 'hex' ); - try { - const { signedMsgOrderParams } = this.decodeSignedMsgOrderParamsMessage( - borshBuf, - isDelegateSigner - ); - if (isUpdateHighLeverageMode(signedMsgOrderParams.bitFlags)) { - remainingAccounts.push({ - pubkey: getHighLeverageModeConfigPublicKey(this.program.programId), - isWritable: true, - isSigner: false, - }); - } - } catch (err) { - console.error('invalid signed order encoding'); + + const { signedMessage, withBuilder } = + this.decodeSignedMsgOrderParamsMessage(borshBuf, isDelegateSigner); + if (isUpdateHighLeverageMode(signedMessage.signedMsgOrderParams.bitFlags)) { + remainingAccounts.push({ + pubkey: getHighLeverageModeConfigPublicKey(this.program.programId), + isWritable: true, + isSigner: false, + }); + } + if (withBuilder) { + remainingAccounts.push({ + pubkey: getBuilderEscrowAccountPublicKey( + this.program.programId, + takerInfo.takerUserAccount.authority + ), + isWritable: true, + isSigner: false, + }); } const messageLengthBuffer = Buffer.alloc(2); @@ -7275,7 +7621,8 @@ export class DriftClient { settleeUserAccount: UserAccount, marketIndex: number, txParams?: TxParams, - optionalIxs?: TransactionInstruction[] + optionalIxs?: TransactionInstruction[], + builderEscrowMap?: BuilderEscrowMap ): Promise { const lookupTableAccounts = await this.fetchAllLookupTableAccounts(); @@ -7284,7 +7631,8 @@ export class DriftClient { await this.settlePNLIx( settleeUserAccountPublicKey, settleeUserAccount, - marketIndex + marketIndex, + builderEscrowMap ), txParams, undefined, @@ -7302,7 +7650,8 @@ export class DriftClient { public async settlePNLIx( settleeUserAccountPublicKey: PublicKey, settleeUserAccount: UserAccount, - marketIndex: number + marketIndex: number, + builderEscrowMap?: BuilderEscrowMap ): Promise { const remainingAccounts = this.getRemainingAccounts({ userAccounts: [settleeUserAccount], @@ -7310,6 +7659,45 @@ export class DriftClient { writableSpotMarketIndexes: [QUOTE_SPOT_MARKET_INDEX], }); + if (builderEscrowMap) { + for (const order of settleeUserAccount.orders) { + if (hasBuilder(order)) { + remainingAccounts.push({ + pubkey: getBuilderEscrowAccountPublicKey( + this.program.programId, + settleeUserAccount.authority + ), + isSigner: false, + isWritable: true, + }); + break; + } + } + + const builderEscrow = await builderEscrowMap.mustGet( + settleeUserAccount.authority.toBase58() + ); + if (builderEscrow) { + const builders = new Map(); + for (const order of builderEscrow.orders) { + if (!isBuilderOrderAvailable(order)) { + if (!builders.has(order.builderIdx)) { + builders.set( + order.builderIdx, + builderEscrow.approvedBuilders[order.builderIdx].authority + ); + } + } + } + if (builders.size > 0) { + this.addBuilderToRemainingAccounts( + Array.from(builders.values()), + remainingAccounts + ); + } + } + } + return await this.program.instruction.settlePnl(marketIndex, { accounts: { state: await this.getStatePublicKey(), @@ -7326,6 +7714,7 @@ export class DriftClient { settleeUserAccount: UserAccount, marketIndexes: number[], mode: SettlePnlMode, + builderEscrowMap?: BuilderEscrowMap, txParams?: TxParams ): Promise { const { txSig } = await this.sendTransaction( @@ -7334,7 +7723,8 @@ export class DriftClient { settleeUserAccountPublicKey, settleeUserAccount, marketIndexes, - mode + mode, + builderEscrowMap ), txParams ), @@ -7350,7 +7740,8 @@ export class DriftClient { marketIndexes: number[], mode: SettlePnlMode, txParams?: TxParams, - optionalIxs?: TransactionInstruction[] + optionalIxs?: TransactionInstruction[], + builderEscrowMap?: BuilderEscrowMap ): Promise { // need multiple TXs because settling more than 4 markets won't fit in a single TX const txsToSign: (Transaction | VersionedTransaction)[] = []; @@ -7364,7 +7755,8 @@ export class DriftClient { settleeUserAccountPublicKey, settleeUserAccount, marketIndexes, - mode + mode, + builderEscrowMap ); const computeUnits = Math.min(300_000 * marketIndexes.length, 1_400_000); const tx = await this.buildTransaction( @@ -7406,7 +7798,8 @@ export class DriftClient { settleeUserAccountPublicKey: PublicKey, settleeUserAccount: UserAccount, marketIndexes: number[], - mode: SettlePnlMode + mode: SettlePnlMode, + builderEscrowMap?: BuilderEscrowMap ): Promise { const remainingAccounts = this.getRemainingAccounts({ userAccounts: [settleeUserAccount], @@ -7414,6 +7807,43 @@ export class DriftClient { writableSpotMarketIndexes: [QUOTE_SPOT_MARKET_INDEX], }); + if (builderEscrowMap) { + for (const order of settleeUserAccount.orders) { + if (hasBuilder(order)) { + remainingAccounts.push({ + pubkey: getBuilderEscrowAccountPublicKey( + this.program.programId, + settleeUserAccount.authority + ), + isSigner: false, + isWritable: true, + }); + break; + } + } + + const builderEscrow = await builderEscrowMap.mustGet( + settleeUserAccount.authority.toBase58() + ); + const builders = new Map(); + for (const order of builderEscrow.orders) { + if (!isBuilderOrderAvailable(order)) { + if (!builders.has(order.builderIdx)) { + builders.set( + order.builderIdx, + builderEscrow.approvedBuilders[order.builderIdx].authority + ); + } + } + } + if (builders.size > 0) { + this.addBuilderToRemainingAccounts( + Array.from(builders.values()), + remainingAccounts + ); + } + } + return await this.program.instruction.settleMultiplePnls( marketIndexes, mode, diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 1c14a231a..0189670de 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -7633,9 +7633,283 @@ "type": "bool" } ] + }, + { + "name": "updateFeatureBitFlagsBuilderReferral", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "enable", + "type": "bool" + } + ] + }, + { + "name": "initializeBuilder", + "accounts": [ + { + "name": "builder", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "initializeBuilderEscrow", + "accounts": [ + { + "name": "builderEscrow", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "numOrders", + "type": "u16" + } + ] + }, + { + "name": "migrateReferrer", + "accounts": [ + { + "name": "builderEscrow", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": false + }, + { + "name": "userStats", + "isMut": false, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "payer", + "isMut": false, + "isSigner": true + } + ], + "args": [] + }, + { + "name": "resizeBuilderEscrowOrders", + "accounts": [ + { + "name": "builderEscrow", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": false + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "numOrders", + "type": "u16" + } + ] + }, + { + "name": "changeApprovedBuilder", + "accounts": [ + { + "name": "builderEscrow", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "builder", + "type": "publicKey" + }, + { + "name": "maxFeeBps", + "type": "u16" + }, + { + "name": "add", + "type": "bool" + } + ] } ], "accounts": [ + { + "name": "Builder", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "docs": [ + "the owner of this account, a builder or referrer" + ], + "type": "publicKey" + }, + { + "name": "totalReferrerRewards", + "type": "u64" + }, + { + "name": "totalBuilderRewards", + "type": "u64" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 18 + ] + } + } + ] + } + }, + { + "name": "BuilderEscrow", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "docs": [ + "the owner of this account, a user" + ], + "type": "publicKey" + }, + { + "name": "referrer", + "type": "publicKey" + }, + { + "name": "padding0", + "type": "u32" + }, + { + "name": "orders", + "type": { + "vec": { + "defined": "BuilderOrder" + } + } + }, + { + "name": "padding1", + "type": "u32" + }, + { + "name": "approvedBuilders", + "type": { + "vec": { + "defined": "BuilderInfo" + } + } + } + ] + } + }, { "name": "OpenbookV2FulfillmentConfig", "type": { @@ -9699,37 +9973,162 @@ } ] } - } - ], - "types": [ + } + ], + "types": [ + { + "name": "UpdatePerpMarketSummaryStatsParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "quoteAssetAmountWithUnsettledLp", + "type": { + "option": "i64" + } + }, + { + "name": "netUnsettledFundingPnl", + "type": { + "option": "i64" + } + }, + { + "name": "updateAmmSummaryStats", + "type": { + "option": "bool" + } + }, + { + "name": "excludeTotalLiqFee", + "type": { + "option": "bool" + } + } + ] + } + }, + { + "name": "BuilderOrder", + "type": { + "kind": "struct", + "fields": [ + { + "name": "builderIdx", + "docs": [ + "the index of the BuilderEscrow.approved_builders list, that this order's fee will settle to. Ignored", + "if bit_flag = Referral." + ], + "type": "u8" + }, + { + "name": "padding0", + "type": { + "array": [ + "u8", + 7 + ] + } + }, + { + "name": "feesAccrued", + "docs": [ + "fees accrued so far for this order slot. This is not exclusively fees from this order_id", + "and may include fees from other orders in the same market. This may be swept to the", + "builder's SpotPosition during settle_pnl." + ], + "type": "u64" + }, + { + "name": "orderId", + "docs": [ + "the order_id of the current active order in this slot. It only carries meaning while bit_flag = Open" + ], + "type": "u32" + }, + { + "name": "feeBps", + "type": "u16" + }, + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "bitFlags", + "docs": [ + "bitflags that describe the state of the order.", + "[`BuilderOrderBitFlag::Init`]: this order slot is available for use.", + "[`BuilderOrderBitFlag::Open`]: this order slot is occupied, `order_id` is the `sub_account_id`'s active order.", + "[`BuilderOrderBitFlag::Completed`]: this order has been filled or canceled, and is waiting to be settled into.", + "the builder's account order_id and sub_account_id are no longer relevant, it may be merged with other orders.", + "[`BuilderOrderBitFlag::Referral`]: this order stores referral rewards waiting to be settled. If it is set, no", + "other bitflag should be set." + ], + "type": "u8" + }, + { + "name": "marketType", + "type": { + "defined": "MarketType" + } + }, + { + "name": "subAccountId", + "docs": [ + "the subaccount_id of the user who created this order. It only carries meaning while bit_flag = Open" + ], + "type": "u16" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 12 + ] + } + } + ] + } + }, { - "name": "UpdatePerpMarketSummaryStatsParams", + "name": "BuilderInfo", "type": { "kind": "struct", "fields": [ { - "name": "quoteAssetAmountWithUnsettledLp", - "type": { - "option": "i64" - } + "name": "authority", + "type": "publicKey" }, { - "name": "netUnsettledFundingPnl", - "type": { - "option": "i64" - } + "name": "maxFeeBps", + "type": "u16" }, { - "name": "updateAmmSummaryStats", + "name": "padding2", "type": { - "option": "bool" + "array": [ + "u8", + 2 + ] } + } + ] + } + }, + { + "name": "BuilderEscrowFixed", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "type": "publicKey" }, { - "name": "excludeTotalLiqFee", - "type": { - "option": "bool" - } + "name": "referrer", + "type": "publicKey" } ] } @@ -10277,6 +10676,65 @@ ] } }, + { + "name": "SignedMsgOrderParamsWithBuilderMessage", + "type": { + "kind": "struct", + "fields": [ + { + "name": "signedMsgOrderParams", + "type": { + "defined": "OrderParams" + } + }, + { + "name": "subAccountId", + "type": "u16" + }, + { + "name": "slot", + "type": "u64" + }, + { + "name": "uuid", + "type": { + "array": [ + "u8", + 8 + ] + } + }, + { + "name": "takeProfitOrderParams", + "type": { + "option": { + "defined": "SignedMsgTriggerOrderParams" + } + } + }, + { + "name": "stopLossOrderParams", + "type": { + "option": { + "defined": "SignedMsgTriggerOrderParams" + } + } + }, + { + "name": "builderIdx", + "type": { + "option": "u8" + } + }, + { + "name": "builderFee", + "type": { + "option": "u16" + } + } + ] + } + }, { "name": "SignedMsgOrderParamsDelegateMessage", "type": { @@ -10324,6 +10782,65 @@ ] } }, + { + "name": "SignedMsgOrderParamsDelegateWithBuilderMessage", + "type": { + "kind": "struct", + "fields": [ + { + "name": "signedMsgOrderParams", + "type": { + "defined": "OrderParams" + } + }, + { + "name": "takerPubkey", + "type": "publicKey" + }, + { + "name": "slot", + "type": "u64" + }, + { + "name": "uuid", + "type": { + "array": [ + "u8", + 8 + ] + } + }, + { + "name": "takeProfitOrderParams", + "type": { + "option": { + "defined": "SignedMsgTriggerOrderParams" + } + } + }, + { + "name": "stopLossOrderParams", + "type": { + "option": { + "defined": "SignedMsgTriggerOrderParams" + } + } + }, + { + "name": "builderIdx", + "type": { + "option": "u8" + } + }, + { + "name": "builderFee", + "type": { + "option": "u16" + } + } + ] + } + }, { "name": "SignedMsgTriggerOrderParams", "type": { @@ -12132,6 +12649,26 @@ ] } }, + { + "name": "BuilderOrderBitFlag", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Init" + }, + { + "name": "Open" + }, + { + "name": "Completed" + }, + { + "name": "Referral" + } + ] + } + }, { "name": "DepositExplanation", "type": { @@ -12868,6 +13405,9 @@ }, { "name": "MedianTriggerPrice" + }, + { + "name": "BuilderReferral" } ] } @@ -13002,6 +13542,9 @@ }, { "name": "NewTriggerReduceOnly" + }, + { + "name": "HasBuilder" } ] } @@ -13016,6 +13559,9 @@ }, { "name": "IsReferred" + }, + { + "name": "BuilderReferral" } ] } @@ -13805,6 +14351,20 @@ "option": "u64" }, "index": false + }, + { + "name": "builderIdx", + "type": { + "option": "u8" + }, + "index": false + }, + { + "name": "builderFee", + "type": { + "option": "u64" + }, + "index": false } ] }, @@ -14521,6 +15081,62 @@ "index": false } ] + }, + { + "name": "BuilderSettleRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "builder", + "type": { + "option": "publicKey" + }, + "index": false + }, + { + "name": "referrer", + "type": { + "option": "publicKey" + }, + "index": false + }, + { + "name": "feeSettled", + "type": "u64", + "index": false + }, + { + "name": "marketIndex", + "type": "u16", + "index": false + }, + { + "name": "marketType", + "type": { + "defined": "MarketType" + }, + "index": false + }, + { + "name": "builderSubAccountId", + "type": "u16", + "index": false + }, + { + "name": "builderTotalReferrerRewards", + "type": "u64", + "index": false + }, + { + "name": "builderTotalBuilderRewards", + "type": "u64", + "index": false + } + ] } ], "errors": [ @@ -16108,9 +16724,64 @@ "code": 6316, "name": "InvalidIfRebalanceSwap", "msg": "Invalid If Rebalance Swap" + }, + { + "code": 6317, + "name": "InvalidBuilderResize", + "msg": "Invalid Builder resize" + }, + { + "code": 6318, + "name": "InvalidBuilderApproval", + "msg": "Invalid builder approval" + }, + { + "code": 6319, + "name": "CouldNotDeserializeBuilderEscrow", + "msg": "Could not deserialize builder escrow" + }, + { + "code": 6320, + "name": "BuilderRevoked", + "msg": "Builder has been revoked" + }, + { + "code": 6321, + "name": "InvalidBuilderFee", + "msg": "Builder fee is greater than max fee bps" + }, + { + "code": 6322, + "name": "BuilderEscrowOrdersAccountFull", + "msg": "BuilderEscrow has too many active orders" + }, + { + "code": 6323, + "name": "BuilderEscrowMissing", + "msg": "BuilderEscrow missing" + }, + { + "code": 6324, + "name": "BuilderMissing", + "msg": "Builder missing" + }, + { + "code": 6325, + "name": "InvalidBuilderAccount", + "msg": "Invalid BuilderAccount" + }, + { + "code": 6326, + "name": "CannotRevokeBuilderWithOpenOrders", + "msg": "Cannot revoke builder with open orders" + }, + { + "code": 6327, + "name": "UnableToLoadBuilderAccount", + "msg": "Unable to load builder account" } ], "metadata": { "address": "dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH" } -} +} \ No newline at end of file diff --git a/sdk/src/index.ts b/sdk/src/index.ts index 902f4a533..ceccb36f0 100644 --- a/sdk/src/index.ts +++ b/sdk/src/index.ts @@ -116,6 +116,7 @@ export * from './dlob/orderBookLevels'; export * from './userMap/userMap'; export * from './userMap/referrerMap'; export * from './userMap/userStatsMap'; +export * from './userMap/builderEscrowMap'; export * from './userMap/userMapConfig'; export * from './math/bankruptcy'; export * from './orderSubscriber'; diff --git a/sdk/src/math/builder.ts b/sdk/src/math/builder.ts new file mode 100644 index 000000000..f8fa2ed75 --- /dev/null +++ b/sdk/src/math/builder.ts @@ -0,0 +1,20 @@ +import { BuilderOrder } from '../types'; + +const FLAG_IS_OPEN = 0x01; +export function isBuilderOrderOpen(order: BuilderOrder): boolean { + return (order.bitFlags & FLAG_IS_OPEN) !== 0; +} + +const FLAG_IS_COMPLETED = 0x02; +export function isBuilderOrderCompleted(order: BuilderOrder): boolean { + return (order.bitFlags & FLAG_IS_COMPLETED) !== 0; +} + +const FLAG_IS_REFERRAL = 0x04; +export function isBuilderOrderReferral(order: BuilderOrder): boolean { + return (order.bitFlags & FLAG_IS_REFERRAL) !== 0; +} + +export function isBuilderOrderAvailable(order: BuilderOrder): boolean { + return !isBuilderOrderOpen(order) && !isBuilderOrderCompleted(order); +} diff --git a/sdk/src/math/orders.ts b/sdk/src/math/orders.ts index 7be65e58c..147219f31 100644 --- a/sdk/src/math/orders.ts +++ b/sdk/src/math/orders.ts @@ -388,6 +388,11 @@ export function isSignedMsgOrder(order: Order): boolean { return (order.bitFlags & FLAG_IS_SIGNED_MSG) !== 0; } +const FLAG_HAS_BUILDER = 0x10; +export function hasBuilder(order: Order): boolean { + return (order.bitFlags & FLAG_HAS_BUILDER) !== 0; +} + export function calculateOrderBaseAssetAmount( order: Order, existingBaseAssetAmount: BN diff --git a/sdk/src/memcmp.ts b/sdk/src/memcmp.ts index 300f2a75d..b0163a69e 100644 --- a/sdk/src/memcmp.ts +++ b/sdk/src/memcmp.ts @@ -112,3 +112,14 @@ export function getSignedMsgUserOrdersFilter(): MemcmpFilter { }, }; } + +export function getBuilderEscrowFilter(): MemcmpFilter { + return { + memcmp: { + offset: 0, + bytes: bs58.encode( + BorshAccountsCoder.accountDiscriminator('BuilderEscrow') + ), + }, + }; +} diff --git a/sdk/src/swift/swiftOrderSubscriber.ts b/sdk/src/swift/swiftOrderSubscriber.ts index bab303279..ad9aae577 100644 --- a/sdk/src/swift/swiftOrderSubscriber.ts +++ b/sdk/src/swift/swiftOrderSubscriber.ts @@ -168,9 +168,7 @@ export class SwiftOrderSubscriber { ).slice(0, 8) ) ); - const signedMessage: - | SignedMsgOrderParamsMessage - | SignedMsgOrderParamsDelegateMessage = + const { signedMessage } = this.driftClient.decodeSignedMsgOrderParamsMessage( signedMsgOrderParamsBuf, isDelegateSigner @@ -248,9 +246,7 @@ export class SwiftOrderSubscriber { ).slice(0, 8) ) ); - const signedMessage: - | SignedMsgOrderParamsMessage - | SignedMsgOrderParamsDelegateMessage = + const { signedMessage } = this.driftClient.decodeSignedMsgOrderParamsMessage( signedMsgOrderParamsBuf, isDelegateSigner diff --git a/sdk/src/types.ts b/sdk/src/types.ts index 06a174f42..83d2f364c 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -1255,6 +1255,28 @@ export type SignedMsgOrderParamsDelegateMessage = { stopLossOrderParams: SignedMsgTriggerOrderParams | null; }; +export type SignedMsgOrderParamsWithBuilderMessage = { + signedMsgOrderParams: OrderParams; + subAccountId: number; + slot: BN; + uuid: Uint8Array; + takeProfitOrderParams: SignedMsgTriggerOrderParams | null; + stopLossOrderParams: SignedMsgTriggerOrderParams | null; + builderIdx: number | null; + builderFee: number | null; +}; + +export type SignedMsgOrderParamsDelegateWithBuilderMessage = { + signedMsgOrderParams: OrderParams; + slot: BN; + uuid: Uint8Array; + takerPubkey: PublicKey; + takeProfitOrderParams: SignedMsgTriggerOrderParams | null; + stopLossOrderParams: SignedMsgTriggerOrderParams | null; + builderIdx: number | null; + builderFee: number | null; +}; + export type SignedMsgTriggerOrderParams = { triggerPrice: BN; baseAssetAmount: BN; @@ -1571,3 +1593,46 @@ export type SignedMsgUserOrdersAccount = { authorityPubkey: PublicKey; signedMsgOrderData: SignedMsgOrderId[]; }; + +export type BuilderAccount = { + authority: PublicKey; + totalReferrerRewards: BN; + totalBuilderRewards: BN; + padding: number[]; +}; + +export type BuilderEscrow = { + authority: PublicKey; + referrer: PublicKey; + orders: BuilderOrder[]; + approvedBuilders: BuilderInfo[]; +}; + +export type BuilderOrder = { + builderIdx: number; + feesAccrued: BN; + orderId: number; + feeBps: number; + marketIndex: number; + bitFlags: number; + marketType: MarketType; // 0: spot, 1: perp + padding: number[]; +}; + +export type BuilderInfo = { + authority: PublicKey; + maxFeeBps: number; + bitFlags: number; +}; + +export type BuilderSettleRecord = { + ts: number; + builder: PublicKey | null; + referrer: PublicKey | null; + feeSettled: BN; + marketIndex: number; + marketType: MarketType; + builderTotalReferrerRewards: BN; + builderTotalBuilderRewards: BN; + builderSubAccountId: number; +}; diff --git a/sdk/src/userMap/builderEscrowMap.ts b/sdk/src/userMap/builderEscrowMap.ts new file mode 100644 index 000000000..ca334d33f --- /dev/null +++ b/sdk/src/userMap/builderEscrowMap.ts @@ -0,0 +1,297 @@ +import { PublicKey, RpcResponseAndContext } from '@solana/web3.js'; +import { DriftClient } from '../driftClient'; +import { BuilderEscrow } from '../types'; +import { getBuilderEscrowAccountPublicKey } from '../addresses/pda'; +import { getBuilderEscrowFilter } from '../memcmp'; + +export class BuilderEscrowMap { + /** + * map from authority pubkey to BuilderEscrow account data. + */ + private authorityEscrowMap = new Map(); + private driftClient: DriftClient; + private parallelSync: boolean; + + private fetchPromise?: Promise; + private fetchPromiseResolver: () => void; + + /** + * Creates a new BuilderEscrowMap instance. + * + * @param {DriftClient} driftClient - The DriftClient instance. + * @param {boolean} parallelSync - Whether to sync accounts in parallel. + */ + constructor(driftClient: DriftClient, parallelSync?: boolean) { + this.driftClient = driftClient; + this.parallelSync = parallelSync !== undefined ? parallelSync : true; + } + + /** + * Subscribe to all BuilderEscrow accounts. + */ + public async subscribe() { + if (this.size() > 0) { + return; + } + + await this.driftClient.subscribe(); + await this.sync(); + } + + public has(authorityPublicKey: string): boolean { + return this.authorityEscrowMap.has(authorityPublicKey); + } + + public get(authorityPublicKey: string): BuilderEscrow | undefined { + return this.authorityEscrowMap.get(authorityPublicKey); + } + + /** + * Enforce that a BuilderEscrow will exist for the given authorityPublicKey, + * reading one from the blockchain if necessary. + * @param authorityPublicKey + * @returns + */ + public async mustGet( + authorityPublicKey: string + ): Promise { + if (!this.has(authorityPublicKey)) { + await this.addBuilderEscrow(authorityPublicKey); + } + return this.get(authorityPublicKey); + } + + public async addBuilderEscrow(authority: string) { + const builderEscrowAccountPublicKey = getBuilderEscrowAccountPublicKey( + this.driftClient.program.programId, + new PublicKey(authority) + ); + + try { + const accountInfo = await this.driftClient.connection.getAccountInfo( + builderEscrowAccountPublicKey, + 'processed' + ); + + if (accountInfo && accountInfo.data) { + const builderEscrow = + this.driftClient.program.account.builderEscrow.coder.accounts.decode( + 'BuilderEscrow', + accountInfo.data + ) as BuilderEscrow; + + this.authorityEscrowMap.set(authority, builderEscrow); + } + } catch (error) { + // BuilderEscrow account doesn't exist for this authority, which is normal + console.debug( + `No BuilderEscrow account found for authority: ${authority}` + ); + } + } + + public size(): number { + return this.authorityEscrowMap.size; + } + + public async sync(): Promise { + if (this.fetchPromise) { + return this.fetchPromise; + } + + this.fetchPromise = new Promise((resolver) => { + this.fetchPromiseResolver = resolver; + }); + + try { + await this.syncAll(); + } finally { + this.fetchPromiseResolver(); + this.fetchPromise = undefined; + } + } + + /** + * A slow, bankrun test friendly version of sync(), uses getAccountInfo on every cached account to refresh data + * @returns + */ + public async slowSync(): Promise { + if (this.fetchPromise) { + return this.fetchPromise; + } + for (const authority of this.authorityEscrowMap.keys()) { + const accountInfo = await this.driftClient.connection.getAccountInfo( + getBuilderEscrowAccountPublicKey( + this.driftClient.program.programId, + new PublicKey(authority) + ), + 'confirmed' + ); + const builderEscrowNew = + this.driftClient.program.account.builderEscrow.coder.accounts.decode( + 'BuilderEscrow', + accountInfo.data + ) as BuilderEscrow; + this.authorityEscrowMap.set(authority, builderEscrowNew); + } + } + + public async syncAll(): Promise { + const rpcRequestArgs = [ + this.driftClient.program.programId.toBase58(), + { + commitment: this.driftClient.opts.commitment, + filters: [getBuilderEscrowFilter()], + encoding: 'base64', + withContext: true, + }, + ]; + + const rpcJSONResponse: any = + // @ts-ignore + await this.driftClient.connection._rpcRequest( + 'getProgramAccounts', + rpcRequestArgs + ); + + const rpcResponseAndContext: RpcResponseAndContext< + Array<{ + pubkey: string; + account: { + data: [string, string]; + }; + }> + > = rpcJSONResponse.result; + + const batchSize = 100; + for (let i = 0; i < rpcResponseAndContext.value.length; i += batchSize) { + const batch = rpcResponseAndContext.value.slice(i, i + batchSize); + + if (this.parallelSync) { + await Promise.all( + batch.map(async (programAccount) => { + try { + // @ts-ignore + const buffer = Buffer.from( + programAccount.account.data[0], + programAccount.account.data[1] + ); + + const builderEscrow = + this.driftClient.program.account.builderEscrow.coder.accounts.decode( + 'BuilderEscrow', + buffer + ) as BuilderEscrow; + + // Extract authority from the account data + const authorityKey = builderEscrow.authority.toBase58(); + this.authorityEscrowMap.set(authorityKey, builderEscrow); + } catch (error) { + console.warn( + `Failed to decode BuilderEscrow account ${programAccount.pubkey}:`, + error + ); + } + }) + ); + } else { + for (const programAccount of batch) { + try { + // @ts-ignore + const buffer = Buffer.from( + programAccount.account.data[0], + programAccount.account.data[1] + ); + + const builderEscrow = + this.driftClient.program.account.builderEscrow.coder.accounts.decode( + 'BuilderEscrow', + buffer + ) as BuilderEscrow; + + // Extract authority from the account data + const authorityKey = builderEscrow.authority.toBase58(); + this.authorityEscrowMap.set(authorityKey, builderEscrow); + } catch (error) { + console.warn( + `Failed to decode BuilderEscrow account ${programAccount.pubkey}:`, + error + ); + } + } + } + + // Add a small delay between batches to avoid overwhelming the RPC + await new Promise((resolve) => setTimeout(resolve, 10)); + } + } + + /** + * Get all BuilderEscrow accounts + */ + public getAll(): Map { + return new Map(this.authorityEscrowMap); + } + + /** + * Get all authorities that have BuilderEscrow accounts + */ + public getAuthorities(): string[] { + return Array.from(this.authorityEscrowMap.keys()); + } + + /** + * Get BuilderEscrow accounts that have approved builders + */ + public getEscrowsWithApprovedBuilders(): Map { + const result = new Map(); + for (const [authority, escrow] of this.authorityEscrowMap) { + if (escrow.approvedBuilders && escrow.approvedBuilders.length > 0) { + result.set(authority, escrow); + } + } + return result; + } + + /** + * Get BuilderEscrow accounts that have active orders + */ + public getEscrowsWithOrders(): Map { + const result = new Map(); + for (const [authority, escrow] of this.authorityEscrowMap) { + if (escrow.orders && escrow.orders.length > 0) { + result.set(authority, escrow); + } + } + return result; + } + + /** + * Get BuilderEscrow account by referrer + */ + public getByReferrer(referrerPublicKey: string): BuilderEscrow | undefined { + for (const escrow of this.authorityEscrowMap.values()) { + if (escrow.referrer.toBase58() === referrerPublicKey) { + return escrow; + } + } + return undefined; + } + + /** + * Get all BuilderEscrow accounts for a specific referrer + */ + public getAllByReferrer(referrerPublicKey: string): BuilderEscrow[] { + const result: BuilderEscrow[] = []; + for (const escrow of this.authorityEscrowMap.values()) { + if (escrow.referrer.toBase58() === referrerPublicKey) { + result.push(escrow); + } + } + return result; + } + + public async unsubscribe() { + this.authorityEscrowMap.clear(); + } +} diff --git a/test-scripts/single-anchor-test.sh b/test-scripts/single-anchor-test.sh index b75e2f22a..8b0fc7f5b 100755 --- a/test-scripts/single-anchor-test.sh +++ b/test-scripts/single-anchor-test.sh @@ -6,7 +6,10 @@ fi export ANCHOR_WALLET=~/.config/solana/id.json -test_files=(admin.ts) +test_files=( + builderCodes.ts + # placeAndMakeSignedMsgBankrun.ts +) for test_file in ${test_files[@]}; do ts-mocha -t 300000 ./tests/${test_file} diff --git a/tests/builderCodes.ts b/tests/builderCodes.ts new file mode 100644 index 000000000..9a9ccad3f --- /dev/null +++ b/tests/builderCodes.ts @@ -0,0 +1,1363 @@ +import * as anchor from '@coral-xyz/anchor'; + +import { Program } from '@coral-xyz/anchor'; + +import { + AccountInfo, + Keypair, + LAMPORTS_PER_SOL, + PublicKey, +} from '@solana/web3.js'; + +import { + TestClient, + OracleSource, + PYTH_LAZER_STORAGE_ACCOUNT_KEY, + PTYH_LAZER_PROGRAM_ID, + assert, + getBuilderAccountPublicKey, + getBuilderEscrowAccountPublicKey, + BuilderAccount, + BuilderEscrow, + BASE_PRECISION, + BN, + PRICE_PRECISION, + getMarketOrderParams, + PositionDirection, + PostOnlyParams, + MarketType, + OrderParams, + SignedMsgOrderParamsWithBuilderMessage, + PEG_PRECISION, + ZERO, + isVariant, + hasBuilder, + parseLogs, + BuilderEscrowMap, + getTokenAmount, + BuilderSettleRecord, + getLimitOrderParams, +} from '../sdk/src'; + +import { + createUserWithUSDCAccount, + initializeQuoteSpotMarket, + mockOracleNoProgram, + mockUSDCMint, + mockUserUSDCAccount, + printTxLogs, +} from './testHelpers'; +import { startAnchor } from 'solana-bankrun'; +import { TestBulkAccountLoader } from '../sdk/src/accounts/testBulkAccountLoader'; +import { BankrunContextWrapper } from '../sdk/src/bankrun/bankrunConnection'; +import dotenv from 'dotenv'; +import { PYTH_STORAGE_DATA } from './pythLazerData'; +import { nanoid } from 'nanoid'; +import { + isBuilderOrderCompleted, + isBuilderOrderReferral, +} from '../sdk/src/math/builder'; + +dotenv.config(); + +const PYTH_STORAGE_ACCOUNT_INFO: AccountInfo = { + executable: false, + lamports: LAMPORTS_PER_SOL, + owner: new PublicKey(PTYH_LAZER_PROGRAM_ID), + rentEpoch: 0, + data: Buffer.from(PYTH_STORAGE_DATA, 'base64'), +}; + +describe('builder codes', () => { + const chProgram = anchor.workspace.Drift as Program; + + let usdcMint: Keypair; + + let builderClient: TestClient; + let builderUSDCAccount: Keypair = null; + + let makerClient: TestClient; + let makerUSDCAccount: PublicKey = null; + + let userUSDCAccount: PublicKey = null; + let userClient: TestClient; + + // user without BuilderEscrow + let user2USDCAccount: PublicKey = null; + let user2Client: TestClient; + + let builderEscrowMap: BuilderEscrowMap; + let bulkAccountLoader: TestBulkAccountLoader; + let bankrunContextWrapper: BankrunContextWrapper; + + let solUsd: PublicKey; + let marketIndexes; + let spotMarketIndexes; + let oracleInfos; + + const usdcAmount = new BN(10000 * 10 ** 6); + + before(async () => { + const context = await startAnchor( + '', + [], + [ + { + address: PYTH_LAZER_STORAGE_ACCOUNT_KEY, + info: PYTH_STORAGE_ACCOUNT_INFO, + }, + ] + ); + + // @ts-ignore + bankrunContextWrapper = new BankrunContextWrapper(context); + + bulkAccountLoader = new TestBulkAccountLoader( + bankrunContextWrapper.connection, + 'processed', + 1 + ); + + solUsd = await mockOracleNoProgram(bankrunContextWrapper, 224.3); + usdcMint = await mockUSDCMint(bankrunContextWrapper); + + marketIndexes = [0]; + spotMarketIndexes = [0, 1]; + oracleInfos = [{ publicKey: solUsd, source: OracleSource.PYTH }]; + + builderClient = new TestClient({ + connection: bankrunContextWrapper.connection.toConnection(), + wallet: bankrunContextWrapper.provider.wallet, + programID: chProgram.programId, + opts: { + commitment: 'confirmed', + }, + activeSubAccountId: 0, + perpMarketIndexes: marketIndexes, + spotMarketIndexes: spotMarketIndexes, + subAccountIds: [], + oracleInfos, + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + }); + await builderClient.initialize(usdcMint.publicKey, true); + await builderClient.subscribe(); + + await builderClient.updateFeatureBitFlagsBuilderReferral(true); + + await initializeQuoteSpotMarket(builderClient, usdcMint.publicKey); + + const periodicity = new BN(0); + await builderClient.initializePerpMarket( + 0, + solUsd, + new BN(10 * 10 ** 13).mul(new BN(Math.sqrt(PRICE_PRECISION.toNumber()))), + new BN(10 * 10 ** 13).mul(new BN(Math.sqrt(PRICE_PRECISION.toNumber()))), + periodicity, + new BN(224 * PEG_PRECISION.toNumber()) + ); + builderUSDCAccount = await mockUserUSDCAccount( + usdcMint, + usdcAmount.add(new BN(1000000000)), + bankrunContextWrapper, + builderClient.wallet.publicKey + ); + await builderClient.initializeUserAccountAndDepositCollateral( + usdcAmount, + builderUSDCAccount.publicKey + ); + // await builderClient.depositIntoSpotMarketRevenuePool(0, new BN(1000000000), builderUSDCAccount.publicKey); + await builderClient.depositIntoPerpMarketFeePool( + 0, + new BN(1000000000), + builderUSDCAccount.publicKey + ); + + [userClient, userUSDCAccount] = await createUserWithUSDCAccount( + bankrunContextWrapper, + usdcMint, + chProgram, + usdcAmount, + marketIndexes, + spotMarketIndexes, + oracleInfos, + bulkAccountLoader, + { + referrer: await builderClient.getUserAccountPublicKey(), + referrerStats: builderClient.getUserStatsAccountPublicKey(), + } + ); + await userClient.deposit( + usdcAmount, + 0, + userUSDCAccount, + undefined, + false, + undefined, + true + ); + + [user2Client, user2USDCAccount] = await createUserWithUSDCAccount( + bankrunContextWrapper, + usdcMint, + chProgram, + usdcAmount, + marketIndexes, + spotMarketIndexes, + oracleInfos, + bulkAccountLoader, + { + referrer: await builderClient.getUserAccountPublicKey(), + referrerStats: builderClient.getUserStatsAccountPublicKey(), + } + ); + await user2Client.deposit( + usdcAmount, + 0, + user2USDCAccount, + undefined, + false, + undefined, + true + ); + + [makerClient, makerUSDCAccount] = await createUserWithUSDCAccount( + bankrunContextWrapper, + usdcMint, + chProgram, + usdcAmount, + marketIndexes, + spotMarketIndexes, + oracleInfos, + bulkAccountLoader + ); + await makerClient.deposit( + usdcAmount, + 0, + makerUSDCAccount, + undefined, + false, + undefined, + true + ); + + builderEscrowMap = new BuilderEscrowMap(userClient, false); + }); + + after(async () => { + await builderClient.unsubscribe(); + await userClient.unsubscribe(); + await user2Client.unsubscribe(); + await makerClient.unsubscribe(); + }); + + it('builder can create builder', async () => { + await builderClient.initializeBuilder(builderClient.wallet.publicKey); + + const builderAccountInfo = + await bankrunContextWrapper.connection.getAccountInfo( + getBuilderAccountPublicKey( + builderClient.program.programId, + builderClient.wallet.publicKey + ) + ); + + const builderAcc: BuilderAccount = + builderClient.program.account.builder.coder.accounts.decodeUnchecked( + 'Builder', + builderAccountInfo.data + ); + assert( + builderAcc.authority.toBase58() === + builderClient.wallet.publicKey.toBase58() + ); + assert(builderAcc.totalBuilderRewards.toNumber() === 0); + assert(builderAcc.totalReferrerRewards.toNumber() === 0); + }); + + it('user can initialize a BuilderEscrow', async () => { + const numOrders = 2; + + // Test the instruction creation + const ix = await userClient.getInitializeBuilderEscrowIx( + userClient.wallet.publicKey, + numOrders + ); + + assert(ix !== null, 'Instruction should be created'); + assert(ix.programId.toBase58() === userClient.program.programId.toBase58()); + + // Test the full transaction + await userClient.initializeBuilderEscrow( + userClient.wallet.publicKey, + numOrders + ); + + const accountInfo = await bankrunContextWrapper.connection.getAccountInfo( + getBuilderEscrowAccountPublicKey( + userClient.program.programId, + userClient.wallet.publicKey + ) + ); + + assert(accountInfo !== null, 'BuilderEscrow account should exist'); + assert( + accountInfo.owner.toBase58() === userClient.program.programId.toBase58() + ); + + const revShareEscrow: BuilderEscrow = + builderClient.program.coder.accounts.decodeUnchecked( + 'BuilderEscrow', + accountInfo.data + ); + assert( + revShareEscrow.authority.toBase58() === + userClient.wallet.publicKey.toBase58() + ); + assert( + revShareEscrow.referrer.toBase58() === + builderClient.wallet.publicKey.toBase58() + ); + assert(revShareEscrow.orders.length === numOrders); + assert(revShareEscrow.approvedBuilders.length === 0); + }); + + it('user can resize BuilderEscrow account', async () => { + const newNumOrders = 10; + + // Test the instruction creation + const ix = await userClient.getResizeBuilderEscrowOrdersIx( + userClient.wallet.publicKey, + newNumOrders + ); + + assert(ix !== null, 'Instruction should be created'); + assert(ix.programId.toBase58() === userClient.program.programId.toBase58()); + + // Test the full transaction + await userClient.resizeBuilderEscrowOrders( + userClient.wallet.publicKey, + newNumOrders + ); + + const accountInfo = await bankrunContextWrapper.connection.getAccountInfo( + getBuilderEscrowAccountPublicKey( + userClient.program.programId, + userClient.wallet.publicKey + ) + ); + + assert( + accountInfo !== null, + 'BuilderEscrow account should exist after resize' + ); + assert( + accountInfo.owner.toBase58() === userClient.program.programId.toBase58() + ); + + const revShareEscrow: BuilderEscrow = + builderClient.program.coder.accounts.decodeUnchecked( + 'BuilderEscrow', + accountInfo.data + ); + assert( + revShareEscrow.authority.toBase58() === + userClient.wallet.publicKey.toBase58() + ); + assert( + revShareEscrow.referrer.toBase58() === + builderClient.wallet.publicKey.toBase58() + ); + assert(revShareEscrow.orders.length === newNumOrders); + }); + + it('user can add/update/remove approved builder from BuilderEscrow', async () => { + const builder = builderClient.wallet; + const maxFeeBps = 150; // 1.5% + + // First add a builder + await userClient.changeApprovedBuilder( + userClient.wallet.publicKey, + builder.publicKey, + maxFeeBps, + true // add + ); + + // Verify the builder was added + let accountInfo = await bankrunContextWrapper.connection.getAccountInfo( + getBuilderEscrowAccountPublicKey( + userClient.program.programId, + userClient.wallet.publicKey + ) + ); + + let revShareEscrow: BuilderEscrow = + userClient.program.coder.accounts.decodeUnchecked( + 'BuilderEscrow', + accountInfo.data + ); + const addedBuilder = revShareEscrow.approvedBuilders.find( + (b) => b.authority.toBase58() === builder.publicKey.toBase58() + ); + assert( + addedBuilder !== undefined, + 'Builder should be in approved builders list before removal' + ); + assert( + revShareEscrow.approvedBuilders.length === 1, + 'Approved builders list should contain 1 builder' + ); + assert( + addedBuilder.maxFeeBps === maxFeeBps, + 'Builder should have correct max fee bps before removal' + ); + + // update the user fee + await userClient.changeApprovedBuilder( + userClient.wallet.publicKey, + builder.publicKey, + maxFeeBps * 5, + true // update existing builder + ); + + // Verify the builder was updated + accountInfo = await bankrunContextWrapper.connection.getAccountInfo( + getBuilderEscrowAccountPublicKey( + userClient.program.programId, + userClient.wallet.publicKey + ) + ); + + revShareEscrow = userClient.program.coder.accounts.decodeUnchecked( + 'BuilderEscrow', + accountInfo.data + ); + const updatedBuilder = revShareEscrow.approvedBuilders.find( + (b) => b.authority.toBase58() === builder.publicKey.toBase58() + ); + assert( + updatedBuilder !== undefined, + 'Builder should be in approved builders list after update' + ); + assert( + updatedBuilder.maxFeeBps === maxFeeBps * 5, + 'Builder should have correct max fee bps after update' + ); + + // Now remove the builder + await userClient.changeApprovedBuilder( + userClient.wallet.publicKey, + builder.publicKey, + maxFeeBps, + false // remove + ); + + // Verify the builder was removed + accountInfo = await bankrunContextWrapper.connection.getAccountInfo( + getBuilderEscrowAccountPublicKey( + userClient.program.programId, + userClient.wallet.publicKey + ) + ); + + revShareEscrow = userClient.program.coder.accounts.decodeUnchecked( + 'BuilderEscrow', + accountInfo.data + ); + const removedBuilder = revShareEscrow.approvedBuilders.find( + (b) => b.authority.toBase58() === builder.publicKey.toBase58() + ); + assert( + removedBuilder.maxFeeBps === 0, + 'Builder should have 0 max fee bps after removal' + ); + }); + + it('user with no BuilderEscrow can place and fill order with no builder', async () => { + const slot = new BN( + await bankrunContextWrapper.connection.toConnection().getSlot() + ); + + const marketIndex = 0; + const baseAssetAmount = BASE_PRECISION; + const takerOrderParams = getMarketOrderParams({ + marketIndex, + direction: PositionDirection.LONG, + baseAssetAmount: baseAssetAmount.muln(2), + price: new BN(230).mul(PRICE_PRECISION), + auctionStartPrice: new BN(226).mul(PRICE_PRECISION), + auctionEndPrice: new BN(230).mul(PRICE_PRECISION), + auctionDuration: 10, + userOrderId: 1, + postOnly: PostOnlyParams.NONE, + marketType: MarketType.PERP, + }) as OrderParams; + const uuid = Uint8Array.from(Buffer.from(nanoid(8))); + + let userOrders = user2Client.getUser().getOpenOrders(); + assert(userOrders.length === 0); + + const takerOrderParamsMessage: SignedMsgOrderParamsWithBuilderMessage = { + signedMsgOrderParams: takerOrderParams, + subAccountId: 0, + slot, + uuid, + takeProfitOrderParams: { + triggerPrice: new BN(235).mul(PRICE_PRECISION), + baseAssetAmount: takerOrderParams.baseAssetAmount, + }, + stopLossOrderParams: { + triggerPrice: new BN(220).mul(PRICE_PRECISION), + baseAssetAmount: takerOrderParams.baseAssetAmount, + }, + builderIdx: null, + builderFee: null, + }; + + const signedOrderParams = user2Client.signSignedMsgOrderParamsMessage( + takerOrderParamsMessage, + false, + true + ); + + await builderClient.placeSignedMsgTakerOrder( + signedOrderParams, + marketIndex, + { + taker: await user2Client.getUserAccountPublicKey(), + takerUserAccount: user2Client.getUserAccount(), + takerStats: user2Client.getUserStatsAccountPublicKey(), + signingAuthority: user2Client.wallet.publicKey, + }, + undefined, + 2 + ); + + await user2Client.fetchAccounts(); + + userOrders = user2Client.getUser().getOpenOrders(); + assert(userOrders.length === 3); + assert(userOrders[0].orderId === 1); + assert(userOrders[0].reduceOnly === true); + assert(hasBuilder(userOrders[0]) === false); + assert(userOrders[1].orderId === 2); + assert(userOrders[1].reduceOnly === true); + assert(hasBuilder(userOrders[1]) === false); + assert(userOrders[2].orderId === 3); + assert(userOrders[2].reduceOnly === false); + assert(hasBuilder(userOrders[2]) === false); + + await user2Client.fetchAccounts(); + + // fill order with vamm + await builderClient.fetchAccounts(); + const fillTx = await makerClient.fillPerpOrder( + await user2Client.getUserAccountPublicKey(), + user2Client.getUserAccount(), + { + marketIndex, + orderId: 3, + }, + undefined, + { + referrer: await builderClient.getUserAccountPublicKey(), + referrerStats: builderClient.getUserStatsAccountPublicKey(), + }, + undefined, + undefined, + undefined, + true + ); + const logs = await printTxLogs( + bankrunContextWrapper.connection.toConnection(), + fillTx + ); + const events = parseLogs(builderClient.program, logs); + assert(events[0].name === 'OrderActionRecord'); + const fillQuoteAssetAmount = events[0].data['quoteAssetAmountFilled'] as BN; + const builderFee = events[0].data['builderFee']; + const takerFee = events[0].data['takerFee'] as BN; + const totalFeePaid = takerFee; + const referrerReward = new BN(events[0].data['referrerReward'] as number); + assert(builderFee == null); + assert(referrerReward.gt(ZERO)); + + await user2Client.fetchAccounts(); + userOrders = user2Client.getUser().getOpenOrders(); + assert(userOrders.length === 2); + + await bankrunContextWrapper.moveTimeForward(100); + + // cancel remaining orders + await user2Client.cancelOrders(); + await user2Client.fetchAccounts(); + + userOrders = user2Client.getUser().getOpenOrders(); + assert(userOrders.length === 0); + + const perpPos = user2Client.getUser().getPerpPosition(0); + assert( + perpPos.quoteAssetAmount.eq(fillQuoteAssetAmount.add(totalFeePaid).neg()) + ); + + await builderClient.fetchAccounts(); + let usdcPos = builderClient.getSpotPosition(0); + const builderUsdcBeforeSettle = getTokenAmount( + usdcPos.scaledBalance, + builderClient.getSpotMarketAccount(0), + usdcPos.balanceType + ); + + await builderClient.fetchAccounts(); + usdcPos = builderClient.getSpotPosition(0); + const builderUsdcAfterSettle = getTokenAmount( + usdcPos.scaledBalance, + builderClient.getSpotMarketAccount(0), + usdcPos.balanceType + ); + assert(builderUsdcAfterSettle.eq(builderUsdcBeforeSettle)); + }); + + it('user can place and fill order with builder', async () => { + const slot = new BN( + await bankrunContextWrapper.connection.toConnection().getSlot() + ); + + // approve builder again + const builder = builderClient.wallet; + const maxFeeBps = 150; // 1.5% + await userClient.changeApprovedBuilder( + userClient.wallet.publicKey, + builder.publicKey, + maxFeeBps, + true // update existing builder + ); + + const marketIndex = 0; + const baseAssetAmount = BASE_PRECISION; + const takerOrderParams = getMarketOrderParams({ + marketIndex, + direction: PositionDirection.LONG, + baseAssetAmount: baseAssetAmount.muln(2), + price: new BN(230).mul(PRICE_PRECISION), + auctionStartPrice: new BN(226).mul(PRICE_PRECISION), + auctionEndPrice: new BN(230).mul(PRICE_PRECISION), + auctionDuration: 10, + userOrderId: 1, + postOnly: PostOnlyParams.NONE, + marketType: MarketType.PERP, + }) as OrderParams; + const uuid = Uint8Array.from(Buffer.from(nanoid(8))); + + // Should fail if we try first without encoding properly + + let userOrders = userClient.getUser().getOpenOrders(); + assert(userOrders.length === 0); + + const builderFeeBps = 7; + const takerOrderParamsMessage: SignedMsgOrderParamsWithBuilderMessage = { + signedMsgOrderParams: takerOrderParams, + subAccountId: 0, + slot, + uuid, + takeProfitOrderParams: { + triggerPrice: new BN(235).mul(PRICE_PRECISION), + baseAssetAmount: takerOrderParams.baseAssetAmount, + }, + stopLossOrderParams: { + triggerPrice: new BN(220).mul(PRICE_PRECISION), + baseAssetAmount: takerOrderParams.baseAssetAmount, + }, + builderIdx: 0, + builderFee: builderFeeBps, + }; + + const signedOrderParams = userClient.signSignedMsgOrderParamsMessage( + takerOrderParamsMessage, + false, + true + ); + + await builderClient.placeSignedMsgTakerOrder( + signedOrderParams, + marketIndex, + { + taker: await userClient.getUserAccountPublicKey(), + takerUserAccount: userClient.getUserAccount(), + takerStats: userClient.getUserStatsAccountPublicKey(), + signingAuthority: userClient.wallet.publicKey, + }, + undefined, + 2 + ); + + await userClient.fetchAccounts(); + + // try to revoke builder with open orders + try { + await userClient.changeApprovedBuilder( + userClient.wallet.publicKey, + builder.publicKey, + 0, + false // remove + ); + assert( + false, + 'should throw error when revoking builder with open orders' + ); + } catch (e) { + assert(e.message.includes('0x18b6')); + } + + userOrders = userClient.getUser().getOpenOrders(); + assert(userOrders.length === 3); + assert(userOrders[0].orderId === 1); + assert(userOrders[0].reduceOnly === true); + assert(hasBuilder(userOrders[0]) === true); + assert(userOrders[1].orderId === 2); + assert(userOrders[1].reduceOnly === true); + assert(hasBuilder(userOrders[1]) === true); + assert(userOrders[2].orderId === 3); + assert(userOrders[2].reduceOnly === false); + assert(hasBuilder(userOrders[2]) === true); + + await builderEscrowMap.slowSync(); + let builderEscrow = (await builderEscrowMap.mustGet( + userClient.wallet.publicKey.toBase58() + )) as BuilderEscrow; + + // check the corresponding revShareEscrow orders are added + for (let i = 0; i < userOrders.length; i++) { + assert(builderEscrow.orders[i]!.builderIdx === 0); + assert(builderEscrow.orders[i]!.feesAccrued.eq(ZERO)); + assert(builderEscrow.orders[i]!.feeBps === builderFeeBps); + assert( + builderEscrow.orders[i]!.orderId === i + 1, + `orderId ${i} is ${builderEscrow.orders[i]!.orderId}` + ); + assert(isVariant(builderEscrow.orders[i]!.marketType, 'perp')); + assert(builderEscrow.orders[i]!.marketIndex === marketIndex); + } + + assert( + builderEscrow.approvedBuilders[0]!.authority.equals(builder.publicKey) + ); + assert(builderEscrow.approvedBuilders[0]!.maxFeeBps === maxFeeBps); + + await userClient.fetchAccounts(); + + // fill order with vamm + await builderClient.fetchAccounts(); + const fillTx = await makerClient.fillPerpOrder( + await userClient.getUserAccountPublicKey(), + userClient.getUserAccount(), + { + marketIndex, + orderId: 3, + }, + undefined, + { + referrer: await builderClient.getUserAccountPublicKey(), + referrerStats: builderClient.getUserStatsAccountPublicKey(), + }, + undefined, + undefined, + undefined, + true + ); + const logs = await printTxLogs( + bankrunContextWrapper.connection.toConnection(), + fillTx + ); + const events = parseLogs(builderClient.program, logs); + assert(events[0].name === 'OrderActionRecord'); + const fillQuoteAssetAmount = events[0].data['quoteAssetAmountFilled'] as BN; + const builderFee = events[0].data['builderFee'] as BN; + const takerFee = events[0].data['takerFee'] as BN; + const totalFeePaid = takerFee.add(builderFee); + const referrerReward = events[0].data['referrerReward'] as number; + assert(builderFee.eq(fillQuoteAssetAmount.muln(builderFeeBps).divn(10000))); + + await userClient.fetchAccounts(); + userOrders = userClient.getUser().getOpenOrders(); + assert(userOrders.length === 2); + + await bankrunContextWrapper.moveTimeForward(100); + + await builderEscrowMap.slowSync(); + builderEscrow = (await builderEscrowMap.mustGet( + userClient.wallet.publicKey.toBase58() + )) as BuilderEscrow; + assert(builderEscrow.orders[2].orderId === 3); + assert(builderEscrow.orders[2].feesAccrued.gt(ZERO)); + assert(isBuilderOrderCompleted(builderEscrow.orders[2])); + + // cancel remaining orders + await userClient.cancelOrders(); + await userClient.fetchAccounts(); + + userOrders = userClient.getUser().getOpenOrders(); + assert(userOrders.length === 0); + + const perpPos = userClient.getUser().getPerpPosition(0); + assert( + perpPos.quoteAssetAmount.eq(fillQuoteAssetAmount.add(totalFeePaid).neg()) + ); + + await builderEscrowMap.slowSync(); + builderEscrow = (await builderEscrowMap.mustGet( + userClient.wallet.publicKey.toBase58() + )) as BuilderEscrow; + assert(builderEscrow.orders[2].bitFlags === 3); + assert(builderEscrow.orders[2].feesAccrued.eq(builderFee)); + + await builderClient.fetchAccounts(); + let usdcPos = builderClient.getSpotPosition(0); + const builderUsdcBeforeSettle = getTokenAmount( + usdcPos.scaledBalance, + builderClient.getSpotMarketAccount(0), + usdcPos.balanceType + ); + + const settleTx = await builderClient.settlePNL( + await userClient.getUserAccountPublicKey(), + userClient.getUserAccount(), + marketIndex, + undefined, + undefined, + builderEscrowMap + ); + + const settleLogs = await printTxLogs( + bankrunContextWrapper.connection.toConnection(), + settleTx + ); + const settleEvents = parseLogs(builderClient.program, settleLogs); + const builderSettleEvents = settleEvents + .filter((e) => e.name === 'BuilderSettleRecord') + .map((e) => e.data) as BuilderSettleRecord[]; + assert(builderSettleEvents.length === 2); + assert(builderSettleEvents[0].builder.equals(builder.publicKey)); + assert(builderSettleEvents[0].referrer == null); + assert(builderSettleEvents[0].feeSettled.eq(builderFee)); + assert(builderSettleEvents[0].marketIndex === marketIndex); + assert(isVariant(builderSettleEvents[0].marketType, 'perp')); + assert(builderSettleEvents[0].builderTotalReferrerRewards.eq(ZERO)); + assert(builderSettleEvents[0].builderTotalBuilderRewards.eq(builderFee)); + + assert(builderSettleEvents[1].builder === null); + assert(builderSettleEvents[1].referrer.equals(builder.publicKey)); + assert(builderSettleEvents[1].feeSettled.eq(new BN(referrerReward))); + assert(builderSettleEvents[1].marketIndex === marketIndex); + assert(isVariant(builderSettleEvents[1].marketType, 'spot')); + assert( + builderSettleEvents[1].builderTotalReferrerRewards.eq( + new BN(referrerReward) + ) + ); + assert(builderSettleEvents[1].builderTotalBuilderRewards.eq(builderFee)); + + await builderEscrowMap.slowSync(); + builderEscrow = (await builderEscrowMap.mustGet( + userClient.wallet.publicKey.toBase58() + )) as BuilderEscrow; + for (const order of builderEscrow.orders) { + assert(order.feesAccrued.eq(ZERO)); + } + + await builderClient.fetchAccounts(); + usdcPos = builderClient.getSpotPosition(0); + const builderUsdcAfterSettle = getTokenAmount( + usdcPos.scaledBalance, + builderClient.getSpotMarketAccount(0), + usdcPos.balanceType + ); + assert( + builderUsdcAfterSettle + .sub(builderUsdcBeforeSettle) + .sub(new BN(referrerReward)) + .eq(builderFee) + ); + }); + + it('user can place and cancel with no fill (no fees accrued, escrow unchanged)', async () => { + const builder = builderClient.wallet; + const maxFeeBps = 150; + await userClient.changeApprovedBuilder( + userClient.wallet.publicKey, + builder.publicKey, + maxFeeBps, + true + ); + + await builderEscrowMap.slowSync(); + const beforeEscrow = (await builderEscrowMap.mustGet( + userClient.wallet.publicKey.toBase58() + )) as BuilderEscrow; + const beforeTotalFees = beforeEscrow.orders.reduce( + (sum, o) => sum.add(o.feesAccrued ?? ZERO), + ZERO + ); + + const marketIndex = 0; + const baseAssetAmount = BASE_PRECISION; + const orderParams = getMarketOrderParams({ + marketIndex, + direction: PositionDirection.LONG, + baseAssetAmount, + price: new BN(230).mul(PRICE_PRECISION), + auctionStartPrice: new BN(226).mul(PRICE_PRECISION), + auctionEndPrice: new BN(230).mul(PRICE_PRECISION), + auctionDuration: 10, + userOrderId: 7, + postOnly: PostOnlyParams.NONE, + marketType: MarketType.PERP, + }) as OrderParams; + const slot = new BN( + await bankrunContextWrapper.connection.toConnection().getSlot() + ); + const uuid = Uint8Array.from(Buffer.from(nanoid(8))); + const builderFeeBps = 5; + const msg: SignedMsgOrderParamsWithBuilderMessage = { + signedMsgOrderParams: orderParams, + subAccountId: 0, + slot, + uuid, + takeProfitOrderParams: null, + stopLossOrderParams: null, + builderIdx: 0, + builderFee: builderFeeBps, + }; + + const signed = userClient.signSignedMsgOrderParamsMessage(msg, false, true); + await builderClient.placeSignedMsgTakerOrder( + signed, + marketIndex, + { + taker: await userClient.getUserAccountPublicKey(), + takerUserAccount: userClient.getUserAccount(), + takerStats: userClient.getUserStatsAccountPublicKey(), + signingAuthority: userClient.wallet.publicKey, + }, + undefined, + 2 + ); + + await userClient.cancelOrders(); + await userClient.fetchAccounts(); + assert(userClient.getUser().getOpenOrders().length === 0); + + await builderEscrowMap.slowSync(); + const afterEscrow = (await builderEscrowMap.mustGet( + userClient.wallet.publicKey.toBase58() + )) as BuilderEscrow; + const afterTotalFees = afterEscrow.orders.reduce( + (sum, o) => sum.add(o.feesAccrued ?? ZERO), + ZERO + ); + assert(afterTotalFees.eq(beforeTotalFees)); + }); + + it('user can place and fill multiple orders (fees accumulate and settle)', async () => { + const builder = builderClient.wallet; + const maxFeeBps = 150; + await userClient.changeApprovedBuilder( + userClient.wallet.publicKey, + builder.publicKey, + maxFeeBps, + true + ); + + const marketIndex = 0; + const baseAssetAmount = BASE_PRECISION; + + function buildMsg(userOrderId: number, feeBps: number, slot: BN) { + const params = getMarketOrderParams({ + marketIndex, + direction: PositionDirection.LONG, + baseAssetAmount, + price: new BN(230).mul(PRICE_PRECISION), + auctionStartPrice: new BN(226).mul(PRICE_PRECISION), + auctionEndPrice: new BN(230).mul(PRICE_PRECISION), + auctionDuration: 10, + userOrderId, + postOnly: PostOnlyParams.NONE, + marketType: MarketType.PERP, + }) as OrderParams; + return { + signedMsgOrderParams: params, + subAccountId: 0, + slot, + uuid: Uint8Array.from(Buffer.from(nanoid(8))), + builderIdx: 0, + builderFee: feeBps, + takeProfitOrderParams: null, + stopLossOrderParams: null, + } as SignedMsgOrderParamsWithBuilderMessage; + } + + await builderEscrowMap.slowSync(); + const escrowStart = (await builderEscrowMap.mustGet( + userClient.wallet.publicKey.toBase58() + )) as BuilderEscrow; + const totalFeesInEscrowStart = escrowStart.orders.reduce( + (sum, o) => sum.add(o.feesAccrued ?? ZERO), + ZERO + ); + + const slot = new BN( + await bankrunContextWrapper.connection.toConnection().getSlot() + ); + const feeBpsA = 6; + const feeBpsB = 9; + + const signedA = userClient.signSignedMsgOrderParamsMessage( + buildMsg(10, feeBpsA, slot), + false, + true + ); + await builderClient.placeSignedMsgTakerOrder( + signedA, + marketIndex, + { + taker: await userClient.getUserAccountPublicKey(), + takerUserAccount: userClient.getUserAccount(), + takerStats: userClient.getUserStatsAccountPublicKey(), + signingAuthority: userClient.wallet.publicKey, + }, + undefined, + 2 + ); + await userClient.fetchAccounts(); + + const signedB = userClient.signSignedMsgOrderParamsMessage( + buildMsg(11, feeBpsB, slot), + false, + true + ); + await builderClient.placeSignedMsgTakerOrder( + signedB, + marketIndex, + { + taker: await userClient.getUserAccountPublicKey(), + takerUserAccount: userClient.getUserAccount(), + takerStats: userClient.getUserStatsAccountPublicKey(), + signingAuthority: userClient.wallet.publicKey, + }, + undefined, + 2 + ); + await userClient.fetchAccounts(); + + const userOrders = userClient.getUser().getOpenOrders(); + assert(userOrders.length === 2); + + // Fill both orders + const fillTxA = await makerClient.fillPerpOrder( + await userClient.getUserAccountPublicKey(), + userClient.getUserAccount(), + { marketIndex, orderId: userOrders[0].orderId }, + undefined, + { + referrer: await builderClient.getUserAccountPublicKey(), + referrerStats: builderClient.getUserStatsAccountPublicKey(), + }, + undefined, + undefined, + undefined, + true + ); + const logsA = await printTxLogs( + bankrunContextWrapper.connection.toConnection(), + fillTxA + ); + const eventsA = parseLogs(builderClient.program, logsA); + const fillEventA = eventsA.find((e) => e.name === 'OrderActionRecord'); + assert(fillEventA !== undefined); + const builderFeeA = fillEventA.data['builderFee'] as BN; + const referrerRewardA = new BN(fillEventA.data['referrerReward'] as number); + + const fillTxB = await makerClient.fillPerpOrder( + await userClient.getUserAccountPublicKey(), + userClient.getUserAccount(), + { marketIndex, orderId: userOrders[1].orderId }, + undefined, + { + referrer: await builderClient.getUserAccountPublicKey(), + referrerStats: builderClient.getUserStatsAccountPublicKey(), + }, + undefined, + undefined, + undefined, + true + ); + const logsB = await printTxLogs( + bankrunContextWrapper.connection.toConnection(), + fillTxB + ); + const eventsB = parseLogs(builderClient.program, logsB); + const fillEventB = eventsB.find((e) => e.name === 'OrderActionRecord'); + assert(fillEventB !== undefined); + const builderFeeB = fillEventB.data['builderFee'] as BN; + const referrerRewardB = new BN(fillEventB.data['referrerReward'] as number); + + await bankrunContextWrapper.moveTimeForward(100); + + await builderEscrowMap.slowSync(); + const escrowAfterFills = (await builderEscrowMap.mustGet( + userClient.wallet.publicKey.toBase58() + )) as BuilderEscrow; + const totalFeesAccrued = escrowAfterFills.orders.reduce( + (sum, o) => sum.add(o.feesAccrued ?? ZERO), + ZERO + ); + const expectedTotal = builderFeeA + .add(builderFeeB) + .add(referrerRewardA) + .add(referrerRewardB); + assert( + totalFeesAccrued.sub(totalFeesInEscrowStart).eq(expectedTotal), + `totalFeesAccrued: ${totalFeesAccrued.toString()}, expectedTotal: ${expectedTotal.toString()}` + ); + + // Settle and verify fees swept to builder + await builderClient.fetchAccounts(); + let usdcPos = builderClient.getSpotPosition(0); + const builderUsdcBefore = getTokenAmount( + usdcPos.scaledBalance, + builderClient.getSpotMarketAccount(0), + usdcPos.balanceType + ); + + await builderClient.settlePNL( + await userClient.getUserAccountPublicKey(), + userClient.getUserAccount(), + marketIndex, + undefined, + undefined, + builderEscrowMap + ); + + await builderEscrowMap.slowSync(); + const escrowAfterSettle = (await builderEscrowMap.mustGet( + userClient.wallet.publicKey.toBase58() + )) as BuilderEscrow; + for (const order of escrowAfterSettle.orders) { + assert(order.feesAccrued.eq(ZERO)); + } + + await builderClient.fetchAccounts(); + usdcPos = builderClient.getSpotPosition(0); + const builderUsdcAfter = getTokenAmount( + usdcPos.scaledBalance, + builderClient.getSpotMarketAccount(0), + usdcPos.balanceType + ); + const usdcDiff = builderUsdcAfter.sub(builderUsdcBefore); + assert( + usdcDiff.eq(expectedTotal), + `usdcDiff: ${usdcDiff.toString()}, expectedTotal: ${expectedTotal.toString()}` + ); + }); + + it('user can place and fill with multiple maker orders', async () => { + const builder = builderClient.wallet; + const maxFeeBps = 150; + await userClient.changeApprovedBuilder( + userClient.wallet.publicKey, + builder.publicKey, + maxFeeBps, + true + ); + + const marketIndex = 0; + const baseAssetAmount = BASE_PRECISION; + + function buildMsg(userOrderId: number, feeBps: number, slot: BN) { + const params = getMarketOrderParams({ + marketIndex, + direction: PositionDirection.LONG, + baseAssetAmount, + price: new BN(230).mul(PRICE_PRECISION), + auctionStartPrice: new BN(226).mul(PRICE_PRECISION), + auctionEndPrice: new BN(230).mul(PRICE_PRECISION), + auctionDuration: 10, + userOrderId, + postOnly: PostOnlyParams.NONE, + marketType: MarketType.PERP, + }) as OrderParams; + return { + signedMsgOrderParams: params, + subAccountId: 0, + slot, + uuid: Uint8Array.from(Buffer.from(nanoid(8))), + builderIdx: 0, + builderFee: feeBps, + takeProfitOrderParams: null, + stopLossOrderParams: null, + } as SignedMsgOrderParamsWithBuilderMessage; + } + + // place maker orders + await makerClient.placeOrders([ + getLimitOrderParams({ + marketIndex: 0, + baseAssetAmount: baseAssetAmount.divn(3), + direction: PositionDirection.SHORT, + price: new BN(223000000), + marketType: MarketType.PERP, + postOnly: PostOnlyParams.SLIDE, + }) as OrderParams, + getLimitOrderParams({ + marketIndex: 0, + baseAssetAmount: baseAssetAmount.divn(3), + direction: PositionDirection.SHORT, + price: new BN(223500000), + marketType: MarketType.PERP, + postOnly: PostOnlyParams.SLIDE, + }) as OrderParams, + ]); + await makerClient.fetchAccounts(); + const makerOrders = makerClient.getUser().getOpenOrders(); + assert(makerOrders.length === 2); + + const slot = new BN( + await bankrunContextWrapper.connection.toConnection().getSlot() + ); + const feeBpsA = 6; + + const signedA = userClient.signSignedMsgOrderParamsMessage( + buildMsg(10, feeBpsA, slot), + false, + true + ); + await builderClient.placeSignedMsgTakerOrder( + signedA, + marketIndex, + { + taker: await userClient.getUserAccountPublicKey(), + takerUserAccount: userClient.getUserAccount(), + takerStats: userClient.getUserStatsAccountPublicKey(), + signingAuthority: userClient.wallet.publicKey, + }, + undefined, + 2 + ); + await userClient.fetchAccounts(); + + const userOrders = userClient.getUser().getOpenOrders(); + assert(userOrders.length === 1); + + // Fill taker against maker orders + const fillTxA = await makerClient.fillPerpOrder( + await userClient.getUserAccountPublicKey(), + userClient.getUserAccount(), + { marketIndex, orderId: userOrders[0].orderId }, + { + maker: await makerClient.getUserAccountPublicKey(), + makerStats: makerClient.getUserStatsAccountPublicKey(), + makerUserAccount: makerClient.getUserAccount(), + // order?: Order; + }, + { + referrer: await builderClient.getUserAccountPublicKey(), + referrerStats: builderClient.getUserStatsAccountPublicKey(), + }, + undefined, + undefined, + undefined, + true + ); + const logsA = await printTxLogs( + bankrunContextWrapper.connection.toConnection(), + fillTxA + ); + const eventsA = parseLogs(builderClient.program, logsA); + const fillEventA = eventsA.filter((e) => e.name === 'OrderActionRecord'); + assert(fillEventA !== undefined); + const builderFeeA = fillEventA.reduce( + (sum, e) => sum.add(e.data['builderFee'] as BN), + ZERO + ); + const referrerRewardA = fillEventA.reduce( + (sum, e) => sum.add(new BN(e.data['referrerReward'] as number)), + ZERO + ); + + await bankrunContextWrapper.moveTimeForward(100); + + await builderEscrowMap.slowSync(); + const escrowAfterFills = (await builderEscrowMap.mustGet( + userClient.wallet.publicKey.toBase58() + )) as BuilderEscrow; + const totalFeesAccrued = escrowAfterFills.orders + .filter((o) => !isBuilderOrderReferral(o)) + .reduce((sum, o) => sum.add(o.feesAccrued ?? ZERO), ZERO); + assert( + totalFeesAccrued.eq(builderFeeA), + `totalFeesAccrued: ${totalFeesAccrued.toString()}, builderFeeA: ${builderFeeA.toString()}` + ); + + // Settle and verify fees swept to builder + await builderClient.fetchAccounts(); + let usdcPos = builderClient.getSpotPosition(0); + const builderUsdcBefore = getTokenAmount( + usdcPos.scaledBalance, + builderClient.getSpotMarketAccount(0), + usdcPos.balanceType + ); + + const settleTx = await builderClient.settlePNL( + await userClient.getUserAccountPublicKey(), + userClient.getUserAccount(), + marketIndex, + undefined, + undefined, + builderEscrowMap + ); + await printTxLogs( + bankrunContextWrapper.connection.toConnection(), + settleTx + ); + + await builderEscrowMap.slowSync(); + const escrowAfterSettle = (await builderEscrowMap.mustGet( + userClient.wallet.publicKey.toBase58() + )) as BuilderEscrow; + for (const order of escrowAfterSettle.orders) { + assert(order.feesAccrued.eq(ZERO)); + } + + await builderClient.fetchAccounts(); + usdcPos = builderClient.getSpotPosition(0); + const builderUsdcAfter = getTokenAmount( + usdcPos.scaledBalance, + builderClient.getSpotMarketAccount(0), + usdcPos.balanceType + ); + assert( + builderUsdcAfter + .sub(builderUsdcBefore) + .eq(builderFeeA.add(referrerRewardA)) + ); + + const builderAccountInfo = + await bankrunContextWrapper.connection.getAccountInfo( + getBuilderAccountPublicKey( + builderClient.program.programId, + builderClient.wallet.publicKey + ) + ); + const builderAcc: BuilderAccount = + builderClient.program.account.builder.coder.accounts.decodeUnchecked( + 'Builder', + builderAccountInfo.data + ); + assert( + builderAcc.authority.toBase58() === + builderClient.wallet.publicKey.toBase58() + ); + }); +}); diff --git a/tests/liquidityProvider.ts b/tests/liquidityProvider.ts index d2a895d31..6284a36cc 100644 --- a/tests/liquidityProvider.ts +++ b/tests/liquidityProvider.ts @@ -309,6 +309,7 @@ describe('liquidity providing', () => { it('burn with standardized baa', async () => { console.log('adding liquidity...'); + await driftClientUser.fetchAccounts(); const initMarginReq = driftClientUser.getInitialMarginRequirement(); assert(initMarginReq.eq(ZERO)); diff --git a/tests/subaccounts.ts b/tests/subaccounts.ts index 2f6f5c5cd..fe0c63acc 100644 --- a/tests/subaccounts.ts +++ b/tests/subaccounts.ts @@ -158,6 +158,7 @@ describe('subaccounts', () => { undefined, donationAmount ); + await driftClient.fetchAccounts(); await driftClient.addUser(1); await driftClient.switchActiveUser(1); diff --git a/tests/switchboardTxCus.ts b/tests/switchboardTxCus.ts index fab886314..9137525fe 100644 --- a/tests/switchboardTxCus.ts +++ b/tests/switchboardTxCus.ts @@ -222,6 +222,6 @@ describe('switchboard place orders cus', () => { const cus = bankrunContextWrapper.connection.findComputeUnitConsumption(txSig); console.log(cus); - assert(cus < 410000); + assert(cus < 412000); }); }); diff --git a/tests/testHelpers.ts b/tests/testHelpers.ts index df4e742ab..8df74e2a3 100644 --- a/tests/testHelpers.ts +++ b/tests/testHelpers.ts @@ -43,6 +43,7 @@ import { PositionDirection, DriftClient, OrderType, + ReferrerInfo, } from '../sdk'; import { TestClient, @@ -401,7 +402,8 @@ export async function initializeAndSubscribeDriftClient( marketIndexes: number[], bankIndexes: number[], oracleInfos: OracleInfo[] = [], - accountLoader?: TestBulkAccountLoader + accountLoader?: TestBulkAccountLoader, + referrerInfo?: ReferrerInfo ): Promise { const driftClient = new TestClient({ connection, @@ -426,7 +428,7 @@ export async function initializeAndSubscribeDriftClient( }, }); await driftClient.subscribe(); - await driftClient.initializeUserAccount(); + await driftClient.initializeUserAccount(0, undefined, referrerInfo); return driftClient; } @@ -438,7 +440,8 @@ export async function createUserWithUSDCAccount( marketIndexes: number[], bankIndexes: number[], oracleInfos: OracleInfo[] = [], - accountLoader?: TestBulkAccountLoader + accountLoader?: TestBulkAccountLoader, + referrerInfo?: ReferrerInfo ): Promise<[TestClient, PublicKey, Keypair]> { const userKeyPair = await createFundedKeyPair(context); const usdcAccount = await createUSDCAccountForUser( @@ -454,7 +457,8 @@ export async function createUserWithUSDCAccount( marketIndexes, bankIndexes, oracleInfos, - accountLoader + accountLoader, + referrerInfo ); return [driftClient, usdcAccount, userKeyPair]; @@ -557,7 +561,6 @@ export async function printTxLogs( const tx = await connection.getTransaction(txSig, { commitment: 'confirmed', }); - console.log('tx logs', tx.meta.logMessages); return tx.meta.logMessages; }