diff --git a/programs/drift/src/state/lp_pool.rs b/programs/drift/src/state/lp_pool.rs index ff6b42d3f..34e444ef7 100644 --- a/programs/drift/src/state/lp_pool.rs +++ b/programs/drift/src/state/lp_pool.rs @@ -311,12 +311,16 @@ impl LPPool { dlp_total_supply: u64, ) -> DriftResult<(u64, u128, i64, i128)> { let lp_fee_to_charge_pct = self.get_mint_redeem_fee(now, false)?; - let lp_fee_to_charge = lp_burn_amount + let mut lp_fee_to_charge = lp_burn_amount .cast::()? .safe_mul(lp_fee_to_charge_pct.cast::()?)? .safe_div(PERCENTAGE_PRECISION_I128)? .cast::()?; + if dlp_total_supply.saturating_sub(lp_burn_amount) <= QUOTE_PRECISION_U64 && lp_fee_to_charge <= (QUOTE_PRECISION_U64 as i64) { + lp_fee_to_charge = (lp_burn_amount.min(QUOTE_PRECISION_U64) as i64); + } + let lp_amount_less_fees = (lp_burn_amount as i128).safe_sub(lp_fee_to_charge as i128)?; msg!("lp_amount_less_fees: {}", lp_amount_less_fees); diff --git a/programs/drift/src/state/lp_pool/tests.rs b/programs/drift/src/state/lp_pool/tests.rs index 839f40137..d146855c3 100644 --- a/programs/drift/src/state/lp_pool/tests.rs +++ b/programs/drift/src/state/lp_pool/tests.rs @@ -371,8 +371,9 @@ mod tests { } }; + // tuple is (market_index, base, price) let amm_inventory_and_prices: Vec<(u16, i64, i64)> = vec![(0, 1_000_000, 142_000_000)]; - let constituents_indexes_and_decimals_and_prices = vec![(1, 6, 142_000_000)]; + let constituents_indexes_and_decimals_and_prices = vec![(1, 9, 142_000_000)]; let prices = vec![142_000_000]; let aum = 0; @@ -399,8 +400,10 @@ mod tests { .unwrap(); assert_eq!(target_zc_mut.len(), 1); - assert_eq!(target_zc_mut.get(0).target_base, -1_000); // despite no aum, desire to reach target + assert_eq!(target_zc_mut.get(0).target_base, -1_000_000); // despite no aum, desire to reach target assert_eq!(target_zc_mut.get(0).last_slot, now_ts); + + } #[test] diff --git a/sdk/src/math/lpPool.ts b/sdk/src/math/lpPool.ts new file mode 100644 index 000000000..d1bd412ea --- /dev/null +++ b/sdk/src/math/lpPool.ts @@ -0,0 +1,65 @@ +import { calculateNetUserPnl } from './market'; + +import { + PerpMarketAccount, + SpotMarketAccount, + SpotBalanceType, + ConstituentAccount, + CacheInfo, + getSignedTokenAmount, + PRICE_PRECISION, +} from '..'; +import { OraclePriceData } from '.././oracles/types'; +import { BN } from '@coral-xyz/anchor'; +import { getTokenAmount } from './spotBalance'; + +export function getLpPoolNAVSpotComponent( + constituent: ConstituentAccount, + spotMarket: SpotMarketAccount, + oraclePriceData: OraclePriceData +): BN { + const tokenPrecision = new BN(Math.pow(10, spotMarket.decimals)); + + return constituent.tokenBalance + .add( + getSignedTokenAmount( + getTokenAmount( + constituent.spotBalance.scaledBalance, + spotMarket, + constituent.spotBalance.balanceType + ), + constituent.spotBalance.balanceType + ) + ) + .mul(oraclePriceData.price) + .div(PRICE_PRECISION) + .div(tokenPrecision); +} + +export function getLpPoolNAVPerpComponent( + ammCacheInfo: CacheInfo, + perpMarket: PerpMarketAccount, + spotMarket: SpotMarketAccount, + oraclePriceData: OraclePriceData +): BN { + const netUserPnl = calculateNetUserPnl(perpMarket, oraclePriceData); + + const pnlPool = getTokenAmount( + perpMarket.pnlPool.scaledBalance, + spotMarket, + SpotBalanceType.DEPOSIT + ); + const feePool = getTokenAmount( + perpMarket.amm.feePool.scaledBalance, + spotMarket, + SpotBalanceType.DEPOSIT + ); + + const currentNetPnlPoolTokenAmount = pnlPool.add(feePool).sub(netUserPnl); + + const perpPerformanceDelta = currentNetPnlPoolTokenAmount.sub( + ammCacheInfo.lastNetPnlPoolTokenAmount + ); + + return perpPerformanceDelta; +} diff --git a/tests/lpPool.ts b/tests/lpPool.ts index 405d14ac4..eb6f4f1b7 100644 --- a/tests/lpPool.ts +++ b/tests/lpPool.ts @@ -606,6 +606,12 @@ describe('LP Pool', () => { await adminClient.sendTransaction(new Transaction().add(createAtaIx), []); + await adminClient.updateLpPoolAum(lpPool, [0, 1]); + lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + assert(lpPool.lastAum.eq(ZERO)); + const tx = new Transaction(); tx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1])); tx.add( @@ -1098,10 +1104,12 @@ describe('LP Pool', () => { lpPoolKey )) as LPPoolAccount; - assert(ammCache.cache[0].quoteOwedFromLp.eq(owedAmount.divn(2))); assert(constituent.tokenBalance.eq(ZERO)); assert(lpPool.lastAum.eq(ZERO)); - + // assert(ammCache.cache[0].quoteOwedFromLp.eq(owedAmount.divn(2))); + expect(ammCache.cache[0].quoteOwedFromLp.toNumber()).to.eq( + owedAmount.divn(2).toNumber() + ); // Deposit here to DLP to make sure aum calc work with perp market debt await overWriteMintAccount( bankrunContextWrapper, @@ -1335,4 +1343,96 @@ describe('LP Pool', () => { 10 ); }); + + it('remove aum then add back', async () => { + const lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + const tx = new Transaction(); + expect(lpPool.lastAum.toNumber()).to.eq(1049220180); + + const lpTokenBalanceBefore = + await bankrunContextWrapper.connection.getTokenAccount( + userLpTokenAccount + ); + expect(Number(lpTokenBalanceBefore.amount.toString())).to.equal(1000000000); + + const mintInfo = await getMint( + bankrunContextWrapper.connection.toConnection(), + lpPool.mint as PublicKey + ); + expect(mintInfo.decimals).to.equal(tokenDecimals); + expect(Number(mintInfo.supply)).to.equal(1000000000); + expect(mintInfo.mintAuthority?.toBase58()).to.equal( + adminClient.getSignerPublicKey().toBase58() + ); + + // console.log(lpPool); + tx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1, 2, 3])); + tx.add( + await adminClient.getLpPoolRemoveLiquidityIx({ + lpPool, + minAmountOut: new BN(1000).mul(QUOTE_PRECISION), + lpToBurn: new BN(1000).mul(QUOTE_PRECISION), + outMarketIndex: 0, + }) + ); + await adminClient.sendTransaction(tx); + + const lpPoolAfter = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + const deltaAum = lpPool.lastAum.sub(lpPoolAfter.lastAum); + + expect(lpPoolAfter.lastAum.toNumber()).to.eq(1363672); // residual fee + expect(deltaAum.toNumber()).to.eq( + 1049820000 - 1363672 - 1400000 // price of 1 dlp + ); + + const mintInfoAfter = await getMint( + bankrunContextWrapper.connection.toConnection(), + lpPool.mint as PublicKey + ); + expect(Number(mintInfoAfter.supply)).to.equal(1000000); + + const lpTokenBalanceAfter = + await bankrunContextWrapper.connection.getTokenAccount( + userLpTokenAccount + ); + expect(Number(lpTokenBalanceAfter.amount)).to.equal(0); + + // TODO: below shoudn't fail (Slippage outside limit: lp_mint_amount_net_fees(0) < min_mint_amount(10)) + const txNext = new Transaction(); + txNext.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1, 2, 3])); + txNext.add( + await adminClient.getLpPoolAddLiquidityIx({ + lpPool, + inAmount: new BN(1000).mul(QUOTE_PRECISION), + minMintAmount: new BN(10), + inMarketIndex: 0, + }) + ); + await adminClient.sendTransaction(txNext); + + const lpPoolAfter2 = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + // expect(lpPoolAfter2.lastAum).to.equal(1000000000); + expect(Number(lpPoolAfter2.lastAum.toNumber())).to.equal(1000314947); + + const mintInfoAfter2 = await getMint( + bankrunContextWrapper.connection.toConnection(), + lpPool.mint as PublicKey + ); + expect(Number(mintInfoAfter2.supply)).to.equal(1000000); + + const lpTokenBalanceAfter2 = + await bankrunContextWrapper.connection.getTokenAccount( + userLpTokenAccount + ); + // expect(Number(lpTokenBalanceAfter2.amount)).to.equal(1000000000); + expect(Number(lpTokenBalanceAfter2.amount)).to.equal(3174); + }); });