Skip to content

Commit

Permalink
feat: batch liquidation wip
Browse files Browse the repository at this point in the history
  • Loading branch information
doomsower committed Jul 11, 2024
1 parent 482c6f4 commit 08331c6
Show file tree
Hide file tree
Showing 12 changed files with 323 additions and 36 deletions.
2 changes: 2 additions & 0 deletions src/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import type { ConfigSchema } from "./schema.js";
const envConfigMapping: Record<keyof ConfigSchema, string | string[]> = {
addressProviderOverride: "ADDRESS_PROVIDER",
appName: "APP_NAME",
batchLiquidatorAddress: "BATCH_LIQUIDATOR_ADDRESS",
debugAccounts: "DEBUG_ACCOUNTS",
debugManagers: "DEBUG_MANAGERS",
castBin: "CAST_BIN",
deployPartialLiquidatorContracts: "DEPLOY_PARTIAL_LIQUIDATOR",
deployBatchLiquidatorContracts: "DEPLOY_BATCG_LIQUIDATOR",
ethProviderRpcs: ["JSON_RPC_PROVIDERS", "JSON_RPC_PROVIDER"],
hfThreshold: "HF_TRESHOLD",
restakingWorkaround: "RESTAKING_WORKAROUND",
Expand Down
2 changes: 2 additions & 0 deletions src/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ export const ConfigSchema = z.object({
optimistic: booleanLike.pipe(z.boolean().optional()),
deployPartialLiquidatorContracts: booleanLike.pipe(z.boolean().optional()),
partialLiquidatorAddress: Address.optional(),
deployBatchLiquidatorContracts: booleanLike.pipe(z.boolean().optional()),
batchLiquidatorAddress: Address.optional(),
slippage: z.coerce.number().min(0).max(10000).int().default(50),
restakingWorkaround: booleanLike.pipe(z.boolean().optional()),

Expand Down
8 changes: 1 addition & 7 deletions src/services/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ import { privateKeyToAccount } from "viem/accounts";
import { arbitrum, base, mainnet, optimism } from "viem/chains";

import type { Config } from "../config/index.js";
import type { CreditAccountData } from "../data/index.js";
import { exceptionsAbis } from "../data/index.js";
import { DI } from "../di.js";
import { TransactionRevertedError } from "../errors/TransactionRevertedError.js";
Expand Down Expand Up @@ -155,14 +154,9 @@ export default class Client {
}

public async liquidate(
ca: CreditAccountData,
request: SimulateContractReturnType["request"],
logger: ILogger,
): Promise<TransactionReceipt> {
const logger = this.logger.child({
account: ca.addr,
borrower: ca.borrower,
manager: ca.managerName,
});
logger.debug("sending liquidation tx");
const { abi, address, args, dataSuffix, functionName, ...rest } = request;
const data = encodeFunctionData({
Expand Down
12 changes: 11 additions & 1 deletion src/services/liquidate/AbstractLiquidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ 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 INotifier, StartedMessage } from "../notifier/index.js";
import type OracleServiceV3 from "../OracleServiceV3.js";
import type { IOptimisticOutputWriter } from "../output/index.js";
import type { RedstoneServiceV3 } from "../RedstoneServiceV3.js";
Expand Down Expand Up @@ -59,6 +59,7 @@ export default abstract class AbstractLiquidator {
#errorHandler?: ErrorHandler;
#compressor?: IDataCompressorContract;
#pathFinder?: PathFinder;
#router?: Address;
#cmCache: Record<string, CreditManagerData> = {};

public async launch(): Promise<void> {
Expand All @@ -67,6 +68,7 @@ export default abstract class AbstractLiquidator {
this.addressProvider.findService("ROUTER", 300),
this.addressProvider.findService("DATA_COMPRESSOR", 300),
]);
this.#router = pfAddr;
this.#compressor = getContract({
abi: iDataCompressorV3Abi,
address: dcAddr,
Expand All @@ -77,6 +79,7 @@ export default abstract class AbstractLiquidator {
this.client.pub,
this.config.network,
);
this.notifier.notify(new StartedMessage());
}

protected newOptimisticResult(acc: CreditAccountData): OptimisticResultV2 {
Expand Down Expand Up @@ -226,4 +229,11 @@ export default abstract class AbstractLiquidator {
}
return this.#pathFinder;
}

protected get router(): Address {
if (!this.#router) {
throw new Error("liquidator not launched");
}
return this.#router;
}
}
143 changes: 143 additions & 0 deletions src/services/liquidate/BatchLiquidator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import {
batchLiquidatorAbi,
iBatchLiquidatorAbi,
} from "@gearbox-protocol/liquidator-v2-contracts/abi";
import { BatchLiquidator_bytecode } from "@gearbox-protocol/liquidator-v2-contracts/bytecode";
import { iCreditFacadeV3Abi } from "@gearbox-protocol/types/abi";
import { type Address, parseEventLogs } from "viem";

import type { CreditAccountData } from "../../data/index.js";
import {
BatchLiquidationErrorMessage,
BatchLiquidationFinishedMessage,
} from "../notifier/messages.js";
import AbstractLiquidator from "./AbstractLiquidator.js";
import type { BatchLiquidationResult, ILiquidatorService } from "./types.js";

export default abstract class BatchLiquidator
extends AbstractLiquidator
implements ILiquidatorService
{
#batchLiquidator?: Address;

public override async launch(): Promise<void> {
await super.launch();
await this.#deployContract();
}

public async liquidate(accounts: CreditAccountData[]): Promise<void> {
if (!accounts.length) {
return;
}
this.logger.warn(`Need to liquidate ${accounts.length} accounts`);
try {
const result = await this.#liquidateBatch(accounts);
this.notifier.notify(
new BatchLiquidationFinishedMessage(
result.liquidated,
result.notLiquidated,
result.receipt,
),
);
} catch (e) {
const decoded = await this.errorHandler.explain(e);
this.logger.error(decoded, "cant liquidate");
this.notifier.notify(
new BatchLiquidationErrorMessage(accounts, decoded.shortMessage),
);
}
}

public async liquidateOptimistic(
accounts: CreditAccountData[],
): Promise<void> {
const total = accounts.length;
this.logger.info(`optimistic batch-liquidation for ${total} accounts`);
const result = await this.#liquidateBatch(accounts);
this.logger.info(
`optimistic batch-liquidation finished: ${result.liquidated.length}/${total} accounts liquidated`,
);
}

async #liquidateBatch(
accounts: CreditAccountData[],
): Promise<BatchLiquidationResult> {
const input = accounts.map(ca =>
this.pathFinder.getEstimateBatchInput(ca, this.config.slippage),
);
const { result } = await this.client.pub.simulateContract({
account: this.client.account,
address: this.batchLiquidator,
abi: iBatchLiquidatorAbi,
functionName: "estimateBatch",
args: [input] as any, // TODO: types
});
this.logger.debug(result, "estimated batch");
const { request } = await this.client.pub.simulateContract({
account: this.client.account,
address: this.batchLiquidator,
abi: iBatchLiquidatorAbi,
functionName: "liquidateBatch",
args: [
result.map(i => ({
calls: i.calls,
creditAccount: i.creditAccount,
creditFacade: accounts.find(
ca => ca.addr === i.creditAccount.toLowerCase(),
)?.creditFacade!, // TODO: checks
})),
this.client.address,
],
});
const receipt = await this.client.liquidate(request as any, this.logger); // TODO: types

const logs = parseEventLogs({
abi: iCreditFacadeV3Abi,
eventName: "LiquidateCreditAccount",
logs: receipt.logs,
});
const liquidated = new Set(
logs.map(l => l.args.creditAccount.toLowerCase() as Address),
);
return {
receipt,
liquidated: accounts.filter(a => liquidated.has(a.addr)),
notLiquidated: accounts.filter(a => !liquidated.has(a.addr)),
};
}

async #deployContract(): Promise<void> {
this.#batchLiquidator = this.config.batchLiquidatorAddress;
if (!this.#batchLiquidator) {
this.logger.debug("deploying batch liquidator");

let hash = await this.client.wallet.deployContract({
abi: batchLiquidatorAbi,
bytecode: BatchLiquidator_bytecode,
args: [this.router],
});
this.logger.debug(
`waiting for BatchLiquidator to deploy, tx hash: ${hash}`,
);
const { contractAddress } =
await this.client.pub.waitForTransactionReceipt({
hash,
timeout: 120_000,
});
if (!contractAddress) {
throw new Error(`BatchLiquidator was not deployed, tx hash: ${hash}`);
}
this.#batchLiquidator = contractAddress;
}
this.logger.debug(
`using batch liquidator contract ${this.#batchLiquidator}`,
);
}

private get batchLiquidator(): Address {
if (!this.#batchLiquidator) {
throw new Error("batch liquidator not deployed");
}
return this.#batchLiquidator;
}
}
6 changes: 3 additions & 3 deletions src/services/liquidate/SingularFullLiquidator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { getDecimals } from "@gearbox-protocol/sdk-gov";
import { iCreditFacadeV3Abi } from "@gearbox-protocol/types/abi";
import type { SimulateContractReturnType } from "viem";
import type { Address, SimulateContractReturnType } from "viem";

import {
type Balance,
Expand All @@ -25,8 +25,8 @@ export default class SingularFullLiquidator extends SingularLiquidator<PathFinde
public async preview(ca: CreditAccountData): Promise<PathFinderCloseResult> {
try {
const cm = await this.getCreditManagerData(ca.creditManager);
const expectedBalances: Record<string, Balance> = {};
const leftoverBalances: Record<string, Balance> = {};
const expectedBalances: Record<Address, Balance> = {};
const leftoverBalances: Record<Address, Balance> = {};
for (const { token, balance, isEnabled } of ca.allBalances) {
expectedBalances[token] = { token, balance };
// filter out dust, we don't want to swap it
Expand Down
21 changes: 6 additions & 15 deletions src/services/liquidate/SingularLiquidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
LiquidationErrorMessage,
LiquidationStartMessage,
LiquidationSuccessMessage,
StartedMessage,
} from "../notifier/index.js";
import AbstractLiquidator from "./AbstractLiquidator.js";
import type {
Expand All @@ -23,21 +22,13 @@ export default abstract class SingularLiquidator<T extends StrategyPreview>
protected abstract readonly name: string;
protected abstract readonly adverb: string;

/**
* Launch LiquidatorService
*/
public async launch(): Promise<void> {
await super.launch();
this.notifier.notify(new StartedMessage());
}

public async liquidate(accounts: CreditAccountData[]): Promise<void> {
if (!accounts.length) {
return;
}
this.logger.warn(`Need to liquidate ${accounts.length} accounts`);
for (const ca of accounts) {
await this._liquidate(ca);
await this.#liquidateOne(ca);
}
}

Expand All @@ -50,7 +41,7 @@ export default abstract class SingularLiquidator<T extends StrategyPreview>

for (let i = 0; i < total; i++) {
const acc = accounts[i];
const result = await this._liquidateOptimistic(acc);
const result = await this.#liquidateOneOptimistic(acc);
const status = result.isError ? "FAIL" : "OK";
const msg = `[${i + 1}/${total}] ${acc.addr} in ${acc.creditManager} ${status}`;
if (result.isError) {
Expand All @@ -65,7 +56,7 @@ export default abstract class SingularLiquidator<T extends StrategyPreview>
);
}

public async _liquidate(ca: CreditAccountData): Promise<void> {
async #liquidateOne(ca: CreditAccountData): Promise<void> {
const logger = this.logger.child({
account: ca.addr,
borrower: ca.borrower,
Expand All @@ -85,7 +76,7 @@ export default abstract class SingularLiquidator<T extends StrategyPreview>
logger.debug({ pathHuman }, "path found");

const { request } = await this.simulate(ca, preview);
const receipt = await this.client.liquidate(ca, request);
const receipt = await this.client.liquidate(request, logger);

this.notifier.alert(
new LiquidationSuccessMessage(ca, this.adverb, receipt, pathHuman),
Expand All @@ -109,7 +100,7 @@ export default abstract class SingularLiquidator<T extends StrategyPreview>
}
}

public async _liquidateOptimistic(
async #liquidateOneOptimistic(
acc: CreditAccountData,
): Promise<OptimisticResultV2> {
const logger = this.logger.child({
Expand Down Expand Up @@ -138,7 +129,7 @@ export default abstract class SingularLiquidator<T extends StrategyPreview>
snapshotId = await this.client.anvil.snapshot();
}
// ------ Actual liquidation (write request start here) -----
const receipt = await this.client.liquidate(acc, request);
const receipt = await this.client.liquidate(request, logger);
logger.debug(`Liquidation tx hash: ${receipt.transactionHash}`);
result.isError = receipt.status === "reverted";
logger.debug(
Expand Down
15 changes: 11 additions & 4 deletions src/services/liquidate/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { IFactory } from "di-at-home";

import type { Config } from "../../config/index.js";
import { DI } from "../../di.js";
import BatchLiquidator from "./BatchLiquidator.js";
import SingularFullLiquidator from "./SingularFullLiquidator.js";
import SingularPartialLiquidator from "./SingularPartialLiquidator.js";
import type { ILiquidatorService } from "./types.js";
Expand All @@ -12,12 +13,18 @@ export class LiquidatorFactory implements IFactory<ILiquidatorService, []> {
config!: Config;

produce(): ILiquidatorService {
if (
this.config.deployPartialLiquidatorContracts ||
this.config.partialLiquidatorAddress
) {
const {
deployBatchLiquidatorContracts,
deployPartialLiquidatorContracts,
partialLiquidatorAddress,
batchLiquidatorAddress,
} = this.config;
if (deployPartialLiquidatorContracts || partialLiquidatorAddress) {
return new SingularPartialLiquidator();
}
if (deployBatchLiquidatorContracts || batchLiquidatorAddress) {
return new BatchLiquidator();
}
return new SingularFullLiquidator();
}
}
8 changes: 7 additions & 1 deletion src/services/liquidate/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { PartialLiquidationCondition } from "@gearbox-protocol/types/optimist";
import type { Address, Hash, Hex } from "viem";
import type { Address, Hash, Hex, TransactionReceipt } from "viem";

import type {
CreditAccountData,
Expand Down Expand Up @@ -79,3 +79,9 @@ export interface MerkleDistributorInfo {
}
>;
}

export interface BatchLiquidationResult {
receipt: TransactionReceipt;
liquidated: CreditAccountData[];
notLiquidated: CreditAccountData[];
}
Loading

0 comments on commit 08331c6

Please sign in to comment.