Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/integ 274 handle bad debt events #175

Merged
merged 10 commits into from
Dec 2, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -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)!;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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`,
Expand Down