diff --git a/packages/liquidation-sdk-viem/examples/whitelisted-erc4626-1inch.ts b/packages/liquidation-sdk-viem/examples/whitelisted-erc4626-1inch.ts index 5a4e44d1..18f9e8af 100644 --- a/packages/liquidation-sdk-viem/examples/whitelisted-erc4626-1inch.ts +++ b/packages/liquidation-sdk-viem/examples/whitelisted-erc4626-1inch.ts @@ -135,8 +135,11 @@ 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 we can realize more than $1000 bad debt. .map(async (seizedAssets) => { const repaidShares = market.getLiquidationRepaidShares(seizedAssets)!; 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 ebeb19e0..c44f38d9 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 { @@ -25,7 +30,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 +65,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 +731,189 @@ 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, + ], + }); + + await client.setNextBlockTimestamp({ + timestamp: (await client.timestamp()) + Time.s.from.y(1n), + }); + + 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: ethPriceUsd, + spotPriceEth: 1 / ethPriceUsd, + }, + }, + }, + ], + }, + }, + }); + + await client.deal({ + erc20: loanToken.address, + account: liquidator.address, + amount: maxUint256, + }); + 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, + collateral - parseEther("0.01"), + 0n, + "0x", + ], + }); + + await syncTimestamp(client, await client.timestamp()); + + 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: "14035289781000635", + }, + ]); + mockParaSwap(encoder, [ + { srcAmount: parseEther("0.01"), dstAmount: "14035289781000635" }, + ]); + + await check(encoder.address, client, client.account, [marketId]); + + const finalAccrualPosition = await fetchAccrualPosition( + borrower.address as Address, + marketId, + client, + ); + + expect(finalAccrualPosition.borrowShares).toEqual(0n); + expect(finalAccrualPosition.collateral).toEqual(0n); + }, + ); + // Cannot run concurrently because `fetch` is mocked globally. test.sequential( `should liquidate on a PT standard market before maturity`,