Skip to content

Commit af45eb2

Browse files
committed
✨ market: implement fixed repay position
1 parent 65c58cf commit af45eb2

File tree

3 files changed

+185
-1
lines changed

3 files changed

+185
-1
lines changed

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export { default as globalUtilization } from "./interest-rate-model/globalUtiliz
3131
export type { default as IRMParameters, IRMFloatingParameters } from "./interest-rate-model/Parameters.ts";
3232

3333
export { default as fixedRepayAssets } from "./market/fixedRepayAssets.js";
34+
export { default as fixedRepayPosition } from "./market/fixedRepayPosition.js";
3435
export { default as floatingDepositRates } from "./market/floatingDepositRates.js";
3536

3637
export { default as abs } from "./vector/abs.js";

src/market/fixedRepayPosition.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type { FixedRepaySnapshot } from "./fixedRepayAssets.js";
2+
import WAD from "../fixed-point-math/WAD.js";
3+
import divWad from "../fixed-point-math/divWad.js";
4+
import min from "../fixed-point-math/min.js";
5+
import mulDiv from "../fixed-point-math/mulDiv.js";
6+
import mulWad from "../fixed-point-math/mulWad.js";
7+
8+
export default function fixedRepayPosition(
9+
{
10+
penaltyRate,
11+
backupFeeRate,
12+
borrowed,
13+
supplied,
14+
unassignedEarnings,
15+
lastAccrual,
16+
principal,
17+
fee,
18+
}: FixedRepaySnapshot,
19+
maturity: number,
20+
assets: bigint,
21+
timestamp = Math.floor(Date.now() / 1000),
22+
) {
23+
const totalPosition = principal + fee;
24+
if (totalPosition === 0n) return 0n;
25+
if (timestamp >= maturity) {
26+
return min(divWad(assets, WAD + BigInt(timestamp - maturity) * penaltyRate), totalPosition);
27+
}
28+
if (assets >= totalPosition) return totalPosition;
29+
if (maturity > lastAccrual) {
30+
unassignedEarnings -= mulDiv(unassignedEarnings, BigInt(timestamp) - lastAccrual, BigInt(maturity) - lastAccrual);
31+
}
32+
if (unassignedEarnings === 0n) return assets;
33+
const backupSupplied = borrowed - min(borrowed, supplied);
34+
if (backupSupplied === 0n) return assets;
35+
const k = divWad(principal, totalPosition);
36+
if (k === 0n) return assets;
37+
const netUnassignedEarnings = mulWad(unassignedEarnings, WAD - backupFeeRate);
38+
if (netUnassignedEarnings === 0n) return assets;
39+
const r = mulDiv(netUnassignedEarnings, k, backupSupplied);
40+
if (r >= WAD) return min(assets + netUnassignedEarnings, totalPosition);
41+
const x = divWad(assets, WAD - r);
42+
if (mulWad(k, x) <= backupSupplied && x <= totalPosition) return x;
43+
return min(assets + netUnassignedEarnings, totalPosition);
44+
}

test/market.test.ts

Lines changed: 140 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import { integrationPreviewerAbi, marketUsdcAbi, ratePreviewerAbi } from "./gene
55
import anvilClient from "./utils/anvilClient.js";
66
import MAX_UINT256 from "../src/fixed-point-math/MAX_UINT256.js";
77
import divWad from "../src/fixed-point-math/divWad.js";
8-
import { MATURITY_INTERVAL } from "../src/interest-rate-model/fixedRate.js";
8+
import mulWad from "../src/fixed-point-math/mulWad.js";
9+
import { MATURITY_INTERVAL, WAD } from "../src/interest-rate-model/fixedRate.js";
910
import fixedRepayAssets, { type FixedRepaySnapshot } from "../src/market/fixedRepayAssets.js";
11+
import fixedRepayPosition from "../src/market/fixedRepayPosition.js";
1012
import floatingDepositRates from "../src/market/floatingDepositRates.js";
1113

1214
describe("floating deposit rate", () => {
@@ -149,4 +151,141 @@ describe("fixed repay", () => {
149151
expect(repayAssets).toBe(0n);
150152
});
151153
});
154+
155+
describe("position", () => {
156+
it("calculates before maturity", async () => {
157+
const timestamp = 69_420;
158+
const repayAssets = 420_000_000n;
159+
160+
const positionAssets = fixedRepayPosition(snapshot, MATURITY_INTERVAL, repayAssets, timestamp);
161+
162+
const { data = "0x" } = await anvilClient.call({
163+
account: inject("deployer"),
164+
to: inject("MarketUSDC"),
165+
data: encodeFunctionData({
166+
functionName: "repayAtMaturity",
167+
args: [BigInt(MATURITY_INTERVAL), positionAssets, MAX_UINT256, inject("deployer")],
168+
abi: marketUsdcAbi,
169+
}),
170+
blockOverrides: { time: BigInt(timestamp) },
171+
});
172+
173+
expect(repayAssets).toBe(decodeFunctionResult({ data, functionName: "repayAtMaturity", abi: marketUsdcAbi }));
174+
});
175+
176+
it("calculates after maturity", async () => {
177+
const timestamp = 69_420 + MATURITY_INTERVAL;
178+
const repayAssets = 420_000_000n;
179+
180+
const positionAssets = fixedRepayPosition(snapshot, MATURITY_INTERVAL, repayAssets, timestamp);
181+
182+
const { data = "0x" } = await anvilClient.call({
183+
account: inject("deployer"),
184+
to: inject("MarketUSDC"),
185+
data: encodeFunctionData({
186+
functionName: "repayAtMaturity",
187+
args: [BigInt(MATURITY_INTERVAL), positionAssets, MAX_UINT256, inject("deployer")],
188+
abi: marketUsdcAbi,
189+
}),
190+
blockOverrides: { time: BigInt(timestamp) },
191+
});
192+
const actualRepayAssets = decodeFunctionResult({ data, functionName: "repayAtMaturity", abi: marketUsdcAbi });
193+
194+
expect(actualRepayAssets).toBeLessThanOrEqual(repayAssets);
195+
expect(repayAssets - actualRepayAssets).toBeLessThanOrEqual(1n);
196+
});
197+
198+
it("calculates with max position", () => {
199+
const positionAssets = fixedRepayPosition(snapshot, MATURITY_INTERVAL, MAX_UINT256, 69_420);
200+
201+
expect(positionAssets).toBe(snapshot.principal + snapshot.fee);
202+
});
203+
204+
it("calculates with empty position", () => {
205+
const positionAssets = fixedRepayPosition(
206+
{ ...snapshot, principal: 0n, fee: 0n },
207+
MATURITY_INTERVAL,
208+
MAX_UINT256,
209+
69_420,
210+
);
211+
212+
expect(positionAssets).toBe(0n);
213+
});
214+
215+
it("calculates without unassigned earnings", () => {
216+
const repayAssets = 420_000_000n;
217+
218+
const positionAssets = fixedRepayPosition(
219+
{ ...snapshot, unassignedEarnings: 0n },
220+
MATURITY_INTERVAL,
221+
repayAssets,
222+
69_420,
223+
);
224+
225+
expect(positionAssets).toBe(repayAssets);
226+
});
227+
228+
it("calculates without backup supplied", () => {
229+
const repayAssets = 420_000_000n;
230+
231+
const positionAssets = fixedRepayPosition(
232+
{ ...snapshot, supplied: snapshot.borrowed },
233+
MATURITY_INTERVAL,
234+
repayAssets,
235+
69_420,
236+
);
237+
238+
expect(positionAssets).toBe(repayAssets);
239+
});
240+
241+
it("calculates without principal", () => {
242+
const repayAssets = 69n;
243+
244+
const positionAssets = fixedRepayPosition({ ...snapshot, principal: 0n }, MATURITY_INTERVAL, repayAssets, 69_420);
245+
246+
expect(positionAssets).toBe(repayAssets);
247+
});
248+
249+
it("calculates without net unassigned earnings", () => {
250+
const repayAssets = 420_000_000n;
251+
252+
const positionAssets = fixedRepayPosition(
253+
{ ...snapshot, unassignedEarnings: 1n },
254+
MATURITY_INTERVAL,
255+
repayAssets,
256+
69_420,
257+
);
258+
259+
expect(positionAssets).toBe(repayAssets);
260+
});
261+
262+
it("calculates with high earnings proportion", () => {
263+
const timestamp = 69_420;
264+
const repayAssets = 420_000_000n;
265+
266+
const positionAssets = fixedRepayPosition(
267+
{ ...snapshot, supplied: snapshot.borrowed - 1n, lastAccrual: BigInt(timestamp) },
268+
MATURITY_INTERVAL,
269+
repayAssets,
270+
timestamp,
271+
);
272+
273+
expect(positionAssets).toBe(repayAssets + mulWad(snapshot.unassignedEarnings, WAD - snapshot.backupFeeRate));
274+
});
275+
276+
it("calculates with high saturated fallback", () => {
277+
const timestamp = 69_420;
278+
const repayAssets = 420_000_000n;
279+
const unassignedEarnings = 420n;
280+
281+
const positionAssets = fixedRepayPosition(
282+
{ ...snapshot, supplied: snapshot.borrowed - 420n, unassignedEarnings, lastAccrual: BigInt(timestamp) },
283+
MATURITY_INTERVAL,
284+
repayAssets,
285+
timestamp,
286+
);
287+
288+
expect(positionAssets).toBe(repayAssets + mulWad(unassignedEarnings, WAD - snapshot.backupFeeRate));
289+
});
290+
});
152291
});

0 commit comments

Comments
 (0)