UNPKG

@indigo-labs/indigo-sdk

Version:

Indigo SDK for interacting with Indigo endpoints via lucid-evolution

423 lines (379 loc) 12.4 kB
/** * The following is the math related to the leverage calculations. * * Leverage is the multiplier you apply to the base deposit and you get the amount of final collateral * the CDP should have. Additionally, the minted amount is used to pay for fees. The leverage a user picks, is * already taking into account the fees, i.e. the fees are paid from the borrowed assets. * * There's a direct relationship between collateral ratio and leverage multiplier. Each leverage multiplier * results in a single collateral ratio and vice versa. Maximum potential leverage is the leverage that * results in collateral ratio being the maintenance collateral ratio of the corresponding iAsset. * * `d` = base deposit * `b` = total borrowed value (including the fees) * `L` = leverage * `f_m` = debt minting fee * `f_r` = reimbursement fee * `c` = collateral ratio * * The following is a detailed derivation of the math: * * 1. Since the redemption fee is proportional to the borrowed amount, * we can express the ADA we get from the order book as `b'=b*(1-f_r)`, * since some of the borrowed amount goes back to the order book. * * 2. Since all the minted iAsset are used to get borrowed ADA, * the value of the minted asset will be `b`. * * 3. The minting fee is a percentage of the value of the minted iAsset. * Therefore the available ADA to add as collateral is `b''=b' - b*f_m = b*(1 - f_r - f_m)`. * * 4. The collateral ratio can now be expressed as `c = (d + b * (1 - f_r - f_m)) / b`. * * 5. Working out the expression, we can express `b` in terms of everything else: `b = d / (c - 1 + f_r + f_m)`. * * 6. The minted amount will be `b / asset_price`. * * 7. Collateral amount of the CDP is `d + b * (1 - f_r - f_m)` * * 8. Leverage calculation: `L = (d + b * (1 - f_r - f_m)) / d`. * * Plugging in the `b` formula we get: `L = (d + (d / (c - 1 + f_r + f_m)) * (1 - f_r - f_m)) / d`. * * Simplified, yields the following: * `L = 1 + ((1 - f_r - f_m) / (c - 1 + f_r + f_m))` * * 9. `b'' = b * (1 - f_r - f_m)` * Solved for `b` yields the following: * `b = b'' / (1 - f_r - f_m)` * * 10. Having leverage and base deposit, we can find `b''`: * `b’’ = d(L - 1)` */ import { UTxO } from '@lucid-evolution/lucid'; import { OCD_DECIMAL_UNIT, OnChainDecimal } from '../../types/on-chain-decimal'; import { bigintMax, bigintMin, fromDecimal } from '../../utils/bigint-utils'; import { array as A, function as F } from 'fp-ts'; import { Decimal } from 'decimal.js'; import { calculateSpendAmtWhenRobBuyOrder, calculateTotalCollateralForRedemption, robCollateralAmtToSpend, } from '../rob/helpers'; import { Rational, rationalFloor, rationalFromInt, rationalMul, } from '../../types/rational'; import { AssetClass } from '@3rd-eye-labs/cardano-offchain-common'; import { RobDatum } from '../rob/types-new'; import { calculateFeeFromRatio } from '../../utils/indigo-helpers'; import { iassetValueOfCollateral } from '../cdp/helpers'; /** * How many LRP redemptions can we fit into a TX with CDP open. */ export const MAX_REDEMPTIONS_WITH_CDP_OPEN = 4; type ROBRedemptionDetails = { utxo: UTxO; redeemedCollateral: bigint; /** * The amount of iAssets paid to ROB. */ iassetsPayoutAmt: bigint; reimbursementIAssetAmt: bigint; }; type ApproximateLeverageRedemptionsResult = { leverage: number; collateralRatio: Rational; redeemedCollateral: bigint; }; /** * We assume exact precision. However, actual redemptions include rounding and * the rounding behaviour changes based on the number of redemptions. * This may slightly tweak the numbers and the result can be different. * * The math is described at the top of this code file. */ export function approximateLeverageRedemptions( baseCollateral: bigint, targetLeverage: number, redemptionReimbursementRatio: Rational, debtMintingFeeRatio: Rational, ): ApproximateLeverageRedemptionsResult { const debtMintingFeeRatioDecimal = Decimal(debtMintingFeeRatio.numerator).div( debtMintingFeeRatio.denominator, ); const redemptionReimbursementRatioDecimal = Decimal( redemptionReimbursementRatio.numerator, ).div(redemptionReimbursementRatio.denominator); const totalFeeRatio = debtMintingFeeRatioDecimal.add( redemptionReimbursementRatioDecimal, ); // b'' const bExFees = Decimal(baseCollateral) .mul(targetLeverage) .minus(baseCollateral) .floor(); // b = b’’ / (1-f_r - f_m) const b = bExFees.div(Decimal(1).minus(totalFeeRatio)).floor(); // c = (d + b * (1 - f_r - f_m)) / b const collateralRatio: Rational = { numerator: fromDecimal(Decimal(baseCollateral).add(bExFees)), denominator: fromDecimal(b), }; return { leverage: targetLeverage, collateralRatio: collateralRatio, redeemedCollateral: fromDecimal(bExFees), }; } export function summarizeActualLeverageRedemptions( lovelacesForRedemptionWithReimbursement: bigint, redemptionReimbursementRatio: Rational, iassetPrice: Rational, // Picking from the beginning until the iasset redemption amount is satisfied. redemptionLrps: [UTxO, RobDatum][], ): { redemptions: ROBRedemptionDetails[]; /** * The actual amount received from redemptions (i.e. without the reimbursement fee). */ totalRedeemedCollateral: bigint; /** * Total amount of IAssets to cover the reimbursement fee. */ totalReimbursedIAsset: bigint; /** * Total amount of IAssets paid to ROBs, including the reimbursement. */ totalIAssetPayout: bigint; } { type Accumulator = { /// The remaining collateral to spend from ROBs remainingCollateralToSpend: bigint; redemptions: ROBRedemptionDetails[]; }; const redemptionDetails = F.pipe( redemptionLrps, A.reduce<[UTxO, RobDatum], Accumulator>( { remainingCollateralToSpend: lovelacesForRedemptionWithReimbursement, redemptions: [], }, (acc, lrp) => { if ( acc.remainingCollateralToSpend <= 0n || iassetValueOfCollateral( acc.remainingCollateralToSpend, iassetPrice, ) <= 0n ) { return acc; } const collateralToSpend = robCollateralAmtToSpend( lrp[0].assets, lrp[1].orderType, ); if (collateralToSpend === 0n) { return acc; } const newRemainingCollateral = bigintMax( acc.remainingCollateralToSpend - collateralToSpend, 0n, ); const collateralToSpendInitial = acc.remainingCollateralToSpend - newRemainingCollateral; const finalPayoutIAssets = calculateSpendAmtWhenRobBuyOrder( collateralToSpendInitial, redemptionReimbursementRatio, iassetPrice, ); const feeIAssetAmt = calculateFeeFromRatio( redemptionReimbursementRatio, finalPayoutIAssets, ); // We need to calculate the new number since redemptionIAssets got corrected by rounding. const finalCollateralToSpend = rationalFloor( rationalMul( rationalFromInt(finalPayoutIAssets - feeIAssetAmt), iassetPrice, ), ); return { remainingCollateralToSpend: acc.remainingCollateralToSpend - finalCollateralToSpend, redemptions: [ ...acc.redemptions, { utxo: lrp[0], iassetsPayoutAmt: finalPayoutIAssets, redeemedCollateral: finalCollateralToSpend, reimbursementIAssetAmt: feeIAssetAmt, }, ], }; }, ), ); const res = F.pipe( redemptionDetails.redemptions, A.reduce< ROBRedemptionDetails, { redeemedCollateral: bigint; payoutIAssets: bigint; reimbursementIAssets: bigint; } >( { redeemedCollateral: 0n, payoutIAssets: 0n, reimbursementIAssets: 0n, }, (acc, details) => { return { redeemedCollateral: acc.redeemedCollateral + details.redeemedCollateral, reimbursementIAssets: acc.reimbursementIAssets + details.reimbursementIAssetAmt, payoutIAssets: acc.payoutIAssets + details.iassetsPayoutAmt, }; }, ), ); return { redemptions: redemptionDetails.redemptions, totalRedeemedCollateral: res.redeemedCollateral, totalReimbursedIAsset: res.reimbursementIAssets, totalIAssetPayout: res.payoutIAssets, }; } /** * The math is described at the top of this code file. */ export function calculateCollateralRatioFromLeverage( iasset: Uint8Array<ArrayBufferLike>, collateralAsset: AssetClass, leverage: number, baseCollateral: bigint, iassetPrice: Rational, debtMintingFeePercentage: OnChainDecimal, redemptionReimbursementPercentage: OnChainDecimal, allLrps: [UTxO, RobDatum][], ): OnChainDecimal | undefined { const debtMintingFeeRatioDecimal = Decimal( debtMintingFeePercentage.getOnChainInt, ) .div(OCD_DECIMAL_UNIT) .div(100); const redemptionReimbursementRatioDecimal = Decimal( redemptionReimbursementPercentage.getOnChainInt, ) .div(OCD_DECIMAL_UNIT) .div(100); const totalFeeRatio = debtMintingFeeRatioDecimal.add( redemptionReimbursementRatioDecimal, ); const maxAvailableCollateralForRedemption = calculateTotalCollateralForRedemption( iasset, collateralAsset, iassetPrice, allLrps, MAX_REDEMPTIONS_WITH_CDP_OPEN, ); if ( leverage <= 1 || baseCollateral <= 0n || maxAvailableCollateralForRedemption <= 0n ) { return undefined; } // b'' const bExFees = Decimal(baseCollateral) .mul(leverage) .minus(baseCollateral) .floor(); // b = b’’ / (1-f_r - f_m) const b = bExFees.div(Decimal(1).minus(totalFeeRatio)).floor(); const cappedB = bigintMin( maxAvailableCollateralForRedemption, fromDecimal(b), ); const cappedBExFees = Decimal(cappedB) .mul(Decimal(1).minus(totalFeeRatio)) .floor(); // c = (d + b * (1 - f_r - f_m)) / b const collateralRatio = Decimal( Decimal(baseCollateral).add(cappedBExFees), ).div(cappedB); return { getOnChainInt: fromDecimal( collateralRatio.mul(100n * OCD_DECIMAL_UNIT).floor(), ), }; } /** * The math is described at the top of this code file. */ export function calculateLeverageFromCollateralRatio( iasset: Uint8Array<ArrayBufferLike>, collateralAsset: AssetClass, collateralRatio: Rational, baseCollateral: bigint, iassetPrice: Rational, debtMintingFeeRatio: Rational, redemptionReimbursementRatio: Rational, allLrps: [UTxO, RobDatum][], ): number | undefined { const debtMintingFeeRatioDecimal = Decimal(debtMintingFeeRatio.numerator).div( debtMintingFeeRatio.denominator, ); const redemptionReimbursementRatioDecimal = Decimal( redemptionReimbursementRatio.numerator, ).div(redemptionReimbursementRatio.denominator); const totalFeeRatio = debtMintingFeeRatioDecimal.add( redemptionReimbursementRatioDecimal, ); const collateralRatioDecimal = Decimal(collateralRatio.numerator).div( collateralRatio.denominator, ); const maxAvailableCollateralForRedemption = calculateTotalCollateralForRedemption( iasset, collateralAsset, iassetPrice, allLrps, MAX_REDEMPTIONS_WITH_CDP_OPEN, ); if ( collateralRatioDecimal.toNumber() <= 1 || baseCollateral <= 0n || maxAvailableCollateralForRedemption <= 0n ) { return undefined; } // The leverage unconstrained by the liquidity in LRP const theoreticalMaxLeverage = Decimal(Decimal(1).minus(totalFeeRatio)) .div(collateralRatioDecimal.minus(1).add(totalFeeRatio)) .add(1); // b'' const bExFees = theoreticalMaxLeverage .mul(baseCollateral) .minus(baseCollateral) .floor(); // b = b’’ / (1-f_r - f_m) const b = bExFees.div(Decimal(1).minus(totalFeeRatio)).floor(); const cappedB = bigintMin( maxAvailableCollateralForRedemption, fromDecimal(b), ); const cappedBExFees = Decimal(cappedB) .mul(Decimal(1).minus(totalFeeRatio)) .floor(); return Decimal(baseCollateral) .add(cappedBExFees) .div(baseCollateral) .toNumber(); }