From 5e3efa5cad48efa379eb202660693ebd6d9f95b0 Mon Sep 17 00:00:00 2001 From: wphan Date: Wed, 8 Nov 2023 14:47:46 -0800 Subject: [PATCH 1/5] add vault.manager_burn_shares --- programs/drift_vaults/src/error.rs | 2 + programs/drift_vaults/src/state/events.rs | 21 ++++ programs/drift_vaults/src/state/vault.rs | 71 ++++++++++++ .../drift_vaults/src/state/withdraw_unit.rs | 100 +++++++++++++++++ programs/drift_vaults/src/tests.rs | 101 ++++++++++++++++++ 5 files changed, 295 insertions(+) diff --git a/programs/drift_vaults/src/error.rs b/programs/drift_vaults/src/error.rs index 4abcfb05..cf9dec16 100644 --- a/programs/drift_vaults/src/error.rs +++ b/programs/drift_vaults/src/error.rs @@ -51,6 +51,8 @@ pub enum ErrorCode { InvalidVaultDeposit, #[msg("OngoingLiquidation")] OngoingLiquidation, + #[msg("InvalidBurnSharesAmount")] + InvalidBurnSharesAmount, } impl From for ErrorCode { diff --git a/programs/drift_vaults/src/state/events.rs b/programs/drift_vaults/src/state/events.rs index 129f7587..d00a7edb 100644 --- a/programs/drift_vaults/src/state/events.rs +++ b/programs/drift_vaults/src/state/events.rs @@ -49,3 +49,24 @@ impl Default for VaultDepositorAction { VaultDepositorAction::Deposit } } + +#[event] +#[derive(Default)] +pub struct BurnVaultSharesRecord { + pub ts: i64, + pub vault: Pubkey, + pub depositor_authority: Pubkey, + pub amount: u64, + + pub spot_market_index: u16, + + pub vault_equity_before: u64, + + pub user_vault_shares_before: u128, + pub manager_shares_before: u128, + pub total_vault_shares_before: u128, + + pub user_vault_shares_after: u128, + pub manager_shares_after: u128, + pub total_vault_shares_after: u128, +} diff --git a/programs/drift_vaults/src/state/vault.rs b/programs/drift_vaults/src/state/vault.rs index e6ac89a0..9974d310 100644 --- a/programs/drift_vaults/src/state/vault.rs +++ b/programs/drift_vaults/src/state/vault.rs @@ -1,4 +1,5 @@ use crate::events::{VaultDepositorAction, VaultDepositorRecord}; +use crate::state::BurnVaultSharesRecord; use crate::{validate, Size, VaultDepositor, WithdrawUnit}; use crate::constants::TIME_FOR_LIQUIDATION; @@ -500,4 +501,74 @@ impl Vault { self.liquidation_delegate = Pubkey::default(); self.liquidation_start_ts = 0; } + + pub fn manager_burn_shares( + &mut self, + burn_amount: u64, + burn_unit: WithdrawUnit, + vault_equity: u64, + now: i64, + ) -> Result<()> { + self.apply_rebase(vault_equity)?; + self.apply_management_fee(vault_equity, now)?; + + let manager_shares_before: u128 = self.get_manager_shares()?; + + let (burn_value, new_total_shares) = burn_unit.get_transfer_value_with_new_total( + burn_amount, + vault_equity, + self.user_shares, + self.total_shares, + )?; + + validate!( + new_total_shares <= self.total_shares, + ErrorCode::InvalidBurnSharesAmount, + "new_total_shares > total_shares" + )?; + + let total_vault_shares_before = self.total_shares; + let user_vault_shares_before = self.user_shares; + + // give manager shares to users + msg!("burn_value: {}", burn_value); + msg!("new_total_shares: {}", new_total_shares); + msg!("user_shares: {}", self.user_shares); + msg!("total_shares: {}", self.total_shares); + + self.total_shares = new_total_shares; + + let manager_shares_after: u128 = self.get_manager_shares()?; + msg!( + "mgr shares: {} -> {}", + manager_shares_before, + manager_shares_after + ); + + validate!( + manager_shares_after < manager_shares_before, + ErrorCode::InvalidBurnSharesAmount, + "Manager shares not reduced" + )?; + + emit!(BurnVaultSharesRecord { + ts: now, + vault: self.pubkey, + depositor_authority: self.manager, + amount: burn_value, + spot_market_index: self.spot_market_index, + vault_equity_before: vault_equity, + user_vault_shares_before, + manager_shares_before, + total_vault_shares_before, + + manager_shares_after, + total_vault_shares_after: self.total_shares, + user_vault_shares_after: self.user_shares, + }); + + self.apply_rebase(vault_equity)?; + + Ok(()) + } } diff --git a/programs/drift_vaults/src/state/withdraw_unit.rs b/programs/drift_vaults/src/state/withdraw_unit.rs index 4c777b6d..34ae3598 100644 --- a/programs/drift_vaults/src/state/withdraw_unit.rs +++ b/programs/drift_vaults/src/state/withdraw_unit.rs @@ -52,6 +52,53 @@ impl WithdrawUnit { } } + /// returns the amount and shares transfered as a result of transfering `amount` and changing + /// `total_shares` to `new_total_shares` + pub fn get_transfer_value_with_new_total( + &self, + amount: u64, + vault_equity: u64, + user_shares: u128, + total_shares: u128, + ) -> VaultResult<(u64, u128)> { + match self { + WithdrawUnit::Token => { + let transfer_value = amount; + let new_total_shares = user_shares + .safe_mul(vault_equity.cast::()?)? + .safe_div(vault_equity.safe_sub(amount)?.cast::()?)?; + + Ok((transfer_value, new_total_shares)) + } + WithdrawUnit::Shares => { + let transfer_value = depositor_shares_to_vault_amount( + amount.cast::()?, + total_shares, + vault_equity, + )?; + + let new_total_shares = user_shares + .safe_mul(vault_equity.cast::()?)? + .safe_div(vault_equity.safe_sub(transfer_value)?.cast::()?)?; + Ok((transfer_value, new_total_shares)) + } + WithdrawUnit::SharesPercent => { + let manager_shares = total_shares.safe_sub(user_shares)?; + let transfer_shares = + WithdrawUnit::get_shares_from_percent(amount.cast()?, manager_shares)?; + let transfer_value = depositor_shares_to_vault_amount( + transfer_shares.cast::()?, + total_shares, + vault_equity, + )?; + let new_total_shares = user_shares + .safe_mul(vault_equity.cast::()?)? + .safe_div(vault_equity.safe_sub(transfer_value)?.cast::()?)?; + Ok((transfer_value, new_total_shares)) + } + } + } + fn get_shares_from_percent(percent: u128, shares: u128) -> VaultResult { validate!( percent <= MAX_WITHDRAW_PERCENT, @@ -61,3 +108,56 @@ impl WithdrawUnit { Ok(shares) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get_value_and_shares_with_new_total() { + // want shares to transfer 100 tokens + let withdraw_unit = WithdrawUnit::Token; + let result = withdraw_unit.get_transfer_value_with_new_total( + 50_000_000, // 50 tokens + 1000_000_000, + 90_000_000, // manager shares = 100 - 90 = 10 + 100_000_000, + ); + assert!(result.is_ok()); + let (value, new_total_shares) = result.unwrap(); + assert_eq!(value, 50_000_000); + // before: manager owns (100-90)/100 * 1000 = 100 + // after: manager owns (94.736842-90)/94.736842 * 1000 = 49.999998 + assert_eq!(new_total_shares, 94736842); + + // want shares to transfer 5 shares + let withdraw_unit = WithdrawUnit::Shares; + let result = withdraw_unit.get_transfer_value_with_new_total( + 5_000_000, // 5 tokens + 1000_000_000, + 90_000_000, // manager shares = 100 - 90 = 10 + 100_000_000, + ); + assert!(result.is_ok()); + let (value, new_total_shares) = result.unwrap(); + assert_eq!(value, 50_000_000); + // before: manager owns (100-90)/100 * 1000 = 100 + // after: manager owns (94.736842-90)/94.736842 * 1000 = 49.999998 + assert_eq!(new_total_shares, 94736842); + + // want shares to transfer 50% of shares + let withdraw_unit = WithdrawUnit::SharesPercent; + let result = withdraw_unit.get_transfer_value_with_new_total( + 500_000, // 0.50 + 1000_000_000, + 90_000_000, // manager shares = 100 - 90 = 10 + 100_000_000, + ); + assert!(result.is_ok()); + let (value, new_total_shares) = result.unwrap(); + assert_eq!(value, 50_000_000); + // before: manager owns (100-90)/100 * 1000 = 100 + // after: manager owns (94.736842-90)/94.736842 * 1000 = 49.999998 + assert_eq!(new_total_shares, 94736842); + } +} diff --git a/programs/drift_vaults/src/tests.rs b/programs/drift_vaults/src/tests.rs index f015461b..d886211b 100644 --- a/programs/drift_vaults/src/tests.rs +++ b/programs/drift_vaults/src/tests.rs @@ -504,4 +504,105 @@ mod vault_fcn { assert_eq!(vd_amount + vault_manager_amount_after, vault_equity - 1); } + + #[test] + fn test_manager_burn_shares_for_users() { + let mut now = 123456789; + let vault = &mut Vault::default(); + vault.management_fee = 0; + vault.last_fee_update_ts = now; + let mut vault_equity: u64 = 0; + let manager_deposit_amount: u64 = 100 * QUOTE_PRECISION_U64; + vault + .manager_deposit(manager_deposit_amount, vault_equity, now) + .unwrap(); + vault_equity += manager_deposit_amount; + + assert_eq!(vault.user_shares, 0); + assert_eq!(vault.total_shares, 100000000); + now += 60 * 60; + + let user_deposit_amount = manager_deposit_amount * 20; + + // first new user deposits $2000 + let vd_0 = + &mut VaultDepositor::new(Pubkey::default(), Pubkey::default(), Pubkey::default(), now); + vd_0.deposit(user_deposit_amount, vault_equity, vault, now) + .unwrap(); + assert_eq!(vault.user_shares, 2000000000); + assert_eq!(vault.total_shares, 2000000000 + 100000000); + vault_equity += user_deposit_amount; + // + let user_amount_0 = depositor_shares_to_vault_amount( + vd_0.checked_vault_shares(vault).unwrap(), + vault.total_shares, + vault_equity, + ) + .unwrap(); + assert_eq!(user_amount_0, user_deposit_amount); + // + + // second new user deposits $2000 + let vd_1 = + &mut VaultDepositor::new(Pubkey::default(), Pubkey::default(), Pubkey::default(), now); + vd_1.deposit(user_deposit_amount, vault_equity, vault, now) + .unwrap(); + assert_eq!(vault.user_shares, 4000000000); + assert_eq!(vault.total_shares, 4000000000 + 100000000); + vault_equity += user_deposit_amount; + // + let user_amount_1 = depositor_shares_to_vault_amount( + vd_1.checked_vault_shares(vault).unwrap(), + vault.total_shares, + vault_equity, + ) + .unwrap(); + assert_eq!(user_amount_1, user_deposit_amount); + // + + let vault_manager_amount_before = depositor_shares_to_vault_amount( + vault.total_shares - vault.user_shares, + vault.total_shares, + vault_equity, + ) + .unwrap(); + assert_eq!(vault_manager_amount_before, manager_deposit_amount); + + // manager burns half of their shares + // let manager_shares = vault.get_manager_shares().unwrap(); + vault + .manager_burn_shares( + (manager_deposit_amount / 2).try_into().unwrap(), + WithdrawUnit::Token, + vault_equity, + now, + ) + .unwrap(); + + let vault_manager_amount_after = depositor_shares_to_vault_amount( + vault.total_shares - vault.user_shares, + vault.total_shares, + vault_equity, + ) + .unwrap(); + assert_eq!(vault_manager_amount_after, 49999999); // rounding + let manager_amount_diff = vault_manager_amount_before - vault_manager_amount_after; + + // first user should half of manager's burnt shares + let user_amount_0 = depositor_shares_to_vault_amount( + vd_0.checked_vault_shares(vault).unwrap(), + vault.total_shares, + vault_equity, + ) + .unwrap(); + assert_eq!(user_amount_0, user_deposit_amount + manager_amount_diff / 2); + + let user_amount_1 = depositor_shares_to_vault_amount( + vd_1.checked_vault_shares(vault).unwrap(), + vault.total_shares, + vault_equity, + ) + .unwrap(); + assert_eq!(user_amount_1, user_deposit_amount + manager_amount_diff / 2); + } } From 6292b1766398f87a6b047f04455ea71888cd2333 Mon Sep 17 00:00:00 2001 From: wphan Date: Wed, 8 Nov 2023 18:49:21 -0800 Subject: [PATCH 2/5] wire up instruction --- .../src/instructions/manager_burn_shares.rs | 54 +++++++++++++++ programs/drift_vaults/src/instructions/mod.rs | 2 + programs/drift_vaults/src/lib.rs | 8 +++ programs/drift_vaults/src/state/events.rs | 10 +-- programs/drift_vaults/src/state/vault.rs | 35 ++-------- programs/drift_vaults/src/tests.rs | 65 ++++++++++++++----- 6 files changed, 121 insertions(+), 53 deletions(-) create mode 100644 programs/drift_vaults/src/instructions/manager_burn_shares.rs diff --git a/programs/drift_vaults/src/instructions/manager_burn_shares.rs b/programs/drift_vaults/src/instructions/manager_burn_shares.rs new file mode 100644 index 00000000..11ed6326 --- /dev/null +++ b/programs/drift_vaults/src/instructions/manager_burn_shares.rs @@ -0,0 +1,54 @@ +use crate::constraints::{is_manager_for_vault, is_user_for_vault, is_user_stats_for_vault}; +use crate::AccountMapProvider; +use crate::{Vault, WithdrawUnit}; +use anchor_lang::prelude::*; +use drift::instructions::optional_accounts::AccountMaps; +use drift::state::user::User; + +pub fn manager_burn_shares<'info>( + ctx: Context<'_, '_, '_, 'info, ManagerBurnShares<'info>>, + burn_amount: u64, + burn_unit: WithdrawUnit, +) -> Result<()> { + let clock = &Clock::get()?; + let mut vault = ctx.accounts.vault.load_mut()?; + let now = clock.unix_timestamp; + + let user = ctx.accounts.drift_user.load()?; + let spot_market_index = vault.spot_market_index; + + let AccountMaps { + perp_market_map, + spot_market_map, + mut oracle_map, + } = ctx.load_maps(clock.slot, Some(spot_market_index))?; + + let vault_equity = + vault.calculate_equity(&user, &perp_market_map, &spot_market_map, &mut oracle_map)?; + + vault.manager_burn_shares(burn_amount, burn_unit, vault_equity, now)?; + + Ok(()) +} + +#[derive(Accounts)] +pub struct ManagerBurnShares<'info> { + #[account( + mut, + constraint = is_manager_for_vault(&vault, &manager)? + )] + pub vault: AccountLoader<'info, Vault>, + pub manager: Signer<'info>, + #[account( + constraint = is_user_stats_for_vault(&vault, &drift_user_stats)? + )] + /// CHECK: checked in drift cpi + pub drift_user_stats: AccountInfo<'info>, + #[account( + constraint = is_user_for_vault(&vault, &drift_user.key())? + )] + /// CHECK: checked in drift cpi + pub drift_user: AccountLoader<'info, User>, + /// CHECK: checked in drift cpi + pub drift_state: AccountInfo<'info>, +} diff --git a/programs/drift_vaults/src/instructions/mod.rs b/programs/drift_vaults/src/instructions/mod.rs index 6a19a88c..7e075842 100644 --- a/programs/drift_vaults/src/instructions/mod.rs +++ b/programs/drift_vaults/src/instructions/mod.rs @@ -7,6 +7,7 @@ pub use initialize_insurance_fund_stake::*; pub use initialize_vault::*; pub use initialize_vault_depositor::*; pub use liquidate::*; +pub use manager_burn_shares::*; pub use manager_cancel_withdraw_request::*; pub use manager_deposit::*; pub use manager_request_withdraw::*; @@ -29,6 +30,7 @@ mod initialize_insurance_fund_stake; mod initialize_vault; mod initialize_vault_depositor; mod liquidate; +mod manager_burn_shares; mod manager_cancel_withdraw_request; mod manager_deposit; mod manager_request_withdraw; diff --git a/programs/drift_vaults/src/lib.rs b/programs/drift_vaults/src/lib.rs index b8b364c2..81bd34ab 100644 --- a/programs/drift_vaults/src/lib.rs +++ b/programs/drift_vaults/src/lib.rs @@ -110,6 +110,14 @@ pub mod drift_vaults { instructions::manager_withdraw(ctx) } + pub fn manager_burn_shares<'info>( + ctx: Context<'_, '_, '_, 'info, ManagerBurnShares<'info>>, + burn_amount: u64, + burn_unit: WithdrawUnit, + ) -> Result<()> { + instructions::manager_burn_shares(ctx, burn_amount, burn_unit) + } + pub fn apply_profit_share<'info>( ctx: Context<'_, '_, '_, 'info, ApplyProfitShare<'info>>, ) -> Result<()> { diff --git a/programs/drift_vaults/src/state/events.rs b/programs/drift_vaults/src/state/events.rs index d00a7edb..7ae53ed8 100644 --- a/programs/drift_vaults/src/state/events.rs +++ b/programs/drift_vaults/src/state/events.rs @@ -57,16 +57,8 @@ pub struct BurnVaultSharesRecord { pub vault: Pubkey, pub depositor_authority: Pubkey, pub amount: u64, - pub spot_market_index: u16, - - pub vault_equity_before: u64, - - pub user_vault_shares_before: u128, - pub manager_shares_before: u128, + pub vault_equity: u64, pub total_vault_shares_before: u128, - - pub user_vault_shares_after: u128, - pub manager_shares_after: u128, pub total_vault_shares_after: u128, } diff --git a/programs/drift_vaults/src/state/vault.rs b/programs/drift_vaults/src/state/vault.rs index 9974d310..95381b66 100644 --- a/programs/drift_vaults/src/state/vault.rs +++ b/programs/drift_vaults/src/state/vault.rs @@ -512,8 +512,6 @@ impl Vault { self.apply_rebase(vault_equity)?; self.apply_management_fee(vault_equity, now)?; - let manager_shares_before: u128 = self.get_manager_shares()?; - let (burn_value, new_total_shares) = burn_unit.get_transfer_value_with_new_total( burn_amount, vault_equity, @@ -522,34 +520,20 @@ impl Vault { )?; validate!( - new_total_shares <= self.total_shares, + new_total_shares < self.total_shares, ErrorCode::InvalidBurnSharesAmount, "new_total_shares > total_shares" )?; let total_vault_shares_before = self.total_shares; - let user_vault_shares_before = self.user_shares; - - // give manager shares to users - msg!("burn_value: {}", burn_value); - msg!("new_total_shares: {}", new_total_shares); - msg!("user_shares: {}", self.user_shares); - msg!("total_shares: {}", self.total_shares); - - self.total_shares = new_total_shares; - let manager_shares_after: u128 = self.get_manager_shares()?; msg!( - "mgr shares: {} -> {}", - manager_shares_before, - manager_shares_after + "burn_value: {}, total_shares: {} -> {}", + burn_value, + total_vault_shares_before, + new_total_shares ); - - validate!( - manager_shares_after < manager_shares_before, - ErrorCode::InvalidBurnSharesAmount, - "Manager shares not reduced" - )?; + self.total_shares = new_total_shares; emit!(BurnVaultSharesRecord { ts: now, @@ -557,14 +541,9 @@ impl Vault { depositor_authority: self.manager, amount: burn_value, spot_market_index: self.spot_market_index, - vault_equity_before: vault_equity, - user_vault_shares_before, - manager_shares_before, + vault_equity, total_vault_shares_before, - - manager_shares_after, total_vault_shares_after: self.total_shares, - user_vault_shares_after: self.user_shares, }); self.apply_rebase(vault_equity)?; diff --git a/programs/drift_vaults/src/tests.rs b/programs/drift_vaults/src/tests.rs index d886211b..8f104dd4 100644 --- a/programs/drift_vaults/src/tests.rs +++ b/programs/drift_vaults/src/tests.rs @@ -512,6 +512,8 @@ mod vault_fcn { vault.management_fee = 0; vault.last_fee_update_ts = now; let mut vault_equity: u64 = 0; + + // manager deposits $100 let manager_deposit_amount: u64 = 100 * QUOTE_PRECISION_U64; vault .manager_deposit(manager_deposit_amount, vault_equity, now) @@ -522,9 +524,8 @@ mod vault_fcn { assert_eq!(vault.total_shares, 100000000); now += 60 * 60; - let user_deposit_amount = manager_deposit_amount * 20; - // first new user deposits $2000 + let user_deposit_amount = manager_deposit_amount * 20; let vd_0 = &mut VaultDepositor::new(Pubkey::default(), Pubkey::default(), Pubkey::default(), now); vd_0.deposit(user_deposit_amount, vault_equity, vault, now) @@ -532,7 +533,6 @@ mod vault_fcn { assert_eq!(vault.user_shares, 2000000000); assert_eq!(vault.total_shares, 2000000000 + 100000000); vault_equity += user_deposit_amount; - // let user_amount_0 = depositor_shares_to_vault_amount( vd_0.checked_vault_shares(vault).unwrap(), vault.total_shares, @@ -540,7 +540,6 @@ mod vault_fcn { ) .unwrap(); assert_eq!(user_amount_0, user_deposit_amount); - // // second new user deposits $2000 let vd_1 = @@ -550,7 +549,6 @@ mod vault_fcn { assert_eq!(vault.user_shares, 4000000000); assert_eq!(vault.total_shares, 4000000000 + 100000000); vault_equity += user_deposit_amount; - // let user_amount_1 = depositor_shares_to_vault_amount( vd_1.checked_vault_shares(vault).unwrap(), vault.total_shares, @@ -558,7 +556,6 @@ mod vault_fcn { ) .unwrap(); assert_eq!(user_amount_1, user_deposit_amount); - // let vault_manager_amount_before = depositor_shares_to_vault_amount( vault.total_shares - vault.user_shares, @@ -568,15 +565,9 @@ mod vault_fcn { .unwrap(); assert_eq!(vault_manager_amount_before, manager_deposit_amount); - // manager burns half of their shares - // let manager_shares = vault.get_manager_shares().unwrap(); + // manager burns half their deposits vault - .manager_burn_shares( - (manager_deposit_amount / 2).try_into().unwrap(), - WithdrawUnit::Token, - vault_equity, - now, - ) + .manager_burn_shares(500_000, WithdrawUnit::SharesPercent, vault_equity, now) .unwrap(); let vault_manager_amount_after = depositor_shares_to_vault_amount( @@ -585,10 +576,10 @@ mod vault_fcn { vault_equity, ) .unwrap(); - assert_eq!(vault_manager_amount_after, 49999999); // rounding + assert_eq!(vault_manager_amount_after, 49_999_999); // rounding let manager_amount_diff = vault_manager_amount_before - vault_manager_amount_after; - // first user should half of manager's burnt shares + // first and second user should each gain half of manager's burnt shares let user_amount_0 = depositor_shares_to_vault_amount( vd_0.checked_vault_shares(vault).unwrap(), vault.total_shares, @@ -605,4 +596,46 @@ mod vault_fcn { .unwrap(); assert_eq!(user_amount_1, user_deposit_amount + manager_amount_diff / 2); } + + #[test] + fn test_manager_cannot_burn_more_shares_than_owned() { + let mut now = 123456789; + let vault = &mut Vault::default(); + vault.management_fee = 0; + vault.last_fee_update_ts = now; + let mut vault_equity: u64 = 0; + let manager_deposit_amount: u64 = 100 * QUOTE_PRECISION_U64; + vault + .manager_deposit(manager_deposit_amount, vault_equity, now) + .unwrap(); + vault_equity += manager_deposit_amount; + + assert_eq!(vault.user_shares, 0); + assert_eq!(vault.total_shares, 100000000); + now += 60 * 60; + + let vault_manager_amount_before = depositor_shares_to_vault_amount( + vault.total_shares - vault.user_shares, + vault.total_shares, + vault_equity, + ) + .unwrap(); + assert_eq!(vault_manager_amount_before, manager_deposit_amount); + + let r = vault.manager_burn_shares( + (manager_deposit_amount * 2).try_into().unwrap(), + WithdrawUnit::Token, + vault_equity, + now, + ); + assert!(r.is_err()); + + let vault_manager_amount_after = depositor_shares_to_vault_amount( + vault.total_shares - vault.user_shares, + vault.total_shares, + vault_equity, + ) + .unwrap(); + assert_eq!(vault_manager_amount_after, vault_manager_amount_before); + } } From 84618b2259f79f495ee03c3088c1607e19c823b4 Mon Sep 17 00:00:00 2001 From: wphan Date: Wed, 8 Nov 2023 18:53:19 -0800 Subject: [PATCH 3/5] prettify --- ts/sdk/src/accounts/vaultAccount.ts | 2 +- ts/sdk/src/accounts/vaultDepositorAccount.ts | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/ts/sdk/src/accounts/vaultAccount.ts b/ts/sdk/src/accounts/vaultAccount.ts index fd74ffe1..cf0fd2b4 100644 --- a/ts/sdk/src/accounts/vaultAccount.ts +++ b/ts/sdk/src/accounts/vaultAccount.ts @@ -5,7 +5,7 @@ import { ONE_YEAR, PERCENTAGE_PRECISION, ZERO, - BN + BN, } from '@drift-labs/sdk'; import { PublicKey } from '@solana/web3.js'; import { DriftVaults } from '../types/drift_vaults'; diff --git a/ts/sdk/src/accounts/vaultDepositorAccount.ts b/ts/sdk/src/accounts/vaultDepositorAccount.ts index 6a43c574..2b27d15b 100644 --- a/ts/sdk/src/accounts/vaultDepositorAccount.ts +++ b/ts/sdk/src/accounts/vaultDepositorAccount.ts @@ -1,5 +1,10 @@ import { Program } from '@coral-xyz/anchor'; -import { BN, BulkAccountLoader, PERCENTAGE_PRECISION, ZERO } from '@drift-labs/sdk'; +import { + BN, + BulkAccountLoader, + PERCENTAGE_PRECISION, + ZERO, +} from '@drift-labs/sdk'; import { PublicKey } from '@solana/web3.js'; import { DriftVaults } from '../types/drift_vaults'; import { VaultDepositor, VaultDepositorAccountEvents } from '../types/types'; From f132b70382b43779369323dc834f7d209f8aebdb Mon Sep 17 00:00:00 2001 From: wphan Date: Wed, 8 Nov 2023 18:59:11 -0800 Subject: [PATCH 4/5] add sdk func --- ts/sdk/src/idl/drift_vaults.json | 92 ++++++++++++++++ ts/sdk/src/types/drift_vaults.ts | 184 +++++++++++++++++++++++++++++++ ts/sdk/src/vaultClient.ts | 61 ++++++++++ 3 files changed, 337 insertions(+) diff --git a/ts/sdk/src/idl/drift_vaults.json b/ts/sdk/src/idl/drift_vaults.json index 622c8156..1d5cc71b 100644 --- a/ts/sdk/src/idl/drift_vaults.json +++ b/ts/sdk/src/idl/drift_vaults.json @@ -678,6 +678,48 @@ ], "args": [] }, + { + "name": "managerBurnShares", + "accounts": [ + { + "name": "vault", + "isMut": true, + "isSigner": false + }, + { + "name": "manager", + "isMut": false, + "isSigner": true + }, + { + "name": "driftUserStats", + "isMut": false, + "isSigner": false + }, + { + "name": "driftUser", + "isMut": false, + "isSigner": false + }, + { + "name": "driftState", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "burnAmount", + "type": "u64" + }, + { + "name": "burnUnit", + "type": { + "defined": "WithdrawUnit" + } + } + ] + }, { "name": "applyProfitShare", "accounts": [ @@ -1539,6 +1581,51 @@ "index": false } ] + }, + { + "name": "BurnVaultSharesRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "vault", + "type": "publicKey", + "index": false + }, + { + "name": "depositorAuthority", + "type": "publicKey", + "index": false + }, + { + "name": "amount", + "type": "u64", + "index": false + }, + { + "name": "spotMarketIndex", + "type": "u16", + "index": false + }, + { + "name": "vaultEquity", + "type": "u64", + "index": false + }, + { + "name": "totalVaultSharesBefore", + "type": "u128", + "index": false + }, + { + "name": "totalVaultSharesAfter", + "type": "u128", + "index": false + } + ] } ], "errors": [ @@ -1651,6 +1738,11 @@ "code": 6021, "name": "OngoingLiquidation", "msg": "OngoingLiquidation" + }, + { + "code": 6022, + "name": "InvalidBurnSharesAmount", + "msg": "InvalidBurnSharesAmount" } ], "metadata": { diff --git a/ts/sdk/src/types/drift_vaults.ts b/ts/sdk/src/types/drift_vaults.ts index 55c4442c..7780cb98 100644 --- a/ts/sdk/src/types/drift_vaults.ts +++ b/ts/sdk/src/types/drift_vaults.ts @@ -678,6 +678,48 @@ export type DriftVaults = { ]; args: []; }, + { + name: 'managerBurnShares'; + accounts: [ + { + name: 'vault'; + isMut: true; + isSigner: false; + }, + { + name: 'manager'; + isMut: false; + isSigner: true; + }, + { + name: 'driftUserStats'; + isMut: false; + isSigner: false; + }, + { + name: 'driftUser'; + isMut: false; + isSigner: false; + }, + { + name: 'driftState'; + isMut: false; + isSigner: false; + } + ]; + args: [ + { + name: 'burnAmount'; + type: 'u64'; + }, + { + name: 'burnUnit'; + type: { + defined: 'WithdrawUnit'; + }; + } + ]; + }, { name: 'applyProfitShare'; accounts: [ @@ -1469,6 +1511,51 @@ export type DriftVaults = { index: false; } ]; + }, + { + name: 'BurnVaultSharesRecord'; + fields: [ + { + name: 'ts'; + type: 'i64'; + index: false; + }, + { + name: 'vault'; + type: 'publicKey'; + index: false; + }, + { + name: 'depositorAuthority'; + type: 'publicKey'; + index: false; + }, + { + name: 'amount'; + type: 'u64'; + index: false; + }, + { + name: 'spotMarketIndex'; + type: 'u16'; + index: false; + }, + { + name: 'vaultEquity'; + type: 'u64'; + index: false; + }, + { + name: 'totalVaultSharesBefore'; + type: 'u128'; + index: false; + }, + { + name: 'totalVaultSharesAfter'; + type: 'u128'; + index: false; + } + ]; } ]; errors: [ @@ -1581,6 +1668,11 @@ export type DriftVaults = { code: 6021; name: 'OngoingLiquidation'; msg: 'OngoingLiquidation'; + }, + { + code: 6022; + name: 'InvalidBurnSharesAmount'; + msg: 'InvalidBurnSharesAmount'; } ]; }; @@ -2265,6 +2357,48 @@ export const IDL: DriftVaults = { ], args: [], }, + { + name: 'managerBurnShares', + accounts: [ + { + name: 'vault', + isMut: true, + isSigner: false, + }, + { + name: 'manager', + isMut: false, + isSigner: true, + }, + { + name: 'driftUserStats', + isMut: false, + isSigner: false, + }, + { + name: 'driftUser', + isMut: false, + isSigner: false, + }, + { + name: 'driftState', + isMut: false, + isSigner: false, + }, + ], + args: [ + { + name: 'burnAmount', + type: 'u64', + }, + { + name: 'burnUnit', + type: { + defined: 'WithdrawUnit', + }, + }, + ], + }, { name: 'applyProfitShare', accounts: [ @@ -3057,6 +3191,51 @@ export const IDL: DriftVaults = { }, ], }, + { + name: 'BurnVaultSharesRecord', + fields: [ + { + name: 'ts', + type: 'i64', + index: false, + }, + { + name: 'vault', + type: 'publicKey', + index: false, + }, + { + name: 'depositorAuthority', + type: 'publicKey', + index: false, + }, + { + name: 'amount', + type: 'u64', + index: false, + }, + { + name: 'spotMarketIndex', + type: 'u16', + index: false, + }, + { + name: 'vaultEquity', + type: 'u64', + index: false, + }, + { + name: 'totalVaultSharesBefore', + type: 'u128', + index: false, + }, + { + name: 'totalVaultSharesAfter', + type: 'u128', + index: false, + }, + ], + }, ], errors: [ { @@ -3169,5 +3348,10 @@ export const IDL: DriftVaults = { name: 'OngoingLiquidation', msg: 'OngoingLiquidation', }, + { + code: 6022, + name: 'InvalidBurnSharesAmount', + msg: 'InvalidBurnSharesAmount', + }, ], }; diff --git a/ts/sdk/src/vaultClient.ts b/ts/sdk/src/vaultClient.ts index 45ffde86..76dbaca6 100644 --- a/ts/sdk/src/vaultClient.ts +++ b/ts/sdk/src/vaultClient.ts @@ -463,6 +463,67 @@ export class VaultClient { .rpc(); } + public async managerBurnShares( + vault: PublicKey, + burnAmount: BN, + burnUnit: WithdrawUnit + ): Promise { + // @ts-ignore + const vaultAccount = (await this.program.account.vault.fetch( + vault + )) as Vault; + + if (!this.driftClient.wallet.publicKey.equals(vaultAccount.manager)) { + throw new Error(`Only the manager of the vault can request a withdraw.`); + } + + const user = new User({ + driftClient: this.driftClient, + userAccountPublicKey: vaultAccount.user, + }); + await user.subscribe(); + const remainingAccounts = this.driftClient.getRemainingAccounts({ + userAccounts: [user.getUserAccount()], + writableSpotMarketIndexes: [vaultAccount.spotMarketIndex], + }); + + const userStatsKey = getUserStatsAccountPublicKey( + this.driftClient.program.programId, + vault + ); + + const driftStateKey = await this.driftClient.getStatePublicKey(); + + const accounts = { + vault, + driftUserStats: userStatsKey, + driftUser: vaultAccount.user, + driftState: driftStateKey, + }; + + if (this.cliMode) { + return await this.program.methods + .managerBurnShares(burnAmount, burnUnit) + .accounts(accounts) + .remainingAccounts(remainingAccounts) + .rpc(); + } else { + const burnSharesIx = this.program.instruction.managerBurnShares( + burnAmount, + burnUnit, + { + accounts: { + manager: this.driftClient.wallet.publicKey, + ...accounts, + }, + remainingAccounts, + } + ); + + return await this.createAndSendTxn([burnSharesIx]); + } + } + public async getApplyProfitShareIx( vault: PublicKey, vaultDepositor: PublicKey From 84f0e7bce9093e99761251c878490875b9b86671 Mon Sep 17 00:00:00 2001 From: wphan Date: Wed, 8 Nov 2023 19:56:54 -0800 Subject: [PATCH 5/5] test with non-zero management fee --- programs/drift_vaults/src/tests.rs | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/programs/drift_vaults/src/tests.rs b/programs/drift_vaults/src/tests.rs index 8f104dd4..f0a199fe 100644 --- a/programs/drift_vaults/src/tests.rs +++ b/programs/drift_vaults/src/tests.rs @@ -509,7 +509,7 @@ mod vault_fcn { fn test_manager_burn_shares_for_users() { let mut now = 123456789; let vault = &mut Vault::default(); - vault.management_fee = 0; + vault.management_fee = 20_000; // 2% vault.last_fee_update_ts = now; let mut vault_equity: u64 = 0; @@ -522,7 +522,6 @@ mod vault_fcn { assert_eq!(vault.user_shares, 0); assert_eq!(vault.total_shares, 100000000); - now += 60 * 60; // first new user deposits $2000 let user_deposit_amount = manager_deposit_amount * 20; @@ -557,13 +556,21 @@ mod vault_fcn { .unwrap(); assert_eq!(user_amount_1, user_deposit_amount); + now += 365 * 24 * 60 * 60; // 1 year of management fees + vault.apply_management_fee(vault_equity, now).unwrap(); let vault_manager_amount_before = depositor_shares_to_vault_amount( vault.total_shares - vault.user_shares, vault.total_shares, vault_equity, ) .unwrap(); - assert_eq!(vault_manager_amount_before, manager_deposit_amount); + assert_eq!(vault_manager_amount_before, 179_999_372); // 2% of $4k + $100, rounding + println!( + "user shares: {}, manager shares: {}, total shares: {}", + vault.user_shares, + vault.total_shares - vault.user_shares, + vault.total_shares + ); // manager burns half their deposits vault @@ -576,17 +583,23 @@ mod vault_fcn { vault_equity, ) .unwrap(); - assert_eq!(vault_manager_amount_after, 49_999_999); // rounding - let manager_amount_diff = vault_manager_amount_before - vault_manager_amount_after; + assert_eq!(vault_manager_amount_after, 89_999_685); // 50% of $180, rounding + + println!( + "user shares: {}, manager shares: {}, total shares: {}", + vault.user_shares, + vault.total_shares - vault.user_shares, + vault.total_shares + ); - // first and second user should each gain half of manager's burnt shares + // first and second user should each gain half of manager's burnt value let user_amount_0 = depositor_shares_to_vault_amount( vd_0.checked_vault_shares(vault).unwrap(), vault.total_shares, vault_equity, ) .unwrap(); - assert_eq!(user_amount_0, user_deposit_amount + manager_amount_diff / 2); + assert_eq!(user_amount_0, 2005_000_157); // 2000 * 0.98 + 90 / 2, rounding let user_amount_1 = depositor_shares_to_vault_amount( vd_1.checked_vault_shares(vault).unwrap(), @@ -594,7 +607,7 @@ mod vault_fcn { vault_equity, ) .unwrap(); - assert_eq!(user_amount_1, user_deposit_amount + manager_amount_diff / 2); + assert_eq!(user_amount_1, 2005_000_157); // 2000 * 0.98 + 90 / 2, rounding } #[test]