@indigo-labs/indigo-sdk
Version:
Indigo SDK for interacting with Indigo endpoints via lucid-evolution
423 lines (379 loc) • 12.4 kB
text/typescript
/**
* 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();
}