Skip to content
Merged
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
2 changes: 2 additions & 0 deletions src/common-utils/utils/apy/get-single-quota-borrow-rate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export interface GetSingleQuotaBorrowRateRate extends CalcQuotaBorrowRateProps {
/**
* Under the hood sums up rates for all given quotas and then multiplies them by 1+feeInterest,
* but it is expected that the ONLY quota will be passed
*
* @deprecated Use `calcBorrowRate` from `sdk/positions` instead.
*/
export function getSingleQuotaBorrowRate(
props: GetSingleQuotaBorrowRateRate,
Expand Down
91 changes: 35 additions & 56 deletions src/common-utils/utils/creditAccount/calc-health-factor.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import type { Address } from "viem";
import {
type Asset,
PERCENTAGE_FACTOR,
PRICE_DECIMALS,
} from "../../../sdk/index.js";
import { BigIntMath } from "../../../sdk/utils/bigint-math.js";
import { PriceUtils } from "../price-math.js";
import type { Asset } from "../../../sdk/index.js";
import { calcHealthFactor as calcHealthFactorFromSnapshot } from "../../../sdk/positions/calcHealthFactor.js";
import type { QuotaInfoIsActiveSlice, TokenDataSlice } from "./types.js";

export interface CalcHealthFactorProps {
Expand All @@ -20,8 +15,6 @@ export interface CalcHealthFactorProps {
tokensList: Record<Address, TokenDataSlice>;
}

const MAX_UINT16 = 65535;

/**
* Computes account health factor in percentage-factor units.
*
Expand All @@ -32,6 +25,9 @@ const MAX_UINT16 = 65535;
* @param props Credit account balances, quotas, prices, thresholds, and debt context.
* @returns Health factor as a number in `PERCENTAGE_FACTOR` scale,
* or `65535` when debt is zero.
*
* @deprecated Use `calcHealthFactor` from `sdk/positions` instead; this
* wrapper only maps the legacy props onto an `AccountSnapshot`.
*/
export function calcHealthFactor({
assets,
Expand All @@ -45,52 +41,35 @@ export function calcHealthFactor({
prices,
tokensList,
}: CalcHealthFactorProps): number {
if (debt === 0n) return MAX_UINT16;

const underlyingDecimals = tokensList[underlyingToken]?.decimals || 18;
const underlyingPrice = prices[underlyingToken] || 0n;

const assetMoney = assets.reduce(
(acc, { token: tokenAddress, balance: amount }) => {
const tokenDecimals = tokensList[tokenAddress]?.decimals || 18;

const lt = liquidationThresholds[tokenAddress] || 0n;
const price = prices[tokenAddress] || 0n;

const tokenMoney = PriceUtils.calcTotalPrice(
price,
amount,
tokenDecimals,
);
const tokenLtMoney = (tokenMoney * lt) / PERCENTAGE_FACTOR;

const { isActive = false } = quotasInfo?.[tokenAddress] || {};
const quota = quotas[tokenAddress];
const quotaBalance = isActive ? quota?.balance || 0n : 0n;
const quotaMoney = PriceUtils.calcTotalPrice(
underlyingPrice,
quotaBalance,
underlyingDecimals,
);

// if quota is undefined, then it is not a quoted token
const money = quota
? BigIntMath.min(quotaMoney, tokenLtMoney)
: tokenLtMoney;

return acc + money;
const decimals: Record<Address, number> = {};
for (const [token, meta] of Object.entries(tokensList)) {
decimals[token as Address] = meta.decimals;
}

const lts: Record<Address, number> = {};
for (const [token, lt] of Object.entries(liquidationThresholds)) {
lts[token as Address] = Number(lt);
}

const activeQuotas: Record<Address, boolean> = {};
for (const [token, info] of Object.entries(quotasInfo)) {
if (info?.isActive) {
activeQuotas[token as Address] = true;
}
}

return calcHealthFactorFromSnapshot({
snapshot: {
creditManager: underlyingToken,
assets,
quotas: Object.values(quotas),
totalDebt: debt,
totalValue: 0n,
},
0n,
);

const borrowedMoney = PriceUtils.calcTotalPrice(
underlyingPrice || PRICE_DECIMALS,
debt,
underlyingDecimals,
);

const hfInPercent =
borrowedMoney > 0n ? (assetMoney * PERCENTAGE_FACTOR) / borrowedMoney : 0n;

return Number(hfInPercent);
underlying: underlyingToken,
decimals,
prices,
liquidationThresholds: lts,
activeQuotas,
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export interface CalcQuotaBorrowRateProps {
*
* @param props Quota balances and per-token quota rates.
* @returns Sum of `balance * rate` terms in percentage-factor scale.
*
* @deprecated Use `calcBorrowRate` from `sdk/positions` instead.
*/
export function calcQuotaBorrowRate({
quotas,
Expand Down
19 changes: 5 additions & 14 deletions src/common-utils/utils/creditAccount/get-time-to-liquidation.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import {
PERCENTAGE_DECIMALS,
PERCENTAGE_FACTOR,
SECONDS_PER_YEAR,
} from "../../../sdk/index.js";
import { calcTimeToLiquidationMs } from "../../../sdk/positions/calcTimeToLiquidationMs.js";

export interface TimeToLiquidationProps {
totalBorrowRate_debt: bigint;
Expand All @@ -19,18 +15,13 @@ export interface TimeToLiquidationProps {
* @param props Current health factor and `totalBorrowRate * debt` term.
* @returns Milliseconds to liquidation as `bigint`, or `null` when already at/under
* liquidation threshold or when borrow-rate exposure is zero.
*
* @deprecated Use `calcTimeToLiquidationMs` from `sdk/positions` instead;
* this wrapper only forwards to the new implementation.
*/
export function getTimeToLiquidation({
healthFactor,
totalBorrowRate_debt,
}: TimeToLiquidationProps) {
if (healthFactor <= PERCENTAGE_FACTOR || totalBorrowRate_debt === 0n)
return null;

// (HF - 1) / (br_D / year) or (HF - 1) * (year / br_D)
const HF_1 = BigInt(healthFactor) - PERCENTAGE_FACTOR;
const brPerYear =
(BigInt(SECONDS_PER_YEAR) * PERCENTAGE_FACTOR * PERCENTAGE_DECIMALS) /
totalBorrowRate_debt;
return (HF_1 * brPerYear * 1000n) / PERCENTAGE_FACTOR;
return calcTimeToLiquidationMs(healthFactor, totalBorrowRate_debt);
}
51 changes: 27 additions & 24 deletions src/common-utils/utils/creditAccount/liquidation-price.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Address } from "viem";
import type { Asset } from "../../../sdk/index.js";
import { PERCENTAGE_FACTOR, PRICE_DECIMALS, WAD } from "../../../sdk/index.js";
import { calcLiquidationPriceForTarget } from "../../../sdk/positions/calcLiquidationPriceForTarget.js";
import type { TokenDataSlice } from "./types.js";

interface LiquidationPriceProps {
Expand All @@ -24,6 +24,9 @@ interface LiquidationPriceProps {
* @param props Debt context, assets, thresholds, and token metadata.
* @returns Target token price in `PRICE_DECIMALS` precision that corresponds
* to liquidation boundary; returns `0n` when target balance or LT is non-positive.
*
* @deprecated Use `calcLiquidationPriceForTarget` from `sdk/positions`
* instead; this wrapper only maps the legacy props onto an `AccountSnapshot`.
*/
export function liquidationPrice({
liquidationThresholds,
Expand All @@ -34,27 +37,27 @@ export function liquidationPrice({
assets,
tokensList,
}: LiquidationPriceProps) {
const underlyingDecimals = tokensList[underlyingToken]?.decimals || 18;
const { balance: underlyingBalance = 0n } = assets[underlyingToken] || {};

// effectiveDebt = Debt - underlyingBalance*LTunderlying
const ltUnderlying = liquidationThresholds[underlyingToken] || 0n;
const effectiveDebt =
((debt - (underlyingBalance * ltUnderlying) / PERCENTAGE_FACTOR) * WAD) /
10n ** BigInt(underlyingDecimals);

const targetDecimals = tokensList[targetToken]?.decimals || 18;
const { balance: targetBalance = 0n } = assets[targetToken] || {};
const effectiveTargetBalance =
(targetBalance * WAD) / 10n ** BigInt(targetDecimals);

const lpLT = liquidationThresholds[targetToken] || 0n;

if (targetBalance <= 0n || lpLT <= 0n) return 0n;

// priceTarget = effectiveDebt / (lpLT*targetBalance)
return (
(effectiveDebt * PRICE_DECIMALS * PERCENTAGE_FACTOR) /
(effectiveTargetBalance * lpLT)
);
const decimals: Record<Address, number> = {};
for (const [token, meta] of Object.entries(tokensList)) {
decimals[token as Address] = meta.decimals;
}

const lts: Record<Address, number> = {};
for (const [token, lt] of Object.entries(liquidationThresholds)) {
lts[token as Address] = Number(lt);
}

return calcLiquidationPriceForTarget({
snapshot: {
creditManager: underlyingToken,
assets: Object.values(assets),
quotas: [],
totalDebt: debt,
totalValue: 0n,
},
targetToken,
underlying: underlyingToken,
decimals,
liquidationThresholds: lts,
});
}
15 changes: 14 additions & 1 deletion src/model/positions.schema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { z } from "zod/v4";
import { ZodAddress } from "../sdk/utils/zod.js";
import { ZodAddress, ZodBigInt, ZodHex } from "../sdk/utils/zod.js";
import { isFilterSet } from "./filters.js";
import {
booleanParamSchema,
Expand Down Expand Up @@ -101,6 +101,16 @@ export const poolPositionSchema = z.object({
pnl: pnlBreakdownSchema.optional(),
});

/**
* {@link BorrowRateBreakdown}
**/
export const borrowRateBreakdownSchema = z.object({
total: bpsSchema,
totalOnDebt: bpsSchema,
base: bpsSchema,
quotas: z.record(ZodAddress(), bpsSchema),
});

/**
* {@link StrategyPosition}
**/
Expand All @@ -117,6 +127,9 @@ export const strategyPositionSchema = z.object({
totalDebt: tokenAmountSchema,
totalValue: tokenAmountSchema,
healthFactor: bpsSchema,
borrowRate: borrowRateBreakdownSchema.optional(),
timeToLiquidation: ZodBigInt().nullable().optional(),
liquidationPrice: ZodBigInt().nullable().optional(),
pnl: pnlBreakdownSchema.optional(),
collaterals: z.array(positionCollateralSchema),
});
Expand Down
85 changes: 85 additions & 0 deletions src/model/positions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,69 @@ export interface PoolPosition {
pnl?: PnlBreakdown;
}

/**
* Cost of a position's debt broken down by source.
*
* The base rate is what the pool charges on the debt; each quoted collateral
* adds its own quota rate on top. Rates are reported in two normalizations:
* relative to the position's total value and relative to its debt.
**/
export interface BorrowRateBreakdown {
/**
* Base rate plus quota rates, relative to the position's total value.
**/
total: Bps;
/**
* Base rate plus quota rates, relative to the debt. This is the rate the
* debt itself grows at, so it feeds {@link PositionMetrics.timeToLiquidation}.
**/
totalOnDebt: Bps;
/**
* Annual cost of the borrowed underlying itself: the pool's base rate plus
* the credit manager's interest fee. Same value `borrowApy` reports.
**/
base: Bps;
/**
* Per-token quota rate contribution, relative to the position's total value.
**/
quotas: Record<Address, Bps>;
}

/**
* Health and cost metrics of a credit account's state, actual or projected.
*
* Previews and operation states carry the whole group; on-chain positions
* report only the fields they lack natively, see {@link StrategyPosition}.
**/
export interface PositionMetrics {
/**
* Health factor in basis points: below `10000` the account is liquidatable.
*
* @example `12500` for a health factor of 1.25
**/
healthFactor: Bps;
/**
* Net rate the whole position earns, collateral yield minus borrow cost.
**/
overallApy: Bps;
/**
* Cost of the debt, broken down by source.
**/
borrowRate: BorrowRateBreakdown;
/**
* Estimated milliseconds until the health factor decays to `10000` under
* the current borrow rate, or `null` when the debt carries no rate (or the
* account is already liquidatable).
**/
timeToLiquidation: bigint | null;
/**
* Price of the single non-underlying collateral at which the account
* becomes liquidatable, in the oracle's 8-decimal fixed point, or `null`
* when the account holds zero or several non-underlying assets.
**/
liquidationPrice: bigint | null;
}

/**
* An open credit account of a wallet.
**/
Expand Down Expand Up @@ -219,6 +282,28 @@ export interface StrategyPosition {
* @example `12500` for a health factor of 1.25
**/
healthFactor: Bps;
/**
* Cost of the debt broken down into the pool's base rate and per-token
* quota rates.
*
* @mode onchain
**/
borrowRate?: BorrowRateBreakdown;
/**
* Estimated milliseconds until the health factor decays to `10000` under
* the current borrow rate, or `null` when it cannot be estimated.
*
* @mode onchain
**/
timeToLiquidation?: bigint | null;
/**
* Price of the single non-underlying collateral at which the account
* becomes liquidatable, in the oracle's 8-decimal fixed point, or `null`
* when the account holds zero or several non-underlying assets.
*
* @mode onchain
**/
liquidationPrice?: bigint | null;
/**
* What the position has earned so far.
*
Expand Down
Loading
Loading