From 3fc4662c42e3d39c7cda53bb0c8fa1a30f74c9e0 Mon Sep 17 00:00:00 2001 From: Jean-Grimal <83286814+Jean-Grimal@users.noreply.github.com> Date: Tue, 4 Feb 2025 14:26:20 +0100 Subject: [PATCH] fix: tests --- .../examples/whitelisted-erc4626-1inch.ts | 7 + .../src/LiquidationEncoder.ts | 27 ++- .../liquidation-sdk-viem/src/tokens/midas.ts | 20 +- .../test/examples/midas.test.ts | 228 ++++++++++++++++++ .../liquidation-sdk-viem/test/midasSetup.ts | 28 +++ 5 files changed, 298 insertions(+), 12 deletions(-) create mode 100644 packages/liquidation-sdk-viem/test/examples/midas.test.ts create mode 100644 packages/liquidation-sdk-viem/test/midasSetup.ts diff --git a/packages/liquidation-sdk-viem/examples/whitelisted-erc4626-1inch.ts b/packages/liquidation-sdk-viem/examples/whitelisted-erc4626-1inch.ts index 3d319364..936f03dc 100644 --- a/packages/liquidation-sdk-viem/examples/whitelisted-erc4626-1inch.ts +++ b/packages/liquidation-sdk-viem/examples/whitelisted-erc4626-1inch.ts @@ -217,6 +217,13 @@ export const check = async < srcToken = mainnetAddresses.usds!; } + // Handle Midas Tokens + + ({ srcAmount, srcToken } = await encoder.handleMidasTokens( + market.params.collateralToken, + seizedAssets, + )); + switch (true) { // In case of Usual tokens, there aren't much liquidity outside of curve, so we use it instead of 1inch/paraswap // Process USD0/USD0++ collateral liquidation with specific process (using curve) diff --git a/packages/liquidation-sdk-viem/src/LiquidationEncoder.ts b/packages/liquidation-sdk-viem/src/LiquidationEncoder.ts index 55ae30c7..49066546 100644 --- a/packages/liquidation-sdk-viem/src/LiquidationEncoder.ts +++ b/packages/liquidation-sdk-viem/src/LiquidationEncoder.ts @@ -476,6 +476,10 @@ export class LiquidationEncoder< const tries: SwapAttempt[] = []; let dstAmount = 0n; + if (initialSrcToken === marketParams.loanToken) { + return { dstAmount: srcAmount }; + } + while (true) { const bestSwap = await fetchBestSwap({ chainId, @@ -629,26 +633,39 @@ export class LiquidationEncoder< }; } - const previewRedeemInstant = Midas.previewRedeemInstant(redemptionParams); + const previewRedeemInstantData = + Midas.previewRedeemInstant(redemptionParams); - if (!previewRedeemInstant) { + if (!previewRedeemInstantData) { return { srcAmount: seizedAssets, srcToken: collateralToken, }; } + const { amountTokenOutWithoutFee, feeAmount } = previewRedeemInstantData; + + if (feeAmount > 0n) { + this.erc20Approve(collateralToken, redemptionVault, feeAmount); + } + this.pushCall( redemptionVault, 0n, encodeFunctionData({ abi: RedemptionVaultAbi, functionName: "redeemInstant", - args: [tokenOut, seizedAssets, previewRedeemInstant], + args: [tokenOut, seizedAssets, amountTokenOutWithoutFee], }), ); - return { srcAmount: previewRedeemInstant, srcToken: tokenOut }; + return { + srcAmount: Midas.convertFromBase18( + amountTokenOutWithoutFee, + redemptionParams.tokenOutDecimals, + ), + srcToken: tokenOut, + }; } async getRedemptionParams( @@ -759,7 +776,7 @@ export class LiquidationEncoder< } async getRedemptionVaultDailyLimits(vault: Address) { - const currentDayNumber = Math.round(Date.now()) / 1000 / (60 * 60 * 24); + const currentDayNumber = Math.round(Date.now() / 1000 / (60 * 60 * 24)); return readContract(this.client, { address: vault, abi: RedemptionVaultAbi, diff --git a/packages/liquidation-sdk-viem/src/tokens/midas.ts b/packages/liquidation-sdk-viem/src/tokens/midas.ts index 164cd553..cd187c19 100644 --- a/packages/liquidation-sdk-viem/src/tokens/midas.ts +++ b/packages/liquidation-sdk-viem/src/tokens/midas.ts @@ -63,15 +63,18 @@ export namespace Midas { if (!tokenData) return undefined; - return _truncate( - (feeData.amountMTokenWithoutFee * usdData.mTokenRate) / - tokenData.tokenRate, - params.tokenOutDecimals, - ); + return { + amountTokenOutWithoutFee: _truncate( + (feeData.amountMTokenWithoutFee * usdData.mTokenRate) / + tokenData.tokenRate, + params.tokenOutDecimals, + ), + feeAmount: feeData.feeAmount, + }; } function _calcAndValidateRedeem(params: PreviewRedeemInstantParams) { - if (params.minAmount < params.amountMTokenIn) return undefined; + if (params.minAmount > params.amountMTokenIn) return undefined; const feeAmount = _getFeeAmount(params); @@ -132,7 +135,10 @@ export namespace Midas { return convertToBase18(convertFromBase18(value, decimals), decimals); } - function convertFromBase18(originalAmount: bigint, decidedDecimals: bigint) { + export function convertFromBase18( + originalAmount: bigint, + decidedDecimals: bigint, + ) { return convert(originalAmount, 18n, decidedDecimals); } diff --git a/packages/liquidation-sdk-viem/test/examples/midas.test.ts b/packages/liquidation-sdk-viem/test/examples/midas.test.ts new file mode 100644 index 00000000..8fa593ca --- /dev/null +++ b/packages/liquidation-sdk-viem/test/examples/midas.test.ts @@ -0,0 +1,228 @@ +import nock from "nock"; +import "evm-maths"; +import fetchMock from "fetch-mock"; + +import { + ChainId, + type InputMarketParams, + type MarketId, + addresses, +} from "@morpho-org/blue-sdk"; +import { BLUE_API_BASE_URL, Time, format } from "@morpho-org/morpho-ts"; + +import { blueAbi, fetchMarket, fetchToken } from "@morpho-org/blue-sdk-viem"; +import { Flashbots } from "@morpho-org/liquidation-sdk-viem"; +import { type AnvilTestClient, testAccount } from "@morpho-org/test"; +import dotenv from "dotenv"; +import { erc20Abi, maxUint256, 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"; +import { type LiquidationTestContext, test } from "../midasSetup.js"; + +dotenv.config(); + +fetchMock.config.fallbackToNetwork = true; +fetchMock.config.overwriteRoutes = false; +fetchMock.config.warnOnFallback = false; + +const healthyDiffSlot = + "0x0000000000000000000000000000000000000000000000000000000000000034"; + +const { morpho } = addresses[ChainId.EthMainnet]; + +const borrower = testAccount(1); + +describe("midas liquidation", () => { + beforeEach>(async ({ client }) => { + vi.spyOn(Flashbots, "sendRawBundle").mockImplementation(async (txs) => { + for (const serializedTransaction of txs) { + await client.sendRawTransaction({ serializedTransaction }); + } + }); + }); + + afterEach(async () => { + vi.useRealTimers(); + vi.restoreAllMocks(); + fetchMock.restore(); + }); + + const syncTimestamp = async (client: AnvilTestClient, timestamp?: bigint) => { + timestamp ??= (await client.timestamp()) + 60n; + + vi.useFakeTimers({ + now: Number(timestamp) * 1000, + toFake: ["Date"], // Avoid faking setTimeout, used to delay retries. + }); + + await client.setNextBlockTimestamp({ timestamp }); + + return timestamp; + }; + + // Cannot run concurrently because `fetch` is mocked globally. + test.sequential( + `should liquidate on the mTBILL/USDC market`, + async ({ client, encoder }) => { + const collateralPriceUsd = 1.015852; + const ethPriceUsd = 2_600; + + const marketId = + "0xb98ad8501bd97ce0684b30b3645e31713e658e98d1955e8b677fb2585eaa9893" as MarketId; // mTBILL/USDC (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 mTokenDataFeed = "0xfCEE9754E8C375e145303b7cE7BEca3201734A2B"; + const tokenOutDataFeed = "0x3aAc6fd73fA4e16Ec683BD4aaF5Ec89bb2C0EdC2"; + + // overwrite data feeds healthy diff slot to make the price feed healthy in the future + + await client.setStorageAt({ + address: mTokenDataFeed, + index: healthyDiffSlot, + value: maxUint256.toString(16) as `0x${string}`, + }); + + await client.setStorageAt({ + address: tokenOutDataFeed, + index: healthyDiffSlot, + value: maxUint256.toString(16) as `0x${string}`, + }); + + 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)! - 10n; + + await client.deal({ + erc20: loanToken.address, + account: borrower.address, + amount: borrowed, + }); + 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 as InputMarketParams, + borrowed, + 0n, + borrower.address, + "0x", + ], + }); + + await client.writeContract({ + account: borrower, + address: morpho, + abi: blueAbi, + functionName: "borrow", + args: [ + market.params as InputMarketParams, + borrowed, + 0n, + borrower.address, + borrower.address, + ], + }); + + await syncTimestamp( + client, + (await client.timestamp()) + Time.s.from.w(5n), + ); + + 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, + spotPriceEth: collateralPriceUsd / ethPriceUsd, + }, + loanAsset: { + address: market.params.loanToken, + decimals: loanToken.decimals, + priceUsd: 1, + spotPriceEth: 1 / ethPriceUsd, + }, + }, + }, + ], + }, + }, + }); + + await client.writeContract({ + account: borrower, + address: morpho, + abi: blueAbi, + functionName: "accrueInterest", + args: [market.params as InputMarketParams], + }); + + client.transport.tracer.next = true; + + await check(encoder.address, client, client.account, [marketId]); + + const decimals = Number(loanToken.decimals); + + const decimalBalance = await client.readContract({ + address: market.params.loanToken, + abi: erc20Abi, + functionName: "balanceOf", + args: [encoder.address], + }); + + expect( + Number(format.number.of(decimalBalance, decimals)), + ).toBeGreaterThan(12); + }, + ); +}); diff --git a/packages/liquidation-sdk-viem/test/midasSetup.ts b/packages/liquidation-sdk-viem/test/midasSetup.ts new file mode 100644 index 00000000..9825e209 --- /dev/null +++ b/packages/liquidation-sdk-viem/test/midasSetup.ts @@ -0,0 +1,28 @@ +import type { AnvilTestClient } from "@morpho-org/test"; +import { type ViemTestContext, createViemTest } from "@morpho-org/test/vitest"; +import { bytecode, executorAbi } from "executooor-viem"; +import { type Chain, mainnet } from "viem/chains"; +import { LiquidationEncoder } from "../src/index.js"; + +export interface LiquidationEncoderTestContext { + encoder: LiquidationEncoder>; +} + +export interface LiquidationTestContext + extends ViemTestContext, + LiquidationEncoderTestContext {} + +export const test = createViemTest(mainnet, { + forkUrl: process.env.MAINNET_RPC_URL, + forkBlockNumber: 21_587_766, +}).extend>({ + encoder: async ({ client }, use) => { + const receipt = await client.deployContractWait({ + abi: executorAbi, + bytecode, + args: [client.account.address], + }); + + await use(new LiquidationEncoder(receipt.contractAddress, client)); + }, +});