From 4a176dd3a30642848a5cfa7b52ccb44e27ff5c2a Mon Sep 17 00:00:00 2001 From: Chester Sim Date: Wed, 11 Mar 2026 14:37:49 +0800 Subject: [PATCH 1/2] feat: add builder parameters to order modification and creation - Introduced `builder_idx` and `builder_fee_tenth_bps` to `OrderParams`, `ModifyOrderParams`, and `ScaleOrderParams`. - Updated `modify_order` and `place_perp_order` functions to handle builder parameters, preserving existing builder attribution when not explicitly provided. - Enhanced the SDK to support builder parameters in order placement and modification, ensuring proper escrow account handling. - Added tests to validate the propagation of builder parameters through order modifications. --- programs/drift/src/controller/orders.rs | 42 ++- programs/drift/src/controller/orders/tests.rs | 111 ++++++++ programs/drift/src/instructions/user.rs | 252 ++++++++++++++++-- programs/drift/src/state/order_params.rs | 4 + .../drift/src/state/scale_order_params.rs | 6 + .../src/state/scale_order_params/tests.rs | 58 ++++ .../src/validation/sig_verification/tests.rs | 64 ++--- sdk/src/driftClient.ts | 138 +++++++++- sdk/src/idl/drift.json | 42 +++ sdk/src/types.ts | 8 + 10 files changed, 669 insertions(+), 56 deletions(-) diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index 9b6807380..630e9a4ee 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -811,6 +811,8 @@ pub fn modify_order( spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, clock: &Clock, + escrow: &mut Option, + builder_codes_enabled: bool, ) -> DriftResult { let user_key = user_loader.key(); let mut user = load_mut!(user_loader)?; @@ -866,6 +868,42 @@ pub fn modify_order( if let Some(order_params) = order_params { if order_params.market_type == MarketType::Perp { + // If modify params don't specify builder fields, try to preserve + // existing builder attribution from the escrow + let (builder_idx, builder_fee_tenth_bps) = if modify_order_params.builder_idx.is_some() + || modify_order_params.builder_fee_tenth_bps.is_some() + { + ( + modify_order_params.builder_idx, + modify_order_params.builder_fee_tenth_bps, + ) + } else if let Some(ref escrow) = escrow { + match escrow.find_order_index(user.sub_account_id, existing_order.order_id) { + Some(idx) => match escrow.get_order(idx) { + Ok(existing_rev_order) => ( + Some(existing_rev_order.builder_idx), + Some(existing_rev_order.fee_tenth_bps), + ), + Err(_) => (None, None), + }, + None => (None, None), + } + } else { + (None, None) + }; + + let mut builder_order = crate::instructions::create_builder_order( + escrow, + builder_idx, + builder_fee_tenth_bps, + builder_codes_enabled, + user.authority, + user.next_order_id, + user.sub_account_id, + &user.orders, + existing_order.market_index, + )?; + place_perp_order( state, &mut user, @@ -877,7 +915,7 @@ pub fn modify_order( clock, order_params, PlaceOrderOptions::default(), - &mut None, + &mut builder_order, )?; } else { place_spot_order( @@ -984,6 +1022,8 @@ fn merge_modify_order_params_with_existing_order( auction_duration, auction_start_price, auction_end_price, + builder_idx: modify_order_params.builder_idx, + builder_fee_tenth_bps: modify_order_params.builder_fee_tenth_bps, })) } diff --git a/programs/drift/src/controller/orders/tests.rs b/programs/drift/src/controller/orders/tests.rs index 98684585b..e1431dfa3 100644 --- a/programs/drift/src/controller/orders/tests.rs +++ b/programs/drift/src/controller/orders/tests.rs @@ -13533,3 +13533,114 @@ mod order_is_low_risk_for_amm { assert!(!is_low); } } + +mod merge_modify_order_params_builder_tests { + use crate::controller::orders::merge_modify_order_params_with_existing_order; + use crate::controller::position::PositionDirection; + use crate::state::order_params::ModifyOrderParams; + use crate::state::user::{MarketType, Order, OrderType}; + + #[test] + fn test_builder_params_passed_through() { + let existing_order = Order { + order_type: OrderType::Limit, + market_type: MarketType::Perp, + direction: PositionDirection::Long, + base_asset_amount: 100, + price: 50, + market_index: 0, + ..Order::default() + }; + + let modify_params = ModifyOrderParams { + builder_idx: Some(3), + builder_fee_tenth_bps: Some(100), + ..ModifyOrderParams::default() + }; + + let result = merge_modify_order_params_with_existing_order(&existing_order, &modify_params) + .unwrap() + .unwrap(); + + assert_eq!(result.builder_idx, Some(3)); + assert_eq!(result.builder_fee_tenth_bps, Some(100)); + } + + #[test] + fn test_builder_params_none_when_not_provided() { + let existing_order = Order { + order_type: OrderType::Limit, + market_type: MarketType::Perp, + direction: PositionDirection::Long, + base_asset_amount: 100, + price: 50, + market_index: 0, + ..Order::default() + }; + + let modify_params = ModifyOrderParams::default(); + + let result = merge_modify_order_params_with_existing_order(&existing_order, &modify_params) + .unwrap() + .unwrap(); + + assert_eq!(result.builder_idx, None); + assert_eq!(result.builder_fee_tenth_bps, None); + } + + #[test] + fn test_builder_idx_without_fee_passes_through() { + let existing_order = Order { + order_type: OrderType::Limit, + market_type: MarketType::Perp, + direction: PositionDirection::Long, + base_asset_amount: 100, + price: 50, + market_index: 0, + ..Order::default() + }; + + let modify_params = ModifyOrderParams { + builder_idx: Some(1), + builder_fee_tenth_bps: None, + ..ModifyOrderParams::default() + }; + + let result = merge_modify_order_params_with_existing_order(&existing_order, &modify_params) + .unwrap() + .unwrap(); + + assert_eq!(result.builder_idx, Some(1)); + assert_eq!(result.builder_fee_tenth_bps, None); + } + + #[test] + fn test_builder_params_with_other_modifications() { + let existing_order = Order { + order_type: OrderType::Limit, + market_type: MarketType::Perp, + direction: PositionDirection::Long, + base_asset_amount: 100, + price: 50, + market_index: 0, + ..Order::default() + }; + + let modify_params = ModifyOrderParams { + price: Some(75), + base_asset_amount: Some(200), + builder_idx: Some(5), + builder_fee_tenth_bps: Some(250), + ..ModifyOrderParams::default() + }; + + let result = merge_modify_order_params_with_existing_order(&existing_order, &modify_params) + .unwrap() + .unwrap(); + + assert_eq!(result.price, 75); + assert_eq!(result.base_asset_amount, 200); + assert_eq!(result.builder_idx, Some(5)); + assert_eq!(result.builder_fee_tenth_bps, Some(250)); + } +} diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index a90a36ce1..d4afd72e2 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -1,5 +1,7 @@ use std::convert::TryFrom; +use std::iter::Peekable; use std::ops::DerefMut; +use std::slice::Iter; use anchor_lang::prelude::*; use anchor_lang::Discriminator; @@ -21,7 +23,7 @@ use crate::controller::spot_position::{ update_spot_balances_and_cumulative_deposits, update_spot_balances_and_cumulative_deposits_with_limits, }; -use crate::error::ErrorCode; +use crate::error::{DriftResult, ErrorCode}; use crate::get_then_update_id; use crate::ids::admin_hot_wallet; use crate::ids::{ @@ -88,7 +90,9 @@ use crate::state::protected_maker_mode_config::ProtectedMakerModeConfig; use crate::state::revenue_share::BuilderInfo; use crate::state::revenue_share::RevenueShare; use crate::state::revenue_share::RevenueShareEscrow; +use crate::state::revenue_share::RevenueShareEscrowZeroCopyMut; use crate::state::revenue_share::RevenueShareOrder; +use crate::state::revenue_share::RevenueShareOrderBitFlag; use crate::state::revenue_share::REVENUE_SHARE_ESCROW_PDA_SEED; use crate::state::revenue_share::REVENUE_SHARE_PDA_SEED; use crate::state::scale_order_params::ScaleOrderParams; @@ -2317,6 +2321,80 @@ pub fn handle_withdraw_from_isolated_perp_position<'c: 'info, 'info>( Ok(()) } +fn maybe_load_builder_escrow<'a>( + remaining_accounts: &mut Peekable>>, + user_loader: &AccountLoader, + builder_codes_enabled: bool, +) -> DriftResult>> { + if builder_codes_enabled { + get_revenue_share_escrow_account(remaining_accounts, &load!(user_loader)?.authority) + } else { + Ok(None) + } +} + +pub(crate) fn create_builder_order<'a>( + escrow: &'a mut Option, + builder_idx: Option, + builder_fee_tenth_bps: Option, + builder_codes_enabled: bool, + user_authority: Pubkey, + user_next_order_id: u32, + user_sub_account_id: u16, + user_orders: &[Order; 32], + market_index: u16, +) -> DriftResult> { + if !builder_codes_enabled || builder_idx.is_none() || builder_fee_tenth_bps.is_none() { + return Ok(None); + } + + let builder_idx = builder_idx.unwrap(); + let builder_fee = builder_fee_tenth_bps.unwrap(); + + if let Some(ref mut escrow) = escrow { + validate!( + escrow.fixed.authority == user_authority, + ErrorCode::InvalidUserAccount, + "RevenueShareEscrow account must be owned by user", + )?; + + let builder = escrow.get_approved_builder_mut(builder_idx)?; + + if builder.is_revoked() { + return Err(ErrorCode::BuilderRevoked.into()); + } + + if builder_fee > builder.max_fee_tenth_bps { + return Err(ErrorCode::InvalidBuilderFee.into()); + } + + let new_order_index = user_orders + .iter() + .position(|order| order.is_available()) + .ok_or(ErrorCode::MaxNumberOfOrders)?; + + match escrow.add_order(RevenueShareOrder::new( + builder_idx, + user_sub_account_id, + user_next_order_id, + builder_fee, + MarketType::Perp, + market_index, + RevenueShareOrderBitFlag::Open as u8, + new_order_index as u8, + )) { + Ok(order_idx) => Ok(escrow.get_order_mut(order_idx).ok()), + Err(_) => { + msg!("Failed to add order, escrow is full"); + Ok(None) + } + } + } else { + msg!("Order has builder fee but no escrow account found"); + Err(ErrorCode::UnableToLoadRevenueShareAccount.into()) + } +} + #[access_control( exchange_not_paused(&ctx.accounts.state) )] @@ -2347,9 +2425,34 @@ pub fn handle_place_perp_order<'c: 'info, 'info>( return Err(print_error!(ErrorCode::InvalidOrderIOC)().into()); } + let builder_codes_enabled = state.builder_codes_enabled(); + let mut escrow = if builder_codes_enabled + && params.builder_idx.is_some() + && params.builder_fee_tenth_bps.is_some() + { + get_revenue_share_escrow_account( + &mut remaining_accounts, + &load!(ctx.accounts.user)?.authority, + )? + } else { + None + }; + let user_key = ctx.accounts.user.key(); let mut user = load_mut!(ctx.accounts.user)?; + let mut builder_order = create_builder_order( + &mut escrow, + params.builder_idx, + params.builder_fee_tenth_bps, + builder_codes_enabled, + user.authority, + user.next_order_id, + user.sub_account_id, + &user.orders, + params.market_index, + )?; + controller::orders::place_perp_order( &ctx.accounts.state, &mut user, @@ -2361,7 +2464,7 @@ pub fn handle_place_perp_order<'c: 'info, 'info>( clock, params, PlaceOrderOptions::default(), - &mut None, + &mut builder_order, )?; Ok(()) @@ -2533,12 +2636,13 @@ pub fn handle_modify_order<'c: 'info, 'info>( let clock = &Clock::get()?; let state = &ctx.accounts.state; + 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, &MarketSet::new(), &MarketSet::new(), clock.slot, @@ -2550,6 +2654,13 @@ pub fn handle_modify_order<'c: 'info, 'info>( None => load!(ctx.accounts.user)?.get_last_order_id(), }; + let builder_codes_enabled = state.builder_codes_enabled(); + let mut escrow = maybe_load_builder_escrow( + &mut remaining_accounts, + &ctx.accounts.user, + builder_codes_enabled, + )?; + controller::orders::modify_order( ModifyOrderId::OrderId(order_id), modify_order_params, @@ -2559,6 +2670,8 @@ pub fn handle_modify_order<'c: 'info, 'info>( &spot_market_map, &mut oracle_map, clock, + &mut escrow, + builder_codes_enabled, )?; Ok(()) @@ -2575,18 +2688,26 @@ pub fn handle_modify_order_by_user_order_id<'c: 'info, 'info>( let clock = &Clock::get()?; let state = &ctx.accounts.state; + 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, &MarketSet::new(), &MarketSet::new(), clock.slot, Some(state.oracle_guard_rails), )?; + let builder_codes_enabled = state.builder_codes_enabled(); + let mut escrow = maybe_load_builder_escrow( + &mut remaining_accounts, + &ctx.accounts.user, + builder_codes_enabled, + )?; + controller::orders::modify_order( ModifyOrderId::UserOrderId(user_order_id), modify_order_params, @@ -2596,6 +2717,8 @@ pub fn handle_modify_order_by_user_order_id<'c: 'info, 'info>( &spot_market_map, &mut oracle_map, clock, + &mut escrow, + builder_codes_enabled, )?; Ok(()) @@ -2651,6 +2774,16 @@ fn place_orders<'c: 'info, 'info>( let high_leverage_mode_config = get_high_leverage_mode_config(&mut remaining_accounts)?; + let builder_codes_enabled = state.builder_codes_enabled(); + let mut escrow = if builder_codes_enabled { + get_revenue_share_escrow_account( + &mut remaining_accounts, + &load!(ctx.accounts.user)?.authority, + )? + } else { + None + }; + // Convert input to order params, expanding scale orders if needed let order_params = match input { PlaceOrdersInput::Orders(params) => params, @@ -2703,6 +2836,18 @@ fn place_orders<'c: 'info, 'info>( }; if params.market_type == MarketType::Perp { + let mut builder_order = create_builder_order( + &mut escrow, + params.builder_idx, + params.builder_fee_tenth_bps, + builder_codes_enabled, + user.authority, + user.next_order_id, + user.sub_account_id, + &user.orders, + params.market_index, + )?; + controller::orders::place_perp_order( state, &mut user, @@ -2714,7 +2859,7 @@ fn place_orders<'c: 'info, 'info>( clock, *params, options, - &mut None, + &mut builder_order, )?; } else { controller::orders::place_spot_order( @@ -2768,6 +2913,17 @@ pub fn handle_place_and_take_perp_order<'c: 'info, 'info>( let high_leverage_mode_config = get_high_leverage_mode_config(remaining_accounts_iter)?; + let builder_referral_enabled = state.builder_referral_enabled(); + let builder_codes_enabled = state.builder_codes_enabled(); + let mut escrow = if builder_codes_enabled || builder_referral_enabled { + get_revenue_share_escrow_account( + remaining_accounts_iter, + &load!(ctx.accounts.user)?.authority, + )? + } else { + None + }; + let is_immediate_or_cancel = params.is_immediate_or_cancel(); controller::repeg::update_amm( @@ -2784,6 +2940,18 @@ pub fn handle_place_and_take_perp_order<'c: 'info, 'info>( let (success_condition, auction_duration_percentage) = parse_optional_params(optional_params); + let mut builder_order = create_builder_order( + &mut escrow, + params.builder_idx, + params.builder_fee_tenth_bps, + builder_codes_enabled, + user.authority, + user.next_order_id, + user.sub_account_id, + &user.orders, + params.market_index, + )?; + controller::orders::place_perp_order( &ctx.accounts.state, &mut user, @@ -2795,7 +2963,7 @@ pub fn handle_place_and_take_perp_order<'c: 'info, 'info>( &clock, params, PlaceOrderOptions::default(), - &mut None, + &mut builder_order, )?; drop(user); @@ -2803,14 +2971,6 @@ pub fn handle_place_and_take_perp_order<'c: 'info, 'info>( let user = &mut ctx.accounts.user; let order_id = load!(user)?.get_last_order_id(); - let builder_referral_enabled = state.builder_referral_enabled(); - let builder_codes_enabled = state.builder_codes_enabled(); - let mut escrow = if builder_codes_enabled || builder_referral_enabled { - get_revenue_share_escrow_account(remaining_accounts_iter, &load!(user)?.authority)? - } else { - None - }; - let (base_asset_amount_filled, _) = controller::orders::fill_perp_order( order_id, &ctx.accounts.state, @@ -2906,9 +3066,34 @@ pub fn handle_place_and_make_perp_order<'c: 'info, 'info>( clock, )?; + let builder_codes_enabled = state.builder_codes_enabled(); + let mut maker_escrow = if builder_codes_enabled + && params.builder_idx.is_some() + && params.builder_fee_tenth_bps.is_some() + { + get_revenue_share_escrow_account( + remaining_accounts_iter, + &load!(ctx.accounts.user)?.authority, + )? + } else { + None + }; + let user_key = ctx.accounts.user.key(); let mut user = load_mut!(ctx.accounts.user)?; + let mut builder_order = create_builder_order( + &mut maker_escrow, + params.builder_idx, + params.builder_fee_tenth_bps, + builder_codes_enabled, + user.authority, + user.next_order_id, + user.sub_account_id, + &user.orders, + params.market_index, + )?; + controller::orders::place_perp_order( state, &mut user, @@ -2920,7 +3105,7 @@ pub fn handle_place_and_make_perp_order<'c: 'info, 'info>( clock, params, PlaceOrderOptions::default(), - &mut None, + &mut builder_order, )?; let (order_id, authority) = (user.get_last_order_id(), user.authority); @@ -2933,8 +3118,7 @@ pub fn handle_place_and_make_perp_order<'c: 'info, 'info>( makers_and_referrer_stats.insert(authority, ctx.accounts.user_stats.clone())?; let builder_referral_enabled = state.builder_referral_enabled(); - let builder_codes_enabled = state.builder_codes_enabled(); - let mut escrow = if builder_codes_enabled || builder_referral_enabled { + let mut taker_escrow = if builder_codes_enabled || builder_referral_enabled { get_revenue_share_escrow_account( remaining_accounts_iter, &load!(ctx.accounts.taker)?.authority, @@ -2958,7 +3142,7 @@ pub fn handle_place_and_make_perp_order<'c: 'info, 'info>( Some(order_id), clock, FillMode::PlaceAndMake, - &mut escrow.as_mut(), + &mut taker_escrow.as_mut(), builder_referral_enabled, )?; @@ -3021,9 +3205,34 @@ pub fn handle_place_and_make_signed_msg_perp_order<'c: 'info, 'info>( clock, )?; + let builder_codes_enabled = state.builder_codes_enabled(); + let mut maker_escrow = if builder_codes_enabled + && params.builder_idx.is_some() + && params.builder_fee_tenth_bps.is_some() + { + get_revenue_share_escrow_account( + remaining_accounts_iter, + &load!(ctx.accounts.user)?.authority, + )? + } else { + None + }; + let user_key = ctx.accounts.user.key(); let mut user = load_mut!(ctx.accounts.user)?; + let mut builder_order = create_builder_order( + &mut maker_escrow, + params.builder_idx, + params.builder_fee_tenth_bps, + builder_codes_enabled, + user.authority, + user.next_order_id, + user.sub_account_id, + &user.orders, + params.market_index, + )?; + controller::orders::place_perp_order( state, &mut user, @@ -3035,7 +3244,7 @@ pub fn handle_place_and_make_signed_msg_perp_order<'c: 'info, 'info>( clock, params, PlaceOrderOptions::default(), - &mut None, + &mut builder_order, )?; let (order_id, authority) = (user.get_last_order_id(), user.authority); @@ -3048,8 +3257,7 @@ pub fn handle_place_and_make_signed_msg_perp_order<'c: 'info, 'info>( makers_and_referrer_stats.insert(authority, ctx.accounts.user_stats.clone())?; let builder_referral_enabled = state.builder_referral_enabled(); - let builder_codes_enabled = state.builder_codes_enabled(); - let mut escrow = if builder_codes_enabled || builder_referral_enabled { + let mut taker_escrow = if builder_codes_enabled || builder_referral_enabled { get_revenue_share_escrow_account( remaining_accounts_iter, &load!(ctx.accounts.taker)?.authority, @@ -3080,7 +3288,7 @@ pub fn handle_place_and_make_signed_msg_perp_order<'c: 'info, 'info>( Some(order_id), clock, FillMode::PlaceAndMake, - &mut escrow.as_mut(), + &mut taker_escrow.as_mut(), builder_referral_enabled, )?; diff --git a/programs/drift/src/state/order_params.rs b/programs/drift/src/state/order_params.rs index a5b81b8bb..a15da358c 100644 --- a/programs/drift/src/state/order_params.rs +++ b/programs/drift/src/state/order_params.rs @@ -36,6 +36,8 @@ pub struct OrderParams { pub auction_duration: Option, // specified in slots pub auction_start_price: Option, // specified in price or oracle_price_offset pub auction_end_price: Option, // specified in price or oracle_price_offset + pub builder_idx: Option, + pub builder_fee_tenth_bps: Option, } #[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq, Debug, Eq)] @@ -942,6 +944,8 @@ pub struct ModifyOrderParams { pub auction_start_price: Option, pub auction_end_price: Option, pub policy: Option, + pub builder_idx: Option, + pub builder_fee_tenth_bps: Option, } impl ModifyOrderParams { diff --git a/programs/drift/src/state/scale_order_params.rs b/programs/drift/src/state/scale_order_params.rs index 5b06df86c..3a9adeb08 100644 --- a/programs/drift/src/state/scale_order_params.rs +++ b/programs/drift/src/state/scale_order_params.rs @@ -51,6 +51,10 @@ pub struct ScaleOrderParams { pub bit_flags: u8, /// Maximum timestamp for orders to be valid pub max_ts: Option, + /// Index of the approved builder in the escrow + pub builder_idx: Option, + /// Fee in tenth basis points for the builder + pub builder_fee_tenth_bps: Option, } impl ScaleOrderParams { @@ -262,6 +266,8 @@ impl ScaleOrderParams { auction_duration: None, auction_start_price: None, auction_end_price: None, + builder_idx: self.builder_idx, + builder_fee_tenth_bps: self.builder_fee_tenth_bps, }); } diff --git a/programs/drift/src/state/scale_order_params/tests.rs b/programs/drift/src/state/scale_order_params/tests.rs index 84c7c632a..b21d98d4e 100644 --- a/programs/drift/src/state/scale_order_params/tests.rs +++ b/programs/drift/src/state/scale_order_params/tests.rs @@ -22,6 +22,8 @@ fn test_validate_order_count_bounds() { post_only: PostOnlyParam::None, bit_flags: 0, max_ts: None, + builder_idx: None, + builder_fee_tenth_bps: None, }; assert!(params.validate(step_size).is_err()); @@ -58,6 +60,8 @@ fn test_validate_price_range() { post_only: PostOnlyParam::None, bit_flags: 0, max_ts: None, + builder_idx: None, + builder_fee_tenth_bps: None, }; assert!(params.validate(step_size).is_err()); @@ -105,6 +109,8 @@ fn test_price_distribution_long() { post_only: PostOnlyParam::None, bit_flags: 0, max_ts: None, + builder_idx: None, + builder_fee_tenth_bps: None, }; let prices = params.calculate_price_distribution().unwrap(); @@ -132,6 +138,8 @@ fn test_price_distribution_short() { post_only: PostOnlyParam::None, bit_flags: 0, max_ts: None, + builder_idx: None, + builder_fee_tenth_bps: None, }; let prices = params.calculate_price_distribution().unwrap(); @@ -161,6 +169,8 @@ fn test_flat_size_distribution() { post_only: PostOnlyParam::None, bit_flags: 0, max_ts: None, + builder_idx: None, + builder_fee_tenth_bps: None, }; let sizes = params.calculate_size_distribution(step_size).unwrap(); @@ -204,6 +214,8 @@ fn test_ascending_size_distribution() { post_only: PostOnlyParam::None, bit_flags: 0, max_ts: None, + builder_idx: None, + builder_fee_tenth_bps: None, }; let sizes = params.calculate_size_distribution(step_size).unwrap(); @@ -261,6 +273,8 @@ fn test_descending_size_distribution() { post_only: PostOnlyParam::None, bit_flags: 0, max_ts: None, + builder_idx: None, + builder_fee_tenth_bps: None, }; let sizes = params.calculate_size_distribution(step_size).unwrap(); @@ -318,6 +332,8 @@ fn test_ascending_size_distribution_3_orders() { post_only: PostOnlyParam::None, bit_flags: 0, max_ts: None, + builder_idx: None, + builder_fee_tenth_bps: None, }; let sizes = params.calculate_size_distribution(step_size).unwrap(); @@ -365,6 +381,8 @@ fn test_flat_distribution_with_remainder() { post_only: PostOnlyParam::None, bit_flags: 0, max_ts: None, + builder_idx: None, + builder_fee_tenth_bps: None, }; let sizes = params.calculate_size_distribution(step_size).unwrap(); @@ -403,6 +421,8 @@ fn test_expand_to_order_params_perp() { post_only: PostOnlyParam::MustPostOnly, bit_flags: 2, // High leverage mode max_ts: Some(12345), + builder_idx: None, + builder_fee_tenth_bps: None, }; let order_params = params.expand_to_order_params(step_size).unwrap(); @@ -452,6 +472,8 @@ fn test_expand_to_order_params_spot() { post_only: PostOnlyParam::None, bit_flags: 0, max_ts: None, + builder_idx: None, + builder_fee_tenth_bps: None, }; let order_params = params.expand_to_order_params(step_size).unwrap(); @@ -492,6 +514,8 @@ fn test_spot_short_scale_orders() { post_only: PostOnlyParam::MustPostOnly, bit_flags: 0, max_ts: Some(99999), + builder_idx: None, + builder_fee_tenth_bps: None, }; let order_params = params.expand_to_order_params(step_size).unwrap(); @@ -535,6 +559,8 @@ fn test_two_orders_price_distribution() { post_only: PostOnlyParam::None, bit_flags: 0, max_ts: None, + builder_idx: None, + builder_fee_tenth_bps: None, }; let prices = params.calculate_price_distribution().unwrap(); @@ -543,6 +569,36 @@ fn test_two_orders_price_distribution() { assert_eq!(prices[1], 100 * PRICE_PRECISION_U64); } +#[test] +fn test_expand_propagates_builder_params() { + let step_size = BASE_PRECISION_U64 / 1000; + + let params = ScaleOrderParams { + market_type: MarketType::Perp, + direction: PositionDirection::Long, + market_index: 0, + total_base_asset_amount: BASE_PRECISION_U64, + start_price: 110 * PRICE_PRECISION_U64, + end_price: 100 * PRICE_PRECISION_U64, + order_count: 3, + size_distribution: SizeDistribution::Flat, + reduce_only: false, + post_only: PostOnlyParam::None, + bit_flags: 0, + max_ts: None, + builder_idx: Some(2), + builder_fee_tenth_bps: Some(50), + }; + + let order_params = params.expand_to_order_params(step_size).unwrap(); + assert_eq!(order_params.len(), 3); + + for op in &order_params { + assert_eq!(op.builder_idx, Some(2)); + assert_eq!(op.builder_fee_tenth_bps, Some(50)); + } +} + #[test] fn test_validate_min_total_size() { let step_size = BASE_PRECISION_U64 / 10; // 0.1 @@ -562,6 +618,8 @@ fn test_validate_min_total_size() { post_only: PostOnlyParam::None, bit_flags: 0, max_ts: None, + builder_idx: None, + builder_fee_tenth_bps: None, }; assert!(params.validate(step_size).is_err()); diff --git a/programs/drift/src/validation/sig_verification/tests.rs b/programs/drift/src/validation/sig_verification/tests.rs index 250d5c871..ca6c3823c 100644 --- a/programs/drift/src/validation/sig_verification/tests.rs +++ b/programs/drift/src/validation/sig_verification/tests.rs @@ -12,8 +12,8 @@ mod sig_verification { let payload = vec![ 200, 213, 166, 94, 34, 52, 245, 93, 0, 1, 0, 1, 0, 202, 154, 59, 0, 0, 0, 0, 0, 248, 89, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 10, 1, 192, 181, 74, 13, 0, 0, 0, 0, - 1, 0, 248, 89, 13, 0, 0, 0, 0, 0, 0, 232, 3, 0, 0, 0, 0, 0, 0, 72, 112, 54, 84, 106, - 83, 48, 107, 0, 0, + 1, 0, 248, 89, 13, 0, 0, 0, 0, 0, 0, 0, 0, 232, 3, 0, 0, 0, 0, 0, 0, 72, 112, 54, 84, + 106, 83, 48, 107, 0, 0, ]; // Test deserialization with non-delegate signer @@ -53,9 +53,9 @@ mod sig_verification { let payload = vec![ 200, 213, 166, 94, 34, 52, 245, 93, 0, 1, 0, 3, 0, 96, 254, 205, 0, 0, 0, 0, 64, 85, 32, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 10, 1, 128, 133, 181, 13, 0, 0, 0, 0, - 1, 64, 85, 32, 14, 0, 0, 0, 0, 2, 0, 41, 9, 0, 0, 0, 0, 0, 0, 67, 82, 79, 51, 105, 114, - 71, 49, 1, 0, 28, 78, 14, 0, 0, 0, 0, 0, 96, 254, 205, 0, 0, 0, 0, 1, 64, 58, 105, 13, - 0, 0, 0, 0, 0, 96, 254, 205, 0, 0, 0, 0, + 1, 64, 85, 32, 14, 0, 0, 0, 0, 0, 0, 2, 0, 41, 9, 0, 0, 0, 0, 0, 0, 67, 82, 79, 51, + 105, 114, 71, 49, 1, 0, 28, 78, 14, 0, 0, 0, 0, 0, 96, 254, 205, 0, 0, 0, 0, 1, 64, 58, + 105, 13, 0, 0, 0, 0, 0, 96, 254, 205, 0, 0, 0, 0, ]; // Test deserialization with delegate signer @@ -103,9 +103,9 @@ mod sig_verification { let payload = vec![ 200, 213, 166, 94, 34, 52, 245, 93, 0, 1, 0, 3, 0, 96, 254, 205, 0, 0, 0, 0, 64, 85, 32, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 10, 1, 128, 133, 181, 13, 0, 0, 0, 0, - 1, 64, 85, 32, 14, 0, 0, 0, 0, 2, 0, 41, 9, 0, 0, 0, 0, 0, 0, 67, 82, 79, 51, 105, 114, - 71, 49, 1, 0, 28, 78, 14, 0, 0, 0, 0, 0, 96, 254, 205, 0, 0, 0, 0, 1, 64, 58, 105, 13, - 0, 0, 0, 0, 0, 96, 254, 205, 0, 0, 0, 0, 1, 1, + 1, 64, 85, 32, 14, 0, 0, 0, 0, 0, 0, 2, 0, 41, 9, 0, 0, 0, 0, 0, 0, 67, 82, 79, 51, + 105, 114, 71, 49, 1, 0, 28, 78, 14, 0, 0, 0, 0, 0, 96, 254, 205, 0, 0, 0, 0, 1, 64, 58, + 105, 13, 0, 0, 0, 0, 0, 96, 254, 205, 0, 0, 0, 0, 1, 1, ]; // Test deserialization with delegate signer @@ -154,9 +154,9 @@ mod sig_verification { let payload = vec![ 200, 213, 166, 94, 34, 52, 245, 93, 0, 1, 0, 3, 0, 96, 254, 205, 0, 0, 0, 0, 64, 85, 32, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 10, 1, 128, 133, 181, 13, 0, 0, 0, 0, - 1, 64, 85, 32, 14, 0, 0, 0, 0, 2, 0, 41, 9, 0, 0, 0, 0, 0, 0, 67, 82, 79, 51, 105, 114, - 71, 49, 1, 0, 28, 78, 14, 0, 0, 0, 0, 0, 96, 254, 205, 0, 0, 0, 0, 1, 64, 58, 105, 13, - 0, 0, 0, 0, 0, 96, 254, 205, 0, 0, 0, 0, 0, 0, 0, 1, 1, + 1, 64, 85, 32, 14, 0, 0, 0, 0, 0, 0, 2, 0, 41, 9, 0, 0, 0, 0, 0, 0, 67, 82, 79, 51, + 105, 114, 71, 49, 1, 0, 28, 78, 14, 0, 0, 0, 0, 0, 96, 254, 205, 0, 0, 0, 0, 1, 64, 58, + 105, 13, 0, 0, 0, 0, 0, 96, 254, 205, 0, 0, 0, 0, 0, 0, 0, 1, 1, ]; // Test deserialization with delegate signer @@ -203,9 +203,9 @@ mod sig_verification { let payload = vec![ 66, 101, 102, 56, 199, 37, 158, 35, 0, 1, 1, 2, 0, 202, 154, 59, 0, 0, 0, 0, 64, 85, 32, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 10, 1, 0, 28, 78, 14, 0, 0, 0, 0, 1, - 128, 151, 47, 14, 0, 0, 0, 0, 242, 208, 117, 159, 92, 135, 34, 224, 147, 14, 64, 92, 7, - 25, 145, 237, 79, 35, 72, 24, 140, 13, 25, 189, 134, 243, 232, 5, 89, 37, 166, 242, 41, - 9, 0, 0, 0, 0, 0, 0, 67, 82, 79, 51, 105, 114, 71, 49, 0, 0, + 128, 151, 47, 14, 0, 0, 0, 0, 0, 0, 242, 208, 117, 159, 92, 135, 34, 224, 147, 14, 64, + 92, 7, 25, 145, 237, 79, 35, 72, 24, 140, 13, 25, 189, 134, 243, 232, 5, 89, 37, 166, + 242, 41, 9, 0, 0, 0, 0, 0, 0, 67, 82, 79, 51, 105, 114, 71, 49, 0, 0, ]; // Test deserialization with delegate signer @@ -248,11 +248,11 @@ mod sig_verification { let payload = vec![ 66, 101, 102, 56, 199, 37, 158, 35, 0, 1, 1, 2, 0, 202, 154, 59, 0, 0, 0, 0, 64, 85, 32, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 10, 1, 0, 28, 78, 14, 0, 0, 0, 0, 1, - 128, 151, 47, 14, 0, 0, 0, 0, 241, 148, 164, 10, 232, 65, 33, 157, 18, 12, 251, 132, - 245, 208, 37, 127, 112, 55, 83, 186, 54, 139, 1, 135, 220, 180, 208, 219, 189, 94, 79, - 148, 41, 9, 0, 0, 0, 0, 0, 0, 67, 82, 79, 51, 105, 114, 71, 49, 1, 128, 133, 181, 13, - 0, 0, 0, 0, 0, 202, 154, 59, 0, 0, 0, 0, 1, 128, 178, 230, 14, 0, 0, 0, 0, 0, 202, 154, - 59, 0, 0, 0, 0, + 128, 151, 47, 14, 0, 0, 0, 0, 0, 0, 241, 148, 164, 10, 232, 65, 33, 157, 18, 12, 251, + 132, 245, 208, 37, 127, 112, 55, 83, 186, 54, 139, 1, 135, 220, 180, 208, 219, 189, 94, + 79, 148, 41, 9, 0, 0, 0, 0, 0, 0, 67, 82, 79, 51, 105, 114, 71, 49, 1, 128, 133, 181, + 13, 0, 0, 0, 0, 0, 202, 154, 59, 0, 0, 0, 0, 1, 128, 178, 230, 14, 0, 0, 0, 0, 0, 202, + 154, 59, 0, 0, 0, 0, ]; // Test deserialization with delegate signer @@ -303,11 +303,11 @@ mod sig_verification { let payload = vec![ 66, 101, 102, 56, 199, 37, 158, 35, 0, 1, 1, 2, 0, 202, 154, 59, 0, 0, 0, 0, 64, 85, 32, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 10, 1, 0, 28, 78, 14, 0, 0, 0, 0, 1, - 128, 151, 47, 14, 0, 0, 0, 0, 241, 148, 164, 10, 232, 65, 33, 157, 18, 12, 251, 132, - 245, 208, 37, 127, 112, 55, 83, 186, 54, 139, 1, 135, 220, 180, 208, 219, 189, 94, 79, - 148, 41, 9, 0, 0, 0, 0, 0, 0, 67, 82, 79, 51, 105, 114, 71, 49, 1, 128, 133, 181, 13, - 0, 0, 0, 0, 0, 202, 154, 59, 0, 0, 0, 0, 1, 128, 178, 230, 14, 0, 0, 0, 0, 0, 202, 154, - 59, 0, 0, 0, 0, 1, 1, + 128, 151, 47, 14, 0, 0, 0, 0, 0, 0, 241, 148, 164, 10, 232, 65, 33, 157, 18, 12, 251, + 132, 245, 208, 37, 127, 112, 55, 83, 186, 54, 139, 1, 135, 220, 180, 208, 219, 189, 94, + 79, 148, 41, 9, 0, 0, 0, 0, 0, 0, 67, 82, 79, 51, 105, 114, 71, 49, 1, 128, 133, 181, + 13, 0, 0, 0, 0, 0, 202, 154, 59, 0, 0, 0, 0, 1, 128, 178, 230, 14, 0, 0, 0, 0, 0, 202, + 154, 59, 0, 0, 0, 0, 1, 1, ]; // Test deserialization with delegate signer @@ -362,9 +362,9 @@ mod sig_verification { let payload = vec![ 200, 213, 166, 94, 34, 52, 245, 93, 0, 1, 0, 3, 0, 96, 254, 205, 0, 0, 0, 0, 64, 85, 32, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 10, 1, 128, 133, 181, 13, 0, 0, 0, 0, - 1, 64, 85, 32, 14, 0, 0, 0, 0, 2, 0, 41, 9, 0, 0, 0, 0, 0, 0, 67, 82, 79, 51, 105, 114, - 71, 49, 1, 0, 28, 78, 14, 0, 0, 0, 0, 0, 96, 254, 205, 0, 0, 0, 0, 0, 1, 255, 255, 1, - 1, 1, 58, 0, + 1, 64, 85, 32, 14, 0, 0, 0, 0, 0, 0, 2, 0, 41, 9, 0, 0, 0, 0, 0, 0, 67, 82, 79, 51, + 105, 114, 71, 49, 1, 0, 28, 78, 14, 0, 0, 0, 0, 0, 96, 254, 205, 0, 0, 0, 0, 0, 1, 255, + 255, 1, 1, 1, 58, 0, ]; // Test deserialization with delegate signer @@ -409,11 +409,11 @@ mod sig_verification { let payload = vec![ 66, 101, 102, 56, 199, 37, 158, 35, 0, 1, 1, 2, 0, 202, 154, 59, 0, 0, 0, 0, 64, 85, 32, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 10, 1, 0, 28, 78, 14, 0, 0, 0, 0, 1, - 128, 151, 47, 14, 0, 0, 0, 0, 241, 148, 164, 10, 232, 65, 33, 157, 18, 12, 251, 132, - 245, 208, 37, 127, 112, 55, 83, 186, 54, 139, 1, 135, 220, 180, 208, 219, 189, 94, 79, - 148, 41, 9, 0, 0, 0, 0, 0, 0, 67, 82, 79, 51, 105, 114, 71, 49, 1, 128, 133, 181, 13, - 0, 0, 0, 0, 0, 202, 154, 59, 0, 0, 0, 0, 1, 128, 178, 230, 14, 0, 0, 0, 0, 0, 202, 154, - 59, 0, 0, 0, 0, 0, 0, 0, 1, 1, + 128, 151, 47, 14, 0, 0, 0, 0, 0, 0, 241, 148, 164, 10, 232, 65, 33, 157, 18, 12, 251, + 132, 245, 208, 37, 127, 112, 55, 83, 186, 54, 139, 1, 135, 220, 180, 208, 219, 189, 94, + 79, 148, 41, 9, 0, 0, 0, 0, 0, 0, 67, 82, 79, 51, 105, 114, 71, 49, 1, 128, 133, 181, + 13, 0, 0, 0, 0, 0, 202, 154, 59, 0, 0, 0, 0, 1, 128, 178, 230, 14, 0, 0, 0, 0, 0, 202, + 154, 59, 0, 0, 0, 0, 0, 0, 0, 1, 1, ]; // Test deserialization with delegate signer diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 54a6c636b..3fe267fbc 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -5026,6 +5026,25 @@ export class DriftClient { }); } + if ( + orderParams.builderIdx != null && + orderParams.builderFeeTenthBps != null + ) { + const escrowAuthority = + depositToTradeArgs?.isMakingNewAccount === true + ? this.authority // new subaccount: authority known even if account isn’t created yet + : this.getUserAccount(subAccountId).authority; + + remainingAccounts.push({ + pubkey: getRevenueShareEscrowAccountPublicKey( + this.program.programId, + escrowAuthority + ), + isWritable: true, + isSigner: false, + }); + } + return await this.program.instruction.placePerpOrder(orderParams, { accounts: { state: await this.getStatePublicKey(), @@ -5524,6 +5543,19 @@ export class DriftClient { } } + if ( + params.some((p) => p.builderIdx != null && p.builderFeeTenthBps != null) + ) { + remainingAccounts.push({ + pubkey: getRevenueShareEscrowAccountPublicKey( + this.program.programId, + this.getUserAccount(subAccountId).authority + ), + isWritable: true, + isSigner: false, + }); + } + const formattedParams = params.map((item) => getOrderParams(item)); const authority = overrides?.authority ?? this.wallet.publicKey; @@ -5575,6 +5607,19 @@ export class DriftClient { } } + if ( + params.some((p) => p.builderIdx != null && p.builderFeeTenthBps != null) + ) { + remainingAccounts.push({ + pubkey: getRevenueShareEscrowAccountPublicKey( + this.program.programId, + this.getUserAccount(subAccountId).authority + ), + isWritable: true, + isSigner: false, + }); + } + const formattedParams = params.map((item) => getOrderParams(item)); const placeOrdersIxs = await this.program.instruction.placeOrders( @@ -5668,6 +5713,17 @@ export class DriftClient { }); } + if (params.builderIdx != null && params.builderFeeTenthBps != null) { + remainingAccounts.push({ + pubkey: getRevenueShareEscrowAccountPublicKey( + this.program.programId, + this.getUserAccount(subAccountId).authority + ), + isWritable: true, + isSigner: false, + }); + } + const formattedParams = { marketType: params.marketType, direction: params.direction, @@ -5681,6 +5737,8 @@ export class DriftClient { postOnly: params.postOnly, bitFlags: params.bitFlags, maxTs: params.maxTs, + builderIdx: params.builderIdx ?? null, + builderFeeTenthBps: params.builderFeeTenthBps ?? null, }; return await this.program.instruction.placeScaleOrders(formattedParams, { @@ -7729,6 +7787,20 @@ export class DriftClient { }); } + if ( + orderParams.builderIdx != null && + orderParams.builderFeeTenthBps != null + ) { + remainingAccounts.push({ + pubkey: getRevenueShareEscrowAccountPublicKey( + this.program.programId, + this.getUserAccount(subAccountId).authority + ), + isWritable: true, + isSigner: false, + }); + } + let optionalParams = null; if (auctionDurationPercentage || successCondition) { optionalParams = @@ -7797,6 +7869,20 @@ export class DriftClient { writablePerpMarketIndexes: [orderParams.marketIndex], }); + if ( + orderParams.builderIdx != null && + orderParams.builderFeeTenthBps != null + ) { + remainingAccounts.push({ + pubkey: getRevenueShareEscrowAccountPublicKey( + this.program.programId, + this.getUserAccount(subAccountId).authority + ), + isWritable: true, + isSigner: false, + }); + } + if (referrerInfo) { remainingAccounts.push({ pubkey: referrerInfo.referrer, @@ -8169,6 +8255,20 @@ export class DriftClient { writablePerpMarketIndexes: [orderParams.marketIndex], }); + if ( + orderParams.builderIdx != null && + orderParams.builderFeeTenthBps != null + ) { + remainingAccounts.push({ + pubkey: getRevenueShareEscrowAccountPublicKey( + this.program.programId, + this.getUserAccount(subAccountId).authority + ), + isWritable: true, + isSigner: false, + }); + } + if (referrerInfo) { remainingAccounts.push({ pubkey: referrerInfo.referrer, @@ -8562,6 +8662,8 @@ export class DriftClient { bitFlags?: number; maxTs?: BN; policy?: number; + builderIdx?: number; + builderFeeTenthBps?: number; }, txParams?: TxParams, subAccountId?: number @@ -8600,6 +8702,8 @@ export class DriftClient { bitFlags, maxTs, policy, + builderIdx, + builderFeeTenthBps, }: { orderId: number; newDirection?: PositionDirection; @@ -8616,6 +8720,8 @@ export class DriftClient { bitFlags?: number; maxTs?: BN; policy?: number; + builderIdx?: number; + builderFeeTenthBps?: number; }, subAccountId?: number, overrides?: { @@ -8634,6 +8740,16 @@ export class DriftClient { useMarketLastSlotCache: true, }); + // we always push the escrow account, so that the modify order instruction can maintain the builder association if any + remainingAccounts.push({ + pubkey: getRevenueShareEscrowAccountPublicKey( + this.program.programId, + userAccount.authority + ), + isWritable: true, + isSigner: false, + }); + const orderParams: ModifyOrderParams = { baseAssetAmount: newBaseAmount || null, direction: newDirection || null, @@ -8649,6 +8765,8 @@ export class DriftClient { bitFlags: bitFlags != undefined ? bitFlags : null, policy: policy || null, maxTs: maxTs || null, + builderIdx: builderIdx ?? null, + builderFeeTenthBps: builderFeeTenthBps ?? null, }; const authority = @@ -8701,6 +8819,8 @@ export class DriftClient { bitFlags?: number; policy?: ModifyOrderPolicy; maxTs?: BN; + builderIdx?: number; + builderFeeTenthBps?: number; }, txParams?: TxParams, subAccountId?: number @@ -8733,6 +8853,8 @@ export class DriftClient { bitFlags, maxTs, policy, + builderIdx, + builderFeeTenthBps, }: { userOrderId: number; newDirection?: PositionDirection; @@ -8749,16 +8871,28 @@ export class DriftClient { bitFlags?: number; policy?: ModifyOrderPolicy; maxTs?: BN; + builderIdx?: number; + builderFeeTenthBps?: number; }, subAccountId?: number ): Promise { const user = await this.getUserAccountPublicKey(subAccountId); + const userAccount = this.getUserAccount(subAccountId); const remainingAccounts = this.getRemainingAccounts({ - userAccounts: [this.getUserAccount(subAccountId)], + userAccounts: [userAccount], useMarketLastSlotCache: true, }); + remainingAccounts.push({ + pubkey: getRevenueShareEscrowAccountPublicKey( + this.program.programId, + userAccount.authority + ), + isWritable: true, + isSigner: false, + }); + const orderParams: ModifyOrderParams = { baseAssetAmount: newBaseAmount || null, direction: newDirection || null, @@ -8774,6 +8908,8 @@ export class DriftClient { bitFlags: bitFlags || null, policy: policy || null, maxTs: maxTs || null, + builderIdx: builderIdx ?? null, + builderFeeTenthBps: builderFeeTenthBps ?? null, }; return await this.program.instruction.modifyOrderByUserId( diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index a215226c6..8d9273813 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -13028,6 +13028,18 @@ "type": { "option": "i64" } + }, + { + "name": "builderIdx", + "type": { + "option": "u8" + } + }, + { + "name": "builderFeeTenthBps", + "type": { + "option": "u16" + } } ] } @@ -13284,6 +13296,18 @@ "type": { "option": "u8" } + }, + { + "name": "builderIdx", + "type": { + "option": "u8" + } + }, + { + "name": "builderFeeTenthBps", + "type": { + "option": "u16" + } } ] } @@ -13380,6 +13404,24 @@ "type": { "option": "i64" } + }, + { + "name": "builderIdx", + "docs": [ + "Index of the approved builder in the escrow" + ], + "type": { + "option": "u8" + } + }, + { + "name": "builderFeeTenthBps", + "docs": [ + "Fee in tenth basis points for the builder" + ], + "type": { + "option": "u16" + } } ] } diff --git a/sdk/src/types.ts b/sdk/src/types.ts index feda6c790..e0d95ec13 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -1286,6 +1286,8 @@ export type OrderParams = { maxTs: BN | null; auctionStartPrice: BN | null; auctionEndPrice: BN | null; + builderIdx: number | null; + builderFeeTenthBps: number | null; }; export class PostOnlyParams { @@ -1329,6 +1331,10 @@ export type ScaleOrderParams = { bitFlags: number; /** Maximum timestamp for orders to be valid */ maxTs: BN | null; + /** Index of the approved builder in the escrow */ + builderIdx: number | null; + /** Fee in tenth basis points for the builder */ + builderFeeTenthBps: number | null; }; export class OrderParamsBitFlag { @@ -1380,6 +1386,8 @@ export const DefaultOrderParams: OrderParams = { maxTs: null, auctionStartPrice: null, auctionEndPrice: null, + builderIdx: null, + builderFeeTenthBps: null, }; export type SignedMsgOrderParamsMessage = { From af266d149de7b7a44b89c4557ceb7541ce30b869 Mon Sep 17 00:00:00 2001 From: Chester Sim Date: Wed, 11 Mar 2026 17:58:01 +0800 Subject: [PATCH 2/2] feat: enhance order processing with maker builder parameters - Added `maker_builder_idx` and `maker_builder_fee` to `OrderActionRecord` and related functions to support maker-specific builder fees. - Updated `fill_perp_order`, `fulfill_perp_order`, and other relevant functions to handle maker builder parameters, ensuring proper fee calculations and escrow handling. - Enhanced SDK to accommodate new maker builder parameters in order placement and fulfillment processes. - Introduced tests to validate the integration of maker builder parameters across order operations. --- programs/drift/src/controller/liquidation.rs | 4 + programs/drift/src/controller/orders.rs | 78 ++++++++++++++++++- .../src/controller/orders/amm_jit_tests.rs | 13 ++++ .../drift/src/controller/orders/fuel_tests.rs | 1 + programs/drift/src/controller/orders/tests.rs | 41 ++++++++++ programs/drift/src/controller/pnl.rs | 2 + programs/drift/src/instructions/keeper.rs | 6 +- .../src/instructions/optional_accounts.rs | 66 ++++++++++++++-- programs/drift/src/instructions/user.rs | 18 ++++- programs/drift/src/math/fees.rs | 29 ++++--- programs/drift/src/math/fees/tests.rs | 15 ++++ programs/drift/src/state/events.rs | 10 ++- sdk/src/driftClient.ts | 30 +++++++ sdk/src/idl/drift.json | 14 ++++ sdk/src/types.ts | 5 ++ 15 files changed, 311 insertions(+), 21 deletions(-) diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index 81e755943..ec0ed27db 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -692,6 +692,8 @@ pub fn liquidate_perp( trigger_price: None, builder_idx: None, builder_fee: None, + maker_builder_idx: None, + maker_builder_fee: None, }; emit!(fill_record); @@ -1075,6 +1077,7 @@ pub fn liquidate_perp_with_fill( drop(user); drop(liquidator); + let mut empty_maker_escrows = Vec::new(); let (fill_base_asset_amount, fill_quote_asset_amount) = fill_perp_order( order_id, state, @@ -1092,6 +1095,7 @@ pub fn liquidate_perp_with_fill( FillMode::Liquidation, &mut None, false, + &mut empty_maker_escrows, )?; let mut user = load_mut!(user_loader)?; diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index 630e9a4ee..096191a85 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -450,6 +450,8 @@ pub fn place_perp_order( None, None, None, + None, + None, )?; emit_stack::<_, { OrderActionRecord::SIZE }>(order_action_record)?; @@ -751,6 +753,8 @@ pub fn cancel_order( None, None, None, + None, + None, )?; emit_stack::<_, { OrderActionRecord::SIZE }>(order_action_record)?; } @@ -1044,6 +1048,7 @@ pub fn fill_perp_order( fill_mode: FillMode, rev_share_escrow: &mut Option<&mut RevenueShareEscrowZeroCopyMut>, builder_referral_feature_enabled: bool, + maker_escrows: &mut Vec, ) -> DriftResult<(u64, u64)> { let now = clock.unix_timestamp; let slot = clock.slot; @@ -1347,6 +1352,7 @@ pub fn fill_perp_order( oracle_stale_for_margin, rev_share_escrow, builder_referral_feature_enabled, + maker_escrows, )?; if base_asset_amount != 0 { @@ -1830,6 +1836,7 @@ fn fulfill_perp_order( oracle_stale_for_margin: bool, rev_share_escrow: &mut Option<&mut RevenueShareEscrowZeroCopyMut>, builder_referral_feature_enabled: bool, + maker_escrows: &mut Vec, ) -> DriftResult<(u64, u64)> { let market_index = user.orders[user_order_index].market_index; @@ -1971,6 +1978,7 @@ fn fulfill_perp_order( fill_mode.is_liquidation(), rev_share_escrow, builder_referral_feature_enabled, + maker_escrows, )?; if maker_fill_base_asset_amount != 0 { @@ -2382,6 +2390,7 @@ pub fn fulfill_perp_order_with_amm( fee_to_market_for_lp: _fee_to_market_for_lp, maker_rebate, builder_fee: builder_fee_option, + .. } = fees::calculate_fee_for_fulfillment_with_amm( user_stats, quote_asset_amount, @@ -2597,6 +2606,8 @@ pub fn fulfill_perp_order_with_amm( None, builder_idx, builder_fee_option, + None, + None, )?; emit_stack::<_, { OrderActionRecord::SIZE }>(order_action_record)?; @@ -2667,6 +2678,7 @@ pub fn fulfill_perp_order_with_match( is_liquidation: bool, rev_share_escrow: &mut Option<&mut RevenueShareEscrowZeroCopyMut>, builder_referral_feature_enabled: bool, + maker_escrows: &mut Vec, ) -> DriftResult<(u64, u64, u64)> { if !are_orders_same_market_but_different_sides( &maker.orders[maker_order_index], @@ -2682,6 +2694,14 @@ pub fn fulfill_perp_order_with_match( msg!("Order has builder but no escrow account included, in the future this will fail."); } + let maker_order_has_builder = maker.orders[maker_order_index].is_has_builder(); + let maker_escrow_pos = maker_escrows + .iter() + .position(|e| e.fixed.authority == maker.authority); + if maker_order_has_builder && maker_escrow_pos.is_none() { + msg!("Maker order has builder but no maker escrow account included, in the future this will fail."); + } + let taker_price = if let Some(taker_limit_price) = taker_limit_price { taker_limit_price } else { @@ -2894,6 +2914,21 @@ pub fn fulfill_perp_order_with_match( builder_referral_feature_enabled, ); + // Look up maker's escrow from the loaded maker escrows by authority + let (maker_builder_order_idx, _, maker_builder_order_fee_bps, maker_builder_idx) = + if let Some(pos) = maker_escrow_pos { + let mut escrow_opt = Some(&mut maker_escrows[pos]); + get_builder_escrow_info( + &mut escrow_opt, + maker.sub_account_id, + maker.orders[maker_order_index].order_id, + market.market_index, + false, // no referral for maker builder + ) + } else { + (None, None, None, None) + }; + let filler_multiplier = if reward_filler { calculate_filler_multiplier_for_matched_orders(maker_price, maker_direction, oracle_price)? } else { @@ -2908,6 +2943,7 @@ pub fn fulfill_perp_order_with_match( referrer_reward, referee_discount, builder_fee: builder_fee_option, + maker_builder_fee: maker_builder_fee_option, .. } = fees::calculate_fee_for_fulfillment_with_match( taker_stats, @@ -2923,8 +2959,10 @@ pub fn fulfill_perp_order_with_match( market.fee_adjustment, taker.is_high_leverage_mode(MarginRequirementType::Initial), builder_order_fee_bps, + maker_builder_order_fee_bps, )?; let builder_fee = builder_fee_option.unwrap_or(0); + let maker_builder_fee = maker_builder_fee_option.unwrap_or(0); if builder_fee != 0 { if let (Some(idx), Some(escrow)) = (builder_order_idx, rev_share_escrow.as_deref_mut()) { @@ -2939,6 +2977,20 @@ pub fn fulfill_perp_order_with_match( } } + // Accrue maker builder fee to maker's escrow + if maker_builder_fee != 0 { + if let (Some(idx), Some(pos)) = (maker_builder_order_idx, maker_escrow_pos) { + let order = maker_escrows[pos].get_order_mut(idx)?; + order.fees_accrued = order.fees_accrued.safe_add(maker_builder_fee)?; + } else { + validate!( + false, + ErrorCode::UnableToLoadRevenueShareAccount, + "Maker order has builder fee but no escrow account found" + )?; + } + } + // 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 @@ -2963,10 +3015,11 @@ pub fn fulfill_perp_order_with_match( taker_stats.increment_total_fees(taker_fee)?; taker_stats.increment_total_referee_discount(referee_discount)?; + // Deduct maker builder fee from maker's rebate controller::position::update_quote_asset_and_break_even_amount( &mut maker.perp_positions[maker_position_index], market, - maker_rebate.cast()?, + maker_rebate.cast::()?.safe_sub(maker_builder_fee.cast()?)?, )?; if let Some(maker_stats) = maker_stats { @@ -3032,12 +3085,20 @@ pub fn fulfill_perp_order_with_match( taker.orders[taker_order_index].update_open_bids_and_asks(), )?; - update_order_after_fill( + let is_maker_filled = update_order_after_fill( &mut maker.orders[maker_order_index], base_asset_amount_fulfilled_by_maker, quote_asset_amount, )?; + if is_maker_filled { + if let (Some(idx), Some(pos)) = (maker_builder_order_idx, maker_escrow_pos) { + maker_escrows[pos] + .get_order_mut(idx)? + .add_bit_flag(RevenueShareOrderBitFlag::Completed); + } + } + decrease_open_bids_and_asks( &mut maker.perp_positions[maker_position_index], &maker.orders[maker_order_index].direction, @@ -3108,6 +3169,8 @@ pub fn fulfill_perp_order_with_match( None, builder_idx, builder_fee_option, + maker_builder_idx, + maker_builder_fee_option, )?; emit_stack::<_, { OrderActionRecord::SIZE }>(order_action_record)?; @@ -3403,6 +3466,8 @@ pub fn trigger_order( Some(trigger_price), None, None, + None, + None, )?; emit!(order_action_record); @@ -4003,6 +4068,8 @@ pub fn place_spot_order( None, None, None, + None, + None, )?; emit_stack::<_, { OrderActionRecord::SIZE }>(order_action_record)?; @@ -5079,6 +5146,7 @@ pub fn fulfill_spot_order_with_match( base_market.fee_adjustment, false, None, + None, )?; // Update taker state @@ -5248,6 +5316,8 @@ pub fn fulfill_spot_order_with_match( None, None, None, + None, + None, )?; emit_stack::<_, { OrderActionRecord::SIZE }>(order_action_record)?; @@ -5524,6 +5594,8 @@ pub fn fulfill_spot_order_with_external_market( None, None, None, + None, + None, )?; emit_stack::<_, { OrderActionRecord::SIZE }>(order_action_record)?; @@ -5732,6 +5804,8 @@ pub fn trigger_spot_order( Some(oracle_price.unsigned_abs()), None, None, + 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 4ab526d1f..509f9da54 100644 --- a/programs/drift/src/controller/orders/amm_jit_tests.rs +++ b/programs/drift/src/controller/orders/amm_jit_tests.rs @@ -369,6 +369,7 @@ pub mod amm_jit { false, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -574,6 +575,7 @@ pub mod amm_jit { false, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -790,6 +792,7 @@ pub mod amm_jit { false, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -1002,6 +1005,7 @@ pub mod amm_jit { false, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -1217,6 +1221,7 @@ pub mod amm_jit { false, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -1440,6 +1445,7 @@ pub mod amm_jit { false, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -1647,6 +1653,7 @@ pub mod amm_jit { false, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -1862,6 +1869,7 @@ pub mod amm_jit { false, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -2065,6 +2073,7 @@ pub mod amm_jit { false, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -2280,6 +2289,7 @@ pub mod amm_jit { false, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -2533,6 +2543,7 @@ pub mod amm_jit { false, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -2833,6 +2844,7 @@ pub mod amm_jit { false, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -3078,6 +3090,7 @@ pub mod amm_jit { false, &mut None, false, + &mut Vec::new(), ) .unwrap(); diff --git a/programs/drift/src/controller/orders/fuel_tests.rs b/programs/drift/src/controller/orders/fuel_tests.rs index 5684875e6..9d244280f 100644 --- a/programs/drift/src/controller/orders/fuel_tests.rs +++ b/programs/drift/src/controller/orders/fuel_tests.rs @@ -307,6 +307,7 @@ pub mod fuel_scoring { false, &mut None, false, + &mut Vec::new(), ) .unwrap(); diff --git a/programs/drift/src/controller/orders/tests.rs b/programs/drift/src/controller/orders/tests.rs index e1431dfa3..434ccc346 100644 --- a/programs/drift/src/controller/orders/tests.rs +++ b/programs/drift/src/controller/orders/tests.rs @@ -316,6 +316,7 @@ pub mod fill_order_protected_maker { FillMode::Fill, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -427,6 +428,7 @@ pub mod fill_order_protected_maker { FillMode::Fill, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -542,6 +544,7 @@ pub mod fulfill_order_with_maker_order { false, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -667,6 +670,7 @@ pub mod fulfill_order_with_maker_order { false, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -792,6 +796,7 @@ pub mod fulfill_order_with_maker_order { false, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -917,6 +922,7 @@ pub mod fulfill_order_with_maker_order { false, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -1041,6 +1047,7 @@ pub mod fulfill_order_with_maker_order { false, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -1132,6 +1139,7 @@ pub mod fulfill_order_with_maker_order { false, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -1224,6 +1232,7 @@ pub mod fulfill_order_with_maker_order { false, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -1316,6 +1325,7 @@ pub mod fulfill_order_with_maker_order { false, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -1408,6 +1418,7 @@ pub mod fulfill_order_with_maker_order { false, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -1520,6 +1531,7 @@ pub mod fulfill_order_with_maker_order { false, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -1637,6 +1649,7 @@ pub mod fulfill_order_with_maker_order { false, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -1759,6 +1772,7 @@ pub mod fulfill_order_with_maker_order { false, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -1882,6 +1896,7 @@ pub mod fulfill_order_with_maker_order { false, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -2029,6 +2044,7 @@ pub mod fulfill_order_with_maker_order { false, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -2151,6 +2167,7 @@ pub mod fulfill_order_with_maker_order { false, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -2285,6 +2302,7 @@ pub mod fulfill_order_with_maker_order { false, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -2438,6 +2456,7 @@ pub mod fulfill_order_with_maker_order { false, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -2589,6 +2608,7 @@ pub mod fulfill_order_with_maker_order { false, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -2741,6 +2761,7 @@ pub mod fulfill_order_with_maker_order { false, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -2874,6 +2895,7 @@ pub mod fulfill_order_with_maker_order { false, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -3006,6 +3028,7 @@ pub mod fulfill_order_with_maker_order { false, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -3409,6 +3432,7 @@ pub mod fulfill_order { false, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -3668,6 +3692,7 @@ pub mod fulfill_order { false, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -3873,6 +3898,7 @@ pub mod fulfill_order { false, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -4094,6 +4120,7 @@ pub mod fulfill_order { false, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -4275,6 +4302,7 @@ pub mod fulfill_order { false, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -4488,6 +4516,7 @@ pub mod fulfill_order { false, &mut None, false, + &mut Vec::new(), ); assert!(result.is_ok()); @@ -4690,6 +4719,7 @@ pub mod fulfill_order { false, &mut None, false, + &mut Vec::new(), ); assert_eq!(result, Err(ErrorCode::InsufficientCollateral)); @@ -4845,6 +4875,7 @@ pub mod fulfill_order { false, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -5027,6 +5058,7 @@ pub mod fulfill_order { false, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -5205,6 +5237,7 @@ pub mod fulfill_order { FillMode::Fill, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -5232,6 +5265,7 @@ pub mod fulfill_order { FillMode::Fill, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -5632,6 +5666,7 @@ pub mod fulfill_order { false, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -5891,6 +5926,7 @@ pub mod fulfill_order { false, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -6074,6 +6110,7 @@ pub mod fulfill_order { false, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -6308,6 +6345,7 @@ pub mod fill_order { FillMode::Fill, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -6512,6 +6550,7 @@ pub mod fill_order { FillMode::Fill, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -6643,6 +6682,7 @@ pub mod fill_order { FillMode::Fill, &mut None, false, + &mut Vec::new(), ) .unwrap(); @@ -6807,6 +6847,7 @@ pub mod fill_order { FillMode::Fill, &mut None, false, + &mut Vec::new(), ); assert_eq!(err, Err(ErrorCode::MaxOpenInterest)); diff --git a/programs/drift/src/controller/pnl.rs b/programs/drift/src/controller/pnl.rs index 3aae2e82a..c7581a39f 100644 --- a/programs/drift/src/controller/pnl.rs +++ b/programs/drift/src/controller/pnl.rs @@ -592,6 +592,8 @@ pub fn settle_expired_position( trigger_price: None, builder_idx: None, builder_fee: None, + maker_builder_idx: None, + maker_builder_fee: None, }; emit!(fill_record); } diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index 0ae4e11ae..6842bf31d 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -30,7 +30,7 @@ use crate::ids::{ serum_program, titan_mainnet_argos_v1, }; use crate::instructions::constraints::*; -use crate::instructions::optional_accounts::get_revenue_share_escrow_account; +use crate::instructions::optional_accounts::{get_revenue_share_escrow_account, load_maker_escrows}; use crate::instructions::optional_accounts::{load_maps, AccountMaps}; use crate::load_mut; use crate::math::casting::Cast; @@ -175,6 +175,9 @@ fn fill_order<'c: 'info, 'info>( None }; + let mut maker_escrows = + load_maker_escrows(&mut remaining_accounts_iter, builder_codes_enabled)?; + controller::repeg::update_amm( market_index, &perp_market_map, @@ -200,6 +203,7 @@ fn fill_order<'c: 'info, 'info>( FillMode::Fill, &mut escrow.as_mut(), builder_referral_enabled, + &mut maker_escrows, )?; Ok(()) diff --git a/programs/drift/src/instructions/optional_accounts.rs b/programs/drift/src/instructions/optional_accounts.rs index c2365bf0e..e4273e011 100644 --- a/programs/drift/src/instructions/optional_accounts.rs +++ b/programs/drift/src/instructions/optional_accounts.rs @@ -288,7 +288,46 @@ pub fn get_revenue_share_escrow_account<'a>( let account_info = account_info.safe_unwrap()?; - // Check size and discriminator without borrowing + if account_info.data_len() < 80 { + return Ok(None); + } + + let borrowed_data = account_info.data.borrow(); + let discriminator: [u8; 8] = RevenueShareEscrow::discriminator(); + if array_ref![&borrowed_data, 0, 8] != &discriminator { + return Ok(None); + } + + // Check authority before consuming — authority is at byte offset 8 (first field after discriminator) + let authority_bytes = array_ref![&borrowed_data, 8, 32]; + if authority_bytes != expected_authority.as_ref() { + return Ok(None); + } + + drop(borrowed_data); + let account_info = account_info_iter.next().safe_unwrap()?; + let escrow = account_info.load_zc_mut()?; + Ok(Some(escrow)) +} + +/// Load a revenue share escrow account checking only the discriminator (no authority validation). +/// Used for loading maker escrows when the maker authority isn't known yet at load time. +pub fn get_revenue_share_escrow_account_unchecked<'a>( + account_info_iter: &mut Peekable>>, +) -> DriftResult>> { + load_revenue_share_escrow(account_info_iter) +} + +fn load_revenue_share_escrow<'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()?; + if account_info.data_len() < 80 { return Ok(None); } @@ -305,11 +344,24 @@ pub fn get_revenue_share_escrow_account<'a>( drop(borrowed_data); let escrow: RevenueShareEscrowZeroCopyMut<'a> = account_info.load_zc_mut()?; - validate!( - escrow.fixed.authority == *expected_authority, - ErrorCode::RevenueShareEscrowAuthorityMismatch, - "invalid RevenueShareEscrow authority" - )?; - Ok(Some(escrow)) } + +/// Load all remaining revenue share escrow accounts from the iterator into a Vec. +/// Each escrow is loaded by discriminator only (no authority check); authority is validated +/// later inside the fill loop where maker.authority is available. +pub fn load_maker_escrows<'a>( + account_info_iter: &mut Peekable>>, + builder_codes_enabled: bool, +) -> DriftResult>> { + let mut escrows = Vec::new(); + if !builder_codes_enabled { + return Ok(escrows); + } + + while let Some(escrow) = get_revenue_share_escrow_account_unchecked(account_info_iter)? { + escrows.push(escrow); + } + + Ok(escrows) +} diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index d4afd72e2..0820b46ba 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -32,7 +32,8 @@ use crate::ids::{ use crate::instructions::constraints::*; use crate::instructions::optional_accounts::get_revenue_share_escrow_account; use crate::instructions::optional_accounts::{ - get_referrer_and_referrer_stats, get_whitelist_token, load_maps, AccountMaps, + get_referrer_and_referrer_stats, get_whitelist_token, load_maker_escrows, load_maps, + AccountMaps, }; use crate::instructions::SpotFulfillmentType; use crate::load; @@ -2105,6 +2106,8 @@ pub fn handle_transfer_perp_position<'c: 'info, 'info>( trigger_price: None, builder_idx: None, builder_fee: None, + maker_builder_idx: None, + maker_builder_fee: None, }; emit_stack::<_, { OrderActionRecord::SIZE }>(fill_record)?; @@ -2971,6 +2974,8 @@ pub fn handle_place_and_take_perp_order<'c: 'info, 'info>( let user = &mut ctx.accounts.user; let order_id = load!(user)?.get_last_order_id(); + let mut maker_escrows = load_maker_escrows(remaining_accounts_iter, builder_codes_enabled)?; + let (base_asset_amount_filled, _) = controller::orders::fill_perp_order( order_id, &ctx.accounts.state, @@ -2991,6 +2996,7 @@ pub fn handle_place_and_take_perp_order<'c: 'info, 'info>( ), &mut escrow.as_mut(), builder_referral_enabled, + &mut maker_escrows, )?; let order_unfilled = load!(ctx.accounts.user)? @@ -3127,6 +3133,8 @@ pub fn handle_place_and_make_perp_order<'c: 'info, 'info>( None }; + let mut maker_escrows: Vec<_> = maker_escrow.into_iter().collect(); + controller::orders::fill_perp_order( taker_order_id, state, @@ -3144,6 +3152,7 @@ pub fn handle_place_and_make_perp_order<'c: 'info, 'info>( FillMode::PlaceAndMake, &mut taker_escrow.as_mut(), builder_referral_enabled, + &mut maker_escrows, )?; let order_exists = load!(ctx.accounts.user)? @@ -3266,6 +3275,12 @@ pub fn handle_place_and_make_signed_msg_perp_order<'c: 'info, 'info>( None }; + let mut maker_escrows = if let Some(escrow) = maker_escrow { + vec![escrow] + } else { + vec![] + }; + let taker_signed_msg_account = ctx.accounts.taker_signed_msg_user_orders.load()?; let taker_order_id = taker_signed_msg_account .iter() @@ -3290,6 +3305,7 @@ pub fn handle_place_and_make_signed_msg_perp_order<'c: 'info, 'info>( FillMode::PlaceAndMake, &mut taker_escrow.as_mut(), builder_referral_enabled, + &mut maker_escrows, )?; let order_exists = load!(ctx.accounts.user)? diff --git a/programs/drift/src/math/fees.rs b/programs/drift/src/math/fees.rs index 8431be24b..87c72dfd3 100644 --- a/programs/drift/src/math/fees.rs +++ b/programs/drift/src/math/fees.rs @@ -31,6 +31,7 @@ pub struct FillFees { pub referrer_reward: u64, pub referee_discount: u64, pub builder_fee: Option, + pub maker_builder_fee: Option, } pub fn calculate_fee_for_fulfillment_with_amm( @@ -95,6 +96,7 @@ pub fn calculate_fee_for_fulfillment_with_amm( referrer_reward: 0, referee_discount: 0, builder_fee: None, + maker_builder_fee: None, }) } else { let mut fee = calculate_taker_fee(quote_asset_amount, &fee_tier, fee_adjustment)?; @@ -154,6 +156,7 @@ pub fn calculate_fee_for_fulfillment_with_amm( referrer_reward, referee_discount, builder_fee, + maker_builder_fee: None, }) } } @@ -287,6 +290,19 @@ fn calculate_filler_reward( Ok(fee) } +fn calculate_builder_fee( + quote_asset_amount: u64, + fee_bps: Option, +) -> DriftResult> { + fee_bps + .map(|bps| { + quote_asset_amount + .safe_mul(bps.cast()?)? + .safe_div(100_000) + }) + .transpose() +} + pub fn calculate_fee_for_fulfillment_with_match( taker_stats: &UserStats, maker_stats: &Option<&mut UserStats>, @@ -301,6 +317,7 @@ pub fn calculate_fee_for_fulfillment_with_match( fee_adjustment: i16, user_high_leverage_mode: bool, builder_fee_bps: Option, + maker_builder_fee_bps: Option, ) -> DriftResult { let taker_fee_tier = determine_user_fee_tier( taker_stats, @@ -352,15 +369,8 @@ 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 { - Some( - quote_asset_amount - .safe_mul(builder_fee_bps.cast()?)? - .safe_div(100_000)?, - ) - } else { - None - }; + let builder_fee = calculate_builder_fee(quote_asset_amount, builder_fee_bps)?; + let maker_builder_fee = calculate_builder_fee(quote_asset_amount, maker_builder_fee_bps)?; Ok(FillFees { user_fee: taker_fee, @@ -371,6 +381,7 @@ pub fn calculate_fee_for_fulfillment_with_match( fee_to_market_for_lp: 0, referee_discount, builder_fee, + maker_builder_fee, }) } diff --git a/programs/drift/src/math/fees/tests.rs b/programs/drift/src/math/fees/tests.rs index 296f3bfce..d07cc218c 100644 --- a/programs/drift/src/math/fees/tests.rs +++ b/programs/drift/src/math/fees/tests.rs @@ -32,6 +32,7 @@ mod calculate_fee_for_taker_and_maker { 0, false, None, + None, ) .unwrap(); @@ -77,6 +78,7 @@ mod calculate_fee_for_taker_and_maker { 0, false, None, + None, ) .unwrap(); @@ -121,6 +123,7 @@ mod calculate_fee_for_taker_and_maker { 0, false, None, + None, ) .unwrap(); @@ -165,6 +168,7 @@ mod calculate_fee_for_taker_and_maker { 0, false, None, + None, ) .unwrap(); @@ -207,6 +211,7 @@ mod calculate_fee_for_taker_and_maker { 0, false, None, + None, ) .unwrap(); @@ -246,6 +251,7 @@ mod calculate_fee_for_taker_and_maker { -50, false, None, + None, ) .unwrap(); @@ -278,6 +284,7 @@ mod calculate_fee_for_taker_and_maker { 50, false, None, + None, ) .unwrap(); @@ -311,6 +318,7 @@ mod calculate_fee_for_taker_and_maker { -50, false, None, + None, ) .unwrap(); @@ -344,6 +352,7 @@ mod calculate_fee_for_taker_and_maker { -50, false, None, + None, ) .unwrap(); @@ -383,6 +392,7 @@ mod calculate_fee_for_taker_and_maker { -100, false, None, + None, ) .unwrap(); @@ -415,6 +425,7 @@ mod calculate_fee_for_taker_and_maker { -100, false, None, + None, ) .unwrap(); @@ -448,6 +459,7 @@ mod calculate_fee_for_taker_and_maker { -100, true, None, + None, ) .unwrap(); @@ -481,6 +493,7 @@ mod calculate_fee_for_taker_and_maker { -100, false, None, + None, ) .unwrap(); @@ -514,6 +527,7 @@ mod calculate_fee_for_taker_and_maker { -100, false, None, + None, ) .unwrap(); @@ -553,6 +567,7 @@ mod calculate_fee_for_taker_and_maker { -50, true, None, + None, ) .unwrap(); diff --git a/programs/drift/src/state/events.rs b/programs/drift/src/state/events.rs index 5d7753f95..659e2f432 100644 --- a/programs/drift/src/state/events.rs +++ b/programs/drift/src/state/events.rs @@ -263,10 +263,14 @@ pub struct OrderActionRecord { pub builder_idx: Option, /// precision: QUOTE_PRECISION builder fee paid by the taker pub builder_fee: Option, + /// the idx of the builder in the maker's [`RevenueShareEscrow`] account + pub maker_builder_idx: Option, + /// precision: QUOTE_PRECISION builder fee paid by the maker + pub maker_builder_fee: Option, } impl Size for OrderActionRecord { - const SIZE: usize = 480; + const SIZE: usize = 496; } pub fn get_order_action_record( @@ -297,6 +301,8 @@ pub fn get_order_action_record( trigger_price: Option, builder_idx: Option, builder_fee: Option, + maker_builder_idx: Option, + maker_builder_fee: Option, ) -> DriftResult { Ok(OrderActionRecord { ts, @@ -352,6 +358,8 @@ pub fn get_order_action_record( trigger_price, builder_idx, builder_fee, + maker_builder_idx, + maker_builder_fee, }) } diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 3fe267fbc..f3c68bc57 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -5905,6 +5905,21 @@ export class DriftClient { }); } + for (const maker of makerInfo) { + const makerOrder = maker.order + ?? maker.makerUserAccount.orders.find((o) => hasBuilder(o)); + if (makerOrder && hasBuilder(makerOrder)) { + remainingAccounts.push({ + pubkey: getRevenueShareEscrowAccountPublicKey( + this.program.programId, + maker.makerUserAccount.authority + ), + isWritable: true, + isSigner: false, + }); + } + } + const orderId = isSignedMsg ? null : order.orderId; return await this.program.instruction.fillPerpOrder(orderId, null, { accounts: { @@ -7801,6 +7816,21 @@ export class DriftClient { }); } + for (const maker of makerInfo) { + const makerOrder = maker.order + ?? maker.makerUserAccount.orders.find((o) => hasBuilder(o)); + if (makerOrder && hasBuilder(makerOrder)) { + remainingAccounts.push({ + pubkey: getRevenueShareEscrowAccountPublicKey( + this.program.programId, + maker.makerUserAccount.authority + ), + isWritable: true, + isSigner: false, + }); + } + } + let optionalParams = null; if (auctionDurationPercentage || successCondition) { optionalParams = diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 8d9273813..39fe7aae3 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -17164,6 +17164,20 @@ "option": "u64" }, "index": false + }, + { + "name": "makerBuilderIdx", + "type": { + "option": "u8" + }, + "index": false + }, + { + "name": "makerBuilderFee", + "type": { + "option": "u64" + }, + "index": false } ] }, diff --git a/sdk/src/types.ts b/sdk/src/types.ts index e0d95ec13..2b5caf435 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -657,6 +657,11 @@ export type OrderActionRecord = { takerExistingBaseAssetAmount: BN | null; makerExistingQuoteEntryAmount: BN | null; makerExistingBaseAssetAmount: BN | null; + triggerPrice: BN | null; + builderIdx: number | null; + builderFee: BN | null; + makerBuilderIdx: number | null; + makerBuilderFee: BN | null; }; export type SwapRecord = {