Skip to content

Commit 2c5c89e

Browse files
committed
Refactor getBrokerUserPositions to utilize calculateDynamicLoanRepayment for dynamic outstanding calculations, improving accuracy by converting rates to WAD format. Introduce helper functions for rate normalization and outstanding calculation, enhancing code clarity and maintainability.
1 parent e0883f1 commit 2c5c89e

4 files changed

Lines changed: 94 additions & 51 deletions

File tree

packages/moolah-lending-sdk/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# @lista-dao/moolah-lending-sdk
22

3+
## 1.0.9
4+
5+
### Patch Changes
6+
7+
- Refactor getBrokerUserPositions to utilize calculateDynamicLoanRepayment for dynamic outstanding calculations, improving accuracy by converting rates to WAD format. Introduce helper functions for rate normalization and outstanding calculation, enhancing code clarity and maintainability.
8+
39
## 1.0.8
410

511
### Patch Changes

packages/moolah-lending-sdk/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@lista-dao/moolah-lending-sdk",
3-
"version": "1.0.8",
3+
"version": "1.0.9",
44
"type": "module",
55
"main": "./dist/index.js",
66
"module": "./dist/index.js",

packages/moolah-lending-sdk/src/__tests__/read/broker/getBrokerUserPositions.test.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { beforeEach, describe, expect, it, vi } from "vitest";
22
import type { Address, PublicClient } from "viem";
3+
import { calculateDynamicLoanRepayment } from "@lista-dao/moolah-sdk-core";
34

45
import { getBrokerUserPositions } from "../../../read/broker/getBrokerUserPositions.js";
56

@@ -12,18 +13,26 @@ const BROKER = "0x1111111111111111111111111111111111111111" as Address;
1213
const RATE_CALCULATOR =
1314
"0x2222222222222222222222222222222222222222" as Address;
1415
const USER = "0x3333333333333333333333333333333333333333" as Address;
15-
const RAY = 10n ** 27n;
16+
const WAD = 10n ** 18n;
1617

1718
describe("getBrokerUserPositions - dynamic debt", () => {
1819
beforeEach(() => {
1920
vi.clearAllMocks();
2021
});
2122

22-
it("computes dynamic outstanding with normalizedDebt * rate / 1e27", async () => {
23+
it("computes dynamic outstanding with legacy decimal-normalized dynamic rate", async () => {
2324
const principal = 17061806491632102441n;
2425
const normalizedDebt = 16878807815476167930n;
2526
const rate = 1011624315990879851775155575n;
26-
const expectedOutstanding = (normalizedDebt * rate) / RAY;
27+
const { totalRepay } = calculateDynamicLoanRepayment(
28+
{
29+
principal,
30+
normalizedDebt,
31+
rate: rate / WAD,
32+
},
33+
18,
34+
);
35+
const expectedOutstanding = totalRepay.roundDown(18).numerator;
2736

2837
mockReadContract.mockImplementation(async ({ functionName }) => {
2938
switch (functionName) {

packages/moolah-lending-sdk/src/read/broker/getBrokerUserPositions.ts

Lines changed: 75 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,84 @@ import {
33
Decimal,
44
LENDING_BROKER_ABI,
55
BROKER_RATE_CALCULATOR_ABI,
6+
calculateDynamicLoanRepayment,
67
calculateFixedLoanRepayment,
78
type BrokerUserPositionsData,
89
type FixedLoanPosition,
910
type DynamicLoanPosition,
1011
type RawFixedTerm,
1112
} from "@lista-dao/moolah-sdk-core";
1213

14+
const ONE_E27 = 10n ** 27n;
1315
const SECONDS_PER_WEEK = 604800n;
16+
const RATE_SCALE_18 = 10n ** 18n;
17+
const FLEXIBLE_RATE_NUMERATOR = 100n;
18+
const FLEXIBLE_RATE_DENOMINATOR = 95n;
1419

1520
/**
1621
* Normalize APR rate from contract format
1722
* Contract stores APR as (1 + rate) * 1e27, we convert to rate * 1e27
1823
*/
1924
function normalizeAprRate(apr: bigint): bigint {
20-
const ONE_E27 = 10n ** 27n;
2125
return apr > ONE_E27 ? apr - ONE_E27 : apr;
2226
}
2327

28+
/**
29+
* Convert broker dynamic rate (27 decimals) to WAD (18 decimals)
30+
* expected by calculateDynamicLoanRepayment.
31+
*/
32+
function normalizeDynamicRateToWad(dynamicRate: bigint): bigint {
33+
return dynamicRate / RATE_SCALE_18;
34+
}
35+
36+
function buildTermRateData(terms: readonly RawFixedTerm[]) {
37+
const termRateByDuration = new Map<string, Decimal>();
38+
let currentFlexibleRate = Decimal.ZERO;
39+
40+
for (const term of terms) {
41+
const normalizedRate = normalizeAprRate(term.apr);
42+
43+
if (term.duration === SECONDS_PER_WEEK) {
44+
currentFlexibleRate = new Decimal(
45+
(normalizedRate * FLEXIBLE_RATE_NUMERATOR) / FLEXIBLE_RATE_DENOMINATOR,
46+
27,
47+
);
48+
}
49+
50+
termRateByDuration.set(
51+
term.duration.toString(),
52+
new Decimal(normalizedRate, 27),
53+
);
54+
}
55+
56+
return { termRateByDuration, currentFlexibleRate };
57+
}
58+
59+
function calculateDynamicOutstanding(
60+
dynamicPosition: DynamicLoanPosition,
61+
dynamicRate: bigint,
62+
loanDecimals: number,
63+
): Decimal | null {
64+
if (dynamicPosition.principal <= 0n) {
65+
return null;
66+
}
67+
68+
const { totalRepay } = calculateDynamicLoanRepayment(
69+
{
70+
principal: dynamicPosition.principal,
71+
normalizedDebt: dynamicPosition.normalizedDebt,
72+
rate: normalizeDynamicRateToWad(dynamicRate),
73+
},
74+
loanDecimals,
75+
);
76+
77+
return totalRepay.roundDown(loanDecimals);
78+
}
79+
80+
function getPositionDuration(position: FixedLoanPosition): bigint {
81+
return position.end > position.start ? position.end - position.start : 0n;
82+
}
83+
2484
/**
2585
* Get broker user positions data
2686
*/
@@ -60,34 +120,12 @@ export async function getBrokerUserPositions(
60120
}) as Promise<bigint>,
61121
]);
62122

63-
// Build term rate map
64-
const termRateByDuration = new Map<string, Decimal>();
65-
let currentFlexibleRate = Decimal.ZERO;
66-
67-
terms.forEach((term) => {
68-
if (term.duration === SECONDS_PER_WEEK) {
69-
// Flexible rate = term rate * 100 / 95
70-
currentFlexibleRate = new Decimal(
71-
(normalizeAprRate(term.apr) * 100n) / 95n,
72-
27,
73-
);
74-
}
75-
const normalizedRate = new Decimal(normalizeAprRate(term.apr), 27);
76-
termRateByDuration.set(term.duration.toString(), normalizedRate);
77-
});
78-
79-
// Calculate dynamic position data
80-
let dynamicOutstanding: Decimal | null = null;
81-
82-
if (dynamicPosition?.principal && dynamicPosition.principal > 0n) {
83-
const normalizedDebt = new Decimal(
84-
dynamicPosition.normalizedDebt ?? dynamicPosition.principal,
85-
loanDecimals,
86-
);
87-
dynamicOutstanding = normalizedDebt
88-
.mul(new Decimal(dynamicRate, 27))
89-
.roundDown(loanDecimals);
90-
}
123+
const { termRateByDuration, currentFlexibleRate } = buildTermRateData(terms);
124+
const dynamicOutstanding = calculateDynamicOutstanding(
125+
dynamicPosition,
126+
dynamicRate,
127+
loanDecimals,
128+
);
91129

92130
// Calculate fixed positions data
93131
let fixedOutstanding = Decimal.ZERO;
@@ -98,23 +136,18 @@ export async function getBrokerUserPositions(
98136
// Add dynamic position to totals
99137
if (dynamicOutstanding && dynamicOutstanding.gt(Decimal.ZERO)) {
100138
totalOutstanding = totalOutstanding.add(dynamicOutstanding);
101-
weightedSum = weightedSum.add(
102-
dynamicOutstanding.mul(currentFlexibleRate ?? Decimal.ZERO),
103-
);
139+
weightedSum = weightedSum.add(dynamicOutstanding.mul(currentFlexibleRate));
104140
}
105141

106142
// Process fixed positions
107143
const currentTimestamp = Math.floor(Date.now() / 1000);
108-
fixedPositions.forEach((position) => {
109-
const principal = BigInt(position.principal ?? 0n);
110-
const principalRepaid = BigInt(position.principalRepaid ?? 0n);
111-
144+
for (const position of fixedPositions) {
112145
// Skip fully repaid or matured positions
113-
if (principal <= principalRepaid) {
114-
return;
146+
if (position.principal <= position.principalRepaid) {
147+
continue;
115148
}
116-
if (Number(position.end ?? 0n) <= currentTimestamp) {
117-
return;
149+
if (Number(position.end) <= currentTimestamp) {
150+
continue;
118151
}
119152

120153
const {
@@ -130,18 +163,13 @@ export async function getBrokerUserPositions(
130163
fixedOutstanding = fixedOutstanding.add(totalRepayNoPenalty);
131164
totalPenalty = totalPenalty.add(new Decimal(penalty, loanDecimals));
132165

133-
const duration =
134-
BigInt(position.end ?? 0n) > BigInt(position.start ?? 0n)
135-
? BigInt(position.end ?? 0n) - BigInt(position.start ?? 0n)
136-
: 0n;
137-
138166
const normalizedFixedRate =
139-
termRateByDuration.get(duration.toString()) ??
167+
termRateByDuration.get(getPositionDuration(position).toString()) ??
140168
new Decimal(normalizeAprRate(position.apr), 27);
141169

142170
totalOutstanding = totalOutstanding.add(totalRepayNoPenalty);
143171
weightedSum = weightedSum.add(totalRepayNoPenalty.mul(normalizedFixedRate));
144-
});
172+
}
145173

146174
// Calculate weighted borrow rate
147175
const weightedBorrowRate = totalOutstanding.gt(Decimal.ZERO)

0 commit comments

Comments
 (0)