-
Notifications
You must be signed in to change notification settings - Fork 0
✨ auditor: add optional haircuts per market #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@exactly/lib": patch | ||
| --- | ||
|
|
||
| ✨ auditor: add optional haircuts per market |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,5 @@ | ||
| import MAX_UINT256 from "../fixed-point-math/MAX_UINT256.js"; | ||
| import WAD from "../fixed-point-math/WAD.js"; | ||
| import divWad from "../fixed-point-math/divWad.js"; | ||
| import divWadUp from "../fixed-point-math/divWadUp.js"; | ||
| import mulDiv from "../fixed-point-math/mulDiv.js"; | ||
|
|
@@ -7,13 +9,15 @@ import mulWad from "../fixed-point-math/mulWad.js"; | |
| export default function accountLiquidity( | ||
| data: AccountLiquidityData, | ||
| timestamp = Math.floor(Date.now() / 1000), | ||
| haircuts?: Haircuts, | ||
| ): { | ||
| adjCollateral: bigint; | ||
| adjDebt: bigint; | ||
| } { | ||
| let adjCollateral = 0n; | ||
| let adjDebt = 0n; | ||
| for (const { | ||
| market, | ||
| isCollateral, | ||
| floatingBorrowAssets, | ||
| floatingDepositAssets, | ||
|
|
@@ -24,15 +28,22 @@ export default function accountLiquidity( | |
| usdPrice, | ||
| } of data) { | ||
| const baseUnit = 10n ** BigInt(decimals); | ||
| if (isCollateral) adjCollateral += adjustCollateral(floatingDepositAssets, usdPrice, baseUnit, adjustFactor); | ||
| const haircut = marketHaircut(haircuts, market); | ||
| if (haircut && (haircut < 0n || haircut > WAD)) throw new Error("haircut outside [0, 1]"); | ||
|
|
||
| if (isCollateral) { | ||
| const adjusted = adjustCollateral(floatingDepositAssets, usdPrice, baseUnit, adjustFactor); | ||
| adjCollateral += haircut ? mulWad(adjusted, WAD - haircut) : adjusted; | ||
| } | ||
|
|
||
| let totalDebt = floatingBorrowAssets; | ||
| for (const { position, maturity } of fixedBorrowPositions) { | ||
| const positionAssets = position.principal + position.fee; | ||
| totalDebt += positionAssets; | ||
| if (timestamp > maturity) totalDebt += mulWad(positionAssets, (BigInt(timestamp) - maturity) * penaltyRate); | ||
| } | ||
| adjDebt += adjustDebt(totalDebt, usdPrice, baseUnit, adjustFactor); | ||
| const debt = adjustDebt(totalDebt, usdPrice, baseUnit, adjustFactor); | ||
| adjDebt += haircut ? (haircut === WAD && debt !== 0n ? MAX_UINT256 : divWadUp(debt, WAD - haircut)) : debt; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a fully haircut market has any debt, this substitutes the division-by-zero result with Useful? React with 👍 / 👎. |
||
| } | ||
|
|
||
| return { adjCollateral, adjDebt }; | ||
|
|
@@ -56,14 +67,37 @@ export function normalizeCollateral( | |
| usdPrice: bigint, | ||
| baseUnit: bigint, | ||
| adjustFactor: bigint, | ||
| haircut?: bigint, | ||
| ) { | ||
| if (haircut === WAD) return adjustedCollateral ? MAX_UINT256 : 0n; | ||
| return divWad( | ||
| mulDiv(haircut ? divWad(adjustedCollateral, WAD - haircut) : adjustedCollateral, baseUnit, usdPrice), | ||
|
cruzdanilo marked this conversation as resolved.
cruzdanilo marked this conversation as resolved.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a partially haircut market has prices or adjust factors that cause truncation, this floors the unhaircuted adjusted amount before converting back to raw assets, but Useful? React with 👍 / 👎. |
||
| adjustFactor, | ||
| ); | ||
| } | ||
|
|
||
| export function normalizeDebt( | ||
| adjustedDebt: bigint, | ||
| usdPrice: bigint, | ||
| baseUnit: bigint, | ||
| adjustFactor: bigint, | ||
| haircut?: bigint, | ||
| ) { | ||
| return divWad(mulDiv(adjustedCollateral, baseUnit, usdPrice), adjustFactor); | ||
| return mulDiv(mulWad(haircut ? mulWad(adjustedDebt, WAD - haircut) : adjustedDebt, adjustFactor), baseUnit, usdPrice); | ||
| } | ||
|
|
||
| export function normalizeDebt(adjustedDebt: bigint, usdPrice: bigint, baseUnit: bigint, adjustFactor: bigint) { | ||
| return mulDiv(mulWad(adjustedDebt, adjustFactor), baseUnit, usdPrice); | ||
| export function marketHaircut(haircuts: Haircuts | undefined, market: string): bigint { | ||
| if (!haircuts) return 0n; | ||
|
|
||
| const exact = haircuts[market]; | ||
| if (exact !== undefined) return exact; | ||
|
|
||
| const lower = market.toLowerCase(); | ||
| return haircuts[lower] ?? Object.entries(haircuts).find(([key]) => key.toLowerCase() === lower)?.[1] ?? 0n; | ||
| } | ||
|
|
||
| export type Haircuts = Readonly<Record<string, bigint>>; | ||
|
|
||
| export type AccountLiquidityData = readonly { | ||
| market: string; | ||
| decimals: number; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,8 @@ | ||
| import accountLiquidity, { type AccountLiquidityData } from "./accountLiquidity.js"; | ||
| import accountLiquidity, { type AccountLiquidityData, type Haircuts } from "./accountLiquidity.js"; | ||
| import MAX_UINT256 from "../fixed-point-math/MAX_UINT256.js"; | ||
| import divWad from "../fixed-point-math/divWad.js"; | ||
|
|
||
| export default function healthFactor(data: AccountLiquidityData, timestamp?: number): bigint { | ||
| const { adjCollateral, adjDebt } = accountLiquidity(data, timestamp); | ||
| export default function healthFactor(data: AccountLiquidityData, timestamp?: number, haircuts?: Haircuts): bigint { | ||
| const { adjCollateral, adjDebt } = accountLiquidity(data, timestamp, haircuts); | ||
| return adjDebt ? divWad(adjCollateral, adjDebt) : MAX_UINT256; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a market has a 100% haircut and no borrow balance or fixed borrow positions,
debtis0n, so this falls through todivWadUp(0n, WAD - haircut)with a zero denominator. That makes otherwise valid collateral-only or unused markets crash inaccountLiquidity(and thereforehealthFactor,borrowLimit, andwithdrawLimit) as soon as callers supply{ [market]: WAD }; zero debt under a full haircut should contribute0nrather than divide by zero.Useful? React with 👍 / 👎.