Skip to content

Commit 5d06c1a

Browse files
committed
🚧 market: fixed repay
1 parent 93c06de commit 5d06c1a

File tree

8 files changed

+137
-11
lines changed

8 files changed

+137
-11
lines changed

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
"@changesets/types": "^6.1.0",
3333
"@eslint-community/eslint-plugin-eslint-comments": "^4.5.0",
3434
"@eslint/js": "^9.28.0",
35-
"@exactly/protocol": "^0.2.20",
35+
"@exactly/protocol": "exactly/protocol#c5363b6",
3636
"@openzeppelin/contracts": "^5.3.0",
3737
"@openzeppelin/contracts-upgradeable": "^5.3.0",
3838
"@openzeppelin/contracts-upgradeable-v4": "npm:@openzeppelin/contracts-upgradeable@^4.9.6",

‎pnpm-lock.yaml‎

Lines changed: 6 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import min from "../fixed-point-math/min.js";
2+
import mulDiv from "../fixed-point-math/mulDiv.js";
3+
import mulWad from "../fixed-point-math/mulWad.js";
4+
5+
export default function fixedRepayAssets(
6+
{
7+
penaltyRate,
8+
backupFeeRate,
9+
borrowed,
10+
supplied,
11+
unassignedEarnings,
12+
lastAccrual,
13+
principal,
14+
fee,
15+
}: FixedRepaySnapshot,
16+
maturity: bigint,
17+
positionAssets: bigint,
18+
timestamp = Math.floor(Date.now() / 1000),
19+
) {
20+
const totalPosition = principal + fee;
21+
if (totalPosition === 0n) return 0n;
22+
if (positionAssets > totalPosition) positionAssets = totalPosition;
23+
if (timestamp >= lastAccrual) {
24+
return positionAssets + mulWad(positionAssets, (BigInt(timestamp) - lastAccrual) * penaltyRate);
25+
}
26+
if (maturity > lastAccrual) {
27+
unassignedEarnings -= mulDiv(unassignedEarnings, BigInt(timestamp) - lastAccrual, maturity - lastAccrual);
28+
}
29+
let yieldAssets = 0n;
30+
const backupSupplied = borrowed - min(borrowed, supplied);
31+
if (backupSupplied) {
32+
const scaledPrincipal = mulDiv(positionAssets, principal, principal + fee);
33+
yieldAssets = mulDiv(unassignedEarnings, min(scaledPrincipal, backupSupplied), backupSupplied);
34+
yieldAssets -= mulWad(yieldAssets, backupFeeRate);
35+
}
36+
return positionAssets - yieldAssets;
37+
}
38+
39+
export interface FixedRepaySnapshot {
40+
penaltyRate: bigint;
41+
backupFeeRate: bigint;
42+
borrowed: bigint;
43+
supplied: bigint;
44+
unassignedEarnings: bigint;
45+
lastAccrual: bigint;
46+
principal: bigint;
47+
fee: bigint;
48+
}
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: bigint,
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 >= lastAccrual) {
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, 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: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import { describe, expect, inject, it } from "vitest";
1+
import { zeroAddress } from "viem";
2+
import { beforeEach, describe, expect, inject, it } from "vitest";
23

3-
import { marketUsdcAbi, ratePreviewerAbi } from "./generated/contracts.js";
4+
import { integrationPreviewerAbi, marketUsdcAbi, ratePreviewerAbi } from "./generated/contracts.js";
45
import anvilClient from "./utils/anvilClient.js";
56
import divWad from "../src/fixed-point-math/divWad.js";
7+
import type { FixedRepaySnapshot } from "../src/market/fixedRepayAssets.js";
68
import floatingDepositRates from "../src/market/floatingDepositRates.js";
79

810
describe("floating deposit rate", () => {
@@ -48,3 +50,16 @@ describe("floating deposit rate", () => {
4850
expect(usdcRate).toBe(rate);
4951
});
5052
});
53+
54+
describe("fixed repay", () => {
55+
let snapshot: FixedRepaySnapshot;
56+
57+
beforeEach(async () => {
58+
snapshot = await anvilClient.readContract({
59+
address: inject("IntegrationPreviewer"),
60+
functionName: "fixedRepaySnapshot",
61+
args: [zeroAddress, inject("MarketUSDC"), 0n],
62+
abi: integrationPreviewerAbi,
63+
});
64+
});
65+
})

‎test/utils/Protocol.s.sol‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { MockBalancerVault } from "@exactly/protocol/mocks/MockBalancerVault.sol
1010
import { MockPriceFeed } from "@exactly/protocol/mocks/MockPriceFeed.sol";
1111
import { Previewer } from "@exactly/protocol/periphery/Previewer.sol";
1212
import { RatePreviewer } from "@exactly/protocol/periphery/RatePreviewer.sol";
13+
import { IntegrationPreviewer } from "@exactly/protocol/periphery/IntegrationPreviewer.sol";
1314

1415
import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
1516

@@ -27,6 +28,7 @@ contract DeployProtocol is Script {
2728
MockWETH public weth;
2829
Previewer public previewer;
2930
RatePreviewer public ratePreviewer;
31+
IntegrationPreviewer public integrationPreviewer;
3032

3133
MockBalancerVault public balancer;
3234

@@ -78,6 +80,7 @@ contract DeployProtocol is Script {
7880

7981
previewer = new Previewer(auditor, IPriceFeed(address(0)));
8082
ratePreviewer = new RatePreviewer(auditor);
83+
integrationPreviewer = new IntegrationPreviewer(auditor);
8184

8285
balancer = new MockBalancerVault();
8386
exa.mint(address(balancer), 1_000_000e18);

‎test/utils/anvil.ts‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,18 @@ export default async function setup({ provide }: TestProject) {
2828
--sender ${deployer} --unlocked ${deployer} --rpc-url ${foundry.rpcUrls.default.http[0]} --broadcast --slow`;
2929
}
3030

31-
// eslint-disable-next-line unicorn/no-unreadable-array-destructuring
32-
const [, , , , , , , , , , , usdc, , marketUSDC, , , , , , , , marketWETH, , , , , , previewer, ratePreviewer] =
33-
parse(Protocol, await import(`broadcast/Protocol.s.sol/${String(foundry.id)}/run-latest.json`)).transactions;
31+
const protocol = parse(
32+
Protocol,
33+
await import(`broadcast/Protocol.s.sol/${String(foundry.id)}/run-latest.json`),
34+
).transactions;
35+
const usdc = protocol[11];
36+
const marketUSDC = protocol[13];
37+
const marketWETH = protocol[21];
38+
const previewer = protocol[27];
39+
const ratePreviewer = protocol[28];
40+
const integrationPreviewer = protocol[29];
3441

42+
provide("IntegrationPreviewer", integrationPreviewer.contractAddress);
3543
provide("MarketUSDC", marketUSDC.contractAddress);
3644
provide("MarketWETH", marketWETH.contractAddress);
3745
provide("Previewer", previewer.contractAddress);
@@ -84,6 +92,11 @@ const Protocol = object({
8492
object({ transactionType: literal("CALL") }),
8593
object({ transactionType: literal("CREATE"), contractName: literal("Previewer"), contractAddress: Address }),
8694
object({ transactionType: literal("CREATE"), contractName: literal("RatePreviewer"), contractAddress: Address }),
95+
object({
96+
transactionType: literal("CREATE"),
97+
contractName: literal("IntegrationPreviewer"),
98+
contractAddress: Address,
99+
}),
87100
object({
88101
transactionType: literal("CREATE"),
89102
contractName: literal("MockBalancerVault"),
@@ -94,6 +107,7 @@ const Protocol = object({
94107

95108
declare module "vitest" {
96109
export interface ProvidedContext {
110+
IntegrationPreviewer: Address;
97111
MarketUSDC: Address;
98112
MarketWETH: Address;
99113
Previewer: Address;

‎wagmi.config.mts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { Abi } from "viem";
55
export default defineConfig({
66
out: "test/generated/contracts.ts",
77
contracts: [
8+
{ name: "IntegrationPreviewer", abi: loadDeployment("IntegrationPreviewer").abi },
89
{ name: "Previewer", abi: loadDeployment("Previewer").abi },
910
{ name: "RatePreviewer", abi: loadDeployment("RatePreviewer").abi },
1011
{ name: "MarketUSDC", abi: loadDeployment("MarketUSDC").abi },

0 commit comments

Comments
 (0)