Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

program: surge pricing for user init #752

Merged
merged 12 commits into from
Dec 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Features

- program: add init user fee ([#752](https://github.com/drift-labs/protocol-v2/pull/752))
- program: vamm gives maker rebate ([#653](https://github.com/drift-labs/protocol-v2/pull/653))

### Fixes
Expand Down
2 changes: 2 additions & 0 deletions programs/drift/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,8 @@ pub enum ErrorCode {
UserReduceOnly,
#[msg("InvalidMarginCalculation")]
InvalidMarginCalculation,
#[msg("CantPayUserInitFee")]
CantPayUserInitFee,
}

#[macro_export]
Expand Down
11 changes: 10 additions & 1 deletion programs/drift/src/instructions/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
liquidation_duration: 0,
initial_pct_to_liquidate: 0,
max_number_of_sub_accounts: 0,
padding: [0; 12],
max_initialize_user_fee: 0,
padding: [0; 10],
};

Ok(())
Expand Down Expand Up @@ -1970,6 +1971,14 @@ pub fn handle_update_state_max_number_of_sub_accounts(
Ok(())
}

pub fn handle_update_state_max_initialize_user_fee(
ctx: Context<AdminUpdateState>,
max_initialize_user_fee: u16,
) -> Result<()> {
ctx.accounts.state.max_initialize_user_fee = max_initialize_user_fee;
Ok(())
}

#[access_control(
perp_market_valid(&ctx.accounts.perp_market)
)]
Expand Down
47 changes: 45 additions & 2 deletions programs/drift/src/instructions/user.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use anchor_lang::prelude::*;
use anchor_lang::Discriminator;
use anchor_spl::token::{Token, TokenAccount};
use solana_program::program::invoke;
use solana_program::system_instruction::transfer;

use crate::controller::orders::{cancel_orders, ModifyOrderId};
use crate::controller::position::PositionDirection;
Expand Down Expand Up @@ -137,8 +139,11 @@ pub fn handle_initialize_user(
let state = &mut ctx.accounts.state;
safe_increment!(state.number_of_sub_accounts, 1);

let max_number_of_sub_accounts = state.max_number_of_sub_accounts();

validate!(
state.number_of_sub_accounts <= state.max_number_of_sub_accounts(),
max_number_of_sub_accounts == 0
|| state.number_of_sub_accounts <= max_number_of_sub_accounts,
ErrorCode::MaxNumberOfUsers
)?;

Expand All @@ -151,6 +156,31 @@ pub fn handle_initialize_user(
referrer: user_stats.referrer
});

drop(user);

let init_fee = state.get_init_user_fee()?;

if init_fee > 0 {
let payer_lamports = ctx.accounts.payer.to_account_info().try_lamports()?;
if payer_lamports < init_fee {
msg!("payer lamports {} init fee {}", payer_lamports, init_fee);
return Err(ErrorCode::CantPayUserInitFee.into());
}

invoke(
&transfer(
&ctx.accounts.payer.key(),
&ctx.accounts.user.key(),
init_fee,
),
&[
ctx.accounts.payer.to_account_info().clone(),
ctx.accounts.user.to_account_info().clone(),
ctx.accounts.system_program.to_account_info().clone(),
],
)?;
}

Ok(())
}

Expand All @@ -175,6 +205,14 @@ pub fn handle_initialize_user_stats(ctx: Context<InitializeUserStats>) -> Result
let state = &mut ctx.accounts.state;
safe_increment!(state.number_of_authorities, 1);

let max_number_of_sub_accounts = state.max_number_of_sub_accounts();

validate!(
max_number_of_sub_accounts == 0
|| state.number_of_authorities <= max_number_of_sub_accounts,
ErrorCode::MaxNumberOfUsers
)?;

Ok(())
}

Expand Down Expand Up @@ -1811,7 +1849,12 @@ pub fn handle_delete_user(ctx: Context<DeleteUser>) -> Result<()> {
let user = &load!(ctx.accounts.user)?;
let user_stats = &mut load_mut!(ctx.accounts.user_stats)?;

validate_user_deletion(user, user_stats)?;
validate_user_deletion(
user,
user_stats,
&ctx.accounts.state,
Clock::get()?.unix_timestamp,
)?;

safe_decrement!(user_stats.number_of_sub_accounts, 1);

Expand Down
7 changes: 7 additions & 0 deletions programs/drift/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -961,6 +961,13 @@ pub mod drift {
handle_update_state_max_number_of_sub_accounts(ctx, max_number_of_sub_accounts)
}

pub fn update_state_max_initialize_user_fee(
ctx: Context<AdminUpdateState>,
max_initialize_user_fee: u16,
) -> Result<()> {
handle_update_state_max_initialize_user_fee(ctx, max_initialize_user_fee)
}

pub fn update_perp_market_oracle(
ctx: Context<RepegCurve>,
oracle: Pubkey,
Expand Down
37 changes: 32 additions & 5 deletions programs/drift/src/state/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ use crate::error::DriftResult;
use crate::math::constants::{
FEE_DENOMINATOR, FEE_PERCENTAGE_DENOMINATOR, MAX_REFERRER_REWARD_EPOCH_UPPER_BOUND,
};
use crate::math::safe_math::SafeMath;
use crate::math::safe_unwrap::SafeUnwrap;
use crate::state::traits::Size;
use crate::PERCENTAGE_PRECISION_U64;
use crate::{LAMPORTS_PER_SOL_U64, PERCENTAGE_PRECISION_U64};

#[cfg(test)]
mod tests;

#[account]
#[derive(Default)]
Expand Down Expand Up @@ -36,7 +40,8 @@ pub struct State {
pub liquidation_duration: u8,
pub initial_pct_to_liquidate: u16,
pub max_number_of_sub_accounts: u16,
pub padding: [u8; 12],
pub max_initialize_user_fee: u16,
pub padding: [u8; 10],
}

#[derive(BitFlags, Clone, Copy, PartialEq, Debug, Eq)]
Expand Down Expand Up @@ -76,9 +81,31 @@ impl State {
}

pub fn max_number_of_sub_accounts(&self) -> u64 {
(self.max_number_of_sub_accounts as u64)
.saturating_mul(100)
.max(25000)
if self.max_number_of_sub_accounts <= 100 {
return self.max_number_of_sub_accounts as u64;
}
(self.max_number_of_sub_accounts as u64).saturating_mul(100)
}

pub fn get_init_user_fee(&self) -> DriftResult<u64> {
let max_init_fee: u64 = (self.max_initialize_user_fee as u64) * LAMPORTS_PER_SOL_U64 / 100;

let target_utilization: u64 = 8 * PERCENTAGE_PRECISION_U64 / 10;

let account_space_utilization: u64 = self
.number_of_sub_accounts
.safe_mul(PERCENTAGE_PRECISION_U64)?
.safe_div(self.max_number_of_sub_accounts().max(1))?;

let init_fee: u64 = if account_space_utilization > target_utilization {
max_init_fee
.safe_mul(account_space_utilization.safe_sub(target_utilization)?)?
.safe_div(PERCENTAGE_PRECISION_U64.safe_sub(target_utilization)?)?
} else {
0
};

Ok(init_fee)
}
}

Expand Down
60 changes: 60 additions & 0 deletions programs/drift/src/state/state/tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
mod get_init_user_fee {
use crate::State;

#[test]
fn it_works() {
let state = State::default();
let init_user_fee = state.get_init_user_fee().unwrap();
assert_eq!(init_user_fee, 0);

let state = State {
max_initialize_user_fee: 1,
max_number_of_sub_accounts: 100,
number_of_sub_accounts: 80,
..State::default()
};

let init_user_fee = state.get_init_user_fee().unwrap();
assert_eq!(init_user_fee, 0);

let state = State {
max_initialize_user_fee: 1,
max_number_of_sub_accounts: 100,
number_of_sub_accounts: 81,
..State::default()
};

let init_user_fee = state.get_init_user_fee().unwrap();
assert_eq!(init_user_fee, 500000);

let state = State {
max_initialize_user_fee: 1,
max_number_of_sub_accounts: 100,
number_of_sub_accounts: 90,
..State::default()
};

let init_user_fee = state.get_init_user_fee().unwrap();
assert_eq!(init_user_fee, 5000000);

let state = State {
max_initialize_user_fee: 1,
max_number_of_sub_accounts: 100,
number_of_sub_accounts: 100,
..State::default()
};

let init_user_fee = state.get_init_user_fee().unwrap();
assert_eq!(init_user_fee, 10000000);

let state = State {
max_initialize_user_fee: 100,
max_number_of_sub_accounts: 100,
number_of_sub_accounts: 100,
..State::default()
};

let init_user_fee = state.get_init_user_fee().unwrap();
assert_eq!(init_user_fee, 1000000000);
}
}
9 changes: 9 additions & 0 deletions programs/drift/src/state/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1450,6 +1450,15 @@ impl UserStats {
pub fn get_total_30d_volume(&self) -> DriftResult<u64> {
self.taker_volume_30d.safe_add(self.maker_volume_30d)
}

pub fn get_age_ts(&self, now: i64) -> i64 {
// upper bound of age of the user stats account
let min_action_ts: i64 = self
.last_filler_volume_30d_ts
.min(self.last_maker_volume_30d_ts)
.min(self.last_taker_volume_30d_ts);
now.saturating_sub(min_action_ts).max(0)
}
}

#[account(zero_copy(unsafe))]
Expand Down
30 changes: 30 additions & 0 deletions programs/drift/src/state/user/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1886,3 +1886,33 @@ mod resting_limit_order {
assert!(order.is_resting_limit_order(slot).unwrap());
}
}

mod get_user_stats_age_ts {
use crate::state::user::UserStats;

#[test]
fn test() {
let user_stats = UserStats::default();

let now = 1;

let age = user_stats.get_age_ts(now);

assert_eq!(age, 1);

let user_stats = UserStats {
last_filler_volume_30d_ts: 2,
last_maker_volume_30d_ts: 3,
last_taker_volume_30d_ts: 4,
..UserStats::default()
};

let now = 5;
let age = user_stats.get_age_ts(now);
assert_eq!(age, 3);

let now = 1;
let age = user_stats.get_age_ts(now);
assert_eq!(age, 0);
}
}
22 changes: 20 additions & 2 deletions programs/drift/src/validation/user.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
use crate::error::{DriftResult, ErrorCode};
use crate::state::spot_market::SpotBalanceType;
use crate::state::user::{OrderStatus, User, UserStats};
use crate::validate;
use crate::{validate, State, THIRTEEN_DAY};
use solana_program::msg;

pub fn validate_user_deletion(user: &User, user_stats: &UserStats) -> DriftResult {
pub fn validate_user_deletion(
user: &User,
user_stats: &UserStats,
state: &State,
now: i64,
) -> DriftResult {
validate!(
!user_stats.is_referrer || user.sub_account_id != 0,
ErrorCode::UserCantBeDeleted,
Expand Down Expand Up @@ -49,6 +54,19 @@ pub fn validate_user_deletion(user: &User, user_stats: &UserStats) -> DriftResul
)?;
}

if state.max_initialize_user_fee > 0 {
let estimated_user_stats_age = user_stats.get_age_ts(now);
if estimated_user_stats_age < THIRTEEN_DAY as i64 {
validate!(
user.idle,
ErrorCode::UserCantBeDeleted,
"user is not idle with fresh user stats account creation ({} < {})",
estimated_user_stats_age,
THIRTEEN_DAY
)?;
}
}

Ok(())
}

Expand Down
14 changes: 14 additions & 0 deletions sdk/src/adminClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -846,6 +846,20 @@ export class AdminClient extends DriftClient {
);
}

public async updateStateMaxInitializeUserFee(
maxInitializeUserFee: number
): Promise<TransactionSignature> {
return await this.program.rpc.updateStateMaxInitializeUserFee(
maxInitializeUserFee,
{
accounts: {
admin: this.wallet.publicKey,
state: await this.getStatePublicKey(),
},
}
);
}

public async updateWithdrawGuardThreshold(
spotMarketIndex: number,
withdrawGuardThreshold: BN
Expand Down
Loading
Loading