From 482c6f4b6234b889c83f3291b3bb93bb11221103 Mon Sep 17 00:00:00 2001 From: doomsower <12031673+doomsower@users.noreply.github.com> Date: Thu, 11 Jul 2024 14:32:01 -0500 Subject: [PATCH] feat: prepare for batch version --- .../AbstractLiquidationStrategyV3.ts | 118 --------- src/services/liquidate/AbstractLiquidator.ts | 229 ++++++++++++++++ src/services/liquidate/LiquidatorService.ts | 249 ------------------ ...egyV3Full.ts => SingularFullLiquidator.ts} | 17 +- src/services/liquidate/SingularLiquidator.ts | 204 ++++++++++++++ ...artial.ts => SingularPartialLiquidator.ts} | 17 +- src/services/liquidate/factory.ts | 23 ++ src/services/liquidate/index.ts | 2 +- src/services/liquidate/types.ts | 44 +--- src/services/scan/AbstractScanService.ts | 38 +-- src/services/scan/ScanServiceV3.ts | 15 +- 11 files changed, 476 insertions(+), 480 deletions(-) delete mode 100644 src/services/liquidate/AbstractLiquidationStrategyV3.ts create mode 100644 src/services/liquidate/AbstractLiquidator.ts delete mode 100644 src/services/liquidate/LiquidatorService.ts rename src/services/liquidate/{LiquidationStrategyV3Full.ts => SingularFullLiquidator.ts} (83%) create mode 100644 src/services/liquidate/SingularLiquidator.ts rename src/services/liquidate/{LiquidationStrategyV3Partial.ts => SingularPartialLiquidator.ts} (98%) create mode 100644 src/services/liquidate/factory.ts diff --git a/src/services/liquidate/AbstractLiquidationStrategyV3.ts b/src/services/liquidate/AbstractLiquidationStrategyV3.ts deleted file mode 100644 index 47ad426..0000000 --- a/src/services/liquidate/AbstractLiquidationStrategyV3.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { iDataCompressorV3Abi } from "@gearbox-protocol/types/abi"; -import type { Address } from "viem"; -import { getContract } from "viem"; - -import type { Config } from "../../config/index.js"; -import { CreditAccountData, CreditManagerData } from "../../data/index.js"; -import { DI } from "../../di.js"; -import type { ILogger } from "../../log/index.js"; -import { PathFinder } from "../../utils/ethers-6-temp/pathfinder/index.js"; -import { TxParserHelper } from "../../utils/ethers-6-temp/txparser/index.js"; -import type { IDataCompressorContract } from "../../utils/index.js"; -import type { AddressProviderService } from "../AddressProviderService.js"; -import type Client from "../Client.js"; -import type OracleServiceV3 from "../OracleServiceV3.js"; -import type { RedstoneServiceV3 } from "../RedstoneServiceV3.js"; - -export default abstract class AbstractLiquidationStrategyV3 { - abstract logger: ILogger; - - @DI.Inject(DI.AddressProvider) - addressProvider!: AddressProviderService; - - @DI.Inject(DI.Config) - config!: Config; - - @DI.Inject(DI.Redstone) - redstone!: RedstoneServiceV3; - - @DI.Inject(DI.Oracle) - oracle!: OracleServiceV3; - - @DI.Inject(DI.Client) - client!: Client; - - #compressor?: IDataCompressorContract; - #pathFinder?: PathFinder; - #cmCache: Record = {}; - - public async launch(): Promise { - const [pfAddr, dcAddr] = await Promise.all([ - this.addressProvider.findService("ROUTER", 300), - this.addressProvider.findService("DATA_COMPRESSOR", 300), - ]); - this.#compressor = getContract({ - abi: iDataCompressorV3Abi, - address: dcAddr, - client: this.client.pub, - }); - this.#pathFinder = new PathFinder( - pfAddr, - this.client.pub, - this.config.network, - ); - } - - public async updateCreditAccountData( - ca: CreditAccountData, - ): Promise { - if (!this.config.optimistic) { - throw new Error( - "updateCreditAccountData should only be used in optimistic mode", - ); - } - const priceUpdates = await this.redstone.dataCompressorUpdates(ca); - const { result } = await this.compressor.simulate.getCreditAccountData([ - ca.addr, - priceUpdates, - ]); - return new CreditAccountData(result); - } - - protected async getCreditManagerData( - addr: Address, - ): Promise { - let cm: CreditManagerData | undefined; - if (this.config.optimistic) { - cm = this.#cmCache[addr.toLowerCase()]; - } - if (!cm) { - cm = new CreditManagerData( - await this.compressor.read.getCreditManagerData([addr]), - ); - if (this.config.optimistic) { - this.#cmCache[addr.toLowerCase()] = cm; - } - } - // TODO: TxParser is really old and weird class, until we refactor it it's the best place to have this - TxParserHelper.addCreditManager(cm); - return cm; - } - - protected async getCreditManagersV3List(): Promise { - const raw = await this.compressor.read.getCreditManagersV3List(); - const result = raw.map(d => new CreditManagerData(d)); - - if (this.config.optimistic) { - for (const cm of result) { - this.#cmCache[cm.address.toLowerCase()] = cm; - } - } - - return result; - } - - protected get compressor(): IDataCompressorContract { - if (!this.#compressor) { - throw new Error("strategy not launched"); - } - return this.#compressor; - } - - protected get pathFinder(): PathFinder { - if (!this.#pathFinder) { - throw new Error("strategy not launched"); - } - return this.#pathFinder; - } -} diff --git a/src/services/liquidate/AbstractLiquidator.ts b/src/services/liquidate/AbstractLiquidator.ts new file mode 100644 index 0000000..4fa0267 --- /dev/null +++ b/src/services/liquidate/AbstractLiquidator.ts @@ -0,0 +1,229 @@ +import { tokenSymbolByAddress } from "@gearbox-protocol/sdk-gov"; +import { iDataCompressorV3Abi, ierc20Abi } from "@gearbox-protocol/types/abi"; +import type { OptimisticResultV2 } from "@gearbox-protocol/types/optimist"; +import type { Address, TransactionReceipt } from "viem"; +import { getContract } from "viem"; + +import type { Config } from "../../config/index.js"; +import { CreditAccountData, CreditManagerData } from "../../data/index.js"; +import { DI } from "../../di.js"; +import { ErrorHandler } from "../../errors/index.js"; +import type { ILogger } from "../../log/index.js"; +import { Logger } from "../../log/index.js"; +import { PathFinder } from "../../utils/ethers-6-temp/pathfinder/index.js"; +import { TxParserHelper } from "../../utils/ethers-6-temp/txparser/index.js"; +import type { IDataCompressorContract } from "../../utils/index.js"; +import type { AddressProviderService } from "../AddressProviderService.js"; +import type Client from "../Client.js"; +import type { INotifier } from "../notifier/index.js"; +import type OracleServiceV3 from "../OracleServiceV3.js"; +import type { IOptimisticOutputWriter } from "../output/index.js"; +import type { RedstoneServiceV3 } from "../RedstoneServiceV3.js"; +import type { ISwapper } from "../swap/index.js"; +import type { OptimisticResults } from "./OptimisiticResults.js"; +import type { StrategyPreview } from "./types.js"; + +export default abstract class AbstractLiquidator { + @Logger("Liquidator") + logger!: ILogger; + + @DI.Inject(DI.Redstone) + redstone!: RedstoneServiceV3; + + @DI.Inject(DI.Notifier) + notifier!: INotifier; + + @DI.Inject(DI.Config) + config!: Config; + + @DI.Inject(DI.AddressProvider) + addressProvider!: AddressProviderService; + + @DI.Inject(DI.Oracle) + oracle!: OracleServiceV3; + + @DI.Inject(DI.Output) + outputWriter!: IOptimisticOutputWriter; + + @DI.Inject(DI.Swapper) + swapper!: ISwapper; + + @DI.Inject(DI.OptimisticResults) + optimistic!: OptimisticResults; + + @DI.Inject(DI.Client) + client!: Client; + + skipList = new Set
(); + + #errorHandler?: ErrorHandler; + #compressor?: IDataCompressorContract; + #pathFinder?: PathFinder; + #cmCache: Record = {}; + + public async launch(): Promise { + this.#errorHandler = new ErrorHandler(this.config, this.logger); + const [pfAddr, dcAddr] = await Promise.all([ + this.addressProvider.findService("ROUTER", 300), + this.addressProvider.findService("DATA_COMPRESSOR", 300), + ]); + this.#compressor = getContract({ + abi: iDataCompressorV3Abi, + address: dcAddr, + client: this.client.pub, + }); + this.#pathFinder = new PathFinder( + pfAddr, + this.client.pub, + this.config.network, + ); + } + + protected newOptimisticResult(acc: CreditAccountData): OptimisticResultV2 { + return { + version: "2", + creditManager: acc.creditManager, + borrower: acc.borrower, + account: acc.addr, + balancesBefore: acc.filterDust(), + hfBefore: acc.healthFactor, + balancesAfter: {}, + hfAfter: 0, + gasUsed: 0, + calls: [], + callsHuman: [], + isError: true, + pathAmount: "0", + liquidatorPremium: "0", + liquidatorProfit: "0", + }; + } + + protected updateAfterPreview( + result: OptimisticResultV2, + preview: StrategyPreview, + ): OptimisticResultV2 { + return { + ...result, + assetOut: preview.assetOut, + amountOut: preview.amountOut, + flashLoanAmount: preview.flashLoanAmount, + calls: preview.calls, + pathAmount: preview.underlyingBalance.toString(), + priceUpdates: preview.priceUpdates, + callsHuman: TxParserHelper.parseMultiCall(preview), + }; + } + + protected async updateAfterLiquidation( + result: OptimisticResultV2, + acc: CreditAccountData, + underlyingBalanceBefore: bigint, + receipt: TransactionReceipt, + ): Promise { + const ca = await this.updateCreditAccountData(acc); + result.balancesAfter = ca.filterDust(); + result.hfAfter = ca.healthFactor; + + const balanceAfter = await this.getExecutorBalance(ca.underlyingToken); + result.gasUsed = Number(receipt.gasUsed); + result.liquidatorPremium = ( + balanceAfter.underlying - underlyingBalanceBefore + ).toString(10); + return result; + } + + protected async getCreditManagerData( + addr: Address, + ): Promise { + let cm: CreditManagerData | undefined; + if (this.config.optimistic) { + cm = this.#cmCache[addr.toLowerCase()]; + } + if (!cm) { + cm = new CreditManagerData( + await this.compressor.read.getCreditManagerData([addr]), + ); + if (this.config.optimistic) { + this.#cmCache[addr.toLowerCase()] = cm; + } + } + // TODO: TxParser is really old and weird class, until we refactor it it's the best place to have this + TxParserHelper.addCreditManager(cm); + return cm; + } + + protected async getCreditManagersV3List(): Promise { + const raw = await this.compressor.read.getCreditManagersV3List(); + const result = raw.map(d => new CreditManagerData(d)); + + if (this.config.optimistic) { + for (const cm of result) { + this.#cmCache[cm.address.toLowerCase()] = cm; + } + } + + return result; + } + + /** + * Fetches credit account data again for optimistic report + * @param ca + * @returns + */ + protected async updateCreditAccountData( + ca: CreditAccountData, + ): Promise { + if (!this.config.optimistic) { + throw new Error( + "updateCreditAccountData should only be used in optimistic mode", + ); + } + const priceUpdates = await this.redstone.dataCompressorUpdates(ca); + const { result } = await this.compressor.simulate.getCreditAccountData([ + ca.addr, + priceUpdates, + ]); + return new CreditAccountData(result); + } + + protected async getExecutorBalance( + underlyingToken: Address, + ): Promise<{ eth: bigint; underlying: bigint }> { + // using promise.all here sometimes results in anvil being stuck + const isWeth = tokenSymbolByAddress[underlyingToken] === "WETH"; + const eth = await this.client.pub.getBalance({ + address: this.client.address, + }); + const underlying = isWeth + ? eth + : await this.client.pub.readContract({ + address: underlyingToken, + abi: ierc20Abi, + functionName: "balanceOf", + args: [this.client.address], + }); + return { eth, underlying }; + } + + protected get errorHandler(): ErrorHandler { + if (!this.#errorHandler) { + throw new Error("liquidator not launched"); + } + return this.#errorHandler; + } + + protected get compressor(): IDataCompressorContract { + if (!this.#compressor) { + throw new Error("liquidator not launched"); + } + return this.#compressor; + } + + protected get pathFinder(): PathFinder { + if (!this.#pathFinder) { + throw new Error("liquidator not launched"); + } + return this.#pathFinder; + } +} diff --git a/src/services/liquidate/LiquidatorService.ts b/src/services/liquidate/LiquidatorService.ts deleted file mode 100644 index 995c1a5..0000000 --- a/src/services/liquidate/LiquidatorService.ts +++ /dev/null @@ -1,249 +0,0 @@ -import { tokenSymbolByAddress } from "@gearbox-protocol/sdk-gov"; -import { ierc20Abi } from "@gearbox-protocol/types/abi"; -import type { OptimisticResultV2 } from "@gearbox-protocol/types/optimist"; -import type { Address, Hex } from "viem"; - -import type { Config } from "../../config/index.js"; -import type { CreditAccountData } from "../../data/index.js"; -import { DI } from "../../di.js"; -import { ErrorHandler } from "../../errors/index.js"; -import { type ILogger, Logger } from "../../log/index.js"; -import { TxParserHelper } from "../../utils/ethers-6-temp/txparser/index.js"; -import type { AddressProviderService } from "../AddressProviderService.js"; -import type Client from "../Client.js"; -import type { INotifier } from "../notifier/index.js"; -import { - LiquidationErrorMessage, - LiquidationStartMessage, - LiquidationSuccessMessage, - StartedMessage, -} from "../notifier/index.js"; -import type { IOptimisticOutputWriter } from "../output/index.js"; -import type { RedstoneServiceV3 } from "../RedstoneServiceV3.js"; -import type { ISwapper } from "../swap/index.js"; -import LiquidationStrategyV3Full from "./LiquidationStrategyV3Full.js"; -import LiquidationStrategyV3Partial from "./LiquidationStrategyV3Partial.js"; -import type { OptimisticResults } from "./OptimisiticResults.js"; -import type { - ILiquidationStrategy, - ILiquidatorService, - StrategyPreview, -} from "./types.js"; - -export interface Balance { - underlying: bigint; - eth: bigint; -} - -@DI.Injectable(DI.Liquidator) -export class LiquidatorService implements ILiquidatorService { - @Logger("Liquidator") - log!: ILogger; - - @DI.Inject(DI.Redstone) - redstone!: RedstoneServiceV3; - - @DI.Inject(DI.Notifier) - notifier!: INotifier; - - @DI.Inject(DI.Config) - config!: Config; - - @DI.Inject(DI.AddressProvider) - addressProvider!: AddressProviderService; - - @DI.Inject(DI.Output) - outputWriter!: IOptimisticOutputWriter; - - @DI.Inject(DI.Swapper) - swapper!: ISwapper; - - @DI.Inject(DI.OptimisticResults) - optimistic!: OptimisticResults; - - @DI.Inject(DI.Client) - client!: Client; - - #errorHandler!: ErrorHandler; - #skipList = new Set
(); - - protected strategy!: ILiquidationStrategy; - - /** - * Launch LiquidatorService - */ - public async launch(): Promise { - this.#errorHandler = new ErrorHandler(this.config, this.log); - const { partialLiquidatorAddress, deployPartialLiquidatorContracts } = - this.config; - this.strategy = - partialLiquidatorAddress || deployPartialLiquidatorContracts - ? (new LiquidationStrategyV3Partial() as any) - : (new LiquidationStrategyV3Full() as any); - await this.strategy.launch(); - this.notifier.notify(new StartedMessage()); - } - - public async liquidate(ca: CreditAccountData): Promise { - const logger = this.log.child({ - account: ca.addr, - borrower: ca.borrower, - manager: ca.managerName, - }); - if (this.#skipList.has(ca.addr)) { - this.log.warn("skipping this account"); - return; - } - logger.info( - `begin ${this.strategy.name} liquidation: HF = ${ca.healthFactor}`, - ); - this.notifier.notify(new LiquidationStartMessage(ca, this.strategy.name)); - let pathHuman: string[] | undefined; - let preview: StrategyPreview | undefined; - try { - preview = await this.strategy.preview(ca); - pathHuman = TxParserHelper.parseMultiCall(preview); - logger.debug({ pathHuman }, "path found"); - - const { request } = await this.strategy.simulate(ca, preview); - const receipt = await this.client.liquidate(ca, request); - - this.notifier.alert( - new LiquidationSuccessMessage( - ca, - this.strategy.adverb, - receipt, - pathHuman, - ), - ); - } catch (e) { - const decoded = await this.#errorHandler.explain(e, ca); - logger.error(decoded, "cant liquidate"); - if (preview?.skipOnFailure) { - this.#skipList.add(ca.addr); - this.log.warn("adding to skip list"); - } - this.notifier.alert( - new LiquidationErrorMessage( - ca, - this.strategy.adverb, - decoded.shortMessage, - pathHuman, - preview?.skipOnFailure, - ), - ); - } - } - - public async liquidateOptimistic( - ca: CreditAccountData, - ): Promise { - let acc = ca; - const logger = this.log.child({ - account: acc.addr, - borrower: acc.borrower, - manager: acc.managerName, - }); - let snapshotId: Hex | undefined; - const optimisticResult: OptimisticResultV2 = { - version: "2", - creditManager: acc.creditManager, - borrower: acc.borrower, - account: acc.addr, - balancesBefore: ca.filterDust(), - hfBefore: acc.healthFactor, - balancesAfter: {}, - hfAfter: 0, - gasUsed: 0, - calls: [], - callsHuman: [], - isError: true, - pathAmount: "0", - liquidatorPremium: "0", - liquidatorProfit: "0", - }; - const start = Date.now(); - try { - const balanceBefore = await this.getExecutorBalance(acc.underlyingToken); - const mlRes = await this.strategy.makeLiquidatable(acc); - snapshotId = mlRes.snapshotId; - optimisticResult.partialLiquidationCondition = - mlRes.partialLiquidationCondition; - logger.debug({ snapshotId }, "previewing..."); - const preview = await this.strategy.preview(acc); - optimisticResult.assetOut = preview.assetOut; - optimisticResult.amountOut = preview.amountOut; - optimisticResult.flashLoanAmount = preview.flashLoanAmount; - optimisticResult.calls = preview.calls; - optimisticResult.pathAmount = preview.underlyingBalance.toString(); - optimisticResult.priceUpdates = preview.priceUpdates; - optimisticResult.callsHuman = TxParserHelper.parseMultiCall(preview); - logger.debug({ pathHuman: optimisticResult.callsHuman }, "path found"); - - const { request } = await this.strategy.simulate(acc, preview); - - // snapshotId might be present if we had to setup liquidation conditions for single account - // otherwise, not write requests has been made up to this point, and it's safe to take snapshot now - if (!snapshotId) { - snapshotId = await this.client.anvil.snapshot(); - } - // ------ Actual liquidation (write request start here) ----- - const receipt = await this.client.liquidate(acc, request); - logger.debug(`Liquidation tx hash: ${receipt.transactionHash}`); - optimisticResult.isError = receipt.status === "reverted"; - logger.debug( - `Liquidation tx receipt: status=${receipt.status}, gas=${receipt.cumulativeGasUsed.toString()}`, - ); - // ------ End of actual liquidation - acc = await this.strategy.updateCreditAccountData(acc); - optimisticResult.balancesAfter = ca.filterDust(); - optimisticResult.hfAfter = acc.healthFactor; - - let balanceAfter = await this.getExecutorBalance(acc.underlyingToken); - optimisticResult.gasUsed = Number(receipt.gasUsed); - optimisticResult.liquidatorPremium = ( - balanceAfter.underlying - balanceBefore.underlying - ).toString(10); - // swap underlying back to ETH - await this.swapper.swap(acc.underlyingToken, balanceAfter.underlying); - balanceAfter = await this.getExecutorBalance(acc.underlyingToken); - optimisticResult.liquidatorProfit = ( - balanceAfter.eth - balanceBefore.eth - ).toString(10); - } catch (e: any) { - const decoded = await this.#errorHandler.explain(e, acc, true); - optimisticResult.traceFile = decoded.traceFile; - optimisticResult.error = - `cannot liquidate: ${decoded.longMessage}`.replaceAll("\n", "\\n"); - logger.error({ decoded }, "cannot liquidate"); - } - - optimisticResult.duration = Date.now() - start; - this.optimistic.push(optimisticResult); - - if (snapshotId) { - await this.client.anvil.revert({ id: snapshotId }); - } - - return optimisticResult; - } - - protected async getExecutorBalance( - underlyingToken: Address, - ): Promise { - // using promise.all here sometimes results in anvil being stuck - const isWeth = tokenSymbolByAddress[underlyingToken] === "WETH"; - const eth = await this.client.pub.getBalance({ - address: this.client.address, - }); - const underlying = isWeth - ? eth - : await this.client.pub.readContract({ - address: underlyingToken, - abi: ierc20Abi, - functionName: "balanceOf", - args: [this.client.address], - }); - return { eth, underlying }; - } -} diff --git a/src/services/liquidate/LiquidationStrategyV3Full.ts b/src/services/liquidate/SingularFullLiquidator.ts similarity index 83% rename from src/services/liquidate/LiquidationStrategyV3Full.ts rename to src/services/liquidate/SingularFullLiquidator.ts index ec6232f..8a674a8 100644 --- a/src/services/liquidate/LiquidationStrategyV3Full.ts +++ b/src/services/liquidate/SingularFullLiquidator.ts @@ -7,20 +7,13 @@ import { type CreditAccountData, exceptionsAbis, } from "../../data/index.js"; -import { type ILogger, Logger } from "../../log/index.js"; import type { PathFinderCloseResult } from "../../utils/ethers-6-temp/pathfinder/index.js"; -import AbstractLiquidationStrategyV3 from "./AbstractLiquidationStrategyV3.js"; -import type { ILiquidationStrategy, MakeLiquidatableResult } from "./types.js"; +import SingularLiquidator from "./SingularLiquidator.js"; +import type { MakeLiquidatableResult } from "./types.js"; -export default class LiquidationStrategyV3Full - extends AbstractLiquidationStrategyV3 - implements ILiquidationStrategy -{ - public readonly name = "full"; - public readonly adverb = "fully"; - - @Logger("LiquidationStrategyV3Full") - logger!: ILogger; +export default class SingularFullLiquidator extends SingularLiquidator { + protected readonly name = "full"; + protected readonly adverb = "fully"; public async makeLiquidatable( ca: CreditAccountData, diff --git a/src/services/liquidate/SingularLiquidator.ts b/src/services/liquidate/SingularLiquidator.ts new file mode 100644 index 0000000..03d514d --- /dev/null +++ b/src/services/liquidate/SingularLiquidator.ts @@ -0,0 +1,204 @@ +import type { OptimisticResultV2 } from "@gearbox-protocol/types/optimist"; +import type { Hex, SimulateContractReturnType } from "viem"; + +import type { CreditAccountData } from "../../data/index.js"; +import { TxParserHelper } from "../../utils/ethers-6-temp/txparser/index.js"; +import { + LiquidationErrorMessage, + LiquidationStartMessage, + LiquidationSuccessMessage, + StartedMessage, +} from "../notifier/index.js"; +import AbstractLiquidator from "./AbstractLiquidator.js"; +import type { + ILiquidatorService, + MakeLiquidatableResult, + StrategyPreview, +} from "./types.js"; + +export default abstract class SingularLiquidator + extends AbstractLiquidator + implements ILiquidatorService +{ + protected abstract readonly name: string; + protected abstract readonly adverb: string; + + /** + * Launch LiquidatorService + */ + public async launch(): Promise { + await super.launch(); + this.notifier.notify(new StartedMessage()); + } + + public async liquidate(accounts: CreditAccountData[]): Promise { + if (!accounts.length) { + return; + } + this.logger.warn(`Need to liquidate ${accounts.length} accounts`); + for (const ca of accounts) { + await this._liquidate(ca); + } + } + + public async liquidateOptimistic( + accounts: CreditAccountData[], + ): Promise { + const total = accounts.length; + const debugS = this.config.debugAccounts ? "selective " : " "; + this.logger.info(`${debugS}optimistic liquidation for ${total} accounts`); + + for (let i = 0; i < total; i++) { + const acc = accounts[i]; + const result = await this._liquidateOptimistic(acc); + const status = result.isError ? "FAIL" : "OK"; + const msg = `[${i + 1}/${total}] ${acc.addr} in ${acc.creditManager} ${status}`; + if (result.isError) { + this.logger.warn(msg); + } else { + this.logger.info(msg); + } + } + const success = this.optimistic.get().filter(r => !r.isError).length; + this.logger.info( + `optimistic liquidation finished: ${success}/${total} accounts liquidated`, + ); + } + + public async _liquidate(ca: CreditAccountData): Promise { + const logger = this.logger.child({ + account: ca.addr, + borrower: ca.borrower, + manager: ca.managerName, + }); + if (this.skipList.has(ca.addr)) { + this.logger.warn("skipping this account"); + return; + } + logger.info(`begin ${this.name} liquidation: HF = ${ca.healthFactor}`); + this.notifier.notify(new LiquidationStartMessage(ca, this.name)); + let pathHuman: string[] | undefined; + let preview: T | undefined; + try { + preview = await this.preview(ca); + pathHuman = TxParserHelper.parseMultiCall(preview); + logger.debug({ pathHuman }, "path found"); + + const { request } = await this.simulate(ca, preview); + const receipt = await this.client.liquidate(ca, request); + + this.notifier.alert( + new LiquidationSuccessMessage(ca, this.adverb, receipt, pathHuman), + ); + } catch (e) { + const decoded = await this.errorHandler.explain(e, ca); + logger.error(decoded, "cant liquidate"); + if (preview?.skipOnFailure) { + this.skipList.add(ca.addr); + this.logger.warn("adding to skip list"); + } + this.notifier.alert( + new LiquidationErrorMessage( + ca, + this.adverb, + decoded.shortMessage, + pathHuman, + preview?.skipOnFailure, + ), + ); + } + } + + public async _liquidateOptimistic( + acc: CreditAccountData, + ): Promise { + const logger = this.logger.child({ + account: acc.addr, + borrower: acc.borrower, + manager: acc.managerName, + }); + let snapshotId: Hex | undefined; + let result = this.newOptimisticResult(acc); + const start = Date.now(); + try { + const balanceBefore = await this.getExecutorBalance(acc.underlyingToken); + const mlRes = await this.makeLiquidatable(acc); + snapshotId = mlRes.snapshotId; + result.partialLiquidationCondition = mlRes.partialLiquidationCondition; + logger.debug({ snapshotId }, "previewing..."); + const preview = await this.preview(acc); + logger.debug({ pathHuman: result.callsHuman }, "path found"); + result = this.updateAfterPreview(result, preview); + + const { request } = await this.simulate(acc, preview); + + // snapshotId might be present if we had to setup liquidation conditions for single account + // otherwise, not write requests has been made up to this point, and it's safe to take snapshot now + if (!snapshotId) { + snapshotId = await this.client.anvil.snapshot(); + } + // ------ Actual liquidation (write request start here) ----- + const receipt = await this.client.liquidate(acc, request); + logger.debug(`Liquidation tx hash: ${receipt.transactionHash}`); + result.isError = receipt.status === "reverted"; + logger.debug( + `Liquidation tx receipt: status=${receipt.status}, gas=${receipt.cumulativeGasUsed.toString()}`, + ); + // ------ End of actual liquidation + result = await this.updateAfterLiquidation( + result, + acc, + balanceBefore.underlying, + receipt, + ); + // swap underlying back to ETH + await this.swapper.swap( + acc.underlyingToken, + balanceBefore.underlying + BigInt(result.liquidatorPremium), + ); + const balanceAfter = await this.getExecutorBalance(acc.underlyingToken); + result.liquidatorProfit = (balanceAfter.eth - balanceBefore.eth).toString( + 10, + ); + } catch (e: any) { + const decoded = await this.errorHandler.explain(e, acc, true); + result.traceFile = decoded.traceFile; + result.error = `cannot liquidate: ${decoded.longMessage}`.replaceAll( + "\n", + "\\n", + ); + logger.error({ decoded }, "cannot liquidate"); + } + + result.duration = Date.now() - start; + this.optimistic.push(result); + + if (snapshotId) { + await this.client.anvil.revert({ id: snapshotId }); + } + + return result; + } + + /** + * For optimistic liquidations only: create conditions that make this account liquidatable + * If strategy implements this scenario, it must make evm_snapshot beforehand and return it as a result + * Id strategy does not support this, return undefined + * @param ca + * @returns evm snapshotId or underfined + */ + abstract makeLiquidatable( + ca: CreditAccountData, + ): Promise; + abstract preview(ca: CreditAccountData): Promise; + /** + * Simulates liquidation + * @param account + * @param preview + * @returns + */ + abstract simulate( + account: CreditAccountData, + preview: T, + ): Promise; +} diff --git a/src/services/liquidate/LiquidationStrategyV3Partial.ts b/src/services/liquidate/SingularPartialLiquidator.ts similarity index 98% rename from src/services/liquidate/LiquidationStrategyV3Partial.ts rename to src/services/liquidate/SingularPartialLiquidator.ts index 52088b5..aae9cfc 100644 --- a/src/services/liquidate/LiquidationStrategyV3Partial.ts +++ b/src/services/liquidate/SingularPartialLiquidator.ts @@ -34,10 +34,9 @@ import { type CreditManagerData, exceptionsAbis, } from "../../data/index.js"; -import { type ILogger, Logger } from "../../log/index.js"; -import AbstractLiquidationStrategyV3 from "./AbstractLiquidationStrategyV3.js"; +import type { ILogger } from "../../log/index.js"; +import SingularLiquidator from "./SingularLiquidator.js"; import type { - ILiquidationStrategy, MakeLiquidatableResult, MerkleDistributorInfo, PartialLiquidationPreview, @@ -51,15 +50,9 @@ interface TokenBalance extends ExcludeArrayProps { weightedBalance: bigint; } -export default class LiquidationStrategyV3Partial - extends AbstractLiquidationStrategyV3 - implements ILiquidationStrategy -{ - public readonly name = "partial"; - public readonly adverb = "partially"; - - @Logger("LiquidationStrategyV3Partial") - logger!: ILogger; +export default class SingularPartialLiquidator extends SingularLiquidator { + protected readonly name = "partial"; + protected readonly adverb = "partially"; #partialLiquidator?: Address; #priceHelper?: IPriceHelperContract; diff --git a/src/services/liquidate/factory.ts b/src/services/liquidate/factory.ts new file mode 100644 index 0000000..3cbf79b --- /dev/null +++ b/src/services/liquidate/factory.ts @@ -0,0 +1,23 @@ +import type { IFactory } from "di-at-home"; + +import type { Config } from "../../config/index.js"; +import { DI } from "../../di.js"; +import SingularFullLiquidator from "./SingularFullLiquidator.js"; +import SingularPartialLiquidator from "./SingularPartialLiquidator.js"; +import type { ILiquidatorService } from "./types.js"; + +@DI.Factory(DI.Liquidator) +export class LiquidatorFactory implements IFactory { + @DI.Inject(DI.Config) + config!: Config; + + produce(): ILiquidatorService { + if ( + this.config.deployPartialLiquidatorContracts || + this.config.partialLiquidatorAddress + ) { + return new SingularPartialLiquidator(); + } + return new SingularFullLiquidator(); + } +} diff --git a/src/services/liquidate/index.ts b/src/services/liquidate/index.ts index c2f49b1..320bab8 100644 --- a/src/services/liquidate/index.ts +++ b/src/services/liquidate/index.ts @@ -1,3 +1,3 @@ -export * from "./LiquidatorService.js"; +export * from "./factory.js"; export * from "./OptimisiticResults.js"; export type * from "./types.js"; diff --git a/src/services/liquidate/types.ts b/src/services/liquidate/types.ts index 7d15ef4..472f2b4 100644 --- a/src/services/liquidate/types.ts +++ b/src/services/liquidate/types.ts @@ -1,8 +1,5 @@ -import type { - OptimisticResultV2, - PartialLiquidationCondition, -} from "@gearbox-protocol/types/optimist"; -import type { Address, Hash, Hex, SimulateContractReturnType } from "viem"; +import type { PartialLiquidationCondition } from "@gearbox-protocol/types/optimist"; +import type { Address, Hash, Hex } from "viem"; import type { CreditAccountData, @@ -33,14 +30,14 @@ export interface PartialLiquidationPreview { export interface ILiquidatorService { launch: () => Promise; - liquidate: (ca: CreditAccountData) => Promise; + liquidate: (accounts: CreditAccountData[]) => Promise; /** * * @param ca * @param redstoneTokens * @returns true is account was successfully liquidated */ - liquidateOptimistic: (ca: CreditAccountData) => Promise; + liquidateOptimistic: (accounts: CreditAccountData[]) => Promise; } export interface StrategyPreview { @@ -65,39 +62,6 @@ export interface StrategyPreview { skipOnFailure?: boolean; } -export interface ILiquidationStrategy { - name: string; - adverb: string; - launch: () => Promise; - /** - * Fetches credit account data again for optimistic report - * @param ca - * @returns - */ - updateCreditAccountData: ( - ca: CreditAccountData, - ) => Promise; - /** - * For optimistic liquidations only: create conditions that make this account liquidatable - * If strategy implements this scenario, it must make evm_snapshot beforehand and return it as a result - * Id strategy does not support this, return undefined - * @param ca - * @returns evm snapshotId or underfined - */ - makeLiquidatable: (ca: CreditAccountData) => Promise; - preview: (ca: CreditAccountData) => Promise; - /** - * Simulates liquidation - * @param account - * @param preview - * @returns - */ - simulate: ( - account: CreditAccountData, - preview: T, - ) => Promise; -} - export interface MakeLiquidatableResult { snapshotId?: Hex; partialLiquidationCondition?: PartialLiquidationCondition; diff --git a/src/services/scan/AbstractScanService.ts b/src/services/scan/AbstractScanService.ts index e8dc575..f6bf4c8 100644 --- a/src/services/scan/AbstractScanService.ts +++ b/src/services/scan/AbstractScanService.ts @@ -55,47 +55,11 @@ export default abstract class AbstractScanService { protected abstract _launch(): Promise; protected abstract onBlock(block: bigint): Promise; - /** - * Liquidate accounts using NORMAL flow - * @param accountsToLiquidate - */ - protected async liquidateNormal( - accountsToLiquidate: CreditAccountData[], - ): Promise { - if (!accountsToLiquidate.length) { - return; - } - this.log.warn(`Need to liquidate ${accountsToLiquidate.length} accounts`); - for (const ca of accountsToLiquidate) { - await this.liquidatorService.liquidate(ca); - } - } - /** * Liquidate accounts using OPTIMISTIC flow * @param accountsToLiquidate */ protected async liquidateOptimistically( accounts: CreditAccountData[], - ): Promise { - const total = accounts.length; - const debugS = this.config.debugAccounts ? "selective " : " "; - this.log.info(`${debugS}optimistic liquidation for ${total} accounts`); - - for (let i = 0; i < total; i++) { - const acc = accounts[i]; - const result = await this.liquidatorService.liquidateOptimistic(acc); - const status = result.isError ? "FAIL" : "OK"; - const msg = `[${i + 1}/${total}] ${acc.addr} in ${acc.creditManager} ${status}`; - if (result.isError) { - this.log.warn(msg); - } else { - this.log.info(msg); - } - } - const success = this.optimistic.get().filter(r => !r.isError).length; - this.log.info( - `optimistic liquidation finished: ${success}/${total} accounts liquidated`, - ); - } + ): Promise {} } diff --git a/src/services/scan/ScanServiceV3.ts b/src/services/scan/ScanServiceV3.ts index 5da44e6..a10a3e2 100644 --- a/src/services/scan/ScanServiceV3.ts +++ b/src/services/scan/ScanServiceV3.ts @@ -15,10 +15,7 @@ import { CreditAccountData } from "../../data/index.js"; import { DI } from "../../di.js"; import { type ILogger, Logger } from "../../log/index.js"; import type { IDataCompressorContract } from "../../utils/index.js"; -import type { - ILiquidatorService, - LiquidatorService, -} from "../liquidate/index.js"; +import type { ILiquidatorService } from "../liquidate/index.js"; import type OracleServiceV3 from "../OracleServiceV3.js"; import type { RedstoneServiceV3 } from "../RedstoneServiceV3.js"; import AbstractScanService from "./AbstractScanService.js"; @@ -48,7 +45,7 @@ export class ScanServiceV3 extends AbstractScanService { redstone!: RedstoneServiceV3; @DI.Inject(DI.Liquidator) - _liquidatorService!: LiquidatorService; + liquidatorService!: ILiquidatorService; #dataCompressor?: IDataCompressorContract; #processing: bigint | null = null; @@ -137,9 +134,9 @@ export class ScanServiceV3 extends AbstractScanService { } if (this.config.optimistic) { - await this.liquidateOptimistically(accounts); + await this.liquidatorService.liquidateOptimistic(accounts); } else { - await this.liquidateNormal(accounts); + await this.liquidatorService.liquidate(accounts); } } @@ -382,10 +379,6 @@ export class ScanServiceV3 extends AbstractScanService { }); } - protected override get liquidatorService(): ILiquidatorService { - return this._liquidatorService; - } - private get dataCompressor(): IDataCompressorContract { if (!this.#dataCompressor) { throw new Error("data compressor not initialized");