@aspan/sdk 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -4
- package/dist/index.d.mts +245 -8
- package/dist/index.d.ts +245 -8
- package/dist/index.js +278 -8
- package/dist/index.mjs +265 -7
- package/package.json +1 -1
- package/src/__tests__/fork.test.ts +2 -2
- package/src/abi/diamond.ts +74 -0
- package/src/abi/sApUSD.ts +100 -0
- package/src/bot/config.ts +51 -1
- package/src/bot/index.ts +69 -1
- package/src/bot/services/fee-manager.ts +303 -0
- package/src/bot/services/risk-keeper.ts +517 -0
- package/src/client.ts +215 -1
- package/src/index.ts +30 -6
- package/src/router.ts +69 -0
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Risk Keeper Service
|
|
3
|
+
* Executes risk management actions: SM2 trigger and TWAP vault cleaning
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
createPublicClient,
|
|
8
|
+
createWalletClient,
|
|
9
|
+
http,
|
|
10
|
+
type Address,
|
|
11
|
+
type PublicClient,
|
|
12
|
+
type WalletClient,
|
|
13
|
+
type Chain,
|
|
14
|
+
type Account,
|
|
15
|
+
type Hash,
|
|
16
|
+
} from "viem";
|
|
17
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
18
|
+
import { bsc } from "viem/chains";
|
|
19
|
+
import type { AspanReadClient } from "../../client";
|
|
20
|
+
import { DiamondABI } from "../../abi/diamond";
|
|
21
|
+
import { SApUSDABI } from "../../abi/sApUSD";
|
|
22
|
+
import type { SlackService } from "./slack";
|
|
23
|
+
import { SlackColors } from "./slack";
|
|
24
|
+
import type { BotConfig } from "../config";
|
|
25
|
+
import { withRetry } from "./rpc-client";
|
|
26
|
+
|
|
27
|
+
// ============ Types ============
|
|
28
|
+
|
|
29
|
+
export interface RiskKeeperConfig {
|
|
30
|
+
/** Diamond contract address */
|
|
31
|
+
diamondAddress: Address;
|
|
32
|
+
/** sApUSD vault address */
|
|
33
|
+
sApUSDAddress: Address;
|
|
34
|
+
/** DEX router address (PancakeSwap) */
|
|
35
|
+
routerAddress: Address;
|
|
36
|
+
/** WBNB address */
|
|
37
|
+
wbnbAddress: Address;
|
|
38
|
+
/** Keeper private key */
|
|
39
|
+
privateKey: `0x${string}`;
|
|
40
|
+
/** RPC URL */
|
|
41
|
+
rpcUrl: string;
|
|
42
|
+
/** Chain */
|
|
43
|
+
chain?: Chain;
|
|
44
|
+
/** SM2 cooldown in ms (default 30 min) */
|
|
45
|
+
sm2CooldownMs?: number;
|
|
46
|
+
/** SM2 max pool percentage per trigger (default 40%) */
|
|
47
|
+
sm2MaxPoolPercent?: number;
|
|
48
|
+
/** TWAP interval in ms (default 10 min) */
|
|
49
|
+
twapIntervalMs?: number;
|
|
50
|
+
/** TWAP percentage per batch (default 5%) */
|
|
51
|
+
twapPercentage?: number;
|
|
52
|
+
/** Max slippage BPS (default 50 = 0.5%) */
|
|
53
|
+
maxSlippageBps?: number;
|
|
54
|
+
/** CR threshold to start TWAP cleaning (default 13500 = 135%) */
|
|
55
|
+
twapStartCR?: bigint;
|
|
56
|
+
/** Max BNB price volatility for TWAP (default 5%) */
|
|
57
|
+
maxVolatilityPercent?: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface KeeperState {
|
|
61
|
+
lastSM2Trigger: number;
|
|
62
|
+
lastTWAPExecution: number;
|
|
63
|
+
isProcessing: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ============ Risk Keeper Service ============
|
|
67
|
+
|
|
68
|
+
export class RiskKeeperService {
|
|
69
|
+
private readonly client: AspanReadClient;
|
|
70
|
+
private readonly publicClient: PublicClient;
|
|
71
|
+
private readonly walletClient: WalletClient;
|
|
72
|
+
private readonly slack: SlackService;
|
|
73
|
+
private readonly config: BotConfig;
|
|
74
|
+
private readonly keeperConfig: RiskKeeperConfig;
|
|
75
|
+
private readonly account: Account;
|
|
76
|
+
|
|
77
|
+
private state: KeeperState = {
|
|
78
|
+
lastSM2Trigger: 0,
|
|
79
|
+
lastTWAPExecution: 0,
|
|
80
|
+
isProcessing: false,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
private sm2IntervalId?: ReturnType<typeof setInterval>;
|
|
84
|
+
private twapIntervalId?: ReturnType<typeof setInterval>;
|
|
85
|
+
|
|
86
|
+
// Defaults
|
|
87
|
+
private readonly SM2_COOLDOWN_MS: number;
|
|
88
|
+
private readonly SM2_MAX_POOL_PERCENT: number;
|
|
89
|
+
private readonly TWAP_INTERVAL_MS: number;
|
|
90
|
+
private readonly TWAP_PERCENTAGE: number;
|
|
91
|
+
private readonly MAX_SLIPPAGE_BPS: number;
|
|
92
|
+
private readonly TWAP_START_CR: bigint;
|
|
93
|
+
private readonly MAX_VOLATILITY_PERCENT: number;
|
|
94
|
+
|
|
95
|
+
// Price tracking for volatility
|
|
96
|
+
private lastBNBPrice: bigint = 0n;
|
|
97
|
+
/** Timestamp of last BNB price check (used for volatility tracking) */
|
|
98
|
+
public lastPriceTime: number = 0;
|
|
99
|
+
|
|
100
|
+
constructor(
|
|
101
|
+
client: AspanReadClient,
|
|
102
|
+
slack: SlackService,
|
|
103
|
+
botConfig: BotConfig,
|
|
104
|
+
keeperConfig: RiskKeeperConfig
|
|
105
|
+
) {
|
|
106
|
+
this.client = client;
|
|
107
|
+
this.slack = slack;
|
|
108
|
+
this.config = botConfig;
|
|
109
|
+
this.keeperConfig = keeperConfig;
|
|
110
|
+
|
|
111
|
+
// Set defaults
|
|
112
|
+
this.SM2_COOLDOWN_MS = keeperConfig.sm2CooldownMs ?? 30 * 60 * 1000; // 30 min
|
|
113
|
+
this.SM2_MAX_POOL_PERCENT = keeperConfig.sm2MaxPoolPercent ?? 40; // 40% max per trigger
|
|
114
|
+
this.TWAP_INTERVAL_MS = keeperConfig.twapIntervalMs ?? 10 * 60 * 1000; // 10 min
|
|
115
|
+
this.TWAP_PERCENTAGE = keeperConfig.twapPercentage ?? 5; // 5%
|
|
116
|
+
this.MAX_SLIPPAGE_BPS = keeperConfig.maxSlippageBps ?? 50; // 0.5%
|
|
117
|
+
this.TWAP_START_CR = keeperConfig.twapStartCR ?? 13500n; // 135%
|
|
118
|
+
this.MAX_VOLATILITY_PERCENT = keeperConfig.maxVolatilityPercent ?? 5; // 5%
|
|
119
|
+
|
|
120
|
+
// Setup account and clients
|
|
121
|
+
this.account = privateKeyToAccount(keeperConfig.privateKey);
|
|
122
|
+
const chain = keeperConfig.chain ?? bsc;
|
|
123
|
+
|
|
124
|
+
this.publicClient = createPublicClient({
|
|
125
|
+
chain,
|
|
126
|
+
transport: http(keeperConfig.rpcUrl),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
this.walletClient = createWalletClient({
|
|
130
|
+
chain,
|
|
131
|
+
transport: http(keeperConfig.rpcUrl),
|
|
132
|
+
account: this.account,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
console.log(`[RiskKeeper] Initialized with address: ${this.account.address}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ============ Lifecycle ============
|
|
139
|
+
|
|
140
|
+
start(): void {
|
|
141
|
+
console.log("[RiskKeeper] Starting risk keeper service...");
|
|
142
|
+
console.log(`[RiskKeeper] SM2 cooldown: ${this.SM2_COOLDOWN_MS / 1000}s`);
|
|
143
|
+
console.log(`[RiskKeeper] TWAP interval: ${this.TWAP_INTERVAL_MS / 1000}s`);
|
|
144
|
+
console.log(`[RiskKeeper] TWAP percentage: ${this.TWAP_PERCENTAGE}%`);
|
|
145
|
+
|
|
146
|
+
// Initial checks
|
|
147
|
+
this.checkAndTriggerSM2();
|
|
148
|
+
this.checkAndExecuteTWAP();
|
|
149
|
+
|
|
150
|
+
// Schedule regular checks
|
|
151
|
+
this.sm2IntervalId = setInterval(
|
|
152
|
+
() => this.checkAndTriggerSM2(),
|
|
153
|
+
30 * 1000 // Check every 30 seconds
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
this.twapIntervalId = setInterval(
|
|
157
|
+
() => this.checkAndExecuteTWAP(),
|
|
158
|
+
this.TWAP_INTERVAL_MS
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
console.log("[RiskKeeper] Service started");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
stop(): void {
|
|
165
|
+
if (this.sm2IntervalId) {
|
|
166
|
+
clearInterval(this.sm2IntervalId);
|
|
167
|
+
this.sm2IntervalId = undefined;
|
|
168
|
+
}
|
|
169
|
+
if (this.twapIntervalId) {
|
|
170
|
+
clearInterval(this.twapIntervalId);
|
|
171
|
+
this.twapIntervalId = undefined;
|
|
172
|
+
}
|
|
173
|
+
console.log("[RiskKeeper] Service stopped");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ============ Stability Mode 2 ============
|
|
177
|
+
|
|
178
|
+
private async checkAndTriggerSM2(): Promise<void> {
|
|
179
|
+
if (this.state.isProcessing) return;
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
// Check cooldown
|
|
183
|
+
const now = Date.now();
|
|
184
|
+
if (now - this.state.lastSM2Trigger < this.SM2_COOLDOWN_MS) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Check if SM2 can be triggered
|
|
189
|
+
const sm2Info = await withRetry(
|
|
190
|
+
() => this.client.canTriggerStabilityMode2(),
|
|
191
|
+
this.config,
|
|
192
|
+
"canTriggerStabilityMode2"
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
if (!sm2Info.canTrigger) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Check if stability pool has balance
|
|
200
|
+
if (sm2Info.potentialConversion === 0n) {
|
|
201
|
+
console.log("[RiskKeeper] SM2 triggered but no conversion possible (empty pool)");
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Get pool balance and enforce 40% max
|
|
206
|
+
const poolStats = await withRetry(
|
|
207
|
+
() => this.client.getStabilityPoolStats(),
|
|
208
|
+
this.config,
|
|
209
|
+
"getStabilityPoolStats"
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
const maxConversion = (poolStats.totalStaked * BigInt(this.SM2_MAX_POOL_PERCENT)) / 100n;
|
|
213
|
+
|
|
214
|
+
// If potential conversion exceeds 40%, we need a lower target CR
|
|
215
|
+
// For simplicity, we'll just warn and proceed - the contract will cap at pool balance anyway
|
|
216
|
+
if (sm2Info.potentialConversion > maxConversion) {
|
|
217
|
+
console.log(`[RiskKeeper] Potential conversion ${sm2Info.potentialConversion} exceeds ${this.SM2_MAX_POOL_PERCENT}% limit (${maxConversion})`);
|
|
218
|
+
console.log(`[RiskKeeper] Will execute with reduced target to stay within limit`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
console.log(`[RiskKeeper] SM2 conditions met! CR: ${sm2Info.currentCR}, Potential: ${sm2Info.potentialConversion}`);
|
|
222
|
+
|
|
223
|
+
this.state.isProcessing = true;
|
|
224
|
+
|
|
225
|
+
// Calculate target CR that respects the 40% limit
|
|
226
|
+
// If potentialConversion > maxConversion, use a lower target
|
|
227
|
+
let targetCR = 14000n; // Default 140%
|
|
228
|
+
if (sm2Info.potentialConversion > maxConversion && maxConversion > 0n) {
|
|
229
|
+
// Calculate what CR we'd get with maxConversion
|
|
230
|
+
// This is approximate - actual CR depends on TVL which we fetch
|
|
231
|
+
const tvl = await withRetry(
|
|
232
|
+
() => this.client.getTVLInUSD(),
|
|
233
|
+
this.config,
|
|
234
|
+
"getTVLInUSD"
|
|
235
|
+
);
|
|
236
|
+
const apUSDSupply = await withRetry(
|
|
237
|
+
() => this.client.getApUSDSupply(),
|
|
238
|
+
this.config,
|
|
239
|
+
"getApUSDSupply"
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
// New supply after burning maxConversion
|
|
243
|
+
const newSupply = apUSDSupply - maxConversion;
|
|
244
|
+
if (newSupply > 0n) {
|
|
245
|
+
// targetCR = TVL * 10000 / newSupply
|
|
246
|
+
targetCR = (tvl * 10000n) / newSupply;
|
|
247
|
+
// Cap at 140% max
|
|
248
|
+
if (targetCR > 14000n) targetCR = 14000n;
|
|
249
|
+
}
|
|
250
|
+
console.log(`[RiskKeeper] Adjusted target CR to ${targetCR} to respect ${this.SM2_MAX_POOL_PERCENT}% limit`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Execute SM2
|
|
254
|
+
const txHash = await this.executeSM2(targetCR);
|
|
255
|
+
|
|
256
|
+
// Wait for confirmation
|
|
257
|
+
const receipt = await this.publicClient.waitForTransactionReceipt({ hash: txHash });
|
|
258
|
+
|
|
259
|
+
if (receipt.status === "success") {
|
|
260
|
+
this.state.lastSM2Trigger = now;
|
|
261
|
+
await this.notifySM2Success(sm2Info.currentCR, sm2Info.potentialConversion, txHash);
|
|
262
|
+
} else {
|
|
263
|
+
await this.notifySM2Failure(txHash, "Transaction reverted");
|
|
264
|
+
}
|
|
265
|
+
} catch (error) {
|
|
266
|
+
console.error("[RiskKeeper] SM2 check/trigger failed:", error);
|
|
267
|
+
await this.notifySM2Failure(undefined, String(error));
|
|
268
|
+
} finally {
|
|
269
|
+
this.state.isProcessing = false;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private async executeSM2(targetCR: bigint): Promise<Hash> {
|
|
274
|
+
console.log(`[RiskKeeper] Executing SM2 with target CR: ${targetCR}`);
|
|
275
|
+
|
|
276
|
+
return this.walletClient.writeContract({
|
|
277
|
+
chain: bsc,
|
|
278
|
+
account: this.account, address: this.keeperConfig.diamondAddress,
|
|
279
|
+
abi: DiamondABI,
|
|
280
|
+
functionName: "triggerStabilityMode2",
|
|
281
|
+
args: [targetCR],
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ============ TWAP Vault Cleaning ============
|
|
286
|
+
|
|
287
|
+
private async checkAndExecuteTWAP(): Promise<void> {
|
|
288
|
+
if (this.state.isProcessing) return;
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
// Check if vault is dirty (has xBNB)
|
|
292
|
+
const vaultState = await this.getVaultState();
|
|
293
|
+
|
|
294
|
+
if (!vaultState.hasXBNB || vaultState.xBNBAmount === 0n) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Check CR is above threshold (safe to sell xBNB)
|
|
299
|
+
const sm = await withRetry(
|
|
300
|
+
() => this.client.getStabilityMode(),
|
|
301
|
+
this.config,
|
|
302
|
+
"getStabilityMode"
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
if (sm.currentCR < this.TWAP_START_CR) {
|
|
306
|
+
console.log(`[RiskKeeper] CR ${sm.currentCR} below TWAP threshold ${this.TWAP_START_CR}, skipping`);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Check BNB price volatility
|
|
311
|
+
const isVolatile = await this.checkVolatility();
|
|
312
|
+
if (isVolatile) {
|
|
313
|
+
console.log(`[RiskKeeper] BNB price too volatile (>${this.MAX_VOLATILITY_PERCENT}%), skipping TWAP`);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
console.log(`[RiskKeeper] Vault dirty with ${vaultState.xBNBAmount} xBNB, executing TWAP clean`);
|
|
318
|
+
|
|
319
|
+
this.state.isProcessing = true;
|
|
320
|
+
|
|
321
|
+
// Calculate amount to clean (X% of total)
|
|
322
|
+
const cleanAmount = (vaultState.xBNBAmount * BigInt(this.TWAP_PERCENTAGE)) / 100n;
|
|
323
|
+
|
|
324
|
+
if (cleanAmount === 0n) {
|
|
325
|
+
console.log("[RiskKeeper] Clean amount too small, skipping");
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Execute TWAP clean
|
|
330
|
+
const txHash = await this.executeVaultClean(cleanAmount);
|
|
331
|
+
|
|
332
|
+
// Wait for confirmation
|
|
333
|
+
const receipt = await this.publicClient.waitForTransactionReceipt({ hash: txHash });
|
|
334
|
+
|
|
335
|
+
if (receipt.status === "success") {
|
|
336
|
+
this.state.lastTWAPExecution = Date.now();
|
|
337
|
+
await this.notifyTWAPSuccess(cleanAmount, txHash);
|
|
338
|
+
} else {
|
|
339
|
+
await this.notifyTWAPFailure(txHash, "Transaction reverted");
|
|
340
|
+
}
|
|
341
|
+
} catch (error) {
|
|
342
|
+
console.error("[RiskKeeper] TWAP check/execute failed:", error);
|
|
343
|
+
await this.notifyTWAPFailure(undefined, String(error));
|
|
344
|
+
} finally {
|
|
345
|
+
this.state.isProcessing = false;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Check if BNB price is too volatile for TWAP execution
|
|
351
|
+
* Returns true if volatility exceeds threshold
|
|
352
|
+
*/
|
|
353
|
+
private async checkVolatility(): Promise<boolean> {
|
|
354
|
+
try {
|
|
355
|
+
const currentPrice = await withRetry(
|
|
356
|
+
() => this.client.getBNBPriceUSD(),
|
|
357
|
+
this.config,
|
|
358
|
+
"getBNBPriceUSD"
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
const now = Date.now();
|
|
362
|
+
|
|
363
|
+
// First run - just record price
|
|
364
|
+
if (this.lastBNBPrice === 0n) {
|
|
365
|
+
this.lastBNBPrice = currentPrice;
|
|
366
|
+
this.lastPriceTime = now;
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Calculate price change percentage
|
|
371
|
+
const priceDiff = currentPrice > this.lastBNBPrice
|
|
372
|
+
? currentPrice - this.lastBNBPrice
|
|
373
|
+
: this.lastBNBPrice - currentPrice;
|
|
374
|
+
|
|
375
|
+
const changePercent = Number((priceDiff * 100n) / this.lastBNBPrice);
|
|
376
|
+
|
|
377
|
+
// Update tracked price
|
|
378
|
+
this.lastBNBPrice = currentPrice;
|
|
379
|
+
this.lastPriceTime = now;
|
|
380
|
+
|
|
381
|
+
if (changePercent > this.MAX_VOLATILITY_PERCENT) {
|
|
382
|
+
console.log(`[RiskKeeper] Volatility: ${changePercent.toFixed(2)}% > ${this.MAX_VOLATILITY_PERCENT}%`);
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return false;
|
|
387
|
+
} catch (error) {
|
|
388
|
+
console.error("[RiskKeeper] Volatility check failed:", error);
|
|
389
|
+
// If we can't check volatility, be conservative and skip
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private async getVaultState(): Promise<{ hasXBNB: boolean; xBNBAmount: bigint }> {
|
|
395
|
+
const result = await this.publicClient.readContract({
|
|
396
|
+
address: this.keeperConfig.sApUSDAddress,
|
|
397
|
+
abi: SApUSDABI,
|
|
398
|
+
functionName: "hasStabilityConversion",
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
return {
|
|
402
|
+
hasXBNB: result[0],
|
|
403
|
+
xBNBAmount: result[1],
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
private async executeVaultClean(xBNBAmount: bigint): Promise<Hash> {
|
|
408
|
+
console.log(`[RiskKeeper] Cleaning ${xBNBAmount} xBNB from vault via Diamond internal conversion`);
|
|
409
|
+
|
|
410
|
+
// Preview output
|
|
411
|
+
const expectedOut = await this.previewClean(xBNBAmount);
|
|
412
|
+
|
|
413
|
+
// Calculate min out with slippage
|
|
414
|
+
const minOut = (expectedOut * BigInt(10000 - this.MAX_SLIPPAGE_BPS)) / 10000n;
|
|
415
|
+
|
|
416
|
+
console.log(`[RiskKeeper] Expected apUSD out: ${Number(expectedOut) / 1e18}, min: ${Number(minOut) / 1e18}`);
|
|
417
|
+
|
|
418
|
+
// Call Diamond's cleanVaultXBNB (internal burn xBNB + mint apUSD)
|
|
419
|
+
return this.walletClient.writeContract({
|
|
420
|
+
chain: bsc,
|
|
421
|
+
account: this.account,
|
|
422
|
+
address: this.client.diamondAddress,
|
|
423
|
+
abi: [{
|
|
424
|
+
type: "function",
|
|
425
|
+
name: "cleanVaultXBNB",
|
|
426
|
+
inputs: [
|
|
427
|
+
{ name: "_xBNBAmount", type: "uint256" },
|
|
428
|
+
{ name: "_minApUSDOut", type: "uint256" },
|
|
429
|
+
],
|
|
430
|
+
outputs: [{ name: "apUSDMinted", type: "uint256" }],
|
|
431
|
+
stateMutability: "nonpayable",
|
|
432
|
+
}],
|
|
433
|
+
functionName: "cleanVaultXBNB",
|
|
434
|
+
args: [xBNBAmount, minOut],
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
private async previewClean(xBNBAmount: bigint): Promise<bigint> {
|
|
439
|
+
return this.publicClient.readContract({
|
|
440
|
+
address: this.client.diamondAddress,
|
|
441
|
+
abi: [{
|
|
442
|
+
type: "function",
|
|
443
|
+
name: "previewCleanVaultXBNB",
|
|
444
|
+
inputs: [{ name: "_xBNBAmount", type: "uint256" }],
|
|
445
|
+
outputs: [{ name: "apUSDOut", type: "uint256" }],
|
|
446
|
+
stateMutability: "view",
|
|
447
|
+
}],
|
|
448
|
+
functionName: "previewCleanVaultXBNB",
|
|
449
|
+
args: [xBNBAmount],
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ============ Notifications ============
|
|
454
|
+
|
|
455
|
+
private async notifySM2Success(cr: bigint, amount: bigint, txHash: Hash): Promise<void> {
|
|
456
|
+
console.log(`[RiskKeeper] SM2 executed successfully: ${txHash}`);
|
|
457
|
+
|
|
458
|
+
await this.slack.send({
|
|
459
|
+
attachments: [{
|
|
460
|
+
color: SlackColors.warning,
|
|
461
|
+
title: ":rotating_light: Stability Mode 2 Triggered",
|
|
462
|
+
text: "Forced conversion executed to restore CR",
|
|
463
|
+
fields: [
|
|
464
|
+
{ title: "CR Before", value: `${Number(cr) / 100}%`, short: true },
|
|
465
|
+
{ title: "Amount Converted", value: `${Number(amount) / 1e18} apUSD`, short: true },
|
|
466
|
+
{ title: "TX", value: `<https://bscscan.com/tx/${txHash}|View>`, short: true },
|
|
467
|
+
],
|
|
468
|
+
ts: Math.floor(Date.now() / 1000),
|
|
469
|
+
}],
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
private async notifySM2Failure(txHash: Hash | undefined, error: string): Promise<void> {
|
|
474
|
+
console.error(`[RiskKeeper] SM2 execution failed: ${error}`);
|
|
475
|
+
|
|
476
|
+
await this.slack.send({
|
|
477
|
+
attachments: [{
|
|
478
|
+
color: SlackColors.danger,
|
|
479
|
+
title: ":x: SM2 Execution Failed",
|
|
480
|
+
text: error,
|
|
481
|
+
fields: txHash ? [{ title: "TX", value: `<https://bscscan.com/tx/${txHash}|View>`, short: true }] : [],
|
|
482
|
+
ts: Math.floor(Date.now() / 1000),
|
|
483
|
+
}],
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
private async notifyTWAPSuccess(amount: bigint, txHash: Hash): Promise<void> {
|
|
488
|
+
console.log(`[RiskKeeper] TWAP clean executed successfully: ${txHash}`);
|
|
489
|
+
|
|
490
|
+
await this.slack.send({
|
|
491
|
+
attachments: [{
|
|
492
|
+
color: SlackColors.success,
|
|
493
|
+
title: ":broom: Vault TWAP Clean Executed",
|
|
494
|
+
text: "xBNB cleaned from stability pool vault",
|
|
495
|
+
fields: [
|
|
496
|
+
{ title: "xBNB Sold", value: `${Number(amount) / 1e18}`, short: true },
|
|
497
|
+
{ title: "TX", value: `<https://bscscan.com/tx/${txHash}|View>`, short: true },
|
|
498
|
+
],
|
|
499
|
+
ts: Math.floor(Date.now() / 1000),
|
|
500
|
+
}],
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
private async notifyTWAPFailure(txHash: Hash | undefined, error: string): Promise<void> {
|
|
505
|
+
console.error(`[RiskKeeper] TWAP execution failed: ${error}`);
|
|
506
|
+
|
|
507
|
+
await this.slack.send({
|
|
508
|
+
attachments: [{
|
|
509
|
+
color: SlackColors.danger,
|
|
510
|
+
title: ":x: TWAP Clean Failed",
|
|
511
|
+
text: error,
|
|
512
|
+
fields: txHash ? [{ title: "TX", value: `<https://bscscan.com/tx/${txHash}|View>`, short: true }] : [],
|
|
513
|
+
ts: Math.floor(Date.now() / 1000),
|
|
514
|
+
}],
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
}
|