export const MAX_FEE_BPS = 10_000n; export const SECONDS_PER_YEAR = BigInt(365 * 24 * 60 * 60); /** * Matches `DEAD_WEIGHT` in the program. The first deposit ever made into a * vault donates this many LP units permanently to the vault to prevent * inflation/donation attacks; subsequent deposits are unaffected. */ export const DEAD_WEIGHT_LP = 1_000n; export function maxBigInt(a: bigint, b: bigint): bigint { return a > b ? a : b; } export function minBigInt(a: bigint, b: bigint): bigint { return a < b ? a : b; } /** Ceiling division for positive bigints. */ export function ceilDivBigInt(a: bigint, b: bigint): bigint { return (a + b - 1n) / b; } export function nowSeconds(): bigint { return BigInt(Math.floor(Date.now() / 1000)); } /** * Mirrors the program-side locked-profit decay: the locked profit drops * linearly over `lockedProfitDegradationDuration` and is zero once that * window has elapsed. Note: the legacy JS SDK measured elapsed time from * `lastUpdatedLockedProfit` (an amount, not a timestamp), which produced * incorrect values; this port follows the program, which uses `lastReport`. */ export function calculateLockedProfit( lastUpdatedLockedProfit: bigint, lockedProfitDegradationDuration: bigint, currentTimeSec: bigint, lastReport: bigint, ): bigint { if (lockedProfitDegradationDuration === 0n) return 0n; const duration = currentTimeSec - lastReport; if (duration <= 0n) return lastUpdatedLockedProfit; if (duration >= lockedProfitDegradationDuration) return 0n; return ( (lastUpdatedLockedProfit * (lockedProfitDegradationDuration - duration)) / lockedProfitDegradationDuration ); } /** * Estimates the LP that would be minted to cover management fees accrued * since `lastManagementFeeUpdateTs`. Returns 0n when the inputs make the * fee calculation undefined (no time elapsed, no assets, etc.). * * Both rounding steps are ceiling divisions, matching the program's * `calc_management_fee_amount_in_asset` and `calc_fee_lp_to_mint` — flooring * the asset fee first would understate the LP that should be minted whenever * the accrued fee isn't an exact multiple of the denominator. */ export function calculateUnrealisedLpFees( currentTotalLp: bigint, assetTotalValue: bigint, lastManagementFeeUpdateTs: bigint, managementFeeBps: bigint, currentTimeSec: bigint = nowSeconds(), ): bigint { if ( lastManagementFeeUpdateTs === 0n || assetTotalValue === 0n || managementFeeBps === 0n ) { return 0n; } const timeElapsed = maxBigInt(0n, currentTimeSec - lastManagementFeeUpdateTs); if (timeElapsed === 0n) return 0n; const feeAmountInAsset = ceilDivBigInt( assetTotalValue * timeElapsed * managementFeeBps, MAX_FEE_BPS * SECONDS_PER_YEAR, ); // Matches the program's `handle_management_fee`: when accrued fees would // meet or exceed the vault's asset value, the on-chain instruction reverts // with FeeExceedsTotalAssetValue. Quoting 0 here would silently mask a // state where every deposit/withdraw transaction is going to fail. if (feeAmountInAsset >= assetTotalValue) { throw new Error( "Accrued management fees exceed vault total asset value (program would revert with FeeExceedsTotalAssetValue)", ); } const lpDenominator = assetTotalValue - feeAmountInAsset; return ceilDivBigInt(feeAmountInAsset * currentTotalLp, lpDenominator); } /** * Total LP supply the program treats as outstanding for accounting: * circulating LP + accumulated (but unharvested) fee LP + dead weight LP. * Matches `Vault::get_total_lp_supply_incl_fees` in the program. */ export function getTotalLpSupplyInclFees(args: { lpSupply: bigint; vaultAccumulatedLpAdminFees: bigint; vaultAccumulatedLpManagerFees: bigint; vaultAccumulatedLpProtocolFees: bigint; vaultDeadWeight: bigint; }): bigint { return ( args.lpSupply + args.vaultAccumulatedLpAdminFees + args.vaultAccumulatedLpManagerFees + args.vaultAccumulatedLpProtocolFees + args.vaultDeadWeight ); } export type AssetsForWithdrawInputs = { vaultTotalValue: bigint; vaultLastUpdatedLockedProfit: bigint; vaultLastReport: bigint; vaultLockedProfitDegradationDuration: bigint; vaultAccumulatedLpAdminFees: bigint; vaultAccumulatedLpManagerFees: bigint; vaultAccumulatedLpProtocolFees: bigint; vaultDeadWeight: bigint; vaultRedemptionFeeBps: number; vaultManagementFeeBps: number; vaultLastManagementFeeUpdateTs: bigint; lpSupply: bigint; lpAmount: bigint; currentTimeSec?: bigint; }; export function calculateAssetsForWithdrawAmount( inputs: AssetsForWithdrawInputs, ): bigint { const { vaultTotalValue, vaultLastUpdatedLockedProfit, vaultLastReport, vaultLockedProfitDegradationDuration, vaultAccumulatedLpAdminFees, vaultAccumulatedLpManagerFees, vaultAccumulatedLpProtocolFees, vaultDeadWeight, vaultRedemptionFeeBps, vaultManagementFeeBps, vaultLastManagementFeeUpdateTs, lpSupply, lpAmount, currentTimeSec = nowSeconds(), } = inputs; if (lpSupply <= 0n) throw new Error("Invalid LP supply"); if (vaultTotalValue <= 0n) throw new Error("Invalid total assets"); const lockedProfit = calculateLockedProfit( vaultLastUpdatedLockedProfit, vaultLockedProfitDegradationDuration, currentTimeSec, vaultLastReport, ); const totalUnlockedValue = vaultTotalValue - lockedProfit; const lpSupplyInclAccumulatedFees = getTotalLpSupplyInclFees({ lpSupply, vaultAccumulatedLpAdminFees, vaultAccumulatedLpManagerFees, vaultAccumulatedLpProtocolFees, vaultDeadWeight, }); const unrealisedLpFees = calculateUnrealisedLpFees( lpSupplyInclAccumulatedFees, vaultTotalValue, vaultLastManagementFeeUpdateTs, BigInt(vaultManagementFeeBps), currentTimeSec, ); const lpSupplyInclFees = lpSupplyInclAccumulatedFees + unrealisedLpFees; // The program floors asset_to_redeem (favours the vault on the redeem side). const numerator = lpAmount * totalUnlockedValue * BigInt(10_000 - vaultRedemptionFeeBps); const denominator = lpSupplyInclFees * 10_000n; return numerator / denominator; } export type LpForWithdrawInputs = Omit & { assetAmount: bigint; }; /** * Computes the LP that must be burned to redeem at least `assetAmount`, * matching `calc_withdraw_lp_to_burn` in the program (which rounds up so * the burn always covers the requested asset amount). Flooring here would * underquote the burn and either revert or short-redeem. */ export function calculateLpForWithdrawAmount( inputs: LpForWithdrawInputs, ): bigint { const { vaultTotalValue, vaultLastUpdatedLockedProfit, vaultLastReport, vaultLockedProfitDegradationDuration, vaultAccumulatedLpAdminFees, vaultAccumulatedLpManagerFees, vaultAccumulatedLpProtocolFees, vaultDeadWeight, vaultRedemptionFeeBps, vaultManagementFeeBps, vaultLastManagementFeeUpdateTs, lpSupply, assetAmount, currentTimeSec = nowSeconds(), } = inputs; if (lpSupply <= 0n) throw new Error("Invalid LP supply"); if (vaultTotalValue <= 0n) throw new Error("Invalid total assets"); const lockedProfit = calculateLockedProfit( vaultLastUpdatedLockedProfit, vaultLockedProfitDegradationDuration, currentTimeSec, vaultLastReport, ); const totalUnlockedValue = vaultTotalValue - lockedProfit; const lpSupplyInclAccumulatedFees = getTotalLpSupplyInclFees({ lpSupply, vaultAccumulatedLpAdminFees, vaultAccumulatedLpManagerFees, vaultAccumulatedLpProtocolFees, vaultDeadWeight, }); const unrealisedLpFees = calculateUnrealisedLpFees( lpSupplyInclAccumulatedFees, vaultTotalValue, vaultLastManagementFeeUpdateTs, BigInt(vaultManagementFeeBps), currentTimeSec, ); const lpSupplyInclFees = lpSupplyInclAccumulatedFees + unrealisedLpFees; const numerator = assetAmount * lpSupplyInclFees * 10_000n; const denominator = totalUnlockedValue * BigInt(10_000 - vaultRedemptionFeeBps); return ceilDivBigInt(numerator, denominator); } export type LpForDepositInputs = { vaultTotalValue: bigint; vaultAccumulatedLpAdminFees: bigint; vaultAccumulatedLpManagerFees: bigint; vaultAccumulatedLpProtocolFees: bigint; vaultDeadWeight: bigint; vaultIssuanceFeeBps: number; vaultManagementFeeBps: number; vaultLastManagementFeeUpdateTs: bigint; lpSupply: bigint; assetAmount: bigint; /** Required when the pool is empty so the 1:1 mint can scale precision correctly. */ assetDecimals: number; /** Required when the pool is empty so the 1:1 mint can scale precision correctly. */ lpDecimals: number; currentTimeSec?: bigint; }; export function calculateLpForDepositAmount(inputs: LpForDepositInputs): bigint { const { vaultTotalValue, vaultAccumulatedLpAdminFees, vaultAccumulatedLpManagerFees, vaultAccumulatedLpProtocolFees, vaultDeadWeight, vaultIssuanceFeeBps, vaultManagementFeeBps, vaultLastManagementFeeUpdateTs, lpSupply, assetAmount, assetDecimals, lpDecimals, currentTimeSec = nowSeconds(), } = inputs; const lpSupplyInclAccumulatedFees = getTotalLpSupplyInclFees({ lpSupply, vaultAccumulatedLpAdminFees, vaultAccumulatedLpManagerFees, vaultAccumulatedLpProtocolFees, vaultDeadWeight, }); const unrealisedLpFees = calculateUnrealisedLpFees( lpSupplyInclAccumulatedFees, vaultTotalValue, vaultLastManagementFeeUpdateTs, BigInt(vaultManagementFeeBps), currentTimeSec, ); const lpSupplyInclFees = lpSupplyInclAccumulatedFees + unrealisedLpFees; let lpToMintBeforeDeadWeight: bigint; if (lpSupplyInclFees === 0n) { lpToMintBeforeDeadWeight = (assetAmount * 10n ** BigInt(lpDecimals)) / 10n ** BigInt(assetDecimals); } else { // The program floors mint quantity (favours existing holders on the // mint side), so we mirror that here. const numerator = assetAmount * lpSupplyInclFees * BigInt(10_000 - vaultIssuanceFeeBps); const totalAssetPostDeposit = vaultTotalValue + assetAmount; const denominator = totalAssetPostDeposit * 10_000n - assetAmount * BigInt(10_000 - vaultIssuanceFeeBps); lpToMintBeforeDeadWeight = numerator / denominator; } // First-ever deposit pays DEAD_WEIGHT_LP into the vault permanently. The // program rejects deposits that don't clear that floor, so we surface the // same condition here rather than silently quoting a value the user can't // actually receive. if (vaultDeadWeight === 0n) { if (lpToMintBeforeDeadWeight < DEAD_WEIGHT_LP) { throw new Error( `First deposit must mint at least ${DEAD_WEIGHT_LP} LP (would mint ${lpToMintBeforeDeadWeight})`, ); } return lpToMintBeforeDeadWeight - DEAD_WEIGHT_LP; } return lpToMintBeforeDeadWeight; }