granite-math-sdk
Version:
Granite math sdk
221 lines (194 loc) • 6.28 kB
text/typescript
import {
absoluteMaxLeverage,
correctedMaxLTV,
unencumberedCollateral,
leverageMaxSlippage,
swapLoss,
computeFlashLoanValues,
convertDebtSharesToAssets,
InterestRateParams,
} from "../../src";
import { createCollateral } from "../utils";
describe("Leverage module tests", () => {
const defaultIrParams: InterestRateParams = {
urKink: 0.7,
baseIR: 0.5,
slope1: 0.75,
slope2: 1.5,
};
describe("absoluteMaxLeverage", () => {
it("computes 1 over 1 minus maxLTV", () => {
expect(absoluteMaxLeverage(0.7)).toBe(1 / (1 - 0.7));
});
it("throws when maxLTV is invalid", () => {
expect(() => absoluteMaxLeverage(1)).toThrow("Invalid maxLTV");
expect(() => absoluteMaxLeverage(0)).toThrow("Invalid maxLTV");
});
});
describe("correctedMaxLTV", () => {
it("returns zero when fees consume the entire LTV", () => {
const collateral = createCollateral(100, 10, 0.7, 0.7);
const result = correctedMaxLTV(collateral, 1, 0);
expect(result).toBe(0);
});
it("applies fees and slippage to maxLTV", () => {
const collateral = createCollateral(100, 1, 0.8, 0.8);
const result = correctedMaxLTV(collateral, 0.01, 0.02);
expect(result).toBeCloseTo((1 - 0.01 - 0.02) * 0.8);
});
it("throws when maxLTV is missing", () => {
const badCollateral = { amount: 100, price: 1 } as any;
expect(() => correctedMaxLTV(badCollateral, 0.01, 0.01)).toThrow(
"Invalid maxLTV",
);
});
});
describe("unencumberedCollateral", () => {
it("equals total collateral value when debt shares are zero", () => {
const collateral = createCollateral(100, 2, 0.5, 0.5);
const result = unencumberedCollateral(
0, // debtShares
10_000, // openInterest
10_000, // totalDebtShares
20_000, // totalAssets
defaultIrParams,
3600, // timeDelta
collateral,
0.7, // maxLTVcorrected
);
expect(result).toBeCloseTo(100 * 2);
});
it("becomes negative when debt is above the max LTV limit", () => {
const collateral = createCollateral(200, 1, 0.6, 0.6);
const result = unencumberedCollateral(
1_000,
20_000,
10_000,
40_000,
defaultIrParams,
7200,
collateral,
0.6, // maxLTVcorrected
);
expect(result).toBeLessThan(0);
});
it("is zero at the boundary when debt equals collateral value times maxLTV", () => {
// Remove time accrual so the share to asset ratio is exact
// openInterest divided by totalDebtShares equals 2 assets per share
// Need 120 assets of debt to hit the boundary which equals 60 shares
const collateral = createCollateral(200, 1, 0.6, 0.6);
const result = unencumberedCollateral(
60,
20_000,
10_000,
40_000,
defaultIrParams,
0, // no accrual
collateral,
0.6, // maxLTVcorrected
);
expect(result).toBeCloseTo(0, 10);
});
it("matches manual computation using convertDebtSharesToAssets", () => {
const collateral = createCollateral(100, 2, 0.5, 0.5); // total value 200
const debtShares = 1_000;
const openInterest = 10_000;
const totalDebtShares = 10_000;
const totalAssets = 20_000;
const timeDelta = 3600;
const debtAssets = convertDebtSharesToAssets(
debtShares,
openInterest,
totalDebtShares,
totalAssets,
defaultIrParams,
timeDelta,
);
const expected = 200 - debtAssets / (collateral.maxLTV as number);
const result = unencumberedCollateral(
debtShares,
openInterest,
totalDebtShares,
totalAssets,
defaultIrParams,
timeDelta,
collateral,
collateral.maxLTV as number, // pass through unchanged when comparing to old formula
);
expect(result).toBeCloseTo(expected);
});
it("is lower with higher utilization or longer time", () => {
const collateral = createCollateral(150, 1, 0.6, 0.6);
const base = unencumberedCollateral(
1_000,
10_000, // lower utilization
10_000,
100_000, // assets large
defaultIrParams,
3600,
collateral,
0.6,
);
const higherUtil = unencumberedCollateral(
1_000,
80_000, // higher utilization
10_000,
100_000,
defaultIrParams,
3600,
collateral,
0.6,
);
const longerTime = unencumberedCollateral(
1_000,
10_000,
10_000,
100_000,
defaultIrParams,
21_600, // longer time
collateral,
0.6,
);
expect(higherUtil).toBeLessThan(base);
expect(longerTime).toBeLessThan(base);
});
});
});
describe("leverageMaxSlippage", () => {
it("grows with target leverage and slippage", () => {
const low = leverageMaxSlippage(100, 0.2);
const highLev = leverageMaxSlippage(100, 0.4);
const highSlip = leverageMaxSlippage(100, 0.2);
expect(highLev).toBeGreaterThan(low);
expect(highSlip).toBe(low);
});
it("throws when slippage is invalid", () => {
expect(() => leverageMaxSlippage(100, 1)).toThrow("Invalid slippage");
expect(() => leverageMaxSlippage(100, -0.1)).toThrow("Invalid slippage");
});
});
describe("swapLoss", () => {
it("computes flash loan minus received value", () => {
const result = swapLoss(1000, 9, 100, 90);
// received value equals 9 * 100 / 90 equals 10
// swap loss equals 1000 minus 10 equals 990
expect(result).toBeCloseTo(990);
});
});
describe("computeFlashLoanValues", () => {
it("derives flash loan notional and implied slippage", () => {
const newCollateralValue = 1000;
const loss = 25;
const { flashLoanValue, slippage } = computeFlashLoanValues(
newCollateralValue,
loss,
);
expect(flashLoanValue).toBeCloseTo(1025);
expect(slippage).toBeCloseTo(25 / 1025);
});
it("handles zero loss", () => {
const { flashLoanValue, slippage } = computeFlashLoanValues(500, 0);
expect(flashLoanValue).toBe(500);
expect(slippage).toBe(0);
});
});