Skip to content
Open
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
132 changes: 129 additions & 3 deletions src/domain/mint/planner/plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,47 @@ export async function planMint({
args: [token, equityInCollateralAsset],
})

// This price is adding the NAV diff from spot on top of the share slippage
const minShares = applySlippageFloor(routerPreview.shares, shareSlippageBps)
const flashLoanAmountInitial = routerPreview.debt
const debtToCollateralQuoteInitial = await quoteDebtToCollateral({
intent: 'exactIn',
inToken: debtAsset,
outToken: collateralAsset,
amountIn: flashLoanAmountInitial,
slippageBps: swapSlippageBps,
})

const previewDepositInitial = await publicClient.readContract({
address: getContractAddresses(chainId).leverageManagerV2 as Address,
abi: leverageManagerV2Abi,
functionName: 'previewDeposit',
args: [token, equityInCollateralAsset + debtToCollateralQuoteInitial.out],
})

const exchangeRateScale =
10n **
BigInt(
Math.max(
leverageTokenConfig.debtAsset.decimals,
leverageTokenConfig.collateralAsset.decimals,
),
)
const collateralToDebtRateFromQuote =
debtToCollateralQuoteInitial.out > 0n
? (flashLoanAmountInitial * exchangeRateScale) / debtToCollateralQuoteInitial.out
: 0n
const collateralToDebtRateFromPreviewDeposit =
(previewDepositInitial.debt * exchangeRateScale) /
(equityInCollateralAsset + debtToCollateralQuoteInitial.out)

const flashLoanAmount = applySlippageFloor(routerPreview.debt, flashLoanAdjustmentBps)
const flashLoanAmountUnadjusted = solveFlashLoanAmountFromImpliedRates({
equityInCollateralAsset,
collateralToDebtRateFromQuote,
collateralToDebtRateFromPreviewDeposit,
exchangeRateScale,
flashLoanAmountInitial,
previewDepositDebtInitialSample: previewDepositInitial.debt,
})
const flashLoanAmount = applySlippageFloor(flashLoanAmountUnadjusted, flashLoanAdjustmentBps)

const debtToCollateralQuote = await quoteDebtToCollateral({
intent: 'exactIn',
Expand All @@ -83,6 +120,21 @@ export async function planMint({
slippageBps: swapSlippageBps,
})

if (debtToCollateralQuote.out <= 0n) {
captureMintPlanError({
errorString: `Debt to collateral quote output ${debtToCollateralQuote.out} is less than or equal to 0`,
shareSlippageBps,
swapSlippageBps,
flashLoanAdjustmentBps,
routerPreview,
debtToCollateralQuote,
flashLoanAmount,
})
throw new Error(
`Insufficient DEX liquidity to mint. Try minting a smaller amount of Leverage Tokens.`,
)
}

const [managerPreview, managerMin] = await publicClient.multicall({
allowFailure: false,
contracts: [
Expand All @@ -101,6 +153,8 @@ export async function planMint({
],
})

const minShares = applySlippageFloor(managerPreview.shares, shareSlippageBps)

if (managerPreview.debt < flashLoanAmount) {
captureMintPlanError({
errorString: `Manager previewed debt ${managerPreview.debt} is less than flash loan amount ${flashLoanAmount}.`,
Expand Down Expand Up @@ -183,3 +237,75 @@ export async function planMint({
quoteSourceId: debtToCollateralQuote.quoteSourceId,
}
}

export function solveFlashLoanAmountFromImpliedRates({
equityInCollateralAsset,
collateralToDebtRateFromQuote,
collateralToDebtRateFromPreviewDeposit,
exchangeRateScale,
flashLoanAmountInitial,
previewDepositDebtInitialSample,
}: {
equityInCollateralAsset: bigint
collateralToDebtRateFromQuote: bigint
collateralToDebtRateFromPreviewDeposit: bigint
exchangeRateScale: bigint
flashLoanAmountInitial: bigint
previewDepositDebtInitialSample: bigint
}): bigint {
if (collateralToDebtRateFromQuote <= collateralToDebtRateFromPreviewDeposit) {
// Under the linearized model, quoteRate <= previewDepositDebtRate means there is no
// finite upper bound for debt >= flash-loan. Use a sampled point that is
// already observed via exact quote and previewDeposit.
return previewDepositDebtInitialSample >= flashLoanAmountInitial
? flashLoanAmountInitial
: previewDepositDebtInitialSample
}

// Derive a bound for flash-loan debt F such that predicted previewDeposit debt covers repayment.
//
// Definitions (all in base units):
// - F = flash-loan debt amount (debt units)
// - E = equityInCollateralAsset (collateral units)
// - q = collateralToDebtRateFromQuote / exchangeRateScale
// where q is debt-per-collateral from the quote sample
// - m = collateralToDebtRateFromPreviewDeposit / exchangeRateScale
// where m is debt-per-collateral from the previewDeposit sample
//
// Approximate relationships:
// - quoteOutCollateral(F) ~= F / q
// - previewDepositDebt(F) ~= m * (E + quoteOutCollateral(F))
// ~= m * (E + F / q)
//
// Safety condition we want:
// - previewDepositDebt(F) >= F
//
// Solve:
// m * (E + F / q) >= F
// mE + (m/q)F >= F
// mE >= F * (1 - m/q)
// F <= mE / (1 - m/q)
// F <= (m * q * E) / (q - m)
//
// Since rates are scaled integers:
// - mScaled = m * exchangeRateScale
// - qScaled = q * exchangeRateScale
// Substituting and simplifying gives:
// F <= (mScaled * qScaled * E) / (exchangeRateScale * (qScaled - mScaled))
const numerator =
collateralToDebtRateFromPreviewDeposit * collateralToDebtRateFromQuote * equityInCollateralAsset
const denominator =
exchangeRateScale * (collateralToDebtRateFromQuote - collateralToDebtRateFromPreviewDeposit)

const maxFlashLoanSatisfyingInequality = denominator > 0n ? numerator / denominator : 0n

// Defensive fallback: if the inequality-derived bound is zero/negative
// (e.g. due to rounding or degenerate sampled rates), use the router
// preview baseline rather than returning an invalid flash-loan amount.
// This value may still work depending on what the flash loan adjustment parameter is set to.
if (maxFlashLoanSatisfyingInequality <= 0n) {
return flashLoanAmountInitial
}

return maxFlashLoanSatisfyingInequality
}
2 changes: 1 addition & 1 deletion src/features/leverage-tokens/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const DEFAULT_COLLATERAL_SLIPPAGE_PERCENT_DISPLAY = '0.5'
// Preset slippage options (percent strings) shown in the advanced UI
export const SHARE_SLIPPAGE_PRESETS_PERCENT_DISPLAY_MINT = ['0.1', '0.5', '1.0'] as const
export const SWAP_SLIPPAGE_PRESETS_PERCENT_DISPLAY = ['0.01', '0.05', '0.1'] as const
export const FLASH_LOAN_ADJUSTMENT_PRESETS_PERCENT_DISPLAY = ['0.5', '1.0', '1.5'] as const
export const FLASH_LOAN_ADJUSTMENT_PRESETS_PERCENT_DISPLAY = ['0.1', '0.5', '1.0'] as const
export const COLLATERAL_SLIPPAGE_PRESETS_PERCENT_DISPLAY_REDEEM = ['0.1', '0.5', '1.0'] as const
export const COLLATERAL_SWAP_ADJUSTMENT_PRESETS_PERCENT_DISPLAY = ['0.01', '0.05', '0.1'] as const

Expand Down
4 changes: 2 additions & 2 deletions src/lib/observability/sentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,10 +192,10 @@ export function captureMintPlanError(params: {
debt: bigint
}
debtToCollateralQuote: Quote
managerPreview: {
managerPreview?: {
debt: bigint
}
managerMin: {
managerMin?: {
debt: bigint
shares: bigint
}
Expand Down
40 changes: 22 additions & 18 deletions tests/integration_new/helpers/mint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ import type { MintPlan } from '@/domain/mint'
import type { DebtToCollateralSwapConfig } from '@/domain/mint/utils/createDebtToCollateralQuote'
import type { QuoteFn } from '@/domain/shared/adapters/types'
import { useApprovalFlow } from '@/features/leverage-tokens/components/leverage-token-mint-modal'
import {
DEFAULT_FLASH_LOAN_ADJUSTMENT_PERCENT_DISPLAY,
DEFAULT_SLIPPAGE_PERCENT_DISPLAY,
DEFAULT_SWAP_SLIPPAGE_PERCENT_DISPLAY,
} from '@/features/leverage-tokens/constants'
import { useDebtToCollateralQuote } from '@/features/leverage-tokens/hooks/mint/useDebtToCollateralQuote'
import { useMintPlanPreview } from '@/features/leverage-tokens/hooks/mint/useMintPlanPreview'
import { useMintWrite } from '@/features/leverage-tokens/hooks/mint/useMintWrite'
Expand All @@ -30,22 +35,21 @@ export class MintExecutionSimulationError extends Error {
}
}

const DEFAULT_START_SHARE_SLIPPAGE_BPS = 100
const DEFAULT_SHARE_SLIPPAGE_INCREMENT_BPS = 100
const DEFAULT_FLASH_LOAN_ADJUSTMENT_INCREMENT_BPS = 100
const MAX_ATTEMPTS = 5

export async function testMint({
client,
wagmiConfig,
leverageTokenConfig,
startShareSlippageBps = DEFAULT_START_SHARE_SLIPPAGE_BPS,
slippageIncrementBps = DEFAULT_SHARE_SLIPPAGE_INCREMENT_BPS,
startFlashLoanAdjustmentBps = Number(DEFAULT_FLASH_LOAN_ADJUSTMENT_PERCENT_DISPLAY) * 100,
flashLoanAdjustmentIncrementBps = DEFAULT_FLASH_LOAN_ADJUSTMENT_INCREMENT_BPS,
}: {
client: AnvilTestClient
wagmiConfig: Config
leverageTokenConfig: LeverageTokenConfig
startShareSlippageBps?: number
slippageIncrementBps?: number
startFlashLoanAdjustmentBps?: number
flashLoanAdjustmentIncrementBps?: number
}) {
const equityInCollateralAsset =
leverageTokenConfig.test.mintIntegrationTest.equityInCollateralAsset
Expand Down Expand Up @@ -74,8 +78,8 @@ export async function testMint({
wagmiConfig,
leverageTokenConfig,
equityInCollateralAsset,
startShareSlippageBps,
slippageIncrementBps,
startFlashLoanAdjustmentBps,
flashLoanAdjustmentIncrementBps,
})

if (!plan) {
Expand Down Expand Up @@ -111,17 +115,17 @@ async function mintWithRetries({
wagmiConfig,
leverageTokenConfig,
equityInCollateralAsset,
startShareSlippageBps,
slippageIncrementBps,
startFlashLoanAdjustmentBps,
flashLoanAdjustmentIncrementBps,
}: {
client: AnvilTestClient
wagmiConfig: Config
leverageTokenConfig: LeverageTokenConfig
equityInCollateralAsset: bigint
startShareSlippageBps: number
slippageIncrementBps: number
startFlashLoanAdjustmentBps: number
flashLoanAdjustmentIncrementBps: number
}): Promise<MintPlan | undefined> {
let shareSlippageBps = startShareSlippageBps
let flashLoanAdjustmentBps = startFlashLoanAdjustmentBps

for (let i = 0; i < MAX_ATTEMPTS; i++) {
try {
Expand All @@ -130,9 +134,9 @@ async function mintWithRetries({
wagmiConfig,
leverageTokenConfig,
equityInCollateralAsset,
shareSlippageBps,
swapSlippageBps: 1,
flashLoanAdjustmentBps: shareSlippageBps, // Setting to the same value as share slippage works fairly consistently
shareSlippageBps: Number(DEFAULT_SLIPPAGE_PERCENT_DISPLAY) * 100,
swapSlippageBps: Number(DEFAULT_SWAP_SLIPPAGE_PERCENT_DISPLAY) * 100,
flashLoanAdjustmentBps,
})

return plan
Expand All @@ -145,8 +149,8 @@ async function mintWithRetries({
.includes('try increasing the flash loan adjustment parameter'))

if (isRetryableError && i < MAX_ATTEMPTS - 1) {
shareSlippageBps += slippageIncrementBps
console.log(`Retrying mint with share slippage bps: ${shareSlippageBps}`)
flashLoanAdjustmentBps += flashLoanAdjustmentIncrementBps
console.log(`Retrying mint with flash loan adjustment bps: ${flashLoanAdjustmentBps}`)
continue
}

Expand Down
Loading