Skip to content

Commit 787be47

Browse files
committed
Merge remote-tracking branch 'origin/beta'
2 parents 13d4744 + 4a34f90 commit 787be47

24 files changed

+1943
-1380
lines changed

biome.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"$schema": "https://biomejs.dev/schemas/2.3.4/schema.json",
2+
"$schema": "https://biomejs.dev/schemas/2.3.5/schema.json",
33
"extends": ["@gearbox-protocol/biome-config"],
44
"vcs": {
55
"enabled": true,

package.json

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,22 @@
1717
},
1818
"dependencies": {
1919
"node-pty": "^1.0.0",
20+
"oblivious-set": "^2.0.0",
2021
"pino-pretty": "^13.1.2"
2122
},
2223
"devDependencies": {
23-
"@aws-sdk/client-s3": "^3.926.0",
24-
"@biomejs/biome": "^2.3.4",
24+
"@aws-sdk/client-s3": "^3.930.0",
25+
"@biomejs/biome": "^2.3.5",
2526
"@commander-js/extra-typings": "^14.0.0",
2627
"@commitlint/cli": "^20.1.0",
2728
"@commitlint/config-conventional": "^20.0.0",
28-
"@gearbox-protocol/biome-config": "^1.0.10",
29-
"@gearbox-protocol/cli-utils": "^5.58.2",
29+
"@gearbox-protocol/biome-config": "^1.0.11",
30+
"@gearbox-protocol/cli-utils": "^5.59.2",
3031
"@gearbox-protocol/liquidator-contracts": "^1.36.0-experimental.41",
3132
"@gearbox-protocol/liquidator-v2-contracts": "^2.4.0",
32-
"@gearbox-protocol/sdk": "10.7.1",
33+
"@gearbox-protocol/sdk": "11.1.1",
3334
"@gearbox-protocol/types": "^1.14.8",
34-
"@types/node": "^24.10.0",
35+
"@types/node": "^24.10.1",
3536
"@uniswap/sdk-core": "^7.9.0",
3637
"@uniswap/v3-sdk": "^3.26.0",
3738
"@vlad-yakovlev/telegram-md": "^2.1.0",
@@ -40,15 +41,15 @@
4041
"axios-retry": "^4.5.0",
4142
"date-fns": "^4.1.0",
4243
"di-at-home": "^0.0.7",
43-
"esbuild": "^0.25.12",
44+
"esbuild": "^0.27.0",
4445
"husky": "^9.1.7",
4546
"lint-staged": "^16.2.6",
4647
"nanoid": "^5.1.6",
4748
"node-pty": "^1.0.0",
4849
"pino": "^10.1.0",
4950
"tsx": "4.20.6",
5051
"typescript": "^5.9.3",
51-
"viem": "^2.38.6",
52+
"viem": "^2.39.0",
5253
"vitest": "^4.0.8",
5354
"zod": "^4.1.12"
5455
},

src/config/common.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,18 @@ export const CommonSchema = z.object({
414414
"Telegram channel where bot will post non-critical notifications",
415415
env: "TELEGRAM_NOTIFICATIONS_CHANNEL",
416416
}),
417+
/**
418+
* Notification cooldown in minutes
419+
*/
420+
notificationCooldown: z.coerce
421+
.number()
422+
.nonnegative()
423+
.default(4 * 60)
424+
.register(zommandRegistry, {
425+
flags: "--notification-cooldown <cooldown>",
426+
description: "Notification cooldown in minutes",
427+
env: "NOTIFICATION_COOLDOWN",
428+
}),
417429
});
418430

419431
export type CommonSchema = z.infer<typeof CommonSchema>;

src/config/full-liquidator.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { zommandRegistry } from "@gearbox-protocol/cli-utils";
1+
import { boolLike, zommandRegistry } from "@gearbox-protocol/cli-utils";
22
import { z } from "zod/v4";
33
import { CommonSchema } from "./common.js";
44

@@ -12,6 +12,18 @@ export const FullLiquidatorSchema = z.object({
1212
description: "Liquidator mode (full/partial/batch/deleverage)",
1313
env: "LIQUIDATION_MODE",
1414
}),
15+
/**
16+
* Whether we should apply loss policy on full liquidation of accounts with bad debt
17+
*/
18+
lossPolicy: z
19+
.enum(["only", "never", "fallback"])
20+
.default("never")
21+
.register(zommandRegistry, {
22+
flags: "--loss-policy <when>",
23+
description:
24+
"Whether we should apply loss policy on full liquidation of accounts with bad debt",
25+
env: "LOSS_POLICY",
26+
}),
1527
});
1628

1729
export type FullLiquidatorSchema = z.infer<typeof FullLiquidatorSchema>;

src/log/index.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,26 @@ import pino from "pino";
55
import { DI } from "../di.js";
66

77
@DI.Factory(DI.Logger)
8-
class LoggerFactory implements IFactory<ILogger, [string]> {
8+
export class LoggerFactory implements IFactory<ILogger, [string]> {
99
#logger: ILogger;
10+
static #logContext: Record<string, any> = {};
11+
12+
public static setLogContext(context: Record<string, any>): void {
13+
LoggerFactory.#logContext = context;
14+
}
15+
16+
public static clearLogContext(): void {
17+
LoggerFactory.#logContext = {};
18+
}
1019

1120
constructor() {
1221
const executionId = process.env.EXECUTION_ID?.split(":").pop();
1322
this.#logger = pino({
1423
level: process.env.LOG_LEVEL ?? "debug",
1524
base: { executionId },
25+
mixin: () => ({
26+
...LoggerFactory.#logContext,
27+
}),
1628
formatters: {
1729
bindings: () => ({}),
1830
level: label => {

src/services/Client.ts

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { nextTick } from "node:process";
22

3-
import { chains, PERCENTAGE_FACTOR } from "@gearbox-protocol/sdk";
3+
import { chains, formatBN, PERCENTAGE_FACTOR } from "@gearbox-protocol/sdk";
44
import type {
55
AnvilClient,
66
AnvilNodeInfo,
@@ -14,6 +14,7 @@ import type {
1414
ContractFunctionArgs,
1515
ContractFunctionName,
1616
EncodeFunctionDataParameters,
17+
FeeValuesEIP1559,
1718
PrivateKeyAccount,
1819
PublicClient,
1920
SimulateContractParameters,
@@ -68,6 +69,8 @@ export default class Client {
6869

6970
#balance?: { value: bigint; status: StatusCode };
7071

72+
#gasFees: { maxFeePerGas?: bigint; maxPriorityFeePerGas?: bigint } = {};
73+
7174
public async launch(): Promise<void> {
7275
const { chainId, network, optimistic, privateKey, pollingInterval } =
7376
this.config;
@@ -110,23 +113,27 @@ export default class Client {
110113
this.logger.debug("running on real rpc");
111114
}
112115
await this.#checkBalance();
116+
if (this.config.optimistic) {
117+
this.#gasFees = await this.pub.estimateFeesPerGas();
118+
this.logger.debug(this.#gasFees, "optimistic gas fees");
119+
}
113120
}
114121

115122
public async liquidate(
116123
request: SimulateContractReturnType["request"],
117-
logger: ILogger,
118124
): Promise<TransactionReceipt> {
119125
if (this.config.dryRun && !this.config.optimistic) {
120126
throw new Error("dry run mode");
121127
}
122-
logger.debug("sending liquidation tx");
128+
this.logger.debug("sending liquidation tx");
123129
const { abi, address, args, dataSuffix, functionName, ...rest } = request;
124130
const data = encodeFunctionData({
125131
abi,
126132
args,
127133
functionName,
128134
} as EncodeFunctionDataParameters);
129135
const req = await this.wallet.prepareTransactionRequest({
136+
...(this.#gasFees as any),
130137
...rest,
131138
to: request.address,
132139
data,
@@ -135,25 +142,39 @@ export default class Client {
135142
if (maxPriorityFeePerGas && maxFeePerGas) {
136143
req.maxPriorityFeePerGas = 10n * maxPriorityFeePerGas;
137144
req.maxFeePerGas = 2n * maxFeePerGas + req.maxPriorityFeePerGas;
138-
logger.debug(
139-
{
140-
maxFeePerGas: req.maxFeePerGas,
141-
maxPriorityFeePerGas: req.maxPriorityFeePerGas,
142-
},
143-
`increase gas fees`,
144-
);
145145
}
146146
if (gas) {
147147
req.gas = (gas * (GAS_X + PERCENTAGE_FACTOR)) / PERCENTAGE_FACTOR;
148148
}
149+
const txCost = req.gas * req.maxFeePerGas + (req.value ?? 0n);
150+
this.logger.debug(
151+
{
152+
maxFeePerGas: req.maxFeePerGas,
153+
maxPriorityFeePerGas: req.maxPriorityFeePerGas,
154+
gas: req.gas,
155+
txCost,
156+
},
157+
`increase gas fees`,
158+
);
159+
160+
if (this.#balance && txCost > this.#balance.value) {
161+
this.logger.warn(
162+
{
163+
txCost,
164+
balance: this.#balance.value,
165+
},
166+
`transaction cost ${formatBN(txCost, 18)} exceeds balance (${formatBN(this.#balance.value, 18)} ETH)`,
167+
);
168+
}
169+
149170
const serializedTransaction = await this.wallet.signTransaction(req);
150171
const hash = await this.wallet.sendRawTransaction({
151172
serializedTransaction,
152173
});
153174
const { data: _data, to, value, account, ...params } = req;
154-
logger.debug({ hash, ...params }, "sent transaction");
175+
this.logger.debug({ hash, ...params }, "sent transaction");
155176
const receipt = await this.#waitForTransactionReceipt(hash);
156-
logger.debug({ hash, status: receipt.status }, "received receipt");
177+
this.logger.debug({ hash, status: receipt.status }, "received receipt");
157178
if (!this.config.optimistic) {
158179
nextTick(() => {
159180
this.#checkBalance().catch(() => {});

src/services/Scanner.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,11 @@ export class Scanner {
7474
? MAX_UINT256 // to discover account with underlying only, whose HF are not affected by zero-let script
7575
: this.config.hfThreshold;
7676
this.#minHealthFactor = this.config.optimistic ? 0n : 1n;
77-
if (this.config.optimistic && this.config.liquidationMode === "partial") {
77+
if (
78+
(this.config.optimistic && this.config.liquidationMode === "partial") ||
79+
(this.config.liquidationMode === "full" &&
80+
this.config.lossPolicy === "only")
81+
) {
7882
this.#maxHealthFactor = MAX_UINT256;
7983
}
8084
if (this.config.liquidationMode === "deleverage") {

src/services/liquidate/AbstractLiquidator.ts

Lines changed: 7 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@ import type {
33
GearboxSDK,
44
ICreditAccountsService,
55
MultiCall,
6-
VersionRange,
76
} from "@gearbox-protocol/sdk";
8-
import { filterDustUSD, isVersionRange } from "@gearbox-protocol/sdk";
7+
import { filterDustUSD } from "@gearbox-protocol/sdk";
98
import { ierc20MetadataAbi } from "@gearbox-protocol/types/abi";
109
import type { OptimisticResult } from "@gearbox-protocol/types/optimist";
1110
import type { Address, TransactionReceipt } from "viem";
@@ -19,10 +18,13 @@ import type Client from "../Client.js";
1918
import { type INotifier, StartedMessage } from "../notifier/index.js";
2019
import type { IOptimisticOutputWriter } from "../output/index.js";
2120
import type { ISwapper } from "../swap/index.js";
21+
import AccountHelper from "./AccountHelper.js";
2222
import type { OptimisticResults } from "./OptimisiticResults.js";
23-
import type { StrategyPreview } from "./types.js";
23+
import type { LiquidationPreview } from "./types.js";
2424

25-
export default abstract class AbstractLiquidator<TConfig extends CommonSchema> {
25+
export default abstract class AbstractLiquidator<
26+
TConfig extends CommonSchema,
27+
> extends AccountHelper {
2628
@Logger("Liquidator")
2729
logger!: ILogger;
2830

@@ -81,7 +83,7 @@ export default abstract class AbstractLiquidator<TConfig extends CommonSchema> {
8183

8284
protected updateAfterPreview(
8385
result: OptimisticResult<bigint>,
84-
preview: StrategyPreview,
86+
preview: LiquidationPreview,
8587
): OptimisticResult<bigint> {
8688
return {
8789
...result,
@@ -137,16 +139,6 @@ export default abstract class AbstractLiquidator<TConfig extends CommonSchema> {
137139
return { eth, underlying };
138140
}
139141

140-
protected caLogger(ca: CreditAccountData): ILogger {
141-
const cm = this.sdk.marketRegister.findCreditManager(ca.creditManager);
142-
return this.logger.child({
143-
account: ca.creditAccount,
144-
borrower: ca.owner,
145-
manager: cm.name,
146-
hf: ca.healthFactor,
147-
});
148-
}
149-
150142
protected get sdk(): GearboxSDK {
151143
return this.creditAccountService.sdk;
152144
}
@@ -157,14 +149,4 @@ export default abstract class AbstractLiquidator<TConfig extends CommonSchema> {
157149
}
158150
return this.#errorHandler;
159151
}
160-
161-
protected checkAccountVersion(
162-
ca: CreditAccountData,
163-
v: VersionRange,
164-
): boolean {
165-
return isVersionRange(
166-
this.sdk.contracts.mustGet(ca.creditFacade).version,
167-
v,
168-
);
169-
}
170152
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import {
2+
type CreditAccountData,
3+
type GearboxSDK,
4+
isVersionRange,
5+
PERCENTAGE_FACTOR,
6+
type VersionRange,
7+
} from "@gearbox-protocol/sdk";
8+
import type { ILogger } from "../../log/index.js";
9+
10+
export default abstract class AccountHelper {
11+
protected abstract sdk: GearboxSDK;
12+
public abstract logger: ILogger;
13+
14+
protected checkAccountVersion(
15+
ca: CreditAccountData,
16+
v: VersionRange,
17+
): boolean {
18+
return isVersionRange(
19+
this.sdk.contracts.mustGet(ca.creditFacade).version,
20+
v,
21+
);
22+
}
23+
24+
/**
25+
* Whether account's total value (minus liquidator's premium) is below its outstanding debt
26+
* @see https://github.com/Gearbox-protocol/core-v3/blob/5144d61af7d117f86d3fa9b4e2aa05535e2e5433/contracts/credit/CreditFacadeV3.sol#L986-L990
27+
* @param ca
28+
* @returns
29+
*/
30+
protected hasBadDebt(ca: CreditAccountData): boolean {
31+
const { creditManager } = this.sdk.marketRegister.findCreditManager(
32+
ca.creditManager,
33+
);
34+
return (
35+
ca.totalValue * BigInt(creditManager.liquidationDiscount) <
36+
(ca.debt + ca.accruedInterest) * PERCENTAGE_FACTOR
37+
);
38+
}
39+
}

src/services/liquidate/BatchLiquidator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ export default class BatchLiquidator
224224
functionName: "liquidateBatch",
225225
args: [liquidateBatchInput, this.client.address],
226226
});
227-
const receipt = await this.client.liquidate(request as any, this.logger); // TODO: types
227+
const receipt = await this.client.liquidate(request as any); // TODO: types
228228
this.logger.debug(
229229
{ tx: receipt.transactionHash, gasUsed: receipt.gasUsed },
230230
"liquidated batch",

0 commit comments

Comments
 (0)