Skip to content

Commit 2b28f93

Browse files
committed
feat: dynamic discovery of partial liquidation bots
1 parent 46cbaaf commit 2b28f93

14 files changed

+426
-444
lines changed

package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,22 @@
2121
"pino-pretty": "^13.1.3"
2222
},
2323
"devDependencies": {
24-
"@aws-sdk/client-s3": "^3.943.0",
24+
"@aws-sdk/client-s3": "^3.948.0",
2525
"@biomejs/biome": "^2.3.8",
2626
"@commander-js/extra-typings": "^14.0.0",
2727
"@commitlint/cli": "^20.2.0",
2828
"@commitlint/config-conventional": "^20.2.0",
2929
"@gearbox-protocol/biome-config": "^1.0.14",
30-
"@gearbox-protocol/cli-utils": "^5.61.1",
30+
"@gearbox-protocol/cli-utils": "^5.62.1",
3131
"@gearbox-protocol/liquidator-contracts": "^1.36.0-experimental.67",
3232
"@gearbox-protocol/liquidator-v2-contracts": "^2.4.0",
33-
"@gearbox-protocol/sdk": "11.10.0",
33+
"@gearbox-protocol/sdk": "11.11.0",
3434
"@gearbox-protocol/types": "^1.14.8",
35-
"@types/node": "^24.10.1",
36-
"@uniswap/sdk-core": "^7.9.0",
35+
"@types/node": "^25.0.0",
36+
"@uniswap/sdk-core": "^7.10.0",
3737
"@uniswap/v3-sdk": "^3.26.0",
3838
"@vlad-yakovlev/telegram-md": "^2.1.0",
39-
"abitype": "^1.2.1",
39+
"abitype": "^1.2.2",
4040
"axios": "^1.13.2",
4141
"axios-retry": "^4.5.0",
4242
"date-fns": "^4.1.0",

src/Liquidator.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { Config } from "./config/index.js";
55
import { DI } from "./di.js";
66
import { type ILogger, Logger } from "./log/index.js";
77
import type Client from "./services/Client.js";
8+
import type DeleverageService from "./services/DeleverageService.js";
89
import type HealthCheckerService from "./services/HealthCheckerService.js";
910
import type { IOptimisticOutputWriter } from "./services/output/index.js";
1011
import type { Scanner } from "./services/Scanner.js";
@@ -20,6 +21,9 @@ export default class Liquidator {
2021
@DI.Inject(DI.Scanner)
2122
scanner!: Scanner;
2223

24+
@DI.Inject(DI.Deleverage)
25+
deleverage!: DeleverageService;
26+
2327
@DI.Inject(DI.CreditAccountService)
2428
caService!: ICreditAccountsService;
2529

@@ -41,6 +45,7 @@ export default class Liquidator {
4145
await this.client.launch();
4246

4347
this.healthChecker.launch();
48+
await this.deleverage.launch();
4449
await this.swapper.launch(this.config.network);
4550
await this.scanner.launch();
4651

src/attachSDK.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
GearboxSDK,
88
VERSION_RANGE_310,
99
} from "@gearbox-protocol/sdk";
10+
import { BotsPlugin } from "@gearbox-protocol/sdk/plugins/bots";
1011
import type { Transport } from "viem";
1112
import type { Config } from "./config/index.js";
1213
import { DI } from "./di.js";
@@ -72,6 +73,9 @@ export default async function attachSDK(): Promise<ICreditAccountsService> {
7273
networkType: config.network,
7374
// we need prices to calculate things like numsplits
7475
ignoreUpdateablePrices: false,
76+
plugins: {
77+
bots: new BotsPlugin(false),
78+
},
7579
redstone: {
7680
historicTimestamp: optimisticTimestamp,
7781
gateways: config.redstoneGateways,

src/config/deleverage-liquidator.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,6 @@ export const DeleverageLiquidatorSchema = z.object({
1313
description: "Liquidator mode (full/partial/batch/deleverage)",
1414
env: "LIQUIDATION_MODE",
1515
}),
16-
/**
17-
* Address of the partial liquidation bot (for deleverage)
18-
*/
19-
partialLiquidationBot: ZodAddress()
20-
// .default("0xc73a6741c77164dd06ff6edb09e8374abdf75c19")
21-
.register(zommandRegistry, {
22-
flags: "--partial-liquidation-bot <address>",
23-
description: "Address of the partial liquidation bot (for deleverage)",
24-
env: "PARTIAL_LIQUIDATION_BOT",
25-
}),
2616
});
2717

2818
export type DeleverageLiquidatorSchema = z.infer<

src/di.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const Injectables = {
1414
Scanner: "Scanner",
1515
Swapper: "Swapper",
1616
Transport: "Transport",
17+
Deleverage: "Deleverage",
1718
} as const;
1819

1920
export const DI = Object.assign(
@@ -34,6 +35,7 @@ export const DI = Object.assign(
3435
Scanner: [];
3536
Swapper: [];
3637
Transport: [];
38+
Deleverage: [];
3739
}>(),
3840
Injectables,
3941
);

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import "./services/Client.js";
33
import "./services/HealthCheckerService.js";
44
import "./services/Scanner.js";
5+
import "./services/DeleverageService.js";
56
import "./services/liquidate/index.js";
67
import "./services/output/index.js";
78
import "./services/notifier/index.js";

src/services/DeleverageService.ts

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import {
2+
type CreditAccountData,
3+
type GearboxSDK,
4+
type ICreditAccountsService,
5+
isVersionRange,
6+
VERSION_RANGE_310,
7+
} from "@gearbox-protocol/sdk";
8+
import { iBotListV310Abi } from "@gearbox-protocol/sdk/abi/310/generated";
9+
import type {
10+
BotsPlugin,
11+
BotParameters as TBotParameters,
12+
} from "@gearbox-protocol/sdk/plugins/bots";
13+
import type { Address } from "viem";
14+
import type { Config } from "../config/index.js";
15+
import { DI } from "../di.js";
16+
import { type ILogger, Logger } from "../log/index.js";
17+
import { DELEVERAGE_PERMISSIONS } from "../utils/permissions.js";
18+
import type { StatusCode } from "../utils/status.js";
19+
import type Client from "./Client.js";
20+
21+
export interface BotParameters extends TBotParameters {
22+
address: Address;
23+
}
24+
25+
export interface DeleverageStatus {
26+
status: StatusCode;
27+
bots: Array<{ address: Address; status: StatusCode }>;
28+
}
29+
30+
@DI.Injectable(DI.Deleverage)
31+
export default class DeleverageService {
32+
@Logger("DeleverageService")
33+
log!: ILogger;
34+
35+
@DI.Inject(DI.Config)
36+
config!: Config;
37+
38+
@DI.Inject(DI.CreditAccountService)
39+
caService!: ICreditAccountsService;
40+
41+
@DI.Inject(DI.Client)
42+
client!: Client;
43+
44+
#bots: BotParameters[] = [];
45+
46+
public async launch(): Promise<void> {
47+
if (this.config.liquidationMode !== "deleverage") {
48+
return;
49+
}
50+
const bots =
51+
await this.sdk.plugins.bots.findDeployedPartialLiquidationBots();
52+
this.#bots = bots.entries().map(([address, bot]) => ({
53+
...bot,
54+
address,
55+
}));
56+
this.log.info(
57+
`Found ${this.#bots.length} deployed partial liquidation bots`,
58+
);
59+
}
60+
61+
public get bots(): BotParameters[] {
62+
return this.#bots;
63+
}
64+
65+
public get bot(): BotParameters {
66+
// TODO: support multiple bots
67+
return this.#bots[0];
68+
}
69+
70+
public async filterDeleverageAccounts(
71+
accounts_: CreditAccountData[],
72+
blockNumber?: bigint,
73+
): Promise<CreditAccountData[]> {
74+
if (this.config.optimistic) {
75+
return accounts_;
76+
}
77+
// TODO: support multiple bots
78+
const bot = this.#bots[0];
79+
80+
const accounts = accounts_.filter(ca => {
81+
const cm = this.caService.sdk.marketRegister.findCreditManager(
82+
ca.creditManager,
83+
);
84+
return isVersionRange(cm.creditFacade.version, VERSION_RANGE_310);
85+
});
86+
87+
const res = await this.client.pub.multicall({
88+
contracts: accounts.map(ca => {
89+
const cm = this.caService.sdk.marketRegister.findCreditManager(
90+
ca.creditManager,
91+
);
92+
return {
93+
address: cm.creditFacade.botList,
94+
abi: iBotListV310Abi,
95+
functionName: "getBotStatus",
96+
args: [bot, ca.creditAccount],
97+
} as const;
98+
}),
99+
allowFailure: true,
100+
blockNumber,
101+
});
102+
const result: CreditAccountData[] = [];
103+
let errored = 0;
104+
for (let i = 0; i < accounts.length; i++) {
105+
const ca = accounts[i];
106+
const r = res[i];
107+
if (r.status === "success") {
108+
const [permissions, forbidden] = r.result;
109+
if (
110+
!!permissions &&
111+
!forbidden &&
112+
(permissions & DELEVERAGE_PERMISSIONS) === DELEVERAGE_PERMISSIONS
113+
) {
114+
result.push(ca);
115+
}
116+
} else if (r.status === "failure") {
117+
errored++;
118+
}
119+
}
120+
this.log.debug(
121+
{ errored, before: accounts_.length, after: result.length },
122+
"filtered accounts for deleverage",
123+
);
124+
return result;
125+
}
126+
127+
private get sdk(): GearboxSDK<{ bots: BotsPlugin }> {
128+
return this.caService.sdk as GearboxSDK<{ bots: BotsPlugin }>;
129+
}
130+
131+
public get status(): DeleverageStatus | undefined {
132+
if (this.config.liquidationMode !== "deleverage") {
133+
return undefined;
134+
}
135+
return {
136+
status: this.bots.length === 1 ? "healthy" : "alert",
137+
bots: this.bots.map(b => ({
138+
address: b.address,
139+
status: "healthy",
140+
})),
141+
};
142+
}
143+
}

src/services/HealthCheckerService.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { Logger } from "../log/index.js";
1414
import { maxStatusCode, type StatusCode } from "../utils/index.js";
1515
import version from "../version.js";
1616
import type Client from "./Client.js";
17+
import type DeleverageService from "./DeleverageService.js";
1718
import type { Scanner } from "./Scanner.js";
1819

1920
const nanoid = customAlphabet("1234567890abcdef", 8);
@@ -32,6 +33,9 @@ export default class HealthCheckerService {
3233
@DI.Inject(DI.CreditAccountService)
3334
caService!: ICreditAccountsService;
3435

36+
@DI.Inject(DI.Deleverage)
37+
deleverage!: DeleverageService;
38+
3539
@DI.Inject(DI.Client)
3640
client!: Client;
3741

@@ -113,6 +117,7 @@ export default class HealthCheckerService {
113117
? "alert"
114118
: "healthy") as StatusCode,
115119
},
120+
deleverage: this.deleverage.status,
116121
providers: (
117122
this.sdk.client as unknown as PublicClient<
118123
Transport<"revolver", RevolverTransportValue>
@@ -124,6 +129,7 @@ export default class HealthCheckerService {
124129
result.timestamp.status,
125130
result.balance?.status,
126131
result.liquidatableAccounts.status,
132+
result.deleverage?.status,
127133
);
128134
return result;
129135
}

src/services/PartialLiquidationBotV310Contract.ts

Lines changed: 0 additions & 67 deletions
This file was deleted.

0 commit comments

Comments
 (0)