Skip to content

Commit d5329fd

Browse files
committed
fix: allow multiple strategies in optimistic mode
1 parent de2afb1 commit d5329fd

File tree

2 files changed

+123
-95
lines changed

2 files changed

+123
-95
lines changed

src/services/liquidate/AbstractLiquidator.ts

Lines changed: 7 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@ import type {
22
CreditAccountData,
33
GearboxSDK,
44
ICreditAccountsService,
5-
MultiCall,
65
} from "@gearbox-protocol/sdk";
76
import { filterDustUSD } from "@gearbox-protocol/sdk";
87
import type { OptimisticResult } from "@gearbox-protocol/types/optimist";
9-
import { type Address, erc20Abi, type TransactionReceipt } from "viem";
8+
import { type Address, erc20Abi } from "viem";
109

1110
import type { CommonSchema, LiqduiatorConfig } from "../../config/index.js";
1211
import { DI } from "../../di.js";
@@ -19,7 +18,11 @@ import type { IOptimisticOutputWriter } from "../output/index.js";
1918
import type { ISwapper } from "../swap/index.js";
2019
import AccountHelper from "./AccountHelper.js";
2120
import type { OptimisticResults } from "./OptimisiticResults.js";
22-
import type { LiquidationPreview } from "./types.js";
21+
22+
export interface ExecutorBalance {
23+
eth: bigint;
24+
underlying: bigint;
25+
}
2326

2427
export default abstract class AbstractLiquidator<
2528
TConfig extends CommonSchema,
@@ -80,48 +83,9 @@ export default abstract class AbstractLiquidator<
8083
};
8184
}
8285

83-
protected updateAfterPreview(
84-
result: OptimisticResult<bigint>,
85-
preview: LiquidationPreview,
86-
): OptimisticResult<bigint> {
87-
return {
88-
...result,
89-
assetOut: preview.assetOut,
90-
amountOut: preview.amountOut,
91-
flashLoanAmount: preview.flashLoanAmount,
92-
calls: preview.calls as MultiCall[],
93-
pathAmount: preview.underlyingBalance,
94-
callsHuman: this.creditAccountService.sdk.parseMultiCall(
95-
preview.calls as MultiCall[],
96-
),
97-
};
98-
}
99-
100-
protected async updateAfterLiquidation(
101-
result: OptimisticResult<bigint>,
102-
acc: CreditAccountData,
103-
underlyingBalanceBefore: bigint,
104-
receipt: TransactionReceipt,
105-
): Promise<OptimisticResult<bigint>> {
106-
const ca = await this.creditAccountService.getCreditAccountData(
107-
acc.creditAccount,
108-
);
109-
if (!ca) {
110-
throw new Error(`account ${acc.creditAccount} not found`);
111-
}
112-
result.balancesAfter = filterDustUSD({ account: ca, sdk: this.sdk });
113-
result.hfAfter = ca.healthFactor;
114-
115-
const balanceAfter = await this.getExecutorBalance(ca.underlying);
116-
result.gasUsed = receipt.gasUsed;
117-
result.liquidatorPremium =
118-
balanceAfter.underlying - underlyingBalanceBefore;
119-
return result;
120-
}
121-
12286
protected async getExecutorBalance(
12387
underlyingToken: Address,
124-
): Promise<{ eth: bigint; underlying: bigint }> {
88+
): Promise<ExecutorBalance> {
12589
// TODO: is this needed?
12690
const isWeth = this.sdk.tokensMeta.symbol(underlyingToken) === "WETH";
12791
const eth = await this.client.pub.getBalance({

src/services/liquidate/SingularLiquidator.ts

Lines changed: 116 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
1-
import type { CreditAccountData } from "@gearbox-protocol/sdk";
2-
import type { OptimisticResult } from "@gearbox-protocol/types/optimist";
3-
import type { Hex } from "viem";
1+
import {
2+
type CreditAccountData,
3+
filterDustUSD,
4+
type MultiCall,
5+
} from "@gearbox-protocol/sdk";
6+
import type {
7+
OptimisticResult,
8+
PartialLiquidationCondition,
9+
} from "@gearbox-protocol/types/optimist";
10+
import type { Hex, TransactionReceipt } from "viem";
411
import type {
512
CommonSchema,
613
FullLiquidatorSchema,
714
PartialLiquidatorSchema,
815
} from "../../config/index.js";
16+
import { TransactionRevertedError } from "../../errors/index.js";
917
import { LoggerFactory } from "../../log/index.js";
1018
import {
1119
LiquidationErrorMessage,
@@ -15,7 +23,17 @@ import {
1523
import AbstractLiquidator from "./AbstractLiquidator.js";
1624
import LiquidationStrategyFull from "./LiquidationStrategyFull.js";
1725
import LiquidationStrategyPartial from "./LiquidationStrategyPartial.js";
18-
import type { ILiquidationStrategy, ILiquidatorService } from "./types.js";
26+
import type {
27+
ILiquidationStrategy,
28+
ILiquidatorService,
29+
LiquidationPreview,
30+
} from "./types.js";
31+
32+
type OptimisticStrategyResult = {
33+
preview?: LiquidationPreview;
34+
partialLiquidationCondition?: PartialLiquidationCondition<bigint>;
35+
receipt?: TransactionReceipt;
36+
} & ({ success: true } | { success: false; error: Error });
1937

2038
export default class SingularLiquidator
2139
extends AbstractLiquidator<CommonSchema>
@@ -188,71 +206,117 @@ export default class SingularLiquidator
188206
},
189207
"liquidating account",
190208
);
191-
let snapshotId: Hex | undefined;
192-
let result = this.newOptimisticResult(acc);
209+
const result = this.newOptimisticResult(acc);
193210
const start = Date.now();
194-
try {
195-
const balanceBefore = await this.getExecutorBalance(acc.underlying);
196-
const mlRes = await this.#optimisticStrategy.makeLiquidatable(acc);
197-
snapshotId = mlRes.snapshotId;
198-
result.partialLiquidationCondition = mlRes.partialLiquidationCondition;
199-
this.logger.debug({ snapshotId }, "previewing...");
200-
const preview = await this.#optimisticStrategy.preview(acc);
201-
result = this.updateAfterPreview(result, preview);
202-
this.logger.debug({ pathHuman: result.callsHuman }, "path found");
203-
204-
const { request } = await this.#optimisticStrategy.simulate(acc, preview);
211+
let strategyResult: OptimisticStrategyResult | undefined;
205212

206-
// snapshotId might be present if we had to setup liquidation conditions for single account
207-
// otherwise, not write requests has been made up to this point, and it's safe to take snapshot now
208-
if (!snapshotId) {
209-
snapshotId = await this.client.anvil.snapshot();
213+
const balanceBefore = await this.getExecutorBalance(acc.underlying);
214+
for (const s of this.#strategies) {
215+
if (!s.isApplicable(acc)) {
216+
this.logger.debug(`strategy ${s.name} is not applicable`);
217+
continue;
210218
}
211-
// ------ Actual liquidation (write request start here) -----
212-
const receipt = await this.client.liquidate(request);
213-
this.logger.debug(`Liquidation tx hash: ${receipt.transactionHash}`);
214-
result.isError = receipt.status === "reverted";
215-
this.logger.debug(
216-
`Liquidation tx receipt: status=${receipt.status}, gas=${receipt.cumulativeGasUsed.toString()}`,
217-
);
218-
// ------ End of actual liquidation
219-
result = await this.updateAfterLiquidation(
220-
result,
221-
acc,
222-
balanceBefore.underlying,
223-
receipt,
219+
const strategyResult = await this.#liquidateOneOptimisticStrategy(acc, s);
220+
if (strategyResult.success) {
221+
break;
222+
}
223+
}
224+
225+
if (strategyResult) {
226+
result.partialLiquidationCondition =
227+
strategyResult.partialLiquidationCondition;
228+
result.assetOut = strategyResult.preview?.assetOut;
229+
result.amountOut = strategyResult.preview?.amountOut;
230+
result.flashLoanAmount = strategyResult.preview?.flashLoanAmount;
231+
result.calls = strategyResult.preview?.calls as MultiCall[];
232+
result.pathAmount = strategyResult.preview?.underlyingBalance ?? 0n;
233+
result.callsHuman = this.creditAccountService.sdk.parseMultiCall([
234+
...(strategyResult.preview?.calls ?? []),
235+
]);
236+
const ca = await this.creditAccountService.getCreditAccountData(
237+
acc.creditAccount,
224238
);
225-
// swap underlying back to ETH
239+
if (!ca) {
240+
throw new Error(`account ${acc.creditAccount} not found`);
241+
}
242+
result.balancesAfter = filterDustUSD({ account: ca, sdk: this.sdk });
243+
result.hfAfter = ca.healthFactor;
244+
result.gasUsed = strategyResult.receipt?.gasUsed ?? 0n;
245+
result.isError = !strategyResult.success;
246+
226247
await this.swapper.swap(
227248
acc.underlying,
228-
balanceBefore.underlying + BigInt(result.liquidatorPremium),
249+
balanceBefore.underlying + result.liquidatorPremium,
229250
);
230251
const balanceAfter = await this.getExecutorBalance(acc.underlying);
252+
result.liquidatorPremium =
253+
balanceAfter.underlying - balanceBefore.underlying;
231254
result.liquidatorProfit = balanceAfter.eth - balanceBefore.eth;
232-
} catch (e: any) {
233-
const decoded = await this.errorHandler.explain(e, acc, true);
234-
result.traceFile = decoded.traceFile;
235-
result.error = `cannot liquidate: ${decoded.longMessage}`.replaceAll(
236-
"\n",
237-
"\\n",
238-
);
239-
this.logger.error(`cannot liquidate: ${decoded.shortMessage}`);
255+
256+
if (strategyResult?.success === false) {
257+
const decoded = await this.errorHandler.explain(
258+
strategyResult.error,
259+
acc,
260+
true,
261+
);
262+
result.traceFile = decoded.traceFile;
263+
result.error = `cannot liquidate: ${decoded.longMessage}`.replaceAll(
264+
"\n",
265+
"\\n",
266+
);
267+
this.logger.error(`cannot liquidate: ${decoded.shortMessage}`);
268+
}
269+
} else {
270+
result.isError = true;
271+
result.error = "no applicable strategy found";
272+
this.logger.error("no applicable strategy found");
240273
}
241274

242275
result.duration = Date.now() - start;
243276
this.optimistic.push(result);
244277

245-
if (snapshotId) {
246-
await this.client.anvil.revert({ id: snapshotId });
247-
}
248-
249278
return result;
250279
}
251280

252-
get #optimisticStrategy(): ILiquidationStrategy {
253-
if (this.config.optimistic && this.#strategies.length !== 1) {
254-
throw new Error("optimistic mode requires exactly one strategy");
281+
async #liquidateOneOptimisticStrategy(
282+
acc: CreditAccountData,
283+
strategy: ILiquidationStrategy,
284+
): Promise<OptimisticStrategyResult> {
285+
let snapshotId: Hex | undefined;
286+
const logger = this.logger.child({ strategy: strategy.name });
287+
let result: OptimisticStrategyResult = { success: true };
288+
try {
289+
const mlRes = await strategy.makeLiquidatable(acc);
290+
snapshotId = mlRes.snapshotId;
291+
result.partialLiquidationCondition = mlRes.partialLiquidationCondition;
292+
logger.debug({ snapshotId, strategy: strategy.name }, "previewing...");
293+
result.preview = await strategy.preview(acc);
294+
logger.debug("preview successful");
295+
296+
const { request } = await strategy.simulate(acc, result.preview);
297+
logger.debug("simulate successful");
298+
299+
// snapshotId might be present if we had to setup liquidation conditions for single account
300+
// otherwise, not write requests has been made up to this point, and it's safe to take snapshot now
301+
if (!snapshotId) {
302+
snapshotId = await this.client.anvil.snapshot();
303+
}
304+
// ------ Actual liquidation (write request start here) -----
305+
result.receipt = await this.client.liquidate(request);
306+
logger.debug(
307+
`Liquidation tx receipt: hash=${result.receipt.transactionHash}, status=${result.receipt.status}, gas=${result.receipt.cumulativeGasUsed.toString()}`,
308+
);
309+
if (result.receipt.status !== "success") {
310+
throw new TransactionRevertedError(result.receipt);
311+
}
312+
} catch (e) {
313+
logger.error(e, "strategy failed");
314+
result = { ...result, success: false, error: e as Error };
315+
} finally {
316+
if (snapshotId) {
317+
await this.client.anvil.revert({ id: snapshotId });
318+
}
255319
}
256-
return this.#strategies[0];
320+
return result;
257321
}
258322
}

0 commit comments

Comments
 (0)