diff --git a/CHANGELOG.md b/CHANGELOG.md index 8637ddf12..cc542cdd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - program: add oracle id for wen ([#1129](https://github.com/drift-labs/protocol-v2/pull/1129)) - program: track fuel ([#1048](https://github.com/drift-labs/protocol-v2/pull/1048)) +- program: track fuel for if staking ([#1127](https://github.com/drift-labs/protocol-v2/pull/1127)) - program: validate fee structure ([#1075](https://github.com/drift-labs/protocol-v2/pull/1075)) ### Fixes diff --git a/programs/drift/src/controller/insurance.rs b/programs/drift/src/controller/insurance.rs index 8fad2c4e8..f7c9686aa 100644 --- a/programs/drift/src/controller/insurance.rs +++ b/programs/drift/src/controller/insurance.rs @@ -11,7 +11,8 @@ use crate::error::ErrorCode; use crate::math::amm::calculate_net_user_pnl; use crate::math::casting::Cast; use crate::math::constants::{ - MAX_APR_PER_REVENUE_SETTLE_TO_INSURANCE_FUND_VAULT, ONE_YEAR, PERCENTAGE_PRECISION, + FUEL_WINDOW_U128, MAX_APR_PER_REVENUE_SETTLE_TO_INSURANCE_FUND_VAULT, ONE_YEAR, + PERCENTAGE_PRECISION, QUOTE_PRECISION_U64, SHARE_OF_REVENUE_ALLOCATED_TO_INSURANCE_FUND_VAULT_DENOMINATOR, SHARE_OF_REVENUE_ALLOCATED_TO_INSURANCE_FUND_VAULT_NUMERATOR, }; @@ -35,6 +36,64 @@ use crate::{emit, validate, GOV_SPOT_MARKET_INDEX, QUOTE_SPOT_MARKET_INDEX}; #[cfg(test)] mod tests; +pub fn update_user_stats_if_stake_amount( + amount: i64, + insurance_vault_amount: u64, + insurance_fund_stake: &mut InsuranceFundStake, + user_stats: &mut UserStats, + spot_market: &mut SpotMarket, + now: i64, +) -> DriftResult { + if spot_market.market_index != QUOTE_SPOT_MARKET_INDEX + && spot_market.market_index != GOV_SPOT_MARKET_INDEX + && spot_market.fuel_boost_insurance == 0 + { + return Ok(()); + } + + let if_stake_amount = if amount >= 0 { + if_shares_to_vault_amount( + insurance_fund_stake.checked_if_shares(spot_market)?, + spot_market.insurance_fund.total_shares, + insurance_vault_amount.safe_add(amount.unsigned_abs())?, + )? + } else { + if_shares_to_vault_amount( + insurance_fund_stake.checked_if_shares(spot_market)?, + spot_market.insurance_fund.total_shares, + insurance_vault_amount.safe_sub(amount.unsigned_abs())?, + )? + }; + + if spot_market.market_index == QUOTE_SPOT_MARKET_INDEX { + user_stats.if_staked_quote_asset_amount = if_stake_amount; + } else if spot_market.market_index == GOV_SPOT_MARKET_INDEX { + user_stats.if_staked_gov_token_amount = if_stake_amount; + } + + if spot_market.fuel_boost_insurance != 0 { + let now_u32: u32 = now.cast()?; + let since_last = now_u32.safe_sub(user_stats.last_fuel_if_bonus_update_ts)?; + + // calculate their stake amount prior to update + let fuel_bonus_insurance = if_stake_amount + .saturating_sub(amount.unsigned_abs()) + .cast::()? + .safe_mul(since_last.cast()?)? + .safe_mul(spot_market.fuel_boost_insurance.cast()?)? + .safe_div(FUEL_WINDOW_U128)? + .cast::()? + / (QUOTE_PRECISION_U64 / 10); + + user_stats.fuel_insurance = user_stats + .fuel_insurance + .saturating_add(fuel_bonus_insurance.cast()?); + user_stats.last_fuel_if_bonus_update_ts = now_u32; + } + + Ok(()) +} + pub fn add_insurance_fund_stake( amount: u64, insurance_vault_amount: u64, @@ -77,19 +136,14 @@ pub fn add_insurance_fund_stake( spot_market.insurance_fund.user_shares = spot_market.insurance_fund.user_shares.safe_add(n_shares)?; - if spot_market.market_index == QUOTE_SPOT_MARKET_INDEX { - user_stats.if_staked_quote_asset_amount = if_shares_to_vault_amount( - insurance_fund_stake.checked_if_shares(spot_market)?, - spot_market.insurance_fund.total_shares, - insurance_vault_amount.safe_add(amount)?, - )?; - } else if spot_market.market_index == GOV_SPOT_MARKET_INDEX { - user_stats.if_staked_gov_token_amount = if_shares_to_vault_amount( - insurance_fund_stake.checked_if_shares(spot_market)?, - spot_market.insurance_fund.total_shares, - insurance_vault_amount.safe_add(amount)?, - )?; - } + update_user_stats_if_stake_amount( + amount.cast()?, + insurance_vault_amount, + insurance_fund_stake, + user_stats, + spot_market, + now, + )?; let if_shares_after = insurance_fund_stake.checked_if_shares(spot_market)?; @@ -237,19 +291,14 @@ pub fn request_remove_insurance_fund_stake( let if_shares_after = insurance_fund_stake.checked_if_shares(spot_market)?; - if spot_market.market_index == QUOTE_SPOT_MARKET_INDEX { - user_stats.if_staked_quote_asset_amount = if_shares_to_vault_amount( - insurance_fund_stake.checked_if_shares(spot_market)?, - spot_market.insurance_fund.total_shares, - insurance_vault_amount, - )?; - } else if spot_market.market_index == GOV_SPOT_MARKET_INDEX { - user_stats.if_staked_gov_token_amount = if_shares_to_vault_amount( - insurance_fund_stake.checked_if_shares(spot_market)?, - spot_market.insurance_fund.total_shares, - insurance_vault_amount, - )?; - } + update_user_stats_if_stake_amount( + 0, + insurance_vault_amount, + insurance_fund_stake, + user_stats, + spot_market, + now, + )?; emit!(InsuranceFundStakeRecord { ts: now, @@ -314,19 +363,14 @@ pub fn cancel_request_remove_insurance_fund_stake( let if_shares_after = insurance_fund_stake.checked_if_shares(spot_market)?; - if spot_market.market_index == 0 { - user_stats.if_staked_quote_asset_amount = if_shares_to_vault_amount( - if_shares_after, - spot_market.insurance_fund.total_shares, - insurance_vault_amount, - )?; - } else if spot_market.market_index == GOV_SPOT_MARKET_INDEX { - user_stats.if_staked_gov_token_amount = if_shares_to_vault_amount( - insurance_fund_stake.checked_if_shares(spot_market)?, - spot_market.insurance_fund.total_shares, - insurance_vault_amount, - )?; - } + update_user_stats_if_stake_amount( + 0, + insurance_vault_amount, + insurance_fund_stake, + user_stats, + spot_market, + now, + )?; emit!(InsuranceFundStakeRecord { ts: now, @@ -415,19 +459,14 @@ pub fn remove_insurance_fund_stake( let if_shares_after = insurance_fund_stake.checked_if_shares(spot_market)?; - if spot_market.market_index == QUOTE_SPOT_MARKET_INDEX { - user_stats.if_staked_quote_asset_amount = if_shares_to_vault_amount( - if_shares_after, - spot_market.insurance_fund.total_shares, - insurance_vault_amount.safe_sub(amount)?, - )?; - } else if spot_market.market_index == GOV_SPOT_MARKET_INDEX { - user_stats.if_staked_gov_token_amount = if_shares_to_vault_amount( - if_shares_after, - spot_market.insurance_fund.total_shares, - insurance_vault_amount.safe_sub(amount)?, - )?; - } + update_user_stats_if_stake_amount( + -(withdraw_amount.cast()?), + insurance_vault_amount, + insurance_fund_stake, + user_stats, + spot_market, + now, + )?; emit!(InsuranceFundStakeRecord { ts: now, diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index 660f09de7..ba7844404 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -1725,11 +1725,12 @@ fn fulfill_perp_order( )?; user_stats.update_fuel_bonus( + user, taker_margin_calculation.fuel_deposits, taker_margin_calculation.fuel_borrows, taker_margin_calculation.fuel_positions, + now, )?; - user.last_fuel_bonus_update_ts = now; if !taker_margin_calculation.meets_margin_requirement() { msg!( @@ -1768,11 +1769,12 @@ fn fulfill_perp_order( if let Some(mut maker_stats) = maker_stats { maker_stats.update_fuel_bonus( + &mut maker, maker_margin_calculation.fuel_deposits, maker_margin_calculation.fuel_borrows, maker_margin_calculation.fuel_positions, + now, )?; - maker.last_fuel_bonus_update_ts = now; } if !maker_margin_calculation.meets_margin_requirement() { @@ -4091,12 +4093,14 @@ fn fulfill_spot_order( .fuel_numerator(user, now), )?; + // user hasnt recieved initial fuel or below global start time user_stats.update_fuel_bonus( + user, taker_margin_calculation.fuel_deposits, taker_margin_calculation.fuel_borrows, taker_margin_calculation.fuel_positions, + now, )?; - user.last_fuel_bonus_update_ts = now; if !taker_margin_calculation.meets_margin_requirement() { msg!( @@ -4159,7 +4163,7 @@ fn fulfill_spot_order( drop(base_market); drop(quote_market); - let maker_margin_calculation = + let maker_margin_calculation: MarginCalculation = calculate_margin_requirement_and_total_collateral_and_liability_info( &maker, perp_market_map, @@ -4183,12 +4187,12 @@ fn fulfill_spot_order( if let Some(mut maker_stats) = maker_stats { maker_stats.update_fuel_bonus( + &mut maker, maker_margin_calculation.fuel_deposits, maker_margin_calculation.fuel_borrows, maker_margin_calculation.fuel_positions, + now, )?; - - maker.last_fuel_bonus_update_ts = now; } if !maker_margin_calculation.meets_margin_requirement() { diff --git a/programs/drift/src/controller/orders/fuel_tests.rs b/programs/drift/src/controller/orders/fuel_tests.rs index c9e203604..b369d2eb4 100644 --- a/programs/drift/src/controller/orders/fuel_tests.rs +++ b/programs/drift/src/controller/orders/fuel_tests.rs @@ -285,7 +285,7 @@ pub mod fuel_scoring { assert_eq!(maker_stats_after.fuel_maker, 5000); assert_eq!(taker_stats.fuel_taker, 2500); - now += 1000000; + now += 100000; let mut margin_context = MarginContext::standard(MarginRequirementType::Initial); @@ -302,12 +302,13 @@ pub mod fuel_scoring { ) .is_err(); assert!(is_errored_attempted); - + assert_eq!(taker.last_fuel_bonus_update_ts as i64, 0); + taker.last_fuel_bonus_update_ts = FUEL_START_TS as u32; margin_context.fuel_bonus_numerator = taker_stats - .get_fuel_bonus_numerator(taker.last_fuel_bonus_update_ts, now) + .get_fuel_bonus_numerator(taker.last_fuel_bonus_update_ts as i64, now) .unwrap(); - assert_eq!(margin_context.fuel_bonus_numerator, 1000000); - assert_eq!(taker.last_fuel_bonus_update_ts, FUEL_START_TS); + assert_eq!(margin_context.fuel_bonus_numerator, 100000); + assert_eq!(taker.last_fuel_bonus_update_ts as i64, FUEL_START_TS); let margin_calc: MarginCalculation = taker .calculate_margin_and_increment_fuel_bonus( @@ -320,8 +321,8 @@ pub mod fuel_scoring { ) .unwrap(); - assert_eq!(margin_calc.fuel_positions, 51669); - // assert_eq!(taker_stats.fuel_positions, 25000000000 + margin_calc.fuel_positions); + assert_eq!(margin_calc.fuel_positions, 5166); + assert_eq!(taker_stats.fuel_positions, margin_calc.fuel_positions); } #[test] diff --git a/programs/drift/src/ids.rs b/programs/drift/src/ids.rs index f5ee137d6..a4736e21d 100644 --- a/programs/drift/src/ids.rs +++ b/programs/drift/src/ids.rs @@ -118,3 +118,8 @@ pub mod usdt_pull_oracle { use solana_program::declare_id; declare_id!("BekJ3P5G3iFeC97sXHuKnUHofCFj9Sbo7uyF2fkKwvit"); } + +pub mod fuel_airdrop_wallet { + use solana_program::declare_id; + declare_id!("5hMjmxexWu954pX9gB9jkHxMqdjpxArQS2XdvkaevRax"); +} diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index d3893c5d0..d1429f24d 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -11,6 +11,7 @@ use solana_program::msg; use crate::controller::token::close_vault; use crate::error::ErrorCode; +use crate::ids::fuel_airdrop_wallet; use crate::instructions::constraints::*; use crate::math::casting::Cast; use crate::math::constants::{ @@ -49,7 +50,7 @@ use crate::state::spot_market::{ }; use crate::state::state::{ExchangeStatus, FeeStructure, OracleGuardRails, State}; use crate::state::traits::Size; -use crate::state::user::UserStats; +use crate::state::user::{User, UserStats}; use crate::validate; use crate::validation::fee_structure::validate_fee_structure; use crate::validation::margin::{validate_margin, validate_margin_weights}; @@ -290,7 +291,8 @@ pub fn handle_initialize_spot_market( fuel_boost_borrows: 0, fuel_boost_taker: 0, fuel_boost_maker: 0, - padding: [0; 43], + fuel_boost_insurance: 0, + padding: [0; 42], insurance_fund: InsuranceFund { vault: *ctx.accounts.insurance_fund_vault.to_account_info().key, unstaking_period: THIRTEEN_DAY, @@ -1054,6 +1056,60 @@ pub fn handle_update_spot_market_expiry( Ok(()) } +pub fn handle_init_user_fuel( + ctx: Context, + fuel_bonus_deposits: Option, + fuel_bonus_borrows: Option, + fuel_bonus_taker: Option, + fuel_bonus_maker: Option, + fuel_bonus_insurance: Option, +) -> Result<()> { + let clock: Clock = Clock::get()?; + let now_u32 = clock.unix_timestamp as u32; + + let user = &mut load_mut!(ctx.accounts.user)?; + let user_stats = &mut load_mut!(ctx.accounts.user_stats)?; + + validate!( + user.last_fuel_bonus_update_ts == 0, + ErrorCode::DefaultError, + "User must not have begun earning fuel" + )?; + + if let Some(fuel_bonus_deposits) = fuel_bonus_deposits { + user_stats.fuel_deposits = user_stats + .fuel_deposits + .saturating_add(fuel_bonus_deposits.cast()?); + } + if let Some(fuel_bonus_borrows) = fuel_bonus_borrows { + user_stats.fuel_borrows = user_stats + .fuel_borrows + .saturating_add(fuel_bonus_borrows.cast()?); + } + + if let Some(fuel_bonus_taker) = fuel_bonus_taker { + user_stats.fuel_taker = user_stats + .fuel_taker + .saturating_add(fuel_bonus_taker.cast()?); + } + if let Some(fuel_bonus_maker) = fuel_bonus_maker { + user_stats.fuel_maker = user_stats + .fuel_maker + .saturating_add(fuel_bonus_maker.cast()?); + } + + if let Some(fuel_bonus_insurance) = fuel_bonus_insurance { + user_stats.fuel_insurance = user_stats + .fuel_insurance + .saturating_add(fuel_bonus_insurance.cast()?); + } + + user.last_fuel_bonus_update_ts = now_u32; + user_stats.last_fuel_if_bonus_update_ts = now_u32; + + Ok(()) +} + #[access_control( perp_market_valid(&ctx.accounts.perp_market) )] @@ -1061,7 +1117,7 @@ pub fn handle_update_perp_market_expiry( ctx: Context, expiry_ts: i64, ) -> Result<()> { - let clock = Clock::get()?; + let clock: Clock = Clock::get()?; let perp_market = &mut load_mut!(ctx.accounts.perp_market)?; msg!("updating perp market {} expiry", perp_market.market_index); @@ -3449,6 +3505,7 @@ pub fn handle_update_spot_market_fuel( fuel_boost_borrows: Option, fuel_boost_taker: Option, fuel_boost_maker: Option, + fuel_boost_insurance: Option, ) -> Result<()> { let spot_market = &mut load_mut!(ctx.accounts.spot_market)?; msg!("spot market {}", spot_market.market_index); @@ -3497,6 +3554,17 @@ pub fn handle_update_spot_market_fuel( msg!("perp_market.fuel_boost_borrows: unchanged"); } + if let Some(fuel_boost_insurance) = fuel_boost_insurance { + msg!( + "perp_market.fuel_boost_insurance: {:?} -> {:?}", + spot_market.fuel_boost_insurance, + fuel_boost_insurance + ); + spot_market.fuel_boost_insurance = fuel_boost_insurance; + } else { + msg!("perp_market.fuel_boost_insurance: unchanged"); + } + Ok(()) } @@ -4154,6 +4222,19 @@ pub struct AdminDisableBidAskTwapUpdate<'info> { pub user_stats: AccountLoader<'info, UserStats>, } +#[derive(Accounts)] +pub struct UpdateUserFuel<'info> { + #[account( + address = fuel_airdrop_wallet::id() + )] + pub admin: Signer<'info>, // todo + pub state: Box>, + #[account(mut)] + pub user: AccountLoader<'info, User>, + #[account(mut)] + pub user_stats: AccountLoader<'info, UserStats>, +} + #[derive(Accounts)] pub struct InitializeProtocolIfSharesTransferConfig<'info> { #[account(mut)] diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index 589c7d1d6..3b102fa29 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -1,11 +1,11 @@ use anchor_lang::prelude::*; use anchor_spl::token::{Token, TokenAccount}; +use crate::controller::insurance::update_user_stats_if_stake_amount; use crate::error::ErrorCode; use crate::instructions::constraints::*; use crate::instructions::optional_accounts::{load_maps, AccountMaps}; use crate::math::constants::QUOTE_SPOT_MARKET_INDEX; -use crate::math::insurance::if_shares_to_vault_amount; use crate::math::margin::{calculate_user_equity, meets_settle_pnl_maintenance_margin_requirement}; use crate::math::orders::{estimate_price_from_side, find_bids_and_asks_from_users}; use crate::math::spot_withdraw::validate_spot_market_vault_amount; @@ -1580,10 +1580,16 @@ pub fn handle_update_user_quote_asset_insurance_stake( )?; if insurance_fund_stake.market_index == 0 && spot_market.market_index == 0 { - user_stats.if_staked_quote_asset_amount = if_shares_to_vault_amount( - insurance_fund_stake.checked_if_shares(spot_market)?, - spot_market.insurance_fund.total_shares, + let clock = Clock::get()?; + let now = clock.unix_timestamp; + + update_user_stats_if_stake_amount( + 0, ctx.accounts.insurance_fund_vault.amount, + insurance_fund_stake, + user_stats, + spot_market, + now, )?; } @@ -1607,10 +1613,16 @@ pub fn handle_update_user_gov_token_insurance_stake( if insurance_fund_stake.market_index == GOV_SPOT_MARKET_INDEX && spot_market.market_index == GOV_SPOT_MARKET_INDEX { - user_stats.if_staked_gov_token_amount = if_shares_to_vault_amount( - insurance_fund_stake.checked_if_shares(spot_market)?, - spot_market.insurance_fund.total_shares, + let clock = Clock::get()?; + let now = clock.unix_timestamp; + + update_user_stats_if_stake_amount( + 0, ctx.accounts.insurance_fund_vault.amount, + insurance_fund_stake, + user_stats, + spot_market, + now, )?; } diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 7b7f2578b..9304c1651 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -149,7 +149,7 @@ pub fn handle_initialize_user<'c: 'info, 'info>( let now_ts = Clock::get()?.unix_timestamp; - user.last_fuel_bonus_update_ts = now_ts; + user.last_fuel_bonus_update_ts = now_ts.cast()?; emit!(NewUserRecord { ts: now_ts, @@ -205,6 +205,7 @@ pub fn handle_initialize_user_stats<'c: 'info, 'info>( last_taker_volume_30d_ts: clock.unix_timestamp, last_maker_volume_30d_ts: clock.unix_timestamp, last_filler_volume_30d_ts: clock.unix_timestamp, + last_fuel_if_bonus_update_ts: clock.unix_timestamp.cast()?, ..UserStats::default() }; diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index d970edc7f..612f6dca0 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -1264,6 +1264,7 @@ pub mod drift { fuel_boost_borrows: Option, fuel_boost_taker: Option, fuel_boost_maker: Option, + fuel_boost_insurance: Option, ) -> Result<()> { handle_update_spot_market_fuel( ctx, @@ -1271,6 +1272,7 @@ pub mod drift { fuel_boost_borrows, fuel_boost_taker, fuel_boost_maker, + fuel_boost_insurance, ) } diff --git a/programs/drift/src/math/constants.rs b/programs/drift/src/math/constants.rs index 2825ff2ab..5e7602850 100644 --- a/programs/drift/src/math/constants.rs +++ b/programs/drift/src/math/constants.rs @@ -205,4 +205,4 @@ pub const SPOT_MARKET_TOKEN_TWAP_WINDOW: i64 = TWENTY_FOUR_HOUR; // FUEL pub const FUEL_WINDOW_U128: u128 = EPOCH_DURATION as u128; -pub const FUEL_START_TS: i64 = 1715745600_i64; // May 15 2024 UTC +pub const FUEL_START_TS: i64 = 1722384000_i64; // July 31 2024 UTC diff --git a/programs/drift/src/state/spot_market.rs b/programs/drift/src/state/spot_market.rs index 776cca8aa..56a4c79ce 100644 --- a/programs/drift/src/state/spot_market.rs +++ b/programs/drift/src/state/spot_market.rs @@ -197,7 +197,10 @@ pub struct SpotMarket { /// fuel multiplier for spot maker /// precision: 10 pub fuel_boost_maker: u8, - pub padding: [u8; 43], + /// fuel multiplier for spot insurance stake + /// precision: 10 + pub fuel_boost_insurance: u8, + pub padding: [u8; 42], } impl Default for SpotMarket { @@ -263,7 +266,8 @@ impl Default for SpotMarket { fuel_boost_borrows: 0, fuel_boost_taker: 0, fuel_boost_maker: 0, - padding: [0; 43], + fuel_boost_insurance: 0, + padding: [0; 42], } } } diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index d647c3dd4..68e74d860 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -127,8 +127,8 @@ pub struct User { /// Whether or not user has open order with auction pub has_open_auction: bool, pub padding1: [u8; 5], - pub last_fuel_bonus_update_ts: i64, - pub padding: [u8; 8], + pub last_fuel_bonus_update_ts: u32, + pub padding: [u8; 12], } impl User { @@ -434,27 +434,17 @@ impl User { pub fn get_fuel_bonus_numerator(&self, now: i64) -> DriftResult { if self.last_fuel_bonus_update_ts > 0 { - now.safe_sub(self.last_fuel_bonus_update_ts) + now.safe_sub(self.last_fuel_bonus_update_ts.cast()?) } else { // start ts for existing accounts pre fuel - return Ok(now.safe_sub(FUEL_START_TS)?.max(0)); + if now > FUEL_START_TS { + return Ok(now.safe_sub(FUEL_START_TS)?); + } else { + return Ok(0); + } } } - pub fn increment_fuel_bonus( - &mut self, - fuel_deposits: u32, - fuel_borrows: u32, - fuel_positions: u32, - user_stats: &mut UserStats, - now: i64, - ) -> DriftResult { - user_stats.update_fuel_bonus(fuel_deposits, fuel_borrows, fuel_positions)?; - self.last_fuel_bonus_update_ts = now; - - Ok(()) - } - pub fn calculate_margin_and_increment_fuel_bonus( &mut self, perp_market_map: &PerpMarketMap, @@ -486,13 +476,13 @@ impl User { )?; user_stats.update_fuel_bonus( + self, margin_calculation.fuel_deposits, margin_calculation.fuel_borrows, margin_calculation.fuel_positions, + now, )?; - self.last_fuel_bonus_update_ts = now; - Ok(margin_calculation) } @@ -540,11 +530,12 @@ impl User { )?; user_stats.update_fuel_bonus( + self, calculation.fuel_deposits, calculation.fuel_borrows, calculation.fuel_positions, + now, )?; - self.last_fuel_bonus_update_ts = now; Ok(true) } @@ -1541,22 +1532,27 @@ pub struct UserStats { /// Whether the user is a referrer. Sub account 0 can not be deleted if user is a referrer pub is_referrer: bool, pub disable_update_perp_bid_ask_twap: bool, - pub padding1: [u8; 6], - /// sub account id for spot deposit, borrow fuel tracking + pub padding1: [u8; 2], + /// accumulated fuel for token amounts of insurance + pub fuel_insurance: u32, + /// accumulated fuel for notional of deposits pub fuel_deposits: u32, - /// accumulate fuel bonus for epoch + /// accumulate fuel bonus for notional of borrows pub fuel_borrows: u32, /// accumulated fuel for perp open interest pub fuel_positions: u32, - /// accumulate fuel bonus for epoch + /// accumulate fuel bonus for taker volume pub fuel_taker: u32, - /// accumulate fuel bonus for epoch + /// accumulate fuel bonus for maker volume pub fuel_maker: u32, /// The amount of tokens staked in the governance spot markets if pub if_staked_gov_token_amount: u64, - pub padding: [u8; 16], + /// last unix ts user stats data was used to update if fuel (u32 to save space) + pub last_fuel_if_bonus_update_ts: u32, + + pub padding: [u8; 12], } impl Default for UserStats { @@ -1577,14 +1573,16 @@ impl Default for UserStats { number_of_sub_accounts_created: 0, is_referrer: false, disable_update_perp_bid_ask_twap: false, - padding1: [0; 6], + padding1: [0; 2], + fuel_insurance: 0, fuel_deposits: 0, fuel_borrows: 0, fuel_taker: 0, fuel_maker: 0, fuel_positions: 0, if_staked_gov_token_amount: 0, - padding: [0; 16], + last_fuel_if_bonus_update_ts: 0, + padding: [0; 12], } } } @@ -1599,8 +1597,12 @@ impl UserStats { last_fuel_bonus_update_ts: i64, now: i64, ) -> DriftResult { - let since_last = now.safe_sub(last_fuel_bonus_update_ts)?; - Ok(since_last) + if last_fuel_bonus_update_ts != 0 { + let since_last = now.safe_sub(last_fuel_bonus_update_ts)?; + return Ok(since_last); + } + + Ok(0) } pub fn update_fuel_bonus_trade(&mut self, fuel_taker: u32, fuel_maker: u32) -> DriftResult { @@ -1612,13 +1614,19 @@ impl UserStats { pub fn update_fuel_bonus( &mut self, + user: &mut User, fuel_deposits: u32, fuel_borrows: u32, fuel_positions: u32, + now: i64, ) -> DriftResult { - self.fuel_deposits = self.fuel_deposits.saturating_add(fuel_deposits); - self.fuel_borrows = self.fuel_borrows.saturating_add(fuel_borrows); - self.fuel_positions = self.fuel_positions.saturating_add(fuel_positions); + if user.last_fuel_bonus_update_ts != 0 || now > FUEL_START_TS { + self.fuel_deposits = self.fuel_deposits.saturating_add(fuel_deposits); + self.fuel_borrows = self.fuel_borrows.saturating_add(fuel_borrows); + self.fuel_positions = self.fuel_positions.saturating_add(fuel_positions); + + user.last_fuel_bonus_update_ts = now.cast()?; + } Ok(()) } diff --git a/sdk/src/adminClient.ts b/sdk/src/adminClient.ts index b3359fef7..ab3501901 100644 --- a/sdk/src/adminClient.ts +++ b/sdk/src/adminClient.ts @@ -3538,14 +3538,16 @@ export class AdminClient extends DriftClient { fuelBoostDeposits?: number, fuelBoostBorrows?: number, fuelBoostTaker?: number, - fuelBoostMaker?: number + fuelBoostMaker?: number, + fuelBoostInsurance?: number ): Promise { const updateSpotMarketFuelIx = await this.getUpdateSpotMarketFuelIx( spotMarketIndex, fuelBoostDeposits || null, fuelBoostBorrows || null, fuelBoostTaker || null, - fuelBoostMaker || null + fuelBoostMaker || null, + fuelBoostInsurance || null ); const tx = await this.buildTransaction(updateSpotMarketFuelIx); @@ -3559,7 +3561,8 @@ export class AdminClient extends DriftClient { fuelBoostDeposits?: number, fuelBoostBorrows?: number, fuelBoostTaker?: number, - fuelBoostMaker?: number + fuelBoostMaker?: number, + fuelBoostInsurance?: number ): Promise { const spotMarketPublicKey = await getSpotMarketPublicKey( this.program.programId, @@ -3571,6 +3574,7 @@ export class AdminClient extends DriftClient { fuelBoostBorrows || null, fuelBoostTaker || null, fuelBoostMaker || null, + fuelBoostInsurance || null, { accounts: { admin: this.isSubscribed diff --git a/sdk/src/bankrun/bankrunConnection.ts b/sdk/src/bankrun/bankrunConnection.ts index 0cd9dc987..55cc53c4c 100644 --- a/sdk/src/bankrun/bankrunConnection.ts +++ b/sdk/src/bankrun/bankrunConnection.ts @@ -118,6 +118,19 @@ export class BankrunContextWrapper { ); await this.context.setClock(newClock); } + + async setTimestamp(unix_timestamp: number): Promise { + const currentClock = await this.context.banksClient.getClock(); + const newUnixTimestamp = BigInt(unix_timestamp); + const newClock = new Clock( + currentClock.slot, + currentClock.epochStartTimestamp, + currentClock.epoch, + currentClock.leaderScheduleEpoch, + newUnixTimestamp + ); + await this.context.setClock(newClock); + } } export class BankrunConnection { diff --git a/sdk/src/constants/numericConstants.ts b/sdk/src/constants/numericConstants.ts index 8403450ac..d859fb696 100644 --- a/sdk/src/constants/numericConstants.ts +++ b/sdk/src/constants/numericConstants.ts @@ -90,6 +90,7 @@ export const ONE_HOUR = new BN(60 * 60); export const ONE_YEAR = new BN(31536000); export const QUOTE_SPOT_MARKET_INDEX = 0; +export const GOV_SPOT_MARKET_INDEX = 15; export const LAMPORTS_PRECISION = new BN(LAMPORTS_PER_SOL); export const LAMPORTS_EXP = new BN(Math.log10(LAMPORTS_PER_SOL)); @@ -107,4 +108,4 @@ export const SLOT_TIME_ESTIMATE_MS = 400; export const DUST_POSITION_SIZE = QUOTE_PRECISION.divn(100); // Dust position is any position smaller than 1c export const FUEL_WINDOW = new BN(60 * 60 * 24 * 28); // 28 days -export const FUEL_START_TS = new BN(1715745600); // unix timestamp +export const FUEL_START_TS = new BN(1722384000); // unix timestamp diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index 42cc46aab..8d8b40b88 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -5198,6 +5198,12 @@ "type": { "option": "u8" } + }, + { + "name": "fuelBoostInsurance", + "type": { + "option": "u8" + } } ] }, @@ -6554,12 +6560,20 @@ ], "type": "u8" }, + { + "name": "fuelBoostInsurance", + "docs": [ + "fuel multiplier for spot insurance stake", + "precision: 10" + ], + "type": "u8" + }, { "name": "padding", "type": { "array": [ "u8", - 43 + 42 ] } } @@ -6918,14 +6932,14 @@ }, { "name": "lastFuelBonusUpdateTs", - "type": "i64" + "type": "u32" }, { "name": "padding", "type": { "array": [ "u8", - 8 + 12 ] } } @@ -7051,21 +7065,28 @@ "type": { "array": [ "u8", - 6 + 2 ] } }, + { + "name": "fuelInsurance", + "docs": [ + "accumulated fuel for token amounts of insurance" + ], + "type": "u32" + }, { "name": "fuelDeposits", "docs": [ - "sub account id for spot deposit, borrow fuel tracking" + "accumulated fuel for notional of deposits" ], "type": "u32" }, { "name": "fuelBorrows", "docs": [ - "accumulate fuel bonus for epoch" + "accumulate fuel bonus for notional of borrows" ], "type": "u32" }, @@ -7079,14 +7100,14 @@ { "name": "fuelTaker", "docs": [ - "accumulate fuel bonus for epoch" + "accumulate fuel bonus for taker volume" ], "type": "u32" }, { "name": "fuelMaker", "docs": [ - "accumulate fuel bonus for epoch" + "accumulate fuel bonus for maker volume" ], "type": "u32" }, @@ -7097,12 +7118,19 @@ ], "type": "u64" }, + { + "name": "lastFuelIfBonusUpdateTs", + "docs": [ + "last unix ts user stats data was used to update if fuel (u32 to save space)" + ], + "type": "u32" + }, { "name": "padding", "type": { "array": [ "u8", - 16 + 12 ] } } diff --git a/sdk/src/math/fuel.ts b/sdk/src/math/fuel.ts index 095105559..89eefd260 100644 --- a/sdk/src/math/fuel.ts +++ b/sdk/src/math/fuel.ts @@ -6,6 +6,20 @@ import { FUEL_WINDOW, } from '../constants/numericConstants'; +export function calculateInsuranceFuelBonus( + spotMarket: SpotMarketAccount, + tokenStakeAmount: BN, + fuelBonusNumerator: BN +): BN { + const result = tokenStakeAmount + .abs() + .mul(fuelBonusNumerator) + .mul(new BN(spotMarket.fuelBoostInsurance)) + .div(FUEL_WINDOW) + .div(QUOTE_PRECISION.div(new BN(10))); + return result; +} + export function calculateSpotFuelBonus( spotMarket: SpotMarketAccount, signedTokenValue: BN, @@ -13,7 +27,7 @@ export function calculateSpotFuelBonus( ): BN { let result: BN; - if (signedTokenValue.abs().lt(new BN(1))) { + if (signedTokenValue.abs().lte(QUOTE_PRECISION)) { result = ZERO; } else if (signedTokenValue.gt(new BN(0))) { result = signedTokenValue diff --git a/sdk/src/types.ts b/sdk/src/types.ts index 4ebc2888a..08ef678f7 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -637,9 +637,9 @@ export type PerpMarketAccount = { feeAdjustment: number; pausedOperations: number; - fuelBoostPosition: number; - fuelBoostMaker: number; fuelBoostTaker: number; + fuelBoostMaker: number; + fuelBoostPosition: number; }; export type HistoricalOracleData = { @@ -741,8 +741,9 @@ export type SpotMarketAccount = { fuelBoostDeposits: number; fuelBoostBorrows: number; - fuelBoostMaker: number; fuelBoostTaker: number; + fuelBoostMaker: number; + fuelBoostInsurance: number; }; export type PoolBalance = { @@ -886,6 +887,9 @@ export type UserStatsAccount = { authority: PublicKey; ifStakedQuoteAssetAmount: BN; + lastFuelBonusUpdateTs: number; // u32 onchain + + fuelInsurance: number; fuelDeposits: number; fuelBorrows: number; fuelPositions: number; @@ -922,8 +926,7 @@ export type UserAccount = { hasOpenOrder: boolean; openAuctions: number; hasOpenAuction: boolean; - - lastFuelBonusUpdateTs: BN; + lastFuelBonusUpdateTs: number; }; export type SpotPosition = { diff --git a/sdk/src/user.ts b/sdk/src/user.ts index 7b7c61fdb..958d0a4d3 100644 --- a/sdk/src/user.ts +++ b/sdk/src/user.ts @@ -31,6 +31,7 @@ import { QUOTE_PRECISION_EXP, QUOTE_SPOT_MARKET_INDEX, SPOT_MARKET_WEIGHT_PRECISION, + GOV_SPOT_MARKET_INDEX, TEN, TEN_THOUSAND, TWO, @@ -89,7 +90,11 @@ import { calculateLiveOracleTwap } from './math/oracles'; import { getPerpMarketTierNumber, getSpotMarketTierNumber } from './math/tiers'; import { StrictOraclePrice } from './oracles/strictOraclePrice'; -import { calculateSpotFuelBonus, calculatePerpFuelBonus } from './math/fuel'; +import { + calculateSpotFuelBonus, + calculatePerpFuelBonus, + calculateInsuranceFuelBonus, +} from './math/fuel'; export class User { driftClient: DriftClient; @@ -890,6 +895,7 @@ export class User { const userAccount: UserAccount = this.getUserAccount(); const result = { + insuranceFuel: ZERO, takerFuel: ZERO, makerFuel: ZERO, depositFuel: ZERO, @@ -914,7 +920,9 @@ export class User { if (includeUnsettled) { const fuelBonusNumerator = BN.max( - now.sub(BN.max(userAccount.lastFuelBonusUpdateTs, FUEL_START_TS)), + now.sub( + BN.max(new BN(userAccount.lastFuelBonusUpdateTs), FUEL_START_TS) + ), ZERO ); @@ -988,6 +996,28 @@ export class User { ); } } + + const userStats: UserStatsAccount = this.driftClient + .getUserStats() + .getAccount(); + + // todo: get real time ifStakedGovTokenAmount using ifStakeAccount + if (userStats.ifStakedGovTokenAmount.gt(ZERO)) { + const spotMarketAccount: SpotMarketAccount = + this.driftClient.getSpotMarketAccount(GOV_SPOT_MARKET_INDEX); + + const fuelBonusNumeratorUserStats = now.sub( + new BN(userStats.lastFuelBonusUpdateTs) + ); + + result.insuranceFuel = result.insuranceFuel.add( + calculateInsuranceFuelBonus( + spotMarketAccount, + userStats.ifStakedGovTokenAmount, + fuelBonusNumeratorUserStats + ) + ); + } } return result; diff --git a/sdk/tests/dlob/helpers.ts b/sdk/tests/dlob/helpers.ts index 374fc0083..c204efdf2 100644 --- a/sdk/tests/dlob/helpers.ts +++ b/sdk/tests/dlob/helpers.ts @@ -187,6 +187,9 @@ export const mockPerpMarkets: Array = [ quoteSpotMarketIndex: 0, feeAdjustment: 0, pausedOperations: 0, + fuelBoostPosition: 0, + fuelBoostMaker: 0, + fuelBoostTaker: 0, }, { status: MarketStatus.INITIALIZED, @@ -226,6 +229,9 @@ export const mockPerpMarkets: Array = [ quoteSpotMarketIndex: 0, feeAdjustment: 0, pausedOperations: 0, + fuelBoostPosition: 0, + fuelBoostMaker: 0, + fuelBoostTaker: 0, }, { status: MarketStatus.INITIALIZED, @@ -265,6 +271,9 @@ export const mockPerpMarkets: Array = [ quoteSpotMarketIndex: 0, feeAdjustment: 0, pausedOperations: 0, + fuelBoostPosition: 0, + fuelBoostMaker: 0, + fuelBoostTaker: 0, }, ]; @@ -351,6 +360,13 @@ export const mockSpotMarkets: Array = [ }, pausedOperations: 0, ifPausedOperations: 0, + maxTokenBorrowsFraction: 0, + minBorrowRate: 0, + fuelBoostDeposits: 0, + fuelBoostBorrows: 0, + fuelBoostTaker: 0, + fuelBoostMaker: 0, + fuelBoostInsurance: 0, }, { status: MarketStatus.ACTIVE, @@ -434,6 +450,13 @@ export const mockSpotMarkets: Array = [ }, pausedOperations: 0, ifPausedOperations: 0, + maxTokenBorrowsFraction: 0, + minBorrowRate: 0, + fuelBoostDeposits: 0, + fuelBoostBorrows: 0, + fuelBoostTaker: 0, + fuelBoostMaker: 0, + fuelBoostInsurance: 0, }, { status: MarketStatus.ACTIVE, @@ -517,6 +540,13 @@ export const mockSpotMarkets: Array = [ }, pausedOperations: 0, ifPausedOperations: 0, + maxTokenBorrowsFraction: 0, + minBorrowRate: 0, + fuelBoostDeposits: 0, + fuelBoostBorrows: 0, + fuelBoostTaker: 0, + fuelBoostMaker: 0, + fuelBoostInsurance: 0, }, ]; diff --git a/sdk/tests/user/helpers.ts b/sdk/tests/user/helpers.ts index afd3bb924..ae2e81b10 100644 --- a/sdk/tests/user/helpers.ts +++ b/sdk/tests/user/helpers.ts @@ -85,4 +85,5 @@ export const mockUserAccount: UserAccount = { hasOpenOrder: false, openAuctions: 0, hasOpenAuction: false, + lastFuelBonusUpdateTs: 0, }; diff --git a/sdk/tests/user/test.ts b/sdk/tests/user/test.ts index 72711beeb..de6e4686e 100644 --- a/sdk/tests/user/test.ts +++ b/sdk/tests/user/test.ts @@ -35,6 +35,8 @@ async function makeMockUser( const mockUser: User = await umap.mustGet('1'); mockUser._isSubscribed = true; mockUser.driftClient._isSubscribed = true; + mockUser.driftClient.accountSubscriber.isSubscribed = true; + const oraclePriceMap = {}; // console.log(perpOraclePriceList, myMockPerpMarkets.length); // console.log(spotOraclePriceList, myMockSpotMarkets.length); diff --git a/tests/fuel.ts b/tests/fuel.ts index c5e7d90f6..208286719 100644 --- a/tests/fuel.ts +++ b/tests/fuel.ts @@ -17,6 +17,7 @@ import { OracleSource, ONE, ContractTier, + FUEL_START_TS, } from '../sdk/src'; import { @@ -201,6 +202,8 @@ describe('place and fill spot order', () => { }, }); await fillerDriftClientUser.subscribe(); + + await bankrunContextWrapper.setTimestamp(FUEL_START_TS.toNumber()); }); after(async () => { @@ -498,14 +501,14 @@ describe('place and fill spot order', () => { // withdraw/borrow .01 sol await takerDriftClient.withdraw( - new BN(LAMPORTS_PER_SOL / 100), + new BN(LAMPORTS_PER_SOL / 10), 1, takerDriftClient.provider.wallet.publicKey ); console.log(takerDriftClientUser.getTokenAmount(1).toString()); assert(takerDriftClientUser.getTokenAmount(1).lt(ZERO)); // 2 for rounding purposes? - assert(takerDriftClientUser.getTokenAmount(1).eqn(-10000001)); // 2 for rounding purposes? + assert(takerDriftClientUser.getTokenAmount(1).toString() == '-100000001'); // 2 for rounding purposes? const fuelDictAfter2 = takerDriftClientUser.getFuelBonus( new BN(currentClock2.unixTimestamp.toString()).addn(36000), @@ -513,9 +516,12 @@ describe('place and fill spot order', () => { true ); console.log(fuelDictAfter2); + + assert(takerDriftClient.getSpotMarketAccount(1).fuelBoostBorrows == 100); + assert(fuelDictAfter2['depositFuel'].gt(ZERO)); assert(fuelDictAfter2['depositFuel'].eqn(2171)); - assert(fuelDictAfter2['borrowFuel'].eqn(4)); + assert(fuelDictAfter2['borrowFuel'].eqn(48)); await takerDriftClientUser.unsubscribe(); await takerDriftClient.unsubscribe(); @@ -604,15 +610,16 @@ describe('place and fill spot order', () => { const makerUSDCAmount = makerDriftClient.getQuoteAssetTokenAmount(); const makerSolAmount = makerDriftClient.getTokenAmount(1); console.log(makerUSDCAmount.toString(), makerSolAmount.toString()); - assert(makerUSDCAmount.gte(new BN(139607920))); - assert(makerSolAmount.lte(new BN(-989999999))); // round borrows up + assert(makerUSDCAmount.gte(new BN(136007200))); + assert(makerSolAmount.lte(new BN(-899999999))); // round borrows up const takerUSDCAmount = takerDriftClient.getQuoteAssetTokenAmount(); const takerSolAmount = takerDriftClient.getTokenAmount(1); console.log(takerUSDCAmount.toString(), takerSolAmount.toString()); - assert(takerUSDCAmount.eq(new BN(60360400))); - assert(takerSolAmount.eq(new BN(989999997))); + assert(takerUSDCAmount.eq(new BN(63964000))); + assert(takerSolAmount.gte(new BN(899999995))); + assert(takerSolAmount.lte(new BN(899999999))); console.log(fillerDriftClient.getQuoteAssetTokenAmount().toNumber()); @@ -629,7 +636,7 @@ describe('place and fill spot order', () => { ); console.log(fuelDictTaker); assert(fuelDictTaker['takerFuel'].gt(ZERO)); - assert(fuelDictTaker['takerFuel'].eqn(3900)); + assert(fuelDictTaker['takerFuel'].eqn(3600)); const fuelDictMaker = makerDriftClientUser.getFuelBonus( new BN(currentClock2.unixTimestamp.toString()), @@ -639,7 +646,7 @@ describe('place and fill spot order', () => { // console.log(fuelDictMaker); assert(fuelDictMaker['takerFuel'].eq(ZERO)); assert(fuelDictMaker['makerFuel'].gt(ZERO)); - assert(fuelDictMaker['makerFuel'].eqn(3900 * 2)); + assert(fuelDictMaker['makerFuel'].eqn(3600 * 2)); await takerDriftClientUser.unsubscribe(); await takerDriftClient.unsubscribe();