@carrot-protocol/clend-rpc 0.0.1-mrgn-fork1-dev-7be6ef2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prettierignore +1 -0
- package/makefile +12 -0
- package/package.json +32 -0
- package/src/addresses.ts +206 -0
- package/src/idl/clend.ts +7509 -0
- package/src/index.ts +31 -0
- package/src/instructions.ts +466 -0
- package/src/jupUtils.ts +347 -0
- package/src/jupiterUtils.ts +288 -0
- package/src/logger.ts +21 -0
- package/src/math.ts +684 -0
- package/src/mockJupiterUtils.ts +109 -0
- package/src/rpc.ts +1296 -0
- package/src/state.ts +512 -0
- package/src/utils.ts +249 -0
- package/test/bank.test.ts +95 -0
- package/test/interest-rate.test.ts +114 -0
- package/test/leverage.test.ts +867 -0
- package/test/token-amounts.test.ts +73 -0
- package/tsconfig.json +17 -0
package/src/utils.ts
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { BN, web3 } from "@coral-xyz/anchor";
|
|
2
|
+
import { TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from "@solana/spl-token";
|
|
3
|
+
import Decimal from "decimal.js";
|
|
4
|
+
import {
|
|
5
|
+
WrappedI80F48,
|
|
6
|
+
InterestRateConfig,
|
|
7
|
+
BankConfig,
|
|
8
|
+
Bank,
|
|
9
|
+
ClendAccount,
|
|
10
|
+
OracleSetup,
|
|
11
|
+
getOracle,
|
|
12
|
+
RiskTier,
|
|
13
|
+
BankOperationalState,
|
|
14
|
+
AssetTag,
|
|
15
|
+
TOKEN_PROGRAMS,
|
|
16
|
+
USDC_MINT,
|
|
17
|
+
JLP_MINT,
|
|
18
|
+
TOKEN_DECIMALS,
|
|
19
|
+
} from "./state";
|
|
20
|
+
import { ClendClient } from "./rpc";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get token program for a mint
|
|
24
|
+
*/
|
|
25
|
+
export function getTokenProgramForMint(mint: web3.PublicKey): web3.PublicKey {
|
|
26
|
+
if (mint.equals(USDC_MINT)) {
|
|
27
|
+
return TOKEN_PROGRAMS.USDC;
|
|
28
|
+
} else if (mint.equals(JLP_MINT)) {
|
|
29
|
+
return TOKEN_PROGRAMS.JLP;
|
|
30
|
+
} else {
|
|
31
|
+
throw new Error(`Unsupported mint: ${mint.toString()}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get token decimals for a mint
|
|
37
|
+
*/
|
|
38
|
+
export function getTokenDecimalsForMint(mint: web3.PublicKey): number {
|
|
39
|
+
if (mint.equals(USDC_MINT)) {
|
|
40
|
+
return TOKEN_DECIMALS.USDC;
|
|
41
|
+
} else if (mint.equals(JLP_MINT)) {
|
|
42
|
+
return TOKEN_DECIMALS.JLP;
|
|
43
|
+
} else {
|
|
44
|
+
throw new Error(`Unsupported mint: ${mint.toString()}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const I80F48_FRACTIONAL_BYTES = 6;
|
|
49
|
+
const I80F48_TOTAL_BYTES = 16;
|
|
50
|
+
const I80F48_DIVISOR = new Decimal(2).pow(8 * I80F48_FRACTIONAL_BYTES);
|
|
51
|
+
|
|
52
|
+
export function bigNumberToWrappedI80F48(value: BN | number): WrappedI80F48 {
|
|
53
|
+
let decimalValue = new Decimal(value.toString());
|
|
54
|
+
const isNegative = decimalValue.isNegative();
|
|
55
|
+
|
|
56
|
+
decimalValue = decimalValue.times(I80F48_DIVISOR);
|
|
57
|
+
let wrappedValue = new BN(decimalValue.round().toFixed()).toArray();
|
|
58
|
+
|
|
59
|
+
if (wrappedValue.length < I80F48_TOTAL_BYTES) {
|
|
60
|
+
const padding = Array(I80F48_TOTAL_BYTES - wrappedValue.length).fill(0);
|
|
61
|
+
wrappedValue.unshift(...padding);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (isNegative) {
|
|
65
|
+
wrappedValue[wrappedValue.length - 1] |= 0x80;
|
|
66
|
+
wrappedValue = wrappedValue.map((v: number) => ~v & 0xff);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
wrappedValue.reverse();
|
|
70
|
+
|
|
71
|
+
return { value: wrappedValue };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function wrappedI80F48toBigNumber(wrapped: WrappedI80F48): BN | number {
|
|
75
|
+
let bytesLE = wrapped.value;
|
|
76
|
+
if (bytesLE.length !== I80F48_TOTAL_BYTES) {
|
|
77
|
+
throw new Error(`Expected a ${I80F48_TOTAL_BYTES}-byte buffer`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let bytesBE = bytesLE.slice();
|
|
81
|
+
bytesBE.reverse();
|
|
82
|
+
|
|
83
|
+
let signChar = "";
|
|
84
|
+
const msb = bytesBE[0];
|
|
85
|
+
if (msb & 0x80) {
|
|
86
|
+
signChar = "-";
|
|
87
|
+
bytesBE = bytesBE.map((v) => ~v & 0xff);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let hex =
|
|
91
|
+
signChar +
|
|
92
|
+
"0x" +
|
|
93
|
+
bytesBE.map((v) => v.toString(16).padStart(2, "0")).join("");
|
|
94
|
+
let decoded = new Decimal(hex).dividedBy(I80F48_DIVISOR);
|
|
95
|
+
let value = decoded.toString();
|
|
96
|
+
|
|
97
|
+
// Check if the value has decimal places
|
|
98
|
+
if (value.includes(".")) {
|
|
99
|
+
return Number(value);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return new BN(value);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function parseInterestRateConfig(data: any): InterestRateConfig {
|
|
106
|
+
return {
|
|
107
|
+
optimalUtilizationRate: data.optimalUtilizationRate,
|
|
108
|
+
plateauInterestRate: data.plateauInterestRate,
|
|
109
|
+
maxInterestRate: data.maxInterestRate,
|
|
110
|
+
insuranceFeeFixedApr: data.insuranceFeeFixedApr,
|
|
111
|
+
insuranceIrFee: data.insuranceIrFee,
|
|
112
|
+
protocolFixedFeeApr: data.protocolFixedFeeApr,
|
|
113
|
+
protocolIrFee: data.protocolIrFee,
|
|
114
|
+
protocolOriginationFee: data.protocolOriginationFee,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function parseRiskTier(data: any): RiskTier {
|
|
119
|
+
// Handle number format
|
|
120
|
+
if (typeof data === "number") {
|
|
121
|
+
if (data === 0) return RiskTier.Collateral;
|
|
122
|
+
if (data === 1) return RiskTier.Isolated;
|
|
123
|
+
throw new Error(`Invalid risk tier number: ${data}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Handle object format
|
|
127
|
+
if (data.collateral) {
|
|
128
|
+
return RiskTier.Collateral;
|
|
129
|
+
} else if (data.isolated) {
|
|
130
|
+
return RiskTier.Isolated;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
throw new Error(`Invalid risk tier format: ${JSON.stringify(data)}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function parseOperationalState(data: any): BankOperationalState {
|
|
137
|
+
// Handle number format
|
|
138
|
+
if (typeof data === "number") {
|
|
139
|
+
if (data === 0) return BankOperationalState.Paused;
|
|
140
|
+
if (data === 1) return BankOperationalState.Operational;
|
|
141
|
+
if (data === 2) return BankOperationalState.ReduceOnly;
|
|
142
|
+
throw new Error(`Invalid operational state number: ${data}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Handle object format
|
|
146
|
+
if (data.paused) {
|
|
147
|
+
return BankOperationalState.Paused;
|
|
148
|
+
} else if (data.operational) {
|
|
149
|
+
return BankOperationalState.Operational;
|
|
150
|
+
} else if (data.reduceOnly) {
|
|
151
|
+
return BankOperationalState.ReduceOnly;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
throw new Error(`Invalid operational state format: ${JSON.stringify(data)}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function parseAssetTag(data: any): AssetTag {
|
|
158
|
+
// Handle number format
|
|
159
|
+
if (typeof data === "number") {
|
|
160
|
+
if (data === 0) return AssetTag.Default;
|
|
161
|
+
if (data === 1) return AssetTag.Sol;
|
|
162
|
+
if (data === 2) return AssetTag.Staked;
|
|
163
|
+
throw new Error(`Invalid asset tag number: ${data}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Handle object format
|
|
167
|
+
if (data.default) {
|
|
168
|
+
return AssetTag.Default;
|
|
169
|
+
} else if (data.sol) {
|
|
170
|
+
return AssetTag.Sol;
|
|
171
|
+
} else if (data.staked) {
|
|
172
|
+
return AssetTag.Staked;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
throw new Error(`Invalid asset tag format: ${JSON.stringify(data)}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function parseOracleSetup(data: any): OracleSetup {
|
|
179
|
+
// Handle number format
|
|
180
|
+
if (typeof data === "number") {
|
|
181
|
+
if (data === 0) return OracleSetup.None;
|
|
182
|
+
if (data === 1) return OracleSetup.PythLegacy;
|
|
183
|
+
if (data === 2) return OracleSetup.PythPushOracle;
|
|
184
|
+
if (data === 3) return OracleSetup.StakedWithPythPush;
|
|
185
|
+
throw new Error(`Invalid oracle setup number: ${data}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Handle object format
|
|
189
|
+
if (data.none) {
|
|
190
|
+
return OracleSetup.None;
|
|
191
|
+
} else if (data.pythLegacy) {
|
|
192
|
+
return OracleSetup.PythLegacy;
|
|
193
|
+
} else if (data.pythPushOracle) {
|
|
194
|
+
return OracleSetup.PythPushOracle;
|
|
195
|
+
} else if (data.stakedWithPythPush) {
|
|
196
|
+
return OracleSetup.StakedWithPythPush;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
throw new Error(`Invalid oracle setup format: ${JSON.stringify(data)}`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function parseBankConfig(data: any): BankConfig {
|
|
203
|
+
return {
|
|
204
|
+
assetWeightInit: data.assetWeightInit,
|
|
205
|
+
assetWeightMaint: data.assetWeightMaint,
|
|
206
|
+
liabilityWeightInit: data.liabilityWeightInit,
|
|
207
|
+
liabilityWeightMaint: data.liabilityWeightMaint,
|
|
208
|
+
depositLimit: new BN(data.depositLimit),
|
|
209
|
+
interestRateConfig: parseInterestRateConfig(data.interestRateConfig),
|
|
210
|
+
operationalState: parseOperationalState(data.operationalState),
|
|
211
|
+
oracleSetup: parseOracleSetup(data.oracleSetup),
|
|
212
|
+
oracleKeys: data.oracleKeys.map((key: any) => new web3.PublicKey(key)),
|
|
213
|
+
borrowLimit: new BN(data.borrowLimit),
|
|
214
|
+
riskTier: parseRiskTier(data.riskTier),
|
|
215
|
+
assetTag: parseAssetTag(data.assetTag),
|
|
216
|
+
totalAssetValueInitLimit: new BN(data.totalAssetValueInitLimit),
|
|
217
|
+
oracleMaxAge: data.oracleMaxAge,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// only return active banks
|
|
222
|
+
export function getClendAccountActiveBanks(
|
|
223
|
+
clendAccount: ClendAccount,
|
|
224
|
+
): web3.PublicKey[] {
|
|
225
|
+
return clendAccount.lendingAccount.balances
|
|
226
|
+
.filter((balance) => balance.active)
|
|
227
|
+
.map((balance) => balance.bankPk);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function getClendAccountRemainingAccounts(
|
|
231
|
+
banks: Bank[],
|
|
232
|
+
): web3.AccountMeta[] {
|
|
233
|
+
const remainingAccounts: web3.AccountMeta[] = [];
|
|
234
|
+
for (const bank of banks) {
|
|
235
|
+
remainingAccounts.push({
|
|
236
|
+
pubkey: bank.key,
|
|
237
|
+
isWritable: true,
|
|
238
|
+
isSigner: false,
|
|
239
|
+
});
|
|
240
|
+
// fetch correct oracle from map
|
|
241
|
+
const oracle = getOracle(bank.mint);
|
|
242
|
+
remainingAccounts.push({
|
|
243
|
+
pubkey: oracle,
|
|
244
|
+
isWritable: false,
|
|
245
|
+
isSigner: false,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
return remainingAccounts;
|
|
249
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { expect } from 'chai';
|
|
2
|
+
import { describe, it } from 'mocha';
|
|
3
|
+
import { BN } from '@coral-xyz/anchor';
|
|
4
|
+
import * as math from '../src/math';
|
|
5
|
+
import { bigNumberToWrappedI80F48 } from '../src/utils';
|
|
6
|
+
|
|
7
|
+
describe('Bank Calculations', () => {
|
|
8
|
+
describe('calculateUtilizationRate', () => {
|
|
9
|
+
it('should return 0 when total supply is 0', () => {
|
|
10
|
+
expect(math.calculateUtilizationRate(0, 100)).to.equal(0);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should calculate utilization rate correctly', () => {
|
|
14
|
+
expect(math.calculateUtilizationRate(1000, 500)).to.equal(0.5);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should cap utilization rate at 1', () => {
|
|
18
|
+
expect(math.calculateUtilizationRate(1000, 1500)).to.equal(1);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('calculateAssetQuantity', () => {
|
|
23
|
+
it('should calculate asset quantity from number shares', () => {
|
|
24
|
+
const shares = 100;
|
|
25
|
+
const shareValue = bigNumberToWrappedI80F48(0.5);
|
|
26
|
+
expect(math.calculateAssetQuantity(shares, shareValue)).to.be.closeTo(50, 0.001);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should calculate asset quantity from BN shares', () => {
|
|
30
|
+
const shares = new BN(100);
|
|
31
|
+
const shareValue = bigNumberToWrappedI80F48(0.5);
|
|
32
|
+
expect(math.calculateAssetQuantity(shares, shareValue)).to.be.closeTo(50, 0.001);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('calculateLiabilityQuantity', () => {
|
|
37
|
+
it('should calculate liability quantity from number shares', () => {
|
|
38
|
+
const shares = 200;
|
|
39
|
+
const shareValue = bigNumberToWrappedI80F48(0.75);
|
|
40
|
+
expect(math.calculateLiabilityQuantity(shares, shareValue)).to.be.closeTo(150, 0.001);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should calculate liability quantity from BN shares', () => {
|
|
44
|
+
const shares = new BN(200);
|
|
45
|
+
const shareValue = bigNumberToWrappedI80F48(0.75);
|
|
46
|
+
expect(math.calculateLiabilityQuantity(shares, shareValue)).to.be.closeTo(150, 0.001);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('calculateTotalAssetQuantity', () => {
|
|
51
|
+
it('should calculate total asset quantity correctly', () => {
|
|
52
|
+
const totalAssetShares = bigNumberToWrappedI80F48(1000);
|
|
53
|
+
const assetShareValue = bigNumberToWrappedI80F48(0.25);
|
|
54
|
+
expect(math.calculateTotalAssetQuantity(totalAssetShares, assetShareValue)).to.be.closeTo(250, 0.001);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('calculateTotalLiabilityQuantity', () => {
|
|
59
|
+
it('should calculate total liability quantity correctly', () => {
|
|
60
|
+
const totalLiabilityShares = bigNumberToWrappedI80F48(500);
|
|
61
|
+
const liabilityShareValue = bigNumberToWrappedI80F48(0.3);
|
|
62
|
+
expect(math.calculateTotalLiabilityQuantity(totalLiabilityShares, liabilityShareValue)).to.be.closeTo(150, 0.001);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('calculateBankUtilizationRate', () => {
|
|
67
|
+
it('should calculate bank utilization rate correctly', () => {
|
|
68
|
+
const totalAssetShares = bigNumberToWrappedI80F48(1000);
|
|
69
|
+
const assetShareValue = bigNumberToWrappedI80F48(1);
|
|
70
|
+
const totalLiabilityShares = bigNumberToWrappedI80F48(500);
|
|
71
|
+
const liabilityShareValue = bigNumberToWrappedI80F48(1);
|
|
72
|
+
|
|
73
|
+
expect(math.calculateBankUtilizationRate(
|
|
74
|
+
totalAssetShares,
|
|
75
|
+
assetShareValue,
|
|
76
|
+
totalLiabilityShares,
|
|
77
|
+
liabilityShareValue
|
|
78
|
+
)).to.be.closeTo(0.5, 0.001);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should return 0 when total assets are 0', () => {
|
|
82
|
+
const totalAssetShares = bigNumberToWrappedI80F48(0);
|
|
83
|
+
const assetShareValue = bigNumberToWrappedI80F48(1);
|
|
84
|
+
const totalLiabilityShares = bigNumberToWrappedI80F48(500);
|
|
85
|
+
const liabilityShareValue = bigNumberToWrappedI80F48(1);
|
|
86
|
+
|
|
87
|
+
expect(math.calculateBankUtilizationRate(
|
|
88
|
+
totalAssetShares,
|
|
89
|
+
assetShareValue,
|
|
90
|
+
totalLiabilityShares,
|
|
91
|
+
liabilityShareValue
|
|
92
|
+
)).to.equal(0);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { expect } from 'chai';
|
|
2
|
+
import { describe, it } from 'mocha';
|
|
3
|
+
import * as math from '../src/math';
|
|
4
|
+
import { bigNumberToWrappedI80F48 } from '../src/utils';
|
|
5
|
+
|
|
6
|
+
describe('Interest Rate Calculations', () => {
|
|
7
|
+
describe('calculateBaseInterestRate', () => {
|
|
8
|
+
it('should calculate interest rate below optimal utilization', () => {
|
|
9
|
+
const utilizationRate = 0.3;
|
|
10
|
+
const optimalUtilizationRate = bigNumberToWrappedI80F48(0.8);
|
|
11
|
+
const plateauInterestRate = bigNumberToWrappedI80F48(0.05);
|
|
12
|
+
const maxInterestRate = bigNumberToWrappedI80F48(0.2);
|
|
13
|
+
|
|
14
|
+
// Below optimal: (0.3 / 0.8) * 0.05 = 0.01875
|
|
15
|
+
expect(math.calculateBaseInterestRate(
|
|
16
|
+
utilizationRate,
|
|
17
|
+
optimalUtilizationRate,
|
|
18
|
+
plateauInterestRate,
|
|
19
|
+
maxInterestRate
|
|
20
|
+
)).to.be.closeTo(0.01875, 0.0001);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should calculate interest rate at optimal utilization', () => {
|
|
24
|
+
const utilizationRate = 0.8;
|
|
25
|
+
const optimalUtilizationRate = bigNumberToWrappedI80F48(0.8);
|
|
26
|
+
const plateauInterestRate = bigNumberToWrappedI80F48(0.05);
|
|
27
|
+
const maxInterestRate = bigNumberToWrappedI80F48(0.2);
|
|
28
|
+
|
|
29
|
+
// At optimal: should equal plateau rate
|
|
30
|
+
expect(math.calculateBaseInterestRate(
|
|
31
|
+
utilizationRate,
|
|
32
|
+
optimalUtilizationRate,
|
|
33
|
+
plateauInterestRate,
|
|
34
|
+
maxInterestRate
|
|
35
|
+
)).to.be.closeTo(0.05, 0.0001);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should calculate interest rate above optimal utilization', () => {
|
|
39
|
+
const utilizationRate = 0.9;
|
|
40
|
+
const optimalUtilizationRate = bigNumberToWrappedI80F48(0.8);
|
|
41
|
+
const plateauInterestRate = bigNumberToWrappedI80F48(0.05);
|
|
42
|
+
const maxInterestRate = bigNumberToWrappedI80F48(0.2);
|
|
43
|
+
|
|
44
|
+
// Above optimal: 0.05 + ((0.9 - 0.8) / (1 - 0.8)) * (0.2 - 0.05) = 0.05 + (0.1 / 0.2) * 0.15 = 0.05 + 0.075 = 0.125
|
|
45
|
+
expect(math.calculateBaseInterestRate(
|
|
46
|
+
utilizationRate,
|
|
47
|
+
optimalUtilizationRate,
|
|
48
|
+
plateauInterestRate,
|
|
49
|
+
maxInterestRate
|
|
50
|
+
)).to.be.closeTo(0.125, 0.0001);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should calculate interest rate at max utilization', () => {
|
|
54
|
+
const utilizationRate = 1;
|
|
55
|
+
const optimalUtilizationRate = bigNumberToWrappedI80F48(0.8);
|
|
56
|
+
const plateauInterestRate = bigNumberToWrappedI80F48(0.05);
|
|
57
|
+
const maxInterestRate = bigNumberToWrappedI80F48(0.2);
|
|
58
|
+
|
|
59
|
+
// At max: should equal max rate
|
|
60
|
+
expect(math.calculateBaseInterestRate(
|
|
61
|
+
utilizationRate,
|
|
62
|
+
optimalUtilizationRate,
|
|
63
|
+
plateauInterestRate,
|
|
64
|
+
maxInterestRate
|
|
65
|
+
)).to.be.closeTo(0.2, 0.0001);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('calculateSupplyApy', () => {
|
|
70
|
+
it('should calculate supply APY correctly', () => {
|
|
71
|
+
const utilizationRate = 0.7;
|
|
72
|
+
const baseInterestRate = 0.1;
|
|
73
|
+
const protocolIrFee = bigNumberToWrappedI80F48(0.1); // 10% fee
|
|
74
|
+
const insuranceIrFee = bigNumberToWrappedI80F48(0.05); // 5% fee
|
|
75
|
+
|
|
76
|
+
// Supply APY = 0.1 * 0.7 * (1 - (0.1 + 0.05)) = 0.07 * 0.85 = 0.0595
|
|
77
|
+
expect(math.calculateSupplyApy(
|
|
78
|
+
utilizationRate,
|
|
79
|
+
baseInterestRate,
|
|
80
|
+
protocolIrFee,
|
|
81
|
+
insuranceIrFee
|
|
82
|
+
)).to.be.closeTo(0.0595, 0.0001);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should return 0 when utilization rate is 0', () => {
|
|
86
|
+
const utilizationRate = 0;
|
|
87
|
+
const baseInterestRate = 0.1;
|
|
88
|
+
const protocolIrFee = bigNumberToWrappedI80F48(0.1);
|
|
89
|
+
const insuranceIrFee = bigNumberToWrappedI80F48(0.05);
|
|
90
|
+
|
|
91
|
+
expect(math.calculateSupplyApy(
|
|
92
|
+
utilizationRate,
|
|
93
|
+
baseInterestRate,
|
|
94
|
+
protocolIrFee,
|
|
95
|
+
insuranceIrFee
|
|
96
|
+
)).to.be.closeTo(0, 0.0001);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('calculateBorrowApy', () => {
|
|
101
|
+
it('should calculate borrow APY correctly', () => {
|
|
102
|
+
const baseInterestRate = 0.08;
|
|
103
|
+
const protocolFixedFeeApr = bigNumberToWrappedI80F48(0.01); // 1% fee
|
|
104
|
+
const insuranceFeeFixedApr = bigNumberToWrappedI80F48(0.005); // 0.5% fee
|
|
105
|
+
|
|
106
|
+
// Borrow APY = 0.08 + 0.01 + 0.005 = 0.095
|
|
107
|
+
expect(math.calculateBorrowApy(
|
|
108
|
+
baseInterestRate,
|
|
109
|
+
protocolFixedFeeApr,
|
|
110
|
+
insuranceFeeFixedApr
|
|
111
|
+
)).to.be.closeTo(0.095, 0.0001);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
});
|