From 6cefa843f14136060ee4b11ebd0c8551757391a2 Mon Sep 17 00:00:00 2001 From: Jean-Grimal <83286814+Jean-Grimal@users.noreply.github.com> Date: Wed, 20 Nov 2024 16:09:32 +0100 Subject: [PATCH 1/9] feat: handle bad debt --- .../examples/whitelisted-erc4626-1inch.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/liquidation-sdk-viem/examples/whitelisted-erc4626-1inch.ts b/packages/liquidation-sdk-viem/examples/whitelisted-erc4626-1inch.ts index 5a4e44d..e7247de 100644 --- a/packages/liquidation-sdk-viem/examples/whitelisted-erc4626-1inch.ts +++ b/packages/liquidation-sdk-viem/examples/whitelisted-erc4626-1inch.ts @@ -135,9 +135,16 @@ export const check = async < .map((_v, i) => seizableCollateral / 2n ** BigInt(i)) .filter( (seizedAssets) => - collateralToken.toUsd(seizedAssets)! > parseEther("1000"), // Do not try seizing less than $1000 collateral. - ) + collateralToken.toUsd(seizedAssets)! > parseEther("1000") || + (accrualPosition.collateral === seizedAssets && + loanToken.toUsd(accrualPosition.borrowAssets)! > + parseEther("1000")), + ) // Do not try seizing less than $1000 collateral, except if there is significant bad debt. .map(async (seizedAssets) => { + seizedAssets = + accrualPosition.collateral === seizedAssets + ? accrualPosition.collateral + : seizedAssets; const repaidShares = market.getLiquidationRepaidShares(seizedAssets)!; From 8a2110fc4f565c53314adb425a7e92e9b57a2f69 Mon Sep 17 00:00:00 2001 From: Jean-Grimal <83286814+Jean-Grimal@users.noreply.github.com> Date: Wed, 20 Nov 2024 16:19:14 +0100 Subject: [PATCH 2/9] fix: collateral amount --- .../examples/whitelisted-erc4626-1inch.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/liquidation-sdk-viem/examples/whitelisted-erc4626-1inch.ts b/packages/liquidation-sdk-viem/examples/whitelisted-erc4626-1inch.ts index e7247de..9908523 100644 --- a/packages/liquidation-sdk-viem/examples/whitelisted-erc4626-1inch.ts +++ b/packages/liquidation-sdk-viem/examples/whitelisted-erc4626-1inch.ts @@ -136,13 +136,13 @@ export const check = async < .filter( (seizedAssets) => collateralToken.toUsd(seizedAssets)! > parseEther("1000") || - (accrualPosition.collateral === seizedAssets && + (accrualPosition.collateral === seizableCollateral && loanToken.toUsd(accrualPosition.borrowAssets)! > parseEther("1000")), ) // Do not try seizing less than $1000 collateral, except if there is significant bad debt. .map(async (seizedAssets) => { seizedAssets = - accrualPosition.collateral === seizedAssets + accrualPosition.collateral === seizableCollateral ? accrualPosition.collateral : seizedAssets; const repaidShares = From 444fc3c2678a85f4f5c1e3950f43ff15ebdf50db Mon Sep 17 00:00:00 2001 From: Jean-Grimal <83286814+Jean-Grimal@users.noreply.github.com> Date: Wed, 20 Nov 2024 17:23:08 +0100 Subject: [PATCH 3/9] fix: bad debt check --- .../examples/whitelisted-erc4626-1inch.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/liquidation-sdk-viem/examples/whitelisted-erc4626-1inch.ts b/packages/liquidation-sdk-viem/examples/whitelisted-erc4626-1inch.ts index 9908523..0ed76d7 100644 --- a/packages/liquidation-sdk-viem/examples/whitelisted-erc4626-1inch.ts +++ b/packages/liquidation-sdk-viem/examples/whitelisted-erc4626-1inch.ts @@ -136,15 +136,11 @@ export const check = async < .filter( (seizedAssets) => collateralToken.toUsd(seizedAssets)! > parseEther("1000") || - (accrualPosition.collateral === seizableCollateral && + (accrualPosition.collateral === seizedAssets && loanToken.toUsd(accrualPosition.borrowAssets)! > parseEther("1000")), ) // Do not try seizing less than $1000 collateral, except if there is significant bad debt. .map(async (seizedAssets) => { - seizedAssets = - accrualPosition.collateral === seizableCollateral - ? accrualPosition.collateral - : seizedAssets; const repaidShares = market.getLiquidationRepaidShares(seizedAssets)!; From 1373734c61a718b531f43f6a884d3b97cb12ebde Mon Sep 17 00:00:00 2001 From: Jean-Grimal <83286814+Jean-Grimal@users.noreply.github.com> Date: Wed, 20 Nov 2024 17:23:31 +0100 Subject: [PATCH 4/9] Update packages/liquidation-sdk-viem/examples/whitelisted-erc4626-1inch.ts Co-authored-by: Romain Milon Signed-off-by: Jean-Grimal <83286814+Jean-Grimal@users.noreply.github.com> --- .../liquidation-sdk-viem/examples/whitelisted-erc4626-1inch.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/liquidation-sdk-viem/examples/whitelisted-erc4626-1inch.ts b/packages/liquidation-sdk-viem/examples/whitelisted-erc4626-1inch.ts index 0ed76d7..18f9e8a 100644 --- a/packages/liquidation-sdk-viem/examples/whitelisted-erc4626-1inch.ts +++ b/packages/liquidation-sdk-viem/examples/whitelisted-erc4626-1inch.ts @@ -139,7 +139,7 @@ export const check = async < (accrualPosition.collateral === seizedAssets && loanToken.toUsd(accrualPosition.borrowAssets)! > parseEther("1000")), - ) // Do not try seizing less than $1000 collateral, except if there is significant bad debt. + ) // Do not try seizing less than $1000 collateral, except if we can realize more than $1000 bad debt. .map(async (seizedAssets) => { const repaidShares = market.getLiquidationRepaidShares(seizedAssets)!; From 9dddb4bd86297835419376663d7e727e62f6d677 Mon Sep 17 00:00:00 2001 From: Jean-Grimal <83286814+Jean-Grimal@users.noreply.github.com> Date: Thu, 21 Nov 2024 11:37:37 +0100 Subject: [PATCH 5/9] test: unprofitable bad debt realisation --- .../whitelisted-erc4626-1inch.test.ts | 189 +++++++++++++++++- 1 file changed, 188 insertions(+), 1 deletion(-) diff --git a/packages/liquidation-sdk-viem/test/examples/whitelisted-erc4626-1inch.test.ts b/packages/liquidation-sdk-viem/test/examples/whitelisted-erc4626-1inch.test.ts index ebeb19e..dae0615 100644 --- a/packages/liquidation-sdk-viem/test/examples/whitelisted-erc4626-1inch.test.ts +++ b/packages/liquidation-sdk-viem/test/examples/whitelisted-erc4626-1inch.test.ts @@ -25,7 +25,13 @@ import { mainnetAddresses, } from "@morpho-org/liquidation-sdk-viem"; import { type AnvilTestClient, testAccount } from "@morpho-org/test"; -import { encodeFunctionData, erc20Abi, maxUint256, parseUnits } from "viem"; +import { + encodeFunctionData, + erc20Abi, + maxUint256, + parseEther, + parseUnits, +} from "viem"; import type { mainnet } from "viem/chains"; import { afterEach, beforeEach, describe, expect, vi } from "vitest"; import { check } from "../../examples/whitelisted-erc4626-1inch.js"; @@ -54,6 +60,7 @@ const pendleRedeemApiMatcher = new RegExp(`${Pendle.getRedeemApiUrl(1)}.*`); const { morpho } = addresses[ChainId.EthMainnet]; const borrower = testAccount(1); +const liquidator = testAccount(2); describe("erc4626-1inch", () => { let swapMockAddress: Address; @@ -719,6 +726,186 @@ describe("erc4626-1inch", () => { }, ); + // Cannot run concurrently because `fetch` is mocked globally. + test.sequential( + `should liquidate on standard market with bad debt but low collateral amount`, + async ({ client, encoder }) => { + const collateralPriceUsd = 3_129; + const ethPriceUsd = 2_653; + + const marketId = + "0xb8fc70e82bc5bb53e773626fcc6a23f7eefa036918d7ef216ecfb1950a94a85e" as MarketId; // wstETH/WETH (96.5%) + + const market = await fetchMarket(marketId, client); + const [collateralToken, loanToken] = await Promise.all([ + fetchToken(market.params.collateralToken, client), + fetchToken(market.params.loanToken, client), + ]); + + const collateral = parseUnits("10000", collateralToken.decimals); + await client.deal({ + erc20: collateralToken.address, + account: borrower.address, + amount: collateral, + }); + await client.approve({ + account: borrower, + address: collateralToken.address, + args: [morpho, maxUint256], + }); + await client.writeContract({ + account: borrower, + address: morpho, + abi: blueAbi, + functionName: "supplyCollateral", + args: [market.params, collateral, borrower.address, "0x"], + }); + + const borrowed = market.getMaxBorrowAssets(collateral)! - 1n; + await client.deal({ + erc20: loanToken.address, + account: borrower.address, + amount: borrowed - market.liquidity, + }); + await client.approve({ + account: borrower, + address: loanToken.address, + args: [morpho, maxUint256], + }); + await client.writeContract({ + account: borrower, + address: morpho, + abi: blueAbi, + functionName: "supply", + args: [ + market.params, + borrowed - market.liquidity, // 100% utilization after borrow. + 0n, + borrower.address, + "0x", + ], + }); + + await client.writeContract({ + account: borrower, + address: morpho, + abi: blueAbi, + functionName: "borrow", + args: [ + market.params as Pick< + typeof market.params, + "collateralToken" | "loanToken" | "oracle" | "irm" | "lltv" + >, + borrowed, + 0n, + borrower.address, + borrower.address, + ], + }); + + const timestamp = await syncTimestamp(client); + + nock(BLUE_API_BASE_URL) + .post("/graphql") + .reply(200, { data: { markets: { items: [{ uniqueKey: marketId }] } } }) + .post("/graphql") + .reply(200, { + data: { + assetByAddress: { + priceUsd: ethPriceUsd, + spotPriceEth: 1, + }, + marketPositions: { + items: [ + { + user: { + address: borrower.address, + }, + market: { + uniqueKey: marketId, + collateralAsset: { + address: market.params.collateralToken, + decimals: collateralToken.decimals, + priceUsd: collateralPriceUsd, + }, + loanAsset: { + address: market.params.loanToken, + decimals: loanToken.decimals, + priceUsd: null, + spotPriceEth: 1 / ethPriceUsd, + }, + }, + }, + ], + }, + }, + }); + + const accrualPosition = await fetchAccrualPosition( + borrower.address as Address, + marketId, + client, + ); + const accruedPosition = accrualPosition.accrueInterest(timestamp); + + const loanAmount = accruedPosition.borrowAssets; + await client.deal({ + erc20: loanToken.address, + account: liquidator.address, + amount: loanAmount, + }); + await client.approve({ + account: liquidator, + address: loanToken.address, + args: [morpho, maxUint256], + }); + await client.writeContract({ + account: liquidator, + address: morpho, + abi: blueAbi, + functionName: "liquidate", + args: [ + market.params, + borrower.address, + accruedPosition.seizableCollateral! - parseEther("0.01"), + 0n, + "0x", + ], + }); + + const newAccrualPosition = await fetchAccrualPosition( + borrower.address as Address, + marketId, + client, + ); + const newAccruedPosition = newAccrualPosition.accrueInterest(timestamp); + + const seizedCollateral = newAccruedPosition.seizableCollateral!; + + mockOneInch(encoder, [ + { + srcAmount: seizedCollateral, + dstAmount: "10000000000000000", + }, + ]); + mockParaSwap(encoder, [ + { srcAmount: seizedCollateral, dstAmount: "10000000000000000" }, + ]); + + await check(encoder.address, client, client.account, [marketId]); + + const finalAccrualPosition = await fetchAccrualPosition( + borrower.address as Address, + marketId, + client, + ); + const finalAccruedPosition = + finalAccrualPosition.accrueInterest(timestamp); + + expect(finalAccruedPosition.borrowShares).toEqual(0n); + }, + ); + // Cannot run concurrently because `fetch` is mocked globally. test.sequential( `should liquidate on a PT standard market before maturity`, From a3630f1a85ad6f65ff90ba6b3ee5245034445315 Mon Sep 17 00:00:00 2001 From: Jean-Grimal <83286814+Jean-Grimal@users.noreply.github.com> Date: Thu, 21 Nov 2024 12:23:29 +0100 Subject: [PATCH 6/9] fix: stop accruing fees --- .../test/examples/whitelisted-erc4626-1inch.test.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/liquidation-sdk-viem/test/examples/whitelisted-erc4626-1inch.test.ts b/packages/liquidation-sdk-viem/test/examples/whitelisted-erc4626-1inch.test.ts index dae0615..19c1e0e 100644 --- a/packages/liquidation-sdk-viem/test/examples/whitelisted-erc4626-1inch.test.ts +++ b/packages/liquidation-sdk-viem/test/examples/whitelisted-erc4626-1inch.test.ts @@ -878,9 +878,8 @@ describe("erc4626-1inch", () => { marketId, client, ); - const newAccruedPosition = newAccrualPosition.accrueInterest(timestamp); - const seizedCollateral = newAccruedPosition.seizableCollateral!; + const seizedCollateral = newAccrualPosition.seizableCollateral!; mockOneInch(encoder, [ { @@ -899,10 +898,8 @@ describe("erc4626-1inch", () => { marketId, client, ); - const finalAccruedPosition = - finalAccrualPosition.accrueInterest(timestamp); - expect(finalAccruedPosition.borrowShares).toEqual(0n); + expect(finalAccrualPosition.borrowShares).toEqual(0n); }, ); From 0dfc46c8a9083217daaacf19bf97a01a5e97e05f Mon Sep 17 00:00:00 2001 From: Jean-Grimal <83286814+Jean-Grimal@users.noreply.github.com> Date: Mon, 25 Nov 2024 16:24:04 +0100 Subject: [PATCH 7/9] test: remove useless acruals --- .../whitelisted-erc4626-1inch.test.ts | 26 +++---------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/packages/liquidation-sdk-viem/test/examples/whitelisted-erc4626-1inch.test.ts b/packages/liquidation-sdk-viem/test/examples/whitelisted-erc4626-1inch.test.ts index 19c1e0e..e48e40b 100644 --- a/packages/liquidation-sdk-viem/test/examples/whitelisted-erc4626-1inch.test.ts +++ b/packages/liquidation-sdk-viem/test/examples/whitelisted-erc4626-1inch.test.ts @@ -803,8 +803,6 @@ describe("erc4626-1inch", () => { ], }); - const timestamp = await syncTimestamp(client); - nock(BLUE_API_BASE_URL) .post("/graphql") .reply(200, { data: { markets: { items: [{ uniqueKey: marketId }] } } }) @@ -841,18 +839,10 @@ describe("erc4626-1inch", () => { }, }); - const accrualPosition = await fetchAccrualPosition( - borrower.address as Address, - marketId, - client, - ); - const accruedPosition = accrualPosition.accrueInterest(timestamp); - - const loanAmount = accruedPosition.borrowAssets; await client.deal({ erc20: loanToken.address, account: liquidator.address, - amount: loanAmount, + amount: maxUint256, }); await client.approve({ account: liquidator, @@ -867,28 +857,20 @@ describe("erc4626-1inch", () => { args: [ market.params, borrower.address, - accruedPosition.seizableCollateral! - parseEther("0.01"), + collateral - parseEther("0.01"), 0n, "0x", ], }); - const newAccrualPosition = await fetchAccrualPosition( - borrower.address as Address, - marketId, - client, - ); - - const seizedCollateral = newAccrualPosition.seizableCollateral!; - mockOneInch(encoder, [ { - srcAmount: seizedCollateral, + srcAmount: parseEther("0.01"), dstAmount: "10000000000000000", }, ]); mockParaSwap(encoder, [ - { srcAmount: seizedCollateral, dstAmount: "10000000000000000" }, + { srcAmount: parseEther("0.01"), dstAmount: "10000000000000000" }, ]); await check(encoder.address, client, client.account, [marketId]); From cafcc061200dce6f66fe19d8bad40ac7ddc9222f Mon Sep 17 00:00:00 2001 From: Jean-Grimal <83286814+Jean-Grimal@users.noreply.github.com> Date: Tue, 26 Nov 2024 14:09:57 +0100 Subject: [PATCH 8/9] fix: tests --- .../whitelisted-erc4626-1inch.test.ts | 34 ++++++++++++++++--- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/packages/liquidation-sdk-viem/test/examples/whitelisted-erc4626-1inch.test.ts b/packages/liquidation-sdk-viem/test/examples/whitelisted-erc4626-1inch.test.ts index e48e40b..6f81afd 100644 --- a/packages/liquidation-sdk-viem/test/examples/whitelisted-erc4626-1inch.test.ts +++ b/packages/liquidation-sdk-viem/test/examples/whitelisted-erc4626-1inch.test.ts @@ -88,11 +88,15 @@ describe("erc4626-1inch", () => { fetchMock.restore(); }); - const syncTimestamp = async (client: AnvilTestClient, timestamp?: bigint) => { + const syncTimestamp = async ( + client: AnvilTestClient, + timestamp?: bigint, + increment?: bigint, + ) => { timestamp ??= (await client.timestamp()) + 60n; vi.useFakeTimers({ - now: Number(timestamp) * 1000, + now: Number(timestamp + (increment ?? 0n)) * 1000, toFake: ["Date"], // Avoid faking setTimeout, used to delay retries. }); @@ -803,6 +807,8 @@ describe("erc4626-1inch", () => { ], }); + await syncTimestamp(client, (await client.timestamp()) + 31536000n, 1n); + nock(BLUE_API_BASE_URL) .post("/graphql") .reply(200, { data: { markets: { items: [{ uniqueKey: marketId }] } } }) @@ -829,7 +835,7 @@ describe("erc4626-1inch", () => { loanAsset: { address: market.params.loanToken, decimals: loanToken.decimals, - priceUsd: null, + priceUsd: ethPriceUsd, spotPriceEth: 1 / ethPriceUsd, }, }, @@ -863,14 +869,31 @@ describe("erc4626-1inch", () => { ], }); + const postLiquidationAccrualPosition = await fetchAccrualPosition( + borrower.address as Address, + marketId, + client, + ); + + const collateralValue = + (collateralPriceUsd * + Number(postLiquidationAccrualPosition.collateral)) / + 10 ** collateralToken.decimals; + const loanValue = + (ethPriceUsd * Number(postLiquidationAccrualPosition.borrowAssets)) / + 10 ** loanToken.decimals; + + expect(loanValue).toBeGreaterThan(1000); + expect(collateralValue).toBeLessThan(1000); + mockOneInch(encoder, [ { srcAmount: parseEther("0.01"), - dstAmount: "10000000000000000", + dstAmount: "14035289781000635", }, ]); mockParaSwap(encoder, [ - { srcAmount: parseEther("0.01"), dstAmount: "10000000000000000" }, + { srcAmount: parseEther("0.01"), dstAmount: "14035289781000635" }, ]); await check(encoder.address, client, client.account, [marketId]); @@ -882,6 +905,7 @@ describe("erc4626-1inch", () => { ); expect(finalAccrualPosition.borrowShares).toEqual(0n); + expect(finalAccrualPosition.collateral).toEqual(0n); }, ); From f6473287d61222645885ca95c2d98a2eeddcc700 Mon Sep 17 00:00:00 2001 From: Jean-Grimal <83286814+Jean-Grimal@users.noreply.github.com> Date: Wed, 27 Nov 2024 18:34:00 +0100 Subject: [PATCH 9/9] fix: revert sync timestamp changes --- .../whitelisted-erc4626-1inch.test.ts | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/liquidation-sdk-viem/test/examples/whitelisted-erc4626-1inch.test.ts b/packages/liquidation-sdk-viem/test/examples/whitelisted-erc4626-1inch.test.ts index 6f81afd..c44f38d 100644 --- a/packages/liquidation-sdk-viem/test/examples/whitelisted-erc4626-1inch.test.ts +++ b/packages/liquidation-sdk-viem/test/examples/whitelisted-erc4626-1inch.test.ts @@ -8,7 +8,12 @@ import { type MarketId, addresses, } from "@morpho-org/blue-sdk"; -import { BLUE_API_BASE_URL, ZERO_ADDRESS, format } from "@morpho-org/morpho-ts"; +import { + BLUE_API_BASE_URL, + Time, + ZERO_ADDRESS, + format, +} from "@morpho-org/morpho-ts"; import type { BuildTxInput } from "@paraswap/sdk"; import { @@ -88,15 +93,11 @@ describe("erc4626-1inch", () => { fetchMock.restore(); }); - const syncTimestamp = async ( - client: AnvilTestClient, - timestamp?: bigint, - increment?: bigint, - ) => { + const syncTimestamp = async (client: AnvilTestClient, timestamp?: bigint) => { timestamp ??= (await client.timestamp()) + 60n; vi.useFakeTimers({ - now: Number(timestamp + (increment ?? 0n)) * 1000, + now: Number(timestamp) * 1000, toFake: ["Date"], // Avoid faking setTimeout, used to delay retries. }); @@ -807,7 +808,9 @@ describe("erc4626-1inch", () => { ], }); - await syncTimestamp(client, (await client.timestamp()) + 31536000n, 1n); + await client.setNextBlockTimestamp({ + timestamp: (await client.timestamp()) + Time.s.from.y(1n), + }); nock(BLUE_API_BASE_URL) .post("/graphql") @@ -869,6 +872,8 @@ describe("erc4626-1inch", () => { ], }); + await syncTimestamp(client, await client.timestamp()); + const postLiquidationAccrualPosition = await fetchAccrualPosition( borrower.address as Address, marketId,