Skip to content

Commit ce7cb7f

Browse files
committed
feat: llamathena workaround
1 parent 898df33 commit ce7cb7f

File tree

7 files changed

+1261
-265
lines changed

7 files changed

+1261
-265
lines changed

package.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,21 @@
2121
"pino-pretty": "^13.1.3"
2222
},
2323
"devDependencies": {
24-
"@aws-sdk/client-s3": "^3.984.0",
24+
"@aws-sdk/client-s3": "^3.986.0",
2525
"@biomejs/biome": "^2.3.14",
2626
"@commander-js/extra-typings": "^14.0.0",
2727
"@commitlint/cli": "^20.4.1",
2828
"@commitlint/config-conventional": "^20.4.1",
2929
"@gearbox-protocol/biome-config": "^1.0.20",
30-
"@gearbox-protocol/cli-utils": "^5.69.3",
30+
"@gearbox-protocol/cli-utils": "^5.70.1",
3131
"@gearbox-protocol/liquidator-contracts": "^1.36.0-experimental.67",
3232
"@gearbox-protocol/liquidator-v2-contracts": "^2.4.0",
33-
"@gearbox-protocol/sdk": "12.6.6",
33+
"@gearbox-protocol/sdk": "12.6.7",
3434
"@gearbox-protocol/types": "^1.14.8",
35-
"@types/node": "^25.2.1",
35+
"@types/node": "^25.2.2",
3636
"@vlad-yakovlev/telegram-md": "^2.1.0",
3737
"abitype": "^1.2.3",
38-
"axios": "^1.13.4",
38+
"axios": "^1.13.5",
3939
"axios-retry": "^4.5.0",
4040
"date-fns": "^4.1.0",
4141
"di-at-home": "^0.0.7",
@@ -44,10 +44,10 @@
4444
"lint-staged": "^16.2.7",
4545
"nanoid": "^5.1.6",
4646
"node-pty": "^1.1.0",
47-
"pino": "^10.3.0",
47+
"pino": "^10.3.1",
4848
"tsx": "4.21.0",
4949
"typescript": "^5.9.3",
50-
"viem": "^2.45.1",
50+
"viem": "^2.45.2",
5151
"vitest": "^4.0.18",
5252
"zod": "^4.3.6"
5353
},

src/config/full-liquidator.ts

Lines changed: 10 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

@@ -38,6 +38,15 @@ export const FullLiquidatorSchema = z.object({
3838
"Whether we should apply loss policy on full liquidation of accounts with bad debt",
3939
env: "LOSS_POLICY",
4040
}),
41+
/**
42+
* Flag to enable llama thena workaround to liquidate 3.0 accounts with llamathena
43+
*/
44+
llamathenaWorkaround: boolLike().optional().register(zommandRegistry, {
45+
flags: "--llamathena-workaround",
46+
description:
47+
"Flag to enable llama thena workaround to liquidate 3.0 accounts wiht llamathena",
48+
env: "LLAMATHENA_WORKAROUND",
49+
}),
4150
});
4251

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

src/services/Scanner.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,10 @@ export class Scanner {
281281
async #getExpiredCreditAccounts(
282282
blockNumber?: bigint,
283283
): Promise<CreditAccountData[]> {
284-
this.log.debug("getting expired credit accounts");
284+
this.log.debug(
285+
{ timestamp: this.caService.sdk.timestamp },
286+
"getting expired credit accounts",
287+
);
285288
const expiredCMs = new AddressSet();
286289
const expiredCmNames: string[] = [];
287290

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import {
2+
AddressSet,
3+
type CreditAccountData,
4+
type GearboxSDK,
5+
type ICreditAccountsService,
6+
PERCENTAGE_FACTOR,
7+
} from "@gearbox-protocol/sdk";
8+
import { Create2Deployer } from "@gearbox-protocol/sdk/dev";
9+
import type { Address } from "@gearbox-protocol/types/optimist";
10+
import type { Hex, SimulateContractReturnType } from "viem";
11+
import { encodeFunctionData } from "viem";
12+
import type {
13+
FullLiquidatorSchema,
14+
LiqduiatorConfig,
15+
} from "../../config/index.js";
16+
import { DI } from "../../di.js";
17+
import { errorAbis } from "../../errors/index.js";
18+
import { type ILogger, Logger } from "../../log/index.js";
19+
import type Client from "../Client.js";
20+
import AccountHelper from "./AccountHelper.js";
21+
import LlamaThenaLiquidatorJson from "./legacy/LlamaThenaLiquidator.json" with {
22+
type: "json",
23+
};
24+
import { AAVE_V3_LENDING_POOL } from "./partial/constants.js";
25+
import type {
26+
ILiquidationStrategy,
27+
LiquidationPreview,
28+
MakeLiquidatableResult,
29+
} from "./types.js";
30+
31+
interface LlamaThenaLiquidationPreview extends LiquidationPreview {
32+
underlyingAmount: bigint;
33+
minUnderlyingBack: bigint;
34+
}
35+
36+
const LLAMATHENA_TOKENS_MAINNET = new AddressSet([
37+
"0x72eD19788Bce2971A5ed6401662230ee57e254B7", // stkcvxllamathena
38+
"0x237926E55f9deee89833a42dEb92d3a6970850B4", // cvxllamathena
39+
"0xd29f8980852c2c76fC3f6E96a7Aa06E0BedCC1B1", // llamathena
40+
"0x9D39A5DE30e57443BfF2A8307A4256c8797A3497", // sUSDe
41+
]);
42+
43+
export default class LiquidationStrategyLlamaThena
44+
extends AccountHelper
45+
implements ILiquidationStrategy<LlamaThenaLiquidationPreview>
46+
{
47+
@DI.Inject(DI.CreditAccountService)
48+
creditAccountService!: ICreditAccountsService;
49+
50+
@DI.Inject(DI.Config)
51+
config!: LiqduiatorConfig<FullLiquidatorSchema>;
52+
53+
@DI.Inject(DI.Client)
54+
client!: Client;
55+
56+
@Logger("LlamaThenaStrategy")
57+
logger!: ILogger;
58+
59+
public readonly name = "llamathena";
60+
61+
#liquidator: Address | undefined;
62+
63+
public async launch(): Promise<void> {
64+
const deployer = new Create2Deployer(this.sdk, this.client.wallet);
65+
const { address } = await deployer.ensureExists({
66+
abi: LlamaThenaLiquidatorJson.abi,
67+
bytecode: LlamaThenaLiquidatorJson.bytecode.object as Hex,
68+
args: [this.owner, AAVE_V3_LENDING_POOL.Mainnet],
69+
});
70+
this.#liquidator = address;
71+
this.logger.info(`LlamaThena legacy liquidator deployed at ${address}`);
72+
}
73+
74+
public async syncState(_blockNumber: bigint): Promise<void> {}
75+
76+
public isApplicable(ca: CreditAccountData): boolean {
77+
if (
78+
this.config.network !== "Mainnet" ||
79+
!this.config.llamathenaWorkaround
80+
) {
81+
return false;
82+
}
83+
for (const { token, balance, mask } of ca.tokens) {
84+
const isEnabled = (mask & ca.enabledTokensMask) !== 0n;
85+
if (isEnabled && balance > 1n && LLAMATHENA_TOKENS_MAINNET.has(token)) {
86+
return true;
87+
}
88+
}
89+
return false;
90+
}
91+
92+
public async makeLiquidatable(
93+
ca: CreditAccountData,
94+
): Promise<MakeLiquidatableResult> {
95+
return { account: ca };
96+
}
97+
98+
public async preview(
99+
ca: CreditAccountData,
100+
): Promise<LlamaThenaLiquidationPreview> {
101+
const market = this.sdk.marketRegister.findByCreditManager(
102+
ca.creditManager,
103+
);
104+
const cm = this.sdk.marketRegister.findCreditManager(ca.creditManager);
105+
const underlying = cm.underlying;
106+
107+
const underlyingValue = market.priceOracle.convertFromUSD(
108+
underlying,
109+
ca.totalValueUSD,
110+
);
111+
112+
// underlyingAmount = underlyingValue * 9800 / PERCENTAGE_FACTOR (98%)
113+
const underlyingAmount = (underlyingValue * 9800n) / PERCENTAGE_FACTOR;
114+
// minUnderlyingBack = underlyingValue * 995 / 1000 (99.5%)
115+
const minUnderlyingBack = (underlyingValue * 995n) / 1000n;
116+
117+
const callData = encodeFunctionData({
118+
abi: LlamaThenaLiquidatorJson.abi,
119+
functionName: "liquidateCreditAccount",
120+
args: [
121+
ca.creditManager,
122+
ca.creditAccount,
123+
underlyingAmount,
124+
minUnderlyingBack,
125+
],
126+
});
127+
128+
return {
129+
calls: [{ target: this.liquidator, callData }],
130+
underlyingBalance: 0n,
131+
underlyingAmount,
132+
minUnderlyingBack,
133+
};
134+
}
135+
136+
public async simulate(
137+
account: CreditAccountData,
138+
preview: LlamaThenaLiquidationPreview,
139+
): Promise<SimulateContractReturnType<unknown[], any, any>> {
140+
const result = await this.client.pub.simulateContract({
141+
account: this.client.account,
142+
abi: [...LlamaThenaLiquidatorJson.abi, ...errorAbis],
143+
address: this.liquidator,
144+
functionName: "liquidateCreditAccount",
145+
args: [
146+
account.creditManager,
147+
account.creditAccount,
148+
preview.underlyingAmount,
149+
preview.minUnderlyingBack,
150+
],
151+
});
152+
return result as unknown as SimulateContractReturnType<unknown[], any, any>;
153+
}
154+
155+
private get liquidator(): Address {
156+
if (!this.#liquidator) {
157+
throw new Error("LlamaThena liquidator not deployed");
158+
}
159+
return this.#liquidator;
160+
}
161+
162+
protected get sdk(): GearboxSDK {
163+
return this.creditAccountService.sdk;
164+
}
165+
166+
protected get owner(): Address {
167+
return this.client.wallet.account.address;
168+
}
169+
}

src/services/liquidate/SingularLiquidator.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import AbstractLiquidator, {
2424
} from "./AbstractLiquidator.js";
2525
import LiquidationStrategyDeleverage from "./LiquidationStrategyDeleverage.js";
2626
import LiquidationStrategyFull from "./LiquidationStrategyFull.js";
27+
import LiquidationStrategyLlamaThena from "./LiquidationStrategyLlamaThena.js";
2728
import LiquidationStrategyPartial from "./LiquidationStrategyPartial.js";
2829
import type {
2930
ILiquidationStrategy,
@@ -54,6 +55,9 @@ export default class SingularLiquidator
5455
switch (liquidationMode) {
5556
case "full": {
5657
const cfg = this.config as unknown as FullLiquidatorSchema;
58+
if (cfg.llamathenaWorkaround && cfg.network === "Mainnet") {
59+
add(new LiquidationStrategyLlamaThena());
60+
}
5761
switch (cfg.lossPolicy) {
5862
case "only":
5963
add(new LiquidationStrategyFull("loss policy", true));

0 commit comments

Comments
 (0)