diff --git a/Cargo.lock b/Cargo.lock index 70adbc8f..10ebbdb8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1000,6 +1000,7 @@ dependencies = [ "ahash 0.8.6", "anchor-lang", "anchor-spl", + "base64 0.13.0", "bytemuck", "drift", "drift-macros", diff --git a/programs/drift_vaults/Cargo.toml b/programs/drift_vaults/Cargo.toml index 9166f3b8..1864664f 100644 --- a/programs/drift_vaults/Cargo.toml +++ b/programs/drift_vaults/Cargo.toml @@ -21,4 +21,7 @@ bytemuck = { version = "1.4.0" } static_assertions = "1.1.0" drift-macros = { git = "https://github.com/drift-labs/drift-macros.git", rev = "c57d87" } ahash = "=0.8.6" -serde = "=1.0.209" \ No newline at end of file +serde = "=1.0.209" + +[dev-dependencies] +base64 = "0.13.0" \ No newline at end of file diff --git a/programs/drift_vaults/src/lib.rs b/programs/drift_vaults/src/lib.rs index 87e45e3d..89b0e3f7 100644 --- a/programs/drift_vaults/src/lib.rs +++ b/programs/drift_vaults/src/lib.rs @@ -8,6 +8,9 @@ mod error; mod instructions; pub mod macros; pub mod state; +#[cfg(test)] +mod test_utils; +#[cfg(test)] mod tests; mod token_cpi; diff --git a/programs/drift_vaults/src/state/traits.rs b/programs/drift_vaults/src/state/traits.rs index 54745415..4d71e617 100644 --- a/programs/drift_vaults/src/state/traits.rs +++ b/programs/drift_vaults/src/state/traits.rs @@ -103,9 +103,11 @@ pub trait VaultDepositorBase { let profit_share_amount = manager_profit_share_amount.safe_add(protocol_profit_share_amount)?; + let net_profit = profit_u128.safe_sub(profit_share_amount)?; + self.set_cumulative_profit_share_amount( self.get_cumulative_profit_share_amount() - .safe_add(profit_u128.cast()?)?, + .safe_add(net_profit.cast()?)?, ); self.set_profit_share_fee_paid( diff --git a/programs/drift_vaults/src/state/vault_depositor.rs b/programs/drift_vaults/src/state/vault_depositor.rs index 1ae41afb..72c67929 100644 --- a/programs/drift_vaults/src/state/vault_depositor.rs +++ b/programs/drift_vaults/src/state/vault_depositor.rs @@ -1131,8 +1131,8 @@ mod vault_v1_tests { // total shares outside of user is now 100M + 7.5M = 107.5M assert_eq!(vault.total_shares, 107_500_000); assert_eq!(withdraw_amount, equity_minus_fee); - // $100 worth of profit that has been realized (this is not total fees paid) - assert_eq!(vd.cumulative_profit_share_amount, 100_000_000); + // $85 = 100 - 10% - 5%, worth of profit that has been realized (this is not total fees paid) + assert_eq!(vd.cumulative_profit_share_amount, 85_000_000); println!("vault shares: {}", vd.checked_vault_shares(&vault).unwrap()); println!("shares base: {}", vd.vault_shares_base); println!("user shares: {}", vault.user_shares); @@ -1245,8 +1245,8 @@ mod vault_v1_tests { // total shares outside of user is now 100M + 5M = 105M assert_eq!(vault.total_shares, 105_000_000); assert_eq!(withdraw_amount, equity_minus_fee); - // $100 worth of profit that has been realized (this is not total fees paid) - assert_eq!(vd.cumulative_profit_share_amount, 100_000_000); + // $90 = $100 - 10% worth of profit that has been realized (this is not total fees paid) + assert_eq!(vd.cumulative_profit_share_amount, 90_000_000); println!("vault shares: {}", vd.checked_vault_shares(&vault).unwrap()); println!("shares base: {}", vd.vault_shares_base); println!("user shares: {}", vault.user_shares); diff --git a/programs/drift_vaults/src/test_utils.rs b/programs/drift_vaults/src/test_utils.rs new file mode 100644 index 00000000..bc3fdfb4 --- /dev/null +++ b/programs/drift_vaults/src/test_utils.rs @@ -0,0 +1,27 @@ +use anchor_lang::prelude::{AccountInfo, Pubkey}; + +pub fn create_account_info<'a>( + key: &'a Pubkey, + is_writable: bool, + lamports: &'a mut u64, + bytes: &'a mut [u8], + owner: &'a Pubkey, +) -> AccountInfo<'a> { + AccountInfo::new(key, false, is_writable, lamports, bytes, owner, false, 0) +} + +#[macro_export] +macro_rules! create_account_info { + ($account:expr, $owner:expr, $name: ident) => { + let key = Pubkey::default(); + let mut lamports = 0; + let mut data = get_account_bytes(&mut $account); + let owner = $type::owner(); + let $name = create_account_info(&key, true, &mut lamports, &mut data[..], $owner); + }; + ($account:expr, $pubkey:expr, $owner:expr, $name: ident) => { + let mut lamports = 0; + let mut data = get_account_bytes(&mut $account); + let $name = create_account_info($pubkey, true, &mut lamports, &mut data[..], $owner); + }; +} diff --git a/programs/drift_vaults/src/tests.rs b/programs/drift_vaults/src/tests.rs index 0da73263..a44031ae 100644 --- a/programs/drift_vaults/src/tests.rs +++ b/programs/drift_vaults/src/tests.rs @@ -1,10 +1,15 @@ #[cfg(test)] mod vault_fcn { + use std::str::FromStr; + use crate::state::traits::VaultDepositorBase; + use crate::test_utils::create_account_info; use crate::withdraw_request::WithdrawRequest; use crate::{Vault, VaultDepositor, WithdrawUnit}; - use anchor_lang::prelude::Pubkey; - use drift::math::constants::{ONE_YEAR, QUOTE_PRECISION_U64}; + use anchor_lang::prelude::{AccountLoader, Pubkey}; + use drift::math::constants::{ + ONE_YEAR, QUOTE_PRECISION, QUOTE_PRECISION_I64, QUOTE_PRECISION_U64, + }; use drift::math::insurance::if_shares_to_vault_amount as depositor_shares_to_vault_amount; #[test] @@ -498,7 +503,7 @@ mod vault_fcn { assert_eq!(cnt, 4); // 4 days assert_eq!( vd.cumulative_profit_share_amount, - (1000 * QUOTE_PRECISION_U64) as i64 + (850 * QUOTE_PRECISION_U64) as i64 // 1000 - 15% profit share ); assert_eq!(vd.net_deposits, (2000 * QUOTE_PRECISION_U64) as i64); @@ -747,6 +752,159 @@ mod vault_fcn { ); assert!(!finishing_liquidation); } + + #[test] + fn test_apply_profit_share_on_net_hwm() { + let mut now = 123456789; + let mut vault = Vault::default(); + let mut vp = None; + vault.management_fee = 0; + vault.profit_share = 100_000; // 10% + vault.last_fee_update_ts = now; + + let mut vault_equity: u64 = 0; + + let deposit_amount = 2000 * QUOTE_PRECISION_U64; + let vd = + &mut VaultDepositor::new(Pubkey::default(), Pubkey::default(), Pubkey::default(), now); + vd.deposit(deposit_amount, vault_equity, &mut vault, &mut vp, now) + .unwrap(); // new user deposits $2000 + vault_equity += deposit_amount; + + let depositor_amount_before = depositor_shares_to_vault_amount( + vd.checked_vault_shares(&vault).unwrap(), + vault.total_shares, + vault_equity, + ) + .unwrap(); + assert_eq!(depositor_amount_before, vault_equity); + + // vault up 10% (user +$200 gross, +$180 net) + now += 60 * 60 * 24; // 1 day later + vault_equity = 2200 * QUOTE_PRECISION_U64; + + vd.apply_profit_share(vault_equity, &mut vault, &mut vp) + .unwrap(); + vault.apply_fee(&mut vp, vault_equity, now).unwrap(); + + let depositor_amount_in_profit = depositor_shares_to_vault_amount( + vd.checked_vault_shares(&vault).unwrap(), + vault.total_shares, + vault_equity, + ) + .unwrap(); + assert_eq!(depositor_amount_in_profit, 2180 * QUOTE_PRECISION_U64); + assert_eq!(vd.cumulative_profit_share_amount, 180 * QUOTE_PRECISION_I64); + assert_eq!(vd.profit_share_fee_paid, 20 * QUOTE_PRECISION_U64); + assert_eq!(vault.total_shares, 2000 * QUOTE_PRECISION); + assert_eq!(vd.checked_vault_shares(&vault).unwrap(), 1981_818_182); + + // vault drawdown 10% + now += 60 * 60 * 24; // 1 day later + vault_equity = 1980 * QUOTE_PRECISION_U64; + + vd.apply_profit_share(vault_equity, &mut vault, &mut vp) + .unwrap(); + vault.apply_fee(&mut vp, vault_equity, now).unwrap(); + + let depositor_amount_in_drawdown = depositor_shares_to_vault_amount( + vd.checked_vault_shares(&vault).unwrap(), + vault.total_shares, + vault_equity, + ) + .unwrap(); + assert_eq!(depositor_amount_in_drawdown, 1962 * QUOTE_PRECISION_U64); + assert_eq!(vd.cumulative_profit_share_amount, 180 * QUOTE_PRECISION_I64); + assert_eq!(vd.profit_share_fee_paid, 20 * QUOTE_PRECISION_U64); + assert_eq!(vault.total_shares, 2000 * QUOTE_PRECISION); + assert_eq!(vd.checked_vault_shares(&vault).unwrap(), 1981_818_182); + + // in profit again (above net hwm (2180), below gross hwm (2200)) + // vd equity = 2210*1982/2000 = 2190 + now += 60 * 60 * 24; // 1 day later + vault_equity = 2210 * QUOTE_PRECISION_U64; + + vd.apply_profit_share(vault_equity, &mut vault, &mut vp) + .unwrap(); + vault.apply_fee(&mut vp, vault_equity, now).unwrap(); + + let depositor_amount_in_profit = depositor_shares_to_vault_amount( + vd.checked_vault_shares(&vault).unwrap(), + vault.total_shares, + vault_equity, + ) + .unwrap(); + assert_eq!(depositor_amount_in_profit, 2188_918_182); + assert_eq!(vd.cumulative_profit_share_amount, 188_918_182); + assert_eq!(vd.profit_share_fee_paid, 20_990_909); + assert_eq!(vault.total_shares, 2000 * QUOTE_PRECISION); + assert_eq!(vd.checked_vault_shares(&vault).unwrap(), 1980_921_432); + } + + #[test] + fn apply_profit_share_on_net_hwm_example() { + let vault_str = String::from("0wjoKwKYdXdTdXBlcmNoYXJnZXIgVmF1bHQgICAgICAgICAgICAgIObOTURhcaZ/hexxlaSKNnYv57PHIZx9B8zN8k75zR8X5YskhtQuJRuc1ZuimumplgthmeBFASQW793js7pxldJCFqweTOnzcxGL+XKJx2Lif7339IdAC/KyKj8JEFZwkobFx9kGlk72vtua5uHjyHSzAViuG0/APV227Zmg8aZjmuxoym5eTWKAUNtoEdxsbA9c5IdRusnrLgc4WGs7FVYLef8oRnsVQX7JdnHagGAmJsU0t+iRQjIn8UdUnf8jwgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQjcnBxIEAAAAAAAAAAAAAMeNOt6kBAAAAAAAAAAAAAC5KoFnAAAAAAAAAAAAAAAAgDoJAAAAAADHSjWPHAAAAADgV+tIGwAAAAAAAAAAAADTz/tkAAAAAHycB+TTAgAAAEibJrr///8aotBlLSwAAJ4FyYFZKQAAAAAAAAAAAAAAuGTZRQAAAAAAAAAAAAAAEQ28vgkBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJ//V2YAAAAAAAAAAOCTBAAAAAAAAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="); + let mut vault_decoded_bytes = base64::decode(vault_str).unwrap(); + let vault_bytes = vault_decoded_bytes.as_mut_slice(); + let mut lamports = 0; + let key = Pubkey::default(); + let owner = Pubkey::from_str("vAuLTsyrvSfZRuRB3XgvkPwNGgYSs9YRYymVebLKoxR").unwrap(); + let vault_account_info = + create_account_info(&key, true, &mut lamports, vault_bytes, &owner); + let vault_loader: AccountLoader = + AccountLoader::try_from(&vault_account_info).unwrap(); + + let vd_str = String::from("V222aldgP9Pmzk1EYXGmf4XscZWkijZ2L+ezxyGcfQfMzfJO+c0fF3+94atStThJMBa2OKTfUXJGJXPLv0FNp+KPmd6vhnmEDTcLJRUaWdAPxV7zBY0GiaKVHFu9h+8uxW0VlL9rvWTAzbMLAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHJ+VZgAAAAAAAAAAAAAAAABJfw8AAAAAAEl/DwAAAAAAAAAAAAAAAKkKggIAAAAAypzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="); + let mut vd_decoded_bytes = base64::decode(vd_str).unwrap(); + let vd_bytes = vd_decoded_bytes.as_mut_slice(); + let mut lamports = 0; + let key = Pubkey::default(); + let owner = Pubkey::from_str("vAuLTsyrvSfZRuRB3XgvkPwNGgYSs9YRYymVebLKoxR").unwrap(); + let vd_account_info = create_account_info(&key, true, &mut lamports, vd_bytes, &owner); + let vd_loader: AccountLoader = + AccountLoader::try_from(&vd_account_info).unwrap(); + + let mut vault = vault_loader.load_mut().unwrap(); + let mut vd = vd_loader.load_mut().unwrap(); + let mut vp = None; + + let vault_equity = 8_045_367_880_000; + let vd_amount = depositor_shares_to_vault_amount( + vd.checked_vault_shares(&vault).unwrap(), + vault.total_shares, + vault_equity, + ) + .unwrap(); + + let vd_hwm = + (vd.total_deposits - vd.total_withdraws) as i64 + vd.cumulative_profit_share_amount; + let unrealized_profit = vd_amount as i64 - vd_hwm; + + assert_eq!(vd_amount, 309_346_825); + assert_eq!(vd_hwm, 302_076_841); + assert_eq!(unrealized_profit, 7_269_984); + + let (manager_profit_share, protocol_profit_share) = vd + .apply_profit_share(vault_equity, &mut vault, &mut vp) + .unwrap(); + + assert_eq!(manager_profit_share, 2_180_995); + assert_eq!(protocol_profit_share, 0); + + let vd_amount = depositor_shares_to_vault_amount( + vd.checked_vault_shares(&vault).unwrap(), + vault.total_shares, + vault_equity, + ) + .unwrap(); + let vd_hwm = + (vd.total_deposits - vd.total_withdraws) as i64 + vd.cumulative_profit_share_amount; + let unrealized_profit = vd_amount as i64 - vd_hwm; + + assert_eq!(vd_amount, 307_165_832); + assert_eq!(vd_hwm, 307_165_830); + assert_eq!(unrealized_profit, 2); + } } #[cfg(test)] @@ -1511,7 +1669,7 @@ mod vault_v1_fcn { assert_eq!(cnt, 4); // 4 days assert_eq!( vd.cumulative_profit_share_amount, - (1000 * QUOTE_PRECISION_U64) as i64 + (850 * QUOTE_PRECISION_U64) as i64 // $850 = $1000 - 15% ); assert_eq!(vd.net_deposits, (2000 * QUOTE_PRECISION_U64) as i64); @@ -1619,7 +1777,7 @@ mod vault_v1_fcn { assert_eq!(cnt, 4); // 4 days assert_eq!( vd.cumulative_profit_share_amount, - (1000 * QUOTE_PRECISION_U64) as i64 + (850 * QUOTE_PRECISION_U64) as i64 // $850 = $1000 - 15% ); assert_eq!(vd.net_deposits, (2000 * QUOTE_PRECISION_U64) as i64);