UNPKG

granite-math-sdk

Version:

Granite math sdk

745 lines (662 loc) 19.9 kB
import { liquidatorMaxRepayAmount, calculateDrop, calculateLiquidationPoint, calculateCollateralToTransfer, Collateral, liquidationRisk, } from "../../src"; import { createCollateral } from "../utils"; describe("Liquidation Module", () => { const defaultIrParams = { slope1: 0.03, slope2: 0.5, baseIR: 0.01, urKink: 0.8, }; describe("Liquidation Trigger Calculations", () => { it("calculates drop correctly with single collateral", () => { const collaterals = [createCollateral(100, 10, 0.8, 0.8)]; const currentDebt = 500; const drop = calculateDrop(collaterals, currentDebt); expect(drop).toBeCloseTo(0.375); }); it("returns 0 when total collateral value is 0", () => { const collaterals: Collateral[] = []; const currentDebt = 1000; const drop = calculateDrop(collaterals, currentDebt); expect(drop).toBe(0); }); it("handles collaterals with undefined liquidationLTV", () => { const collaterals = [ { amount: 100, price: 10, maxLTV: 0.8, } as Collateral, ]; const currentDebt = 500; expect(() => calculateDrop(collaterals, currentDebt)).toThrow( "LiquidationLTV is not defined", ); }); }); describe("Liquidation Point Calculations", () => { it("calculates liquidation point correctly", () => { const lp = calculateLiquidationPoint( 0.8, // accountLiqLTV 100, // debt shares 1000, // open interest 1000, // total debt shares 10000, // total assets (10% utilization rate) defaultIrParams, 1, // second ); expect(lp).toBeCloseTo(125); }); it("returns 0 when accountLiqLTV is 0", () => { const lp = calculateLiquidationPoint( 0, 100, 1000, 1000, 10000, defaultIrParams, 1, ); expect(lp).toBe(0); }); }); describe("Liquidator Max Repay Calculations", () => { it("prevents repayment higher than actual debt (regression test for original issue)", () => { const collateral: Collateral = { liquidationPremium: 0.01, // 1% premium maxLTV: 0.6, liquidationLTV: 0.7, amount: 1000, price: 1, }; // This was the original problematic test case const amt = liquidatorMaxRepayAmount( 10000, // debtShares 1000, // openInterest 10000, // totalDebtShares 20000, // totalAssets defaultIrParams, 3600, // 1 hour collateral, [collateral], // Pass the same collateral as allCollaterals ); // Should never exceed openInterest (1000) expect(amt).toBeLessThanOrEqual(1000); // Should be capped by collateralValue / (1 + premium) 990.099 expect(amt).toBeCloseTo(990.099, 3); // Verify it's using the collateral cap rather than maxRepayCalc const collateralCap = collateral.amount / (1 + collateral.liquidationPremium!); expect(amt).toBeCloseTo(collateralCap, 3); }); it("calculates max repay with proper secured value consideration", () => { const collateral: Collateral = { liquidationPremium: 0.1, // 10% premium maxLTV: 0.6, liquidationLTV: 0.7, amount: 1000, price: 1, }; const amt = liquidatorMaxRepayAmount( 1000, // debtShares (all debt) 1000, // openInterest 1000, // totalDebtShares 2000, // totalAssets defaultIrParams, 3600, collateral, [collateral], ); // Calculations: // collateralValue = 1000 // securedValue = 1000 * 0.7 = 700 // debtAssets 1000 (plus small interest) // denominator = 1 - (1 + 0.1) * 0.7 = 0.23 // maxRepayCalc = (1000 - 700) / 0.23 1304.35 // collateralCap = 1000 / 1.1 909.09 // Should return min(909.09, 1000) = 909.09 expect(amt).toBeCloseTo(909.09, 2); }); it("handles multi-collateral case correctly", () => { const collateralA: Collateral = { liquidationPremium: 0.1, // 10% premium maxLTV: 0.7, liquidationLTV: 0.78, amount: 100, price: 10, // Value = 1000 }; const collateralB: Collateral = { liquidationPremium: 0.12, // 12% premium maxLTV: 0.6, liquidationLTV: 0.7, amount: 664, price: 1, // Value = 664 }; const collateralC: Collateral = { liquidationPremium: 0.15, // 15% premium maxLTV: 0.5, liquidationLTV: 0.6, amount: 350, price: 1, // Value = 350 }; const allCollaterals = [collateralA, collateralB, collateralC]; // Test liquidating collateral B const amtB = liquidatorMaxRepayAmount( 1000, // debtShares 1014, // openInterest (matches image) 1000, // totalDebtShares 2000, // totalAssets defaultIrParams, 3600, collateralB, allCollaterals, ); // Calculations for collateral B: // totalSecuredValue = (1000 x 0.78) + (664 x 0.70) + (350 x 0.60) = 1247.8 // denominator = 1 - (1 + 0.12) x 0.70 = 0.156 // maxRepayCalc = (1014 - 1247.8) / 0.156 = -1497.44 // collateralCap = 664 / (1 + 0.12) = 592.86 // maxRepayAllowed = max(min(-1497.44, 592.86), 0) = 0 expect(amtB).toBe(0); // Test liquidating collateral B with higher debt const amtBHighDebt = liquidatorMaxRepayAmount( 2000, // Higher debt shares 2014, // Higher openInterest 1000, 2000, defaultIrParams, 3600, collateralB, allCollaterals, ); // For higher debt: // debtAssets 2014 // totalSecuredValue = 1247.8 (same as before) // maxRepayCalc = (2014 - 1247.8) / 0.156 = 4910.38 // collateralCap = 664 / 1.12 = 592.86 // maxRepayAllowed = max(min(4910.38, 592.86), 0) = 592.86 expect(amtBHighDebt).toBeCloseTo(592.86, 2); }); it("returns 0 when debt is fully secured by collateral", () => { const collateral: Collateral = { liquidationPremium: 0.1, maxLTV: 0.6, liquidationLTV: 0.7, amount: 2000, // High collateral amount price: 1, }; const amt = liquidatorMaxRepayAmount( 100, // Small debt 1000, 1000, 2000, defaultIrParams, 3600, collateral, [collateral], ); // Debt is fully secured by collateral, so no liquidation needed expect(amt).toBe(0); }); it("respects collateral cap when debt is high", () => { const collateral: Collateral = { liquidationPremium: 0.1, maxLTV: 0.6, liquidationLTV: 0.7, amount: 100, // Low collateral amount price: 1, }; const amt = liquidatorMaxRepayAmount( 1000, // High debt 1000, 1000, 2000, defaultIrParams, 3600, collateral, [collateral], ); // Collateral cap = 100 / 1.1 90.91 expect(amt).toBeCloseTo(90.91, 2); }); it("handles zero collateral amount", () => { const collateral: Collateral = { liquidationPremium: 0.1, maxLTV: 0.6, liquidationLTV: 0.7, amount: 0, price: 1, }; const amt = liquidatorMaxRepayAmount( 1000, 1000, 1000, 2000, defaultIrParams, 3600, collateral, [collateral], ); expect(amt).toBe(0); }); it("throws error when liquidation parameters are undefined", () => { const collateral = { maxLTV: 0.6, amount: 1000, price: 1, } as Collateral; expect(() => liquidatorMaxRepayAmount( 1000, 1000, 1000, 2000, defaultIrParams, 3600, collateral, [collateral], ), ).toThrow("Liquidation LTV or liquidation premium are not defined"); }); it("handles denominator near zero case", () => { const collateral: Collateral = { liquidationPremium: 0.25, // 25% premium maxLTV: 0.7, liquidationLTV: 0.79, amount: 1000, price: 1, }; const amt = liquidatorMaxRepayAmount( 2000, // High debt to ensure positive maxRepayCalc 2000, 1000, 2000, defaultIrParams, 3600, collateral, [collateral], ); // Should still be capped by collateralCap = 1000/1.25 = 800 expect(amt).toBeCloseTo(800, 2); }); it("handles multiple collaterals of same type", () => { const collateral1: Collateral = { liquidationPremium: 0.1, maxLTV: 0.6, liquidationLTV: 0.7, amount: 500, price: 1, }; const collateral2: Collateral = { liquidationPremium: 0.1, maxLTV: 0.6, liquidationLTV: 0.7, amount: 500, price: 1, }; const amt = liquidatorMaxRepayAmount( 1500, 1500, 1000, 2000, defaultIrParams, 3600, collateral1, [collateral1, collateral2], ); // Total secured value = (500 + 500) * 0.7 = 700 // maxRepayCalc = (1500 - 700) / (1 - 1.1 * 0.7) = 2666.67 // collateralCap = 500 / 1.1 = 454.55 expect(amt).toBeCloseTo(454.55, 2); }); it("handles different price scales correctly", () => { const collateral: Collateral = { liquidationPremium: 0.1, maxLTV: 0.6, liquidationLTV: 0.7, amount: 1, // 1 BTC price: 50000, // $50,000 per BTC }; const amt = liquidatorMaxRepayAmount( 60000, 60000, 1000, 100000, defaultIrParams, 3600, collateral, [collateral], ); // collateralCap = (1 * 50000) / 1.1 = 45454.55 expect(amt).toBeCloseTo(45454.55, 2); }); it("handles zero and negative prices", () => { const zeroPrice: Collateral = { liquidationPremium: 0.1, maxLTV: 0.6, liquidationLTV: 0.7, amount: 1000, price: 0, }; const negativePrice: Collateral = { liquidationPremium: 0.1, maxLTV: 0.6, liquidationLTV: 0.7, amount: 1000, price: -1, }; const amtZero = liquidatorMaxRepayAmount( 1000, 1000, 1000, 2000, defaultIrParams, 3600, zeroPrice, [zeroPrice], ); const amtNegative = liquidatorMaxRepayAmount( 1000, 1000, 1000, 2000, defaultIrParams, 3600, negativePrice, [negativePrice], ); expect(amtZero).toBe(0); expect(amtNegative).toBe(0); }); it("throws error when any collateral in allCollaterals has undefined liquidationLTV", () => { const validCollateral: Collateral = { liquidationPremium: 0.1, maxLTV: 0.6, liquidationLTV: 0.7, amount: 1000, price: 1, }; const invalidCollateral = { liquidationPremium: 0.1, maxLTV: 0.6, // liquidationLTV is undefined amount: 500, price: 1, } as Collateral; expect(() => liquidatorMaxRepayAmount( 1000, 1000, 1000, 2000, defaultIrParams, 3600, validCollateral, // Main collateral is valid [validCollateral, invalidCollateral], // But one of allCollaterals is invalid ), ).toThrow("LiquidationLTV is not defined for one or more collaterals"); }); }); describe("Collateral Transfer Calculations", () => { it("calculates collateral to transfer correctly", () => { const collateral: Collateral = { liquidationPremium: 0.1, maxLTV: 0.6, liquidationLTV: 0.7, amount: 1000, price: 2, }; const collateralToTransfer = calculateCollateralToTransfer( 100, collateral, ); // For repayAmount = 100: // liquidationReward = 100 * 0.1 = 10 // collateralToTransfer = (100 + 10) / 2 = 55 expect(collateralToTransfer).toBe(55); }); it("handles different price scales in collateral transfer", () => { const collateral: Collateral = { liquidationPremium: 0.1, maxLTV: 0.6, liquidationLTV: 0.7, amount: 1, price: 50000, // $50,000 per unit }; const collateralToTransfer = calculateCollateralToTransfer( 5500, collateral, ); // For repayAmount = 5500: // liquidationReward = 5500 * 0.1 = 550 // collateralToTransfer = (5500 + 550) / 50000 = 0.1210 expect(collateralToTransfer).toBeCloseTo(0.121, 4); }); it("handles precision edge cases in collateral transfer", () => { const collateral: Collateral = { liquidationPremium: 0.1, maxLTV: 0.6, liquidationLTV: 0.7, amount: 1, price: 0.0001, // Very small price }; const collateralToTransfer = calculateCollateralToTransfer( 0.00001, collateral, ); expect(collateralToTransfer).toBeGreaterThan(0); expect(Number.isFinite(collateralToTransfer)).toBe(true); }); it("throws error when liquidation premium is undefined", () => { const collateral = { maxLTV: 0.6, liquidationLTV: 0.7, amount: 1000, price: 1, } as Collateral; expect(() => calculateCollateralToTransfer(100, collateral)).toThrow( "Liquidation premium is not defined", ); }); }); describe("calculateDrop extra cases", () => { it("handles multiple collaterals", () => { const collaterals: Collateral[] = [ createCollateral(100, 10, 0.8, 0.8), // secured = 800 { amount: 50, price: 20, maxLTV: 0.6, liquidationLTV: 0.6 }, // secured = 600 ]; const currentDebt = 700; // total secured = 1400, drop = 1 - 700/1400 = 0.5 expect(calculateDrop(collaterals, currentDebt)).toBeCloseTo(0.5, 6); }); it("returns a negative value when debt exceeds secured collateral", () => { const collaterals: Collateral[] = [ createCollateral(100, 10, 0.8, 0.8), // secured = 800 { amount: 50, price: 20, maxLTV: 0.6, liquidationLTV: 0.6 }, // secured = 600 ]; const currentDebt = 2000; // 1 - 2000/1400 = -0.428571... expect(calculateDrop(collaterals, currentDebt)).toBeCloseTo( -0.4285714286, 6, ); }); it("ignores zero priced collateral contributions in the sum", () => { const collaterals: Collateral[] = [ createCollateral(100, 10, 0.8, 0.8), // secured = 800 { amount: 100, price: 0, maxLTV: 0.7, liquidationLTV: 0.7 }, // secured = 0 ]; const currentDebt = 400; // total secured = 800, drop = 1 - 400/800 = 0.5 expect(calculateDrop(collaterals, currentDebt)).toBeCloseTo(0.5, 6); }); }); describe("calculateLiquidationPoint properties", () => { it("scales inversely with accountLiqLTV", () => { const args = { debtShares: 100, openInterest: 1000, totalDebtShares: 1000, totalAssets: 10000, timeDelta: 1, }; const lpAt08 = calculateLiquidationPoint( 0.8, args.debtShares, args.openInterest, args.totalDebtShares, args.totalAssets, defaultIrParams, args.timeDelta, ); const lpAt05 = calculateLiquidationPoint( 0.5, args.debtShares, args.openInterest, args.totalDebtShares, args.totalAssets, defaultIrParams, args.timeDelta, ); // LP(0.5) should be 1.6x LP(0.8) expect(lpAt05).toBeCloseTo(lpAt08 * (0.8 / 0.5), 6); }); }); describe("liquidationRisk coverage", () => { it("matches baseline liquidation point when debtAssets is zero", () => { const accountLiqLTV = 0.8; const initialDebtShares = 100; const initialOpenInterest = 1000; const initialTotalDebtShares = 1000; const initialTotalAssets = 10000; const timeDelta = 1; const baseline = calculateLiquidationPoint( accountLiqLTV, initialDebtShares, initialOpenInterest, initialTotalDebtShares, initialTotalAssets, defaultIrParams, timeDelta, ); const risk = liquidationRisk( 0, // debtAssets initialOpenInterest, initialTotalDebtShares, initialTotalAssets, 0, // protocolReservePercentage defaultIrParams, timeDelta, initialDebtShares, initialOpenInterest, initialTotalDebtShares, initialTotalAssets, accountLiqLTV, ); expect(risk).toBeCloseTo(baseline, 6); }); it("increases as additional debtAssets increases", () => { const accountLiqLTV = 0.7; const initialDebtShares = 200; const initialOpenInterest = 2000; const initialTotalDebtShares = 2000; const initialTotalAssets = 15000; const timeDelta = 3600; const riskSmall = liquidationRisk( 100, // smaller added debt initialOpenInterest, initialTotalDebtShares, initialTotalAssets, 0, defaultIrParams, timeDelta, initialDebtShares, initialOpenInterest, initialTotalDebtShares, initialTotalAssets, accountLiqLTV, ); const riskLarge = liquidationRisk( 500, // larger added debt initialOpenInterest, initialTotalDebtShares, initialTotalAssets, 0, defaultIrParams, timeDelta, initialDebtShares, initialOpenInterest, initialTotalDebtShares, initialTotalAssets, accountLiqLTV, ); expect(riskLarge).toBeGreaterThan(riskSmall); }); }); describe("liquidatorMaxRepayAmount extra edges", () => { it("returns 0 when denominator is negative", () => { const collateral: Collateral = { liquidationPremium: 0.5, // makes denominator negative with liqLTV 0.8 maxLTV: 0.6, liquidationLTV: 0.8, amount: 1000, price: 1, }; const amt = liquidatorMaxRepayAmount( 1000, 1000, 1000, 2000, defaultIrParams, 1, collateral, [collateral], ); expect(amt).toBe(0); }); }); describe("calculateCollateralToTransfer extra edges", () => { it("returns 0 when repayAmount is 0", () => { const collateral: Collateral = { liquidationPremium: 0.1, maxLTV: 0.6, liquidationLTV: 0.7, amount: 1000, price: 2, }; expect(calculateCollateralToTransfer(0, collateral)).toBe(0); }); it("returns Infinity when price is 0", () => { const collateral: Collateral = { liquidationPremium: 0.1, maxLTV: 0.6, liquidationLTV: 0.7, amount: 1000, price: 0, }; const v = calculateCollateralToTransfer(100, collateral); expect(v).toBe(Infinity); }); it("supports negative liquidation premiums arithmetically", () => { const collateral: Collateral = { liquidationPremium: -0.05, maxLTV: 0.6, liquidationLTV: 0.7, amount: 1000, price: 10, }; // (100 + 100 * -0.05) / 10 = 9.5 expect(calculateCollateralToTransfer(100, collateral)).toBeCloseTo( 9.5, 6, ); }); }); });