diff --git a/src/generated/IACL.ts b/src/generated/IACL.ts new file mode 100644 index 0000000..1615fea --- /dev/null +++ b/src/generated/IACL.ts @@ -0,0 +1,292 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ +/* eslint-disable */ +import type { + BaseContract, + BigNumber, + BytesLike, + CallOverrides, + PopulatedTransaction, + Signer, + utils, +} from "ethers"; +import type { + FunctionFragment, + Result, + EventFragment, +} from "@ethersproject/abi"; +import type { Listener, Provider } from "@ethersproject/providers"; +import { + OnEvent, + PromiseOrValue, + TypedEvent, + TypedEventFilter, + TypedListener, +} from "@gearbox-protocol/sdk/lib/types/common"; + +export interface IACLInterface extends utils.Interface { + functions: { + "isConfigurator(address)": FunctionFragment; + "isPausableAdmin(address)": FunctionFragment; + "isUnpausableAdmin(address)": FunctionFragment; + "owner()": FunctionFragment; + "version()": FunctionFragment; + }; + + getFunction( + nameOrSignatureOrTopic: + | "isConfigurator" + | "isPausableAdmin" + | "isUnpausableAdmin" + | "owner" + | "version", + ): FunctionFragment; + + encodeFunctionData( + functionFragment: "isConfigurator", + values: [PromiseOrValue], + ): string; + encodeFunctionData( + functionFragment: "isPausableAdmin", + values: [PromiseOrValue], + ): string; + encodeFunctionData( + functionFragment: "isUnpausableAdmin", + values: [PromiseOrValue], + ): string; + encodeFunctionData(functionFragment: "owner", values?: undefined): string; + encodeFunctionData(functionFragment: "version", values?: undefined): string; + + decodeFunctionResult( + functionFragment: "isConfigurator", + data: BytesLike, + ): Result; + decodeFunctionResult( + functionFragment: "isPausableAdmin", + data: BytesLike, + ): Result; + decodeFunctionResult( + functionFragment: "isUnpausableAdmin", + data: BytesLike, + ): Result; + decodeFunctionResult(functionFragment: "owner", data: BytesLike): Result; + decodeFunctionResult(functionFragment: "version", data: BytesLike): Result; + + events: { + "PausableAdminAdded(address)": EventFragment; + "PausableAdminRemoved(address)": EventFragment; + "UnpausableAdminAdded(address)": EventFragment; + "UnpausableAdminRemoved(address)": EventFragment; + }; + + getEvent(nameOrSignatureOrTopic: "PausableAdminAdded"): EventFragment; + getEvent(nameOrSignatureOrTopic: "PausableAdminRemoved"): EventFragment; + getEvent(nameOrSignatureOrTopic: "UnpausableAdminAdded"): EventFragment; + getEvent(nameOrSignatureOrTopic: "UnpausableAdminRemoved"): EventFragment; +} + +export interface PausableAdminAddedEventObject { + newAdmin: string; +} +export type PausableAdminAddedEvent = TypedEvent< + [string], + PausableAdminAddedEventObject +>; + +export type PausableAdminAddedEventFilter = + TypedEventFilter; + +export interface PausableAdminRemovedEventObject { + admin: string; +} +export type PausableAdminRemovedEvent = TypedEvent< + [string], + PausableAdminRemovedEventObject +>; + +export type PausableAdminRemovedEventFilter = + TypedEventFilter; + +export interface UnpausableAdminAddedEventObject { + newAdmin: string; +} +export type UnpausableAdminAddedEvent = TypedEvent< + [string], + UnpausableAdminAddedEventObject +>; + +export type UnpausableAdminAddedEventFilter = + TypedEventFilter; + +export interface UnpausableAdminRemovedEventObject { + admin: string; +} +export type UnpausableAdminRemovedEvent = TypedEvent< + [string], + UnpausableAdminRemovedEventObject +>; + +export type UnpausableAdminRemovedEventFilter = + TypedEventFilter; + +export interface IACL extends BaseContract { + contractName: "IACL"; + + connect(signerOrProvider: Signer | Provider | string): this; + attach(addressOrName: string): this; + deployed(): Promise; + + interface: IACLInterface; + + queryFilter( + event: TypedEventFilter, + fromBlockOrBlockhash?: string | number | undefined, + toBlock?: string | number | undefined, + ): Promise>; + + listeners( + eventFilter?: TypedEventFilter, + ): Array>; + listeners(eventName?: string): Array; + removeAllListeners( + eventFilter: TypedEventFilter, + ): this; + removeAllListeners(eventName?: string): this; + off: OnEvent; + on: OnEvent; + once: OnEvent; + removeListener: OnEvent; + + functions: { + isConfigurator( + account: PromiseOrValue, + overrides?: CallOverrides, + ): Promise<[boolean]>; + + isPausableAdmin( + addr: PromiseOrValue, + overrides?: CallOverrides, + ): Promise<[boolean]>; + + isUnpausableAdmin( + addr: PromiseOrValue, + overrides?: CallOverrides, + ): Promise<[boolean]>; + + owner(overrides?: CallOverrides): Promise<[string]>; + + version(overrides?: CallOverrides): Promise<[BigNumber]>; + }; + + isConfigurator( + account: PromiseOrValue, + overrides?: CallOverrides, + ): Promise; + + isPausableAdmin( + addr: PromiseOrValue, + overrides?: CallOverrides, + ): Promise; + + isUnpausableAdmin( + addr: PromiseOrValue, + overrides?: CallOverrides, + ): Promise; + + owner(overrides?: CallOverrides): Promise; + + version(overrides?: CallOverrides): Promise; + + callStatic: { + isConfigurator( + account: PromiseOrValue, + overrides?: CallOverrides, + ): Promise; + + isPausableAdmin( + addr: PromiseOrValue, + overrides?: CallOverrides, + ): Promise; + + isUnpausableAdmin( + addr: PromiseOrValue, + overrides?: CallOverrides, + ): Promise; + + owner(overrides?: CallOverrides): Promise; + + version(overrides?: CallOverrides): Promise; + }; + + filters: { + "PausableAdminAdded(address)"( + newAdmin?: PromiseOrValue | null, + ): PausableAdminAddedEventFilter; + PausableAdminAdded( + newAdmin?: PromiseOrValue | null, + ): PausableAdminAddedEventFilter; + + "PausableAdminRemoved(address)"( + admin?: PromiseOrValue | null, + ): PausableAdminRemovedEventFilter; + PausableAdminRemoved( + admin?: PromiseOrValue | null, + ): PausableAdminRemovedEventFilter; + + "UnpausableAdminAdded(address)"( + newAdmin?: PromiseOrValue | null, + ): UnpausableAdminAddedEventFilter; + UnpausableAdminAdded( + newAdmin?: PromiseOrValue | null, + ): UnpausableAdminAddedEventFilter; + + "UnpausableAdminRemoved(address)"( + admin?: PromiseOrValue | null, + ): UnpausableAdminRemovedEventFilter; + UnpausableAdminRemoved( + admin?: PromiseOrValue | null, + ): UnpausableAdminRemovedEventFilter; + }; + + estimateGas: { + isConfigurator( + account: PromiseOrValue, + overrides?: CallOverrides, + ): Promise; + + isPausableAdmin( + addr: PromiseOrValue, + overrides?: CallOverrides, + ): Promise; + + isUnpausableAdmin( + addr: PromiseOrValue, + overrides?: CallOverrides, + ): Promise; + + owner(overrides?: CallOverrides): Promise; + + version(overrides?: CallOverrides): Promise; + }; + + populateTransaction: { + isConfigurator( + account: PromiseOrValue, + overrides?: CallOverrides, + ): Promise; + + isPausableAdmin( + addr: PromiseOrValue, + overrides?: CallOverrides, + ): Promise; + + isUnpausableAdmin( + addr: PromiseOrValue, + overrides?: CallOverrides, + ): Promise; + + owner(overrides?: CallOverrides): Promise; + + version(overrides?: CallOverrides): Promise; + }; +} diff --git a/src/generated/IACL__factory.ts b/src/generated/IACL__factory.ts new file mode 100644 index 0000000..59492a8 --- /dev/null +++ b/src/generated/IACL__factory.ts @@ -0,0 +1,177 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ +/* eslint-disable */ + +import { Contract, Signer, utils } from "ethers"; +import type { Provider } from "@ethersproject/providers"; +import type { IACL, IACLInterface } from "./IACL"; + +const _abi = [ + { + inputs: [ + { + internalType: "address", + name: "addr", + type: "address", + }, + ], + name: "AddressNotPausableAdminException", + type: "error", + }, + { + inputs: [ + { + internalType: "address", + name: "addr", + type: "address", + }, + ], + name: "AddressNotUnpausableAdminException", + type: "error", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "newAdmin", + type: "address", + }, + ], + name: "PausableAdminAdded", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "admin", + type: "address", + }, + ], + name: "PausableAdminRemoved", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "newAdmin", + type: "address", + }, + ], + name: "UnpausableAdminAdded", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "admin", + type: "address", + }, + ], + name: "UnpausableAdminRemoved", + type: "event", + }, + { + inputs: [ + { + internalType: "address", + name: "account", + type: "address", + }, + ], + name: "isConfigurator", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "addr", + type: "address", + }, + ], + name: "isPausableAdmin", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "addr", + type: "address", + }, + ], + name: "isUnpausableAdmin", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "owner", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "version", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, +] as const; + +export class IACL__factory { + static readonly abi = _abi; + static createInterface(): IACLInterface { + return new utils.Interface(_abi) as IACLInterface; + } + static connect(address: string, signerOrProvider: Signer | Provider): IACL { + return new Contract(address, _abi, signerOrProvider) as IACL; + } +} diff --git a/src/services/liquidate/LiquidationStrategyV3Partial.ts b/src/services/liquidate/LiquidationStrategyV3Partial.ts index 5621ec4..41387d7 100644 --- a/src/services/liquidate/LiquidationStrategyV3Partial.ts +++ b/src/services/liquidate/LiquidationStrategyV3Partial.ts @@ -1,5 +1,7 @@ import { AaveFLTaker__factory, + ICreditConfiguratorV3__factory, + ICreditManagerV3__factory, Liquidator__factory, } from "@gearbox-protocol/liquidator-v2-contracts"; import { ILiquidator__factory } from "@gearbox-protocol/liquidator-v2-contracts/dist/factories"; @@ -9,14 +11,23 @@ import { CreditManagerData, formatBN, getDecimals, + PERCENTAGE_FACTOR, tokenSymbolByAddress, } from "@gearbox-protocol/sdk"; import { ADDRESS_0X0, contractsByNetwork } from "@gearbox-protocol/sdk-gov"; -import type { BigNumber, BigNumberish, ContractReceipt, Wallet } from "ethers"; +import type { + BigNumber, + BigNumberish, + ContractReceipt, + providers, + Wallet, +} from "ethers"; import { Service } from "typedi"; +import { IACL__factory } from "../../generated/IACL__factory"; import { Logger, LoggerInterface } from "../../log"; import { accountName, managerName } from "../utils"; +import { impersonate, stopImpersonate } from "../utils/impersonate"; import AbstractLiquidationStrategyV3 from "./AbstractLiquidationStrategyV3"; import type { ILiquidationStrategy, PartialLiquidationPreview } from "./types"; @@ -38,47 +49,7 @@ export default class LiquidationStrategyV3Partial logger: LoggerInterface; #partialLiquidator?: ILiquidator; - - public async makeLiquidatable( - ca: CreditAccountData, - ): Promise { - if (!this.config.optimistic) { - throw new Error("makeLiquidatable only works in optimistic mode"); - } - const logger = this.logger.child({ - account: ca.addr, - borrower: ca.borrower, - manager: managerName(ca), - }); - - const cm = new CreditManagerData( - await this.compressor.getCreditManagerData(ca.creditManager), - ); - const balances = await this.#prepareAccountTokens(ca, cm); - // const snapshotId = await ( - // this.executor.provider as providers.JsonRpcProvider - // ).send("evm_snapshot", []); - - // LTnew = LT * k, where - // - // totalDebt - Bunderlying * LTunderlying - // k = ---------------------------------------------- - // sum(p * b* LT) - let divisor = 0n; - let dividend = ca.borrowedAmountPlusInterestAndFees; // TODO: USDT fee - for (const [t, { balance, balanceInUnderlying, lt }] of Object.entries( - balances, - )) { - if (t === cm.underlyingToken) { - dividend -= balance * lt; - } else { - divisor += balanceInUnderlying * lt; - } - } - const k = Number((dividend * 10000n) / divisor) / 10000; - logger.debug({ k }, "calculated LT lowering multiplier"); - return undefined; - } + #configuratorAddr?: string; public async launch(): Promise { await super.launch(); @@ -108,21 +79,44 @@ export default class LiquidationStrategyV3Partial await this.#configurePartialLiquidator(router, bot); } + public async makeLiquidatable( + ca: CreditAccountData, + ): Promise { + if (!this.config.optimistic) { + throw new Error("makeLiquidatable only works in optimistic mode"); + } + const logger = this.#caLogger(ca); + const cm = new CreditManagerData( + await this.compressor.getCreditManagerData(ca.creditManager), + ); + + const newLTs = await this.#calcNewLTs(ca, cm); + const snapshotId = await ( + this.executor.provider as providers.JsonRpcProvider + ).send("evm_snapshot", []); + + await this.#setNewLTs(ca, cm, newLTs); + const updCa = await this.compressor.callStatic.getCreditAccountData( + ca.addr, + [], + ); + logger.debug({ + hfNew: updCa.healthFactor.toString(), + hfOld: ca.healthFactor.toString(), + isSuccessful: updCa.isSuccessful, + }); + return snapshotId; + } + public async preview( ca: CreditAccountData, ): Promise { - const logger = this.logger.child({ - account: ca.addr, - borrower: ca.borrower, - manager: managerName(ca), - }); + const logger = this.#caLogger(ca); const cm = new CreditManagerData( await this.compressor.getCreditManagerData(ca.creditManager), ); const balances = await this.#prepareAccountTokens(ca, cm); - - // this should do it, we are only interested in keys of a record - const connectors = this.pathFinder.getAvailableConnectors(balances as any); + const connectors = this.pathFinder.getAvailableConnectors(ca.balances); // TODO: maybe this should be refreshed every loop iteration const priceUpdates = await this.redstone.liquidationPreviewUpdates(ca); @@ -130,27 +124,20 @@ export default class LiquidationStrategyV3Partial balances, )) { const symb = tokenSymbolByAddress[assetOut.toLowerCase()]; - logger.debug( - `trying ${formatBN(balance, getDecimals(assetOut))} ${symb} == ${formatBN(balanceInUnderlying, getDecimals(cm.underlyingToken))} ${tokenSymbolByAddress[cm.underlyingToken]}`, - ); + logger.debug({ + assetOut: `${assetOut} (${symb})`, + amountOut: `${balance} (${formatBN(balance, getDecimals(assetOut))})`, + flashLoanAmount: `${balanceInUnderlying} (${formatBN(balanceInUnderlying, getDecimals(cm.underlyingToken))}) ${tokenSymbolByAddress[cm.underlyingToken]}`, + priceUpdates, + connectors, + slippage: this.config.slippage, + }); // naively try to figure out amount that works for (let i = 1n; i <= 10n; i++) { const amountOut = (i * balance) / 10n; const flashLoanAmount = (i * balanceInUnderlying) / 10n; - logger.debug( - { - creditManager: `${cm.address} (${cm.name})`, - creditAccount: ca.addr, - assetOut: `${assetOut} (${symb})`, - amountOut: `${amountOut} (${formatBN(amountOut, getDecimals(assetOut))})`, - flashLoanAmount: `${flashLoanAmount} (${formatBN(flashLoanAmount, getDecimals(cm.underlyingToken))}) ${tokenSymbolByAddress[cm.underlyingToken]}`, - priceUpdates, - connectors, - slippage: this.config.slippage, - }, - `trying partial liqudation: ${i * 10n}% of ${symb} out`, - ); + logger.debug(`trying partial liqudation: ${i * 10n}% of ${symb} out`); try { const result = await this.partialLiquidator.callStatic.previewPartialLiquidation( @@ -176,6 +163,7 @@ export default class LiquidationStrategyV3Partial }; } } catch (e) { + // console.log(">>>> failed"); console.log(e); } } @@ -225,6 +213,81 @@ export default class LiquidationStrategyV3Partial ); } + /** + * Given credit accounts, calculates new liquidation thresholds that needs to be set to drop account health factor a bit to make it eligible for partial liquidation + * @param ca + */ + async #calcNewLTs( + ca: CreditAccountData, + cm: CreditManagerData, + factor = 9000n, + ): Promise> { + const logger = this.#caLogger(ca); + const balances = await this.#prepareAccountTokens(ca, cm); + // const snapshotId = await ( + // this.executor.provider as providers.JsonRpcProvider + // ).send("evm_snapshot", []); + + // LTnew = LT * k, where + // + // PERCENTAGE_FACTOR * totalDebt - B_underlying * LT_underlying + // k = ------------------------------------------------------------- + // sum(p * b* LT) + let divisor = 0n; + let dividend = PERCENTAGE_FACTOR * ca.borrowedAmountPlusInterestAndFees; // TODO: USDT fee + for (const [t, { balance, balanceInUnderlying, lt }] of Object.entries( + balances, + )) { + if (t === cm.underlyingToken) { + dividend -= balance * lt; + } else { + divisor += balanceInUnderlying * lt; + } + } + const k = (factor * dividend) / divisor; + + const result: Record = {}; + const ltChangesHuman: Record = {}; + for (const [t, { lt }] of Object.entries(balances)) { + if (t !== cm.underlyingToken) { + result[t] = (lt * k) / PERCENTAGE_FACTOR; + ltChangesHuman[tokenSymbolByAddress[t]] = `${lt} => ${result[t]}`; + } + } + logger.debug( + ltChangesHuman, + "need to change LTs to enable partial liquidation", + ); + return result; + } + + async #setNewLTs( + ca: CreditAccountData, + cm: CreditManagerData, + lts: Record, + ): Promise { + const logger = this.#caLogger(ca); + const configuratorAddr = await this.getConfiguratorAddr(); + const impConfiurator = await impersonate( + this.executor.provider, + configuratorAddr, + ); + const cc = ICreditConfiguratorV3__factory.connect( + cm.creditConfigurator, + impConfiurator, + ); + const mgr = ICreditManagerV3__factory.connect( + cm.address, + this.executor.provider, + ); + for (const [t, lt] of Object.entries(lts)) { + await cc.setLiquidationThreshold(t, lt); + const newLT = await mgr.liquidationThresholds(t); + logger.debug(`set LT of ${tokenSymbolByAddress[t]} to ${lt}: ${newLT}`); + } + await stopImpersonate(this.executor.provider, configuratorAddr); + } + public async estimate( account: CreditAccountData, preview: PartialLiquidationPreview, @@ -349,10 +412,28 @@ export default class LiquidationStrategyV3Partial } } + #caLogger(ca: CreditAccountData): LoggerInterface { + return this.logger.child({ + account: ca.addr, + borrower: ca.borrower, + manager: managerName(ca), + }); + } + private get partialLiquidator(): ILiquidator { if (!this.#partialLiquidator) { throw new Error("strategy not launched"); } return this.#partialLiquidator; } + + private async getConfiguratorAddr(): Promise { + if (!this.#configuratorAddr) { + const aclAddr = await this.addressProvider.findService("ACL", 0); + const acl = IACL__factory.connect(aclAddr, this.executor.provider); + this.#configuratorAddr = await acl.owner(); + this.logger.debug(`configurator address: ${this.#configuratorAddr}`); + } + return this.#configuratorAddr; + } } diff --git a/src/services/utils/impersonate.ts b/src/services/utils/impersonate.ts new file mode 100644 index 0000000..595c410 --- /dev/null +++ b/src/services/utils/impersonate.ts @@ -0,0 +1,26 @@ +import type { ethers, providers } from "ethers"; + +export async function impersonate( + provider: providers.Provider, + address: string, +): Promise { + await (provider as providers.JsonRpcProvider).send( + "anvil_impersonateAccount", + [address], + ); + // await (provider as providers.JsonRpcProvider).send("anvil_setBalance", [ + // address, + // "0x10000000000000000000", + // ]); + return (provider as providers.JsonRpcProvider).getSigner(address); +} + +export async function stopImpersonate( + provider: providers.Provider, + address: string, +): Promise { + await (provider as providers.JsonRpcProvider).send( + "anvil_stopImpersonatingAccount", + [address], + ); +}