diff --git a/README.md b/README.md index 91d9857..62c4826 100644 --- a/README.md +++ b/README.md @@ -47,17 +47,29 @@ To run a liquidation check, you just have to set the right environment variables - `PRIVATE_KEY`: the private key of the account that will be used to send the transactions. If not provided, you'll run the bot in read only mode. Your address must be an allowed liquidator of the flash liquidator contract. The two example addresses in the `.env.example` file are the ones of Morpho Labs. - `ALCHEMY_KEY`: the Alchemy key to connect to the Ethereum network. -- `LIQUIDATOR_ADDRESSES`: a comma separated list of the liquidator contract addresses to use. +- `LIQUIDATOR_ADDRESSES`: a comma separated list of the liquidator contract addresses to use. Only used if you use the `--flash` option. - `PROFITABLE_THRESHOLD`: the liquidation threshold to use (in USD). - `BATCH_SIZE`: The number of parallel queries sent to the Ethereum network. - `PROTOCOLS`: The underlying protocols to use (comma separated list). - `DELAY`: The delay between two liquidations check. If not provided, the bot will run only once. Then, you can just run: - +#### EOA Liquidation ```shell yarn run:bot ``` +Without the `--flash` option, the bot will use your wallet to liquidate without using any contract. In this case, you need to have +the funds to liquidate the user. Else, the script will throw an error. + +#### Flash Liquidation +```shell +yarn run:bot --flash +``` +In order to use flash liquidation, you have to deploy the contracts first, and then set the `LIQUIDATOR_ADDRESSES` environment variable. + +### Remarks +The script can throw an error and stop execution in some case, specially if you have a blockchain provider error, if you have not enough +funds to send any transaction, or if you have not enough funds to liquidate the user with EOA mode. ### Remotely diff --git a/package.json b/package.json index c7d437e..e2ad6cf 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "license": "MIT", "scripts": { "test": "yarn hardhat test", - "run:bot": "yarn build:bot && hardhat run scripts/runBot.ts", + "run:bot": "yarn build:bot && ts-node scripts/runBot.ts", "lint:fix": "npx prettier '**/*.{json,sol,md,ts}' --write", "deploy:contracts": "hardhat run scripts/deploy.ts --network mainnet", "compile": "hardhat compile", diff --git a/scripts/runBot.ts b/scripts/runBot.ts index 1b24733..00bfbf6 100644 --- a/scripts/runBot.ts +++ b/scripts/runBot.ts @@ -9,19 +9,30 @@ import LiquidationBot from "../src/LiquidationBot"; import ConsoleLog from "../src/loggers/ConsoleLog"; import { ILiquidator__factory } from "../typechain"; import { AVAILABLE_PROTOCOLS } from "../config"; +import { ILiquidationHandler } from "../src/LiquidationHandler/LiquidationHandler.interface"; +import LiquidatorHandler from "../src/LiquidationHandler/LiquidatorHandler"; +import { + MorphoAaveV2, + MorphoCompound, +} from "@morpho-labs/morpho-ethers-contract"; +import ReadOnlyHandler from "../src/LiquidationHandler/ReadOnlyHandler"; +import EOAHandler from "../src/LiquidationHandler/EOAHandler"; dotenv.config(); const initializers: Record< string, - ( - provider: providers.Provider - ) => Promise<{ fetcher: IFetcher; adapter: IMorphoAdapter }> + (provider: providers.Provider) => Promise<{ + fetcher: IFetcher; + adapter: IMorphoAdapter; + morpho: MorphoCompound | MorphoAaveV2; + }> > = { aave: initAave, compound: initCompound, }; const main = async (): Promise => { + const useFlashLiquidator = process.argv.includes("--flash"); const pk = process.env.PRIVATE_KEY; const provider = new providers.AlchemyProvider(1, process.env.ALCHEMY_KEY); @@ -33,12 +44,15 @@ const main = async (): Promise => { } // Check liquidator addresses + const liquidatorAddresses = process.env.LIQUIDATOR_ADDRESSES?.split(","); - if (!liquidatorAddresses) throw new Error("No liquidator addresses found"); - liquidatorAddresses.forEach((liquidatorAddress) => { - if (!isAddress(liquidatorAddress)) - throw new Error(`Invalid liquidator address ${liquidatorAddress}`); - }); + if (useFlashLiquidator) { + if (!liquidatorAddresses) throw new Error("No liquidator addresses found"); + liquidatorAddresses.forEach((liquidatorAddress) => { + if (!isAddress(liquidatorAddress)) + throw new Error(`Invalid liquidator address ${liquidatorAddress}`); + }); + } // Check protocols const protocols = process.env.PROTOCOLS?.split(","); @@ -47,22 +61,38 @@ const main = async (): Promise => { if (!AVAILABLE_PROTOCOLS.includes(protocol)) throw new Error(`Invalid protocol ${protocol}`); }); - if (protocols.length !== liquidatorAddresses.length) + if (useFlashLiquidator && protocols.length !== liquidatorAddresses!.length) throw new Error( "Number of protocols and liquidator addresses must be the same" ); + const logger = new ConsoleLog(); for (let i = 0; i < protocols.length; i++) { const protocol = protocols[i]; - const liquidatorAddress = liquidatorAddresses[i]; + const { adapter, fetcher, morpho } = await initializers[protocol](provider); + + let liquidationHandler: ILiquidationHandler; + if (useFlashLiquidator) { + liquidationHandler = new LiquidatorHandler( + ILiquidator__factory.connect(liquidatorAddresses![i], provider as any), + wallet!, + logger + ); + console.log("Using flash liquidator"); + } else if (!wallet) { + liquidationHandler = new ReadOnlyHandler(logger); + console.log("Using read only handler"); + } else { + liquidationHandler = new EOAHandler(morpho, wallet, logger); + console.log("Using EOA handler"); + } console.time(protocol); console.timeLog(protocol, `Starting bot initialization`); - const { adapter, fetcher } = await initializers[protocol](provider); const bot = new LiquidationBot( - new ConsoleLog(), + logger, fetcher, - wallet, - ILiquidator__factory.connect(liquidatorAddress, provider as any), + provider, + liquidationHandler, adapter, { profitableThresholdUSD: parseUnits( diff --git a/src/LiquidationBot.ts b/src/LiquidationBot.ts index ec18a52..0b079af 100644 --- a/src/LiquidationBot.ts +++ b/src/LiquidationBot.ts @@ -1,15 +1,18 @@ -import { BigNumber, getDefaultProvider, providers, Signer } from "ethers"; +import { BigNumber, providers } from "ethers"; import { Logger } from "./interfaces/logger"; import { IFetcher } from "./interfaces/IFetcher"; import { formatUnits, parseUnits } from "ethers/lib/utils"; -import { pow10 } from "../test/helpers"; import stablecoins from "./constant/stablecoins"; import { ethers } from "hardhat"; import config from "../config"; import underlyings from "./constant/underlyings"; import { getPoolData, UniswapPool } from "./uniswap/pools"; import { IMorphoAdapter } from "./morpho/Morpho.interface"; -import { ILiquidator } from "../typechain"; +import { + ILiquidationHandler, + LiquidationParams, +} from "./LiquidationHandler/LiquidationHandler.interface"; +import { PercentMath } from "@morpho-labs/ethers-utils/lib/maths"; export interface LiquidationBotSettings { profitableThresholdUSD: BigNumber; @@ -26,21 +29,14 @@ export default class LiquidationBot { constructor( public readonly logger: Logger, public readonly fetcher: IFetcher, - public readonly signer: Signer | undefined, - public readonly liquidator: ILiquidator, + public readonly provider: providers.Provider, + public readonly liquidationHandler: ILiquidationHandler, public readonly adapter: IMorphoAdapter, settings: Partial = {} ) { this.settings = { ...defaultSettings, ...settings }; } - get provider() { - if (this.signer?.provider) return this.signer.provider; - if (process.env.ALCHEMY_KEY) - return new providers.AlchemyProvider("1", process.env.ALCHEMY_KEY); - return getDefaultProvider(); - } - async computeLiquidableUsers() { let lastId = ""; let hasMore = true; @@ -68,6 +64,24 @@ export default class LiquidationBot { return liquidableUsers; } + async liquidate( + poolTokenBorrowed: string, + poolTokenCollateral: string, + user: string, + amount: BigNumber, + swapPath: string + ) { + const liquidationParams: LiquidationParams = { + poolTokenBorrowed, + poolTokenCollateral, + underlyingBorrowed: underlyings[poolTokenBorrowed.toLowerCase()], + user, + amount, + swapPath, + }; + return this.liquidationHandler.handleLiquidation(liquidationParams); + } + async getUserLiquidationParams(userAddress: string) { // first fetch all user balances const markets = await this.adapter.getMarkets(); @@ -181,26 +195,13 @@ export default class LiquidationBot { ); } - isProfitable(toLiquidate: BigNumber, price: BigNumber) { - return toLiquidate - .mul(price) - .div(pow10(18)) - .mul(7) - .div(100) - .gt(this.settings.profitableThresholdUSD); - } - - async liquidate(...args: any) { - if (!this.signer) return; - const tx = await this.liquidator - .connect(this.signer) - // @ts-ignore - .liquidate(...args, { gasLimit: 8_000_000 }) - .catch(this.logError.bind(this)); - if (!tx) return; - this.logger.log(tx); - const receipt = await tx.wait().catch(this.logError.bind(this)); - if (receipt) this.logger.log(`Gas used: ${receipt.gasUsed.toString()}`); + async isProfitable(market: string, toLiquidate: BigNumber, price: BigNumber) { + const rewards = await this.adapter.getLiquidationBonus(market); + const usdAmount = await this.adapter.toUsd(market, toLiquidate, price); + return PercentMath.percentMul( + usdAmount, + rewards.sub(PercentMath.BASE_PERCENT) + ).gt(this.settings.profitableThresholdUSD); } async checkPoolLiquidity(borrowMarket: string, collateralMarket: string) { @@ -256,24 +257,37 @@ export default class LiquidationBot { const liquidationsParams = await Promise.all( users.map((u) => this.getUserLiquidationParams(u.address)) ); - const toLiquidate = liquidationsParams.filter((user) => - this.isProfitable(user.toLiquidate, user.debtMarket.price) - ); + const toLiquidate = ( + await Promise.all( + liquidationsParams.map(async (user) => { + if ( + await this.isProfitable( + user.debtMarket.market, + user.toLiquidate, + user.debtMarket.price + ) + ) + return user; + return null; + }) + ) + ).filter(Boolean); if (toLiquidate.length > 0) { this.logger.log(`${toLiquidate.length} users to liquidate`); for (const userToLiquidate of toLiquidate) { const swapPath = this.getPath( - userToLiquidate.debtMarket.market, - userToLiquidate.collateralMarket.market - ); - await this.liquidate( - userToLiquidate.debtMarket.market, - userToLiquidate.collateralMarket.market, - userToLiquidate.userAddress, - userToLiquidate.toLiquidate, - true, - swapPath + userToLiquidate!.debtMarket.market, + userToLiquidate!.collateralMarket.market ); + const liquidateParams: LiquidationParams = { + poolTokenBorrowed: userToLiquidate!.debtMarket.market, + poolTokenCollateral: userToLiquidate!.collateralMarket.market, + underlyingBorrowed: underlyings[userToLiquidate!.debtMarket.market], + user: userToLiquidate!.userAddress, + amount: userToLiquidate!.toLiquidate, + swapPath, + }; + await this.liquidationHandler.handleLiquidation(liquidateParams); } } } diff --git a/src/LiquidationHandler/EOAHandler.ts b/src/LiquidationHandler/EOAHandler.ts new file mode 100644 index 0000000..b532f54 --- /dev/null +++ b/src/LiquidationHandler/EOAHandler.ts @@ -0,0 +1,109 @@ +import { + ILiquidationHandler, + LiquidationParams, +} from "./LiquidationHandler.interface"; +import { + ERC20__factory, + MorphoAaveV2, + MorphoCompound, +} from "@morpho-labs/morpho-ethers-contract"; +import { BigNumberish, constants, Overrides, Signer } from "ethers"; +import { Logger } from "../interfaces/logger"; + +export interface EOAHandlerOptions { + overrides: Overrides; + checkAllowance: boolean; + checkBalance: boolean; + approveMax: boolean; +} +export const defaultOptions: EOAHandlerOptions = { + overrides: { gasLimit: 3_000_000 }, + checkAllowance: true, + checkBalance: true, + approveMax: true, +}; + +// A list of tokens that need to approve zero before to increase the allowance +const APPROVE_ZERO_TOKENS = ["0x0000000000000000000000000000000000000000"]; + +export default class EOAHandler implements ILiquidationHandler { + options: EOAHandlerOptions; + constructor( + private readonly morpho: MorphoCompound | MorphoAaveV2, + private readonly signer: Signer, + private readonly logger: Logger, + options: Partial = {} + ) { + this.options = { ...defaultOptions, ...options }; + } + + public async handleLiquidation({ + user, + poolTokenBorrowed, + poolTokenCollateral, + amount, + underlyingBorrowed, + }: LiquidationParams): Promise { + if (this.options.checkBalance) { + await this._checkBalance(underlyingBorrowed, amount); + } + await this._checkAllowance(underlyingBorrowed, amount); + const tx = await this.morpho + .connect(this.signer) + .liquidate( + poolTokenBorrowed, + poolTokenCollateral, + user, + amount, + this.options.overrides + ) + .catch(this.logger.error.bind(this)); + if (!tx) return; + this.logger.log(tx); + const receipt = await tx.wait().catch(this.logger.error.bind(this)); + if (receipt) this.logger.log(`Gas used: ${receipt.gasUsed.toString()}`); + } + + private async _approve(token: string, amount: BigNumberish): Promise { + const erc20 = ERC20__factory.connect(token, this.signer); + const tx = await erc20 + .approve(this.morpho.address, amount, this.options.overrides) + .catch(this.logger.error.bind(this)); + if (!tx) return; + this.logger.log(tx); + const receipt = await tx.wait().catch(this.logger.error.bind(this)); + if (receipt) this.logger.log(`Gas used: ${receipt.gasUsed.toString()}`); + } + + private async _checkAllowance( + token: string, + amount: BigNumberish + ): Promise { + token = token.toLowerCase(); + const erc20 = ERC20__factory.connect(token, this.signer); + const allowance = await erc20.allowance( + await this.signer.getAddress(), + this.morpho.address + ); + if (allowance.lt(amount)) { + this.logger.log(`Allowance is not enough for ${token}`); + if (APPROVE_ZERO_TOKENS.includes(token)) { + await this._approve(token, 0); + } + await this._approve( + token, + this.options.approveMax ? constants.MaxUint256 : amount + ); + this.logger.log(`Allowance updated for ${token}`); + } + } + + private async _checkBalance( + underlyingBorrowed: string, + amount: BigNumberish + ): Promise { + const erc20 = ERC20__factory.connect(underlyingBorrowed, this.signer); + const balance = await erc20.balanceOf(await this.signer.getAddress()); + if (balance.lt(amount)) throw new Error("Insufficient balance"); + } +} diff --git a/src/LiquidationHandler/LiquidationHandler.interface.ts b/src/LiquidationHandler/LiquidationHandler.interface.ts new file mode 100644 index 0000000..58f83ac --- /dev/null +++ b/src/LiquidationHandler/LiquidationHandler.interface.ts @@ -0,0 +1,14 @@ +import { BigNumberish } from "ethers"; + +export interface LiquidationParams { + poolTokenBorrowed: string; + poolTokenCollateral: string; + underlyingBorrowed: string; + user: string; + amount: BigNumberish; + swapPath: string; +} + +export interface ILiquidationHandler { + handleLiquidation: (liquidation: LiquidationParams) => Promise; +} diff --git a/src/LiquidationHandler/LiquidatorHandler.ts b/src/LiquidationHandler/LiquidatorHandler.ts new file mode 100644 index 0000000..c380b6d --- /dev/null +++ b/src/LiquidationHandler/LiquidatorHandler.ts @@ -0,0 +1,55 @@ +import { + ILiquidationHandler, + LiquidationParams, +} from "./LiquidationHandler.interface"; +import { ILiquidator } from "../../typechain"; +import { Overrides, Signer } from "ethers"; +import { Logger } from "../interfaces/logger"; + +export interface LiquidatorHandlerOptions { + stakeTokens: boolean; + overrides: Overrides; +} +const defaultOptions: LiquidatorHandlerOptions = { + stakeTokens: true, + overrides: { gasLimit: 3_000_000 }, +}; + +export default class LiquidatorHandler implements ILiquidationHandler { + options: LiquidatorHandlerOptions; + constructor( + private liquidator: ILiquidator, + private signer: Signer, + private logger: Logger, + options: Partial = {} + ) { + this.options = { ...defaultOptions, ...options }; + } + + async handleLiquidation({ + poolTokenCollateral, + poolTokenBorrowed, + user, + amount, + swapPath, + }: LiquidationParams): Promise { + if (!this.signer) return; + const tx = await this.liquidator + .connect(this.signer) + // @ts-ignore + .liquidate( + poolTokenBorrowed, + poolTokenCollateral, + user, + amount, + this.options.stakeTokens, + swapPath, + this.options.overrides + ) + .catch(this.logger.error(this)); + if (!tx) return; + this.logger.log(tx); + const receipt = await tx.wait().catch(this.logger.error.bind(this)); + if (receipt) this.logger.log(`Gas used: ${receipt.gasUsed.toString()}`); + } +} diff --git a/src/LiquidationHandler/ReadOnlyHandler.ts b/src/LiquidationHandler/ReadOnlyHandler.ts new file mode 100644 index 0000000..23f08b4 --- /dev/null +++ b/src/LiquidationHandler/ReadOnlyHandler.ts @@ -0,0 +1,9 @@ +import { ILiquidationHandler } from "./LiquidationHandler.interface"; +import { Logger } from "../interfaces/logger"; + +export default class ReadOnlyHandler implements ILiquidationHandler { + constructor(private logger: Logger) {} + async handleLiquidation(): Promise { + this.logger.log("Read only mode, no liquidation will be performed"); + } +} diff --git a/src/handlers/botHandler.ts b/src/handlers/botHandler.ts index eff43b1..c5555d1 100644 --- a/src/handlers/botHandler.ts +++ b/src/handlers/botHandler.ts @@ -6,14 +6,16 @@ import { getPrivateKey } from "../secrets/privateKey"; import { ILiquidator__factory } from "../../typechain"; import initCompound from "../initializers/compound"; import initAave from "../initializers/aave"; +import LiquidatorHandler from "../LiquidationHandler/LiquidatorHandler"; export const handler = async () => { const privateKey = await getPrivateKey(!!process.env.FROM_ENV); + if (!privateKey) throw new Error("No private key found"); const provider = new providers.AlchemyProvider(1, process.env.ALCHEMY_KEY); const isCompound = process.env.IS_COMPOUND; - - const signer = privateKey ? new Wallet(privateKey, provider) : undefined; + const logger = new ConsoleLog(); + const signer = new Wallet(privateKey, provider); const flashLiquidator = ILiquidator__factory.connect( process.env.LIQUIDATOR_ADDRESS!, provider as any @@ -21,11 +23,16 @@ export const handler = async () => { const { adapter, fetcher } = await (isCompound ? initCompound(provider) : initAave(provider)); + const liquidationHandler = new LiquidatorHandler( + flashLiquidator, + signer, + logger + ); const bot = new LiquidationBot( - new ConsoleLog(), + logger, fetcher, - signer, - flashLiquidator, + signer.provider, + liquidationHandler, adapter, { profitableThresholdUSD: parseUnits( diff --git a/src/initializers/aave.ts b/src/initializers/aave.ts index 2b5d863..f650493 100644 --- a/src/initializers/aave.ts +++ b/src/initializers/aave.ts @@ -35,7 +35,7 @@ const initAave = async (provider: providers.Provider) => { provider as any ); const adapter = new MorphoAaveAdapter(lens, oracle); - return { adapter, fetcher }; + return { adapter, fetcher, morpho }; }; export default initAave; diff --git a/src/initializers/compound.ts b/src/initializers/compound.ts index fe3a9c4..41dfb24 100644 --- a/src/initializers/compound.ts +++ b/src/initializers/compound.ts @@ -30,7 +30,7 @@ const initCompound = async (provider: providers.Provider) => { provider as any ); const adapter = new MorphoCompoundAdapter(lens, oracle); - return { adapter, fetcher }; + return { adapter, fetcher, morpho }; }; export default initCompound; diff --git a/src/interfaces/logger.ts b/src/interfaces/logger.ts index 80e8dd5..3357f43 100644 --- a/src/interfaces/logger.ts +++ b/src/interfaces/logger.ts @@ -1,5 +1,6 @@ export interface Logger { log: (toLog: any) => any; table: (toLog: any) => any; + error: (toLog: any) => any; flush: () => any | Promise; } diff --git a/src/loggers/ConsoleLog.ts b/src/loggers/ConsoleLog.ts index 71b59c4..2573e20 100644 --- a/src/loggers/ConsoleLog.ts +++ b/src/loggers/ConsoleLog.ts @@ -9,5 +9,9 @@ export default class ConsoleLog implements Logger { console.table(stg); } + error(stg: any) { + console.error(stg); + } + flush() {} } diff --git a/src/loggers/NoLogger.ts b/src/loggers/NoLogger.ts index 426c62b..cd54503 100644 --- a/src/loggers/NoLogger.ts +++ b/src/loggers/NoLogger.ts @@ -3,5 +3,6 @@ import { Logger } from "../interfaces/logger"; export default class NoLogger implements Logger { log() {} table() {} + error() {} flush() {} } diff --git a/src/morpho/Morpho.interface.ts b/src/morpho/Morpho.interface.ts index 7c9e0db..4ab3d0e 100644 --- a/src/morpho/Morpho.interface.ts +++ b/src/morpho/Morpho.interface.ts @@ -27,4 +27,10 @@ export interface IMorphoAdapter { liquidationBonus: BigNumber; } ) => Promise<{ toLiquidate: BigNumber; rewardedUSD: BigNumber }>; + + toUsd: ( + market: string, + amount: BigNumber, + price: BigNumber + ) => Promise; } diff --git a/src/morpho/MorphoAaveAdapter.ts b/src/morpho/MorphoAaveAdapter.ts index 8948f61..82f039c 100644 --- a/src/morpho/MorphoAaveAdapter.ts +++ b/src/morpho/MorphoAaveAdapter.ts @@ -25,6 +25,11 @@ export default class MorphoAaveAdapter implements IMorphoAdapter { ); } + public async toUsd(market: string, amount: BigNumber, price: BigNumber) { + const decimals = await this._getDecimals(await this._getUnderlying(market)); + return amount.mul(price).div(pow10(decimals)); + } + public async getMaxLiquidationAmount( debtMarket: { market: string; diff --git a/src/morpho/MorphoCompoundAdapter.ts b/src/morpho/MorphoCompoundAdapter.ts index 3223c96..dfb0260 100644 --- a/src/morpho/MorphoCompoundAdapter.ts +++ b/src/morpho/MorphoCompoundAdapter.ts @@ -14,6 +14,14 @@ export default class MorphoCompoundAdapter implements IMorphoAdapter { private oracle: SimplePriceOracle | CompoundOracle ) {} + public async toUsd( + market: string, + amount: BigNumber, + price: BigNumber + ): Promise { + return WadRayMath.wadMul(amount, price); + } + public async getMaxLiquidationAmount( debtMarket: { price: BigNumber; @@ -74,7 +82,7 @@ export default class MorphoCompoundAdapter implements IMorphoAdapter { return this.lens.getAllMarkets(); } - public async getLiquidationBonus(market: string): Promise { + public async getLiquidationBonus(): Promise { return MorphoCompoundAdapter.LIQUIDATION_BONUS; } } diff --git a/test/aave/FlashMintLiquidationBot.morpho-aave.ts b/test/aave/FlashMintLiquidationBot.morpho-aave.ts index 8abc277..c41d44a 100644 --- a/test/aave/FlashMintLiquidationBot.morpho-aave.ts +++ b/test/aave/FlashMintLiquidationBot.morpho-aave.ts @@ -23,6 +23,7 @@ import { MorphoAaveV2Lens__factory, } from "@morpho-labs/morpho-ethers-contract"; import MorphoAaveAdapter from "../../src/morpho/MorphoAaveAdapter"; +import LiquidatorHandler from "../../src/LiquidationHandler/LiquidatorHandler"; describe("Test Liquidation Bot for Morpho-Aave", () => { let snapshotId: number; @@ -113,11 +114,18 @@ describe("Test Liquidation Bot for Morpho-Aave", () => { lens, oracle as unknown as AavePriceOracle ); + + const handler = new LiquidatorHandler( + flashLiquidator, + liquidator, + new NoLogger() + ); + bot = new LiquidationBot( new NoLogger(), fetcher, - liquidator, - flashLiquidator, + liquidator.provider!, + handler, adapter, { profitableThresholdUSD: parseUnits("0.01"), diff --git a/test/compound/FlashMintLiquidationBot.ts b/test/compound/FlashMintLiquidationBot.ts index 40d985c..fe318ae 100644 --- a/test/compound/FlashMintLiquidationBot.ts +++ b/test/compound/FlashMintLiquidationBot.ts @@ -22,6 +22,7 @@ import { MorphoCompoundLens__factory, } from "@morpho-labs/morpho-ethers-contract"; import MorphoCompoundAdapter from "../../src/morpho/MorphoCompoundAdapter"; +import LiquidatorHandler from "../../src/LiquidationHandler/LiquidatorHandler"; describe("Test Liquidation Bot for Morpho-Compound", () => { let snapshotId: number; @@ -117,13 +118,22 @@ describe("Test Liquidation Bot for Morpho-Compound", () => { }; ({ admin, oracle, comptroller } = await setupCompound(morpho, owner)); const adapter = new MorphoCompoundAdapter(lens, oracle); + + const handler = new LiquidatorHandler( + flashLiquidator, + liquidator, + new NoLogger() + ); + bot = new LiquidationBot( new NoLogger(), fetcher, - liquidator, - flashLiquidator, + liquidator.provider!, + handler, adapter, - { profitableThresholdUSD: parseUnits("10") } + { + profitableThresholdUSD: parseUnits("10"), + } ); await comptroller.connect(admin)._setPriceOracle(oracle.address); @@ -378,13 +388,20 @@ describe("Test Liquidation Bot for Morpho-Compound", () => { ) ).to.emit(flashLiquidator, "Liquidated"); }); + it("Should detect a non profitable liquidation", async () => { const [userToLiquidate] = await bot.computeLiquidableUsers(); const params = await bot.getUserLiquidationParams(userToLiquidate.address); - expect(bot.isProfitable(params.toLiquidate, params.debtMarket.price)).to.be - .false; + expect( + await bot.isProfitable( + params.debtMarket.market, + params.toLiquidate, + params.debtMarket.price + ) + ).to.be.false; }); + it("Should return correct params for a liquidable user with a WETH debt", async () => { const borrowerAddress = await borrower.getAddress(); const toSupply = parseUnits("1500"); @@ -535,13 +552,6 @@ describe("Test Liquidation Bot for Morpho-Compound", () => { ) ).to.emit(flashLiquidator, "Liquidated"); }); - it("Should detect a non profitable liquidation", async () => { - const [userToLiquidate] = await bot.computeLiquidableUsers(); - const params = await bot.getUserLiquidationParams(userToLiquidate.address); - - expect(bot.isProfitable(params.toLiquidate, params.debtMarket.price)).to.be - .false; - }); it("Should liquidate from the bot function", async () => { const [userToLiquidate] = await bot.computeLiquidableUsers(); const params = await bot.getUserLiquidationParams(userToLiquidate.address); @@ -555,7 +565,6 @@ describe("Test Liquidation Bot for Morpho-Compound", () => { params.collateralMarket.market, params.userAddress, params.toLiquidate, - true, path ) ).to.emit(flashLiquidator, "Liquidated");