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 @@ -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";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -81,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.
});

Expand Down Expand Up @@ -719,6 +730,185 @@ 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 syncTimestamp(client, (await client.timestamp()) + 31536000n, 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",
],
});

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