@aspan/sdk 0.4.6 → 0.4.7

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.
@@ -0,0 +1,2084 @@
1
+ /**
2
+ * Aspan Risk Control Tests — Dual Mode (Fork + Live)
3
+ *
4
+ * Supports two execution modes:
5
+ *
6
+ * 1. Fork Mode (Anvil):
7
+ * Full test suite with CR manipulation, snapshot/revert, whale funding.
8
+ * Requires a running Anvil instance:
9
+ * anvil --fork-url https://bsc-dataseed.binance.org/ --fork-block-number <recent>
10
+ * Run: npm run test:risk (or: ANVIL_RPC=http://127.0.0.1:8545 npm run test:risk)
11
+ *
12
+ * 2. Live Mode (Real BSC):
13
+ * Tests at the current on-chain CR zone using your own wallet funds.
14
+ * Fork-only tests (CR manipulation, oracle staleness, stability pool SM2) are skipped.
15
+ * Run: PRIVATE_KEY=0x... npm run test:risk
16
+ * Optional: BSC_RPC_URL=https://... to specify RPC endpoint
17
+ *
18
+ * Covers:
19
+ * FC-01: Dynamic Fee Control (Zone A/B/C/D fee verification, auto-switch, recovery) [Fork]
20
+ * FC-02: Stability Pool Intervention (SM2, dirty pool, exchangeRate) [Fork]
21
+ * FC-04: Extreme Scenarios (Oracle freeze mode) [Fork]
22
+ * P-01: apUSD Stablecoin Full Paths (USDT → apUSD → USDT per zone) [Fork]
23
+ * P-02: xBNB / LST Full Paths (BNB/slisBNB/asBNB → xBNB per zone) [Fork]
24
+ * P-03: Zap Cross-boundary Paths [Fork]
25
+ * P-04: Stability Pool Closed Loop (deposit → SM2 → dual-asset withdraw) [Fork]
26
+ * L-01~06: Current zone fee + path + stats verification [Live]
27
+ */
28
+
29
+ import { describe, it, expect, beforeAll, afterAll } from "vitest";
30
+ import { privateKeyToAccount } from "viem/accounts";
31
+ import {
32
+ createPublicClient,
33
+ createTestClient,
34
+ createWalletClient,
35
+ decodeEventLog,
36
+ http,
37
+ parseEther,
38
+ zeroAddress,
39
+ type Address,
40
+ type Hash,
41
+ } from "viem";
42
+ import { bsc } from "viem/chains";
43
+ import {
44
+ AspanClient,
45
+ AspanReadClient,
46
+ AspanRouterClient,
47
+ AspanRouterReadClient,
48
+ DiamondABI,
49
+ formatAmount,
50
+ formatCR,
51
+ parseAmount,
52
+ encodeV3Path,
53
+ type CurrentFees,
54
+ type FeeTier,
55
+ } from "../index";
56
+
57
+ // ============ Configuration ============
58
+
59
+ const ANVIL_RPC = process.env.ANVIL_RPC || "http://127.0.0.1:8545";
60
+ const BSC_RPC_URL = process.env.BSC_RPC_URL || "https://bsc-dataseed.binance.org/";
61
+ const ANVIL_DEFAULT_KEY = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
62
+
63
+ const DIAMOND = "0x6a11B30d3a70727d5477D6d8090e144443fA1c78" as Address;
64
+ const ROUTER = "0x34a64c4EbDe830773083BA8c9469456616F6723b" as Address;
65
+
66
+ const TOKENS = {
67
+ WBNB: "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c" as Address,
68
+ USDT: "0x55d398326f99059fF775485246999027B3197955" as Address,
69
+ USDC: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d" as Address,
70
+ slisBNB: "0xB0b84D294e0C75A6abe60171b70edEb2EFd14A1B" as Address,
71
+ asBNB: "0x77734e70b6E88b4d82fE632a168EDf6e700912b6" as Address,
72
+ wclisBNB: "0x448f7c2fa4e5135a4a5B50879602cf3CD428e108" as Address,
73
+ apUSD: "0x4570047eeB5aDb4081c5d470494EB5134e34A287" as Address,
74
+ xBNB: "0x0A0c9CD826e747D99F90D63e780B3727Da4D0d43" as Address,
75
+ sApUSD: "0xE2BE739C4aA4126ee72D612d9548C38B1B0e5A1b" as Address,
76
+ };
77
+
78
+ // Whale addresses for token funding (Binance hot wallets on BSC)
79
+ const USDT_WHALE = "0xF977814e90dA44bFA03b6295A0616a897441aceC" as Address;
80
+ const USDC_WHALE = "0x8894E0a0c962CB723c1ef8580d0d3Bfe0f8BC51e" as Address;
81
+ const FEE_RECIPIENT = "0xcFe16C686b0697c90FC83f27150e4fc31B107874" as Address;
82
+
83
+ // ERC20 Transfer event topic0
84
+ const TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
85
+
86
+ // Fee rates and CR boundaries - populated dynamically from contract in beforeAll
87
+ let ZONE_FEES: Record<string, {
88
+ xBNBMintFee: number; xBNBRedeemFee: number;
89
+ apUSDMintFee: number; apUSDRedeemFee: number;
90
+ apUSDMintDisabled: boolean;
91
+ }> = {};
92
+
93
+ let ZONE_CR: Record<string, bigint> = {};
94
+ let ZONE_LAST_LABEL = "D";
95
+
96
+ const ERC20_ABI = [
97
+ { name: "balanceOf", type: "function", stateMutability: "view", inputs: [{ type: "address" }], outputs: [{ type: "uint256" }] },
98
+ { name: "approve", type: "function", stateMutability: "nonpayable", inputs: [{ type: "address" }, { type: "uint256" }], outputs: [{ type: "bool" }] },
99
+ { name: "transfer", type: "function", stateMutability: "nonpayable", inputs: [{ type: "address" }, { type: "uint256" }], outputs: [{ type: "bool" }] },
100
+ ] as const;
101
+
102
+ const isAnvilAvailable = async (): Promise<boolean> => {
103
+ try {
104
+ const client = createPublicClient({ transport: http(ANVIL_RPC) });
105
+ await client.getChainId();
106
+ return true;
107
+ } catch {
108
+ return false;
109
+ }
110
+ };
111
+
112
+ const IS_FORK = await isAnvilAvailable();
113
+ const IS_LIVE = !IS_FORK && !!process.env.PRIVATE_KEY;
114
+ const RPC_URL = IS_FORK ? ANVIL_RPC : BSC_RPC_URL;
115
+ const PRIVATE_KEY_HEX = (IS_FORK ? ANVIL_DEFAULT_KEY : process.env.PRIVATE_KEY) as `0x${string}`;
116
+
117
+ // ============ Main Test Suite ============
118
+
119
+ describe.skipIf(!IS_FORK && !IS_LIVE)("Risk Control Tests", () => {
120
+ let diamondRead: AspanReadClient;
121
+ let diamondWrite: AspanClient;
122
+ let routerRead: AspanRouterReadClient;
123
+ let routerWrite: AspanRouterClient;
124
+ let publicClient: ReturnType<typeof createPublicClient>;
125
+ let testClient: ReturnType<typeof createTestClient>;
126
+ let walletClient: ReturnType<typeof createWalletClient>;
127
+ let account: ReturnType<typeof privateKeyToAccount>;
128
+
129
+ const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));
130
+
131
+ // ============ Helpers ============
132
+
133
+ const getBalance = async (token: Address): Promise<bigint> =>
134
+ publicClient.readContract({
135
+ address: token, abi: ERC20_ABI, functionName: "balanceOf", args: [account.address],
136
+ }) as Promise<bigint>;
137
+
138
+ const MAX_UINT256 = 2n ** 256n - 1n;
139
+ const approvedPairs = new Set<string>();
140
+ const MIN_GAS_BNB = parseEther("0.0003");
141
+ let txWaitMs = 500;
142
+
143
+ const approve = async (token: Address, spender: Address, amount: bigint) => {
144
+ const key = `${token.toLowerCase()}-${spender.toLowerCase()}`;
145
+ if (approvedPairs.has(key)) return;
146
+ const hash = await walletClient.writeContract({
147
+ address: token, abi: ERC20_ABI, functionName: "approve", args: [spender, MAX_UINT256],
148
+ chain: bsc, account: account,
149
+ });
150
+ await publicClient.waitForTransactionReceipt({ hash });
151
+ approvedPairs.add(key);
152
+ if (IS_LIVE) await sleep(500);
153
+ };
154
+
155
+ const ensureGas = async (label: string): Promise<boolean> => {
156
+ const bnbBal = await publicClient.getBalance({ address: account.address });
157
+ if (bnbBal < MIN_GAS_BNB) {
158
+ console.log(` ⚠️ [${label}] Insufficient BNB for gas: ${formatAmount(bnbBal)} BNB (need ≥ ${formatAmount(MIN_GAS_BNB)})`);
159
+ return false;
160
+ }
161
+ return true;
162
+ };
163
+
164
+ const waitTx = async (hash: Hash) => {
165
+ const receipt = await publicClient.waitForTransactionReceipt({ hash });
166
+ expect(receipt.status).toBe("success");
167
+ if (IS_LIVE) await sleep(txWaitMs);
168
+ return receipt;
169
+ };
170
+
171
+ const getCR = () => diamondRead.getCollateralRatio();
172
+ const getFees = () => diamondRead.getCurrentFees();
173
+ const dl = () => BigInt(Math.floor(Date.now() / 1000) + 7200);
174
+
175
+ /** Atomic read of CR + TVL + supply via multicall (same block guarantee) */
176
+ const readProtocolState = async () => {
177
+ const results = await publicClient.multicall({
178
+ contracts: [
179
+ { address: DIAMOND, abi: DiamondABI, functionName: "getCollateralRatio" },
180
+ { address: DIAMOND, abi: DiamondABI, functionName: "getTVLInBNB" },
181
+ { address: DIAMOND, abi: DiamondABI, functionName: "getTVLInUSD" },
182
+ { address: DIAMOND, abi: DiamondABI, functionName: "getApUSDSupply" },
183
+ ] as const,
184
+ allowFailure: true,
185
+ });
186
+ let cr = results[0].status === "success" ? (results[0].result as bigint) : 0n;
187
+ if (cr > 100000000n) cr = 0n;
188
+ const tvlBNB = results[1].status === "success" ? (results[1].result as bigint) : 0n;
189
+ const tvlUSD = results[2].status === "success" ? (results[2].result as bigint) : 0n;
190
+ const supply = results[3].status === "success" ? (results[3].result as bigint) : 0n;
191
+ return { cr, tvlBNB, tvlUSD, supply };
192
+ };
193
+
194
+ const getZone = (cr: bigint): string => {
195
+ const zones = Object.entries(ZONE_CR)
196
+ .map(([key, min]) => ({ label: key.replace("_MIN", ""), min }))
197
+ .sort((a, b) => (a.min > b.min ? -1 : 1));
198
+ for (const { label, min } of zones) {
199
+ if (cr >= min) return label;
200
+ }
201
+ return ZONE_LAST_LABEL;
202
+ };
203
+
204
+ const verifyCR = async (label: string) => {
205
+ const { cr: contractCR, tvlUSD: tvlInUSD, supply: apUSDSupply } = await readProtocolState();
206
+
207
+ let calculatedCR = 0n;
208
+ if (apUSDSupply > 0n) {
209
+ calculatedCR = tvlInUSD * 10000n / apUSDSupply;
210
+ }
211
+
212
+ const zone = getZone(contractCR);
213
+ console.log(` ${label}: TVL=$${formatAmount(tvlInUSD)} | apUSD=${formatAmount(apUSDSupply)} | CR(contract)=${formatCR(contractCR)} | CR(calc)=${formatCR(calculatedCR)} | Zone ${zone}`);
214
+
215
+ if (apUSDSupply > 0n) {
216
+ const diff = contractCR > calculatedCR
217
+ ? contractCR - calculatedCR
218
+ : calculatedCR - contractCR;
219
+ expect(diff).toBeLessThanOrEqual(1n);
220
+ }
221
+
222
+ return { contractCR, calculatedCR, tvlInUSD, apUSDSupply, zone };
223
+ };
224
+
225
+ const verifyFees = (fees: CurrentFees, zone: keyof typeof ZONE_FEES, label: string) => {
226
+ const expected = ZONE_FEES[zone];
227
+ console.log(` ${label}: CR=${formatCR(fees.currentCR)} | xBNBMint=${fees.xBNBMintFee} xBNBRedeem=${fees.xBNBRedeemFee} apUSDMint=${fees.apUSDMintFee} apUSDRedeem=${fees.apUSDRedeemFee} disabled=${fees.apUSDMintDisabled}`);
228
+ expect(fees.xBNBMintFee).toBe(expected.xBNBMintFee);
229
+ expect(fees.xBNBRedeemFee).toBe(expected.xBNBRedeemFee);
230
+ expect(fees.apUSDMintFee).toBe(expected.apUSDMintFee);
231
+ expect(fees.apUSDRedeemFee).toBe(expected.apUSDRedeemFee);
232
+ expect(fees.apUSDMintDisabled).toBe(expected.apUSDMintDisabled);
233
+ };
234
+
235
+ /**
236
+ * Parse Diamond event args (lstAmount, feeBPS, etc.) from a receipt.
237
+ */
238
+ const parseDiamondEvent = (receipt: any, eventName: string): any | null => {
239
+ for (const log of receipt.logs) {
240
+ try {
241
+ const decoded = decodeEventLog({
242
+ abi: DiamondABI,
243
+ data: log.data,
244
+ topics: log.topics,
245
+ });
246
+ if (decoded.eventName === eventName) {
247
+ return decoded.args;
248
+ }
249
+ } catch { /* not a Diamond event */ }
250
+ }
251
+ return null;
252
+ };
253
+
254
+ /**
255
+ * Parse ERC20 Transfer events that send tokens to the fee recipient address.
256
+ */
257
+ const parseFeeIncome = (receipt: any): Array<{ token: Address; amount: bigint }> => {
258
+ const recipientTopic = "0x" + FEE_RECIPIENT.slice(2).toLowerCase().padStart(64, "0");
259
+ const results: Array<{ token: Address; amount: bigint }> = [];
260
+ for (const log of receipt.logs) {
261
+ if (
262
+ log.topics?.length >= 3 &&
263
+ log.topics[0] === TRANSFER_TOPIC &&
264
+ log.topics[2]?.toLowerCase() === recipientTopic
265
+ ) {
266
+ results.push({ token: log.address as Address, amount: BigInt(log.data) });
267
+ }
268
+ }
269
+ return results;
270
+ };
271
+
272
+ const tokenName = (addr: Address): string =>
273
+ Object.entries(TOKENS).find(([, a]) => a.toLowerCase() === addr.toLowerCase())?.[0] || addr.slice(0, 10);
274
+
275
+ /**
276
+ * Verify fees from events + actual treasury income for a mint+redeem round-trip.
277
+ * 1. Event feeBPS matches expected zone fee
278
+ * 2. Treasury (FEE_RECIPIENT) actually received tokens
279
+ * 3. Treasury income amount cross-check:
280
+ * - Mint fee: lstAmount * feeBPS / 10000 (fee in LST)
281
+ * - apUSD redeem fee: lstAmount * feeBPS / (10000 - feeBPS) (fee in LST, deducted from output)
282
+ * - xBNB redeem fee: xBNBAmount * feeBPS / 10000 (fee in xBNB, deducted from input)
283
+ */
284
+ const verifyEventFees = (
285
+ mintReceipt: any,
286
+ redeemReceipt: any,
287
+ isXBNB: boolean,
288
+ zone: keyof typeof ZONE_FEES,
289
+ label: string,
290
+ ) => {
291
+ const mintEvent = isXBNB ? "XBNBMinted" : "ApUSDMinted";
292
+ const redeemEvent = isXBNB ? "XBNBRedeemed" : "ApUSDRedeemed";
293
+ const expectedMint = isXBNB ? ZONE_FEES[zone].xBNBMintFee : ZONE_FEES[zone].apUSDMintFee;
294
+ const expectedRedeem = isXBNB ? ZONE_FEES[zone].xBNBRedeemFee : ZONE_FEES[zone].apUSDRedeemFee;
295
+
296
+ // 1) Verify event feeBPS
297
+ const mintArgs = parseDiamondEvent(mintReceipt, mintEvent);
298
+ const redeemArgs = parseDiamondEvent(redeemReceipt, redeemEvent);
299
+ const actualMintFee = mintArgs ? Number(mintArgs.feeBPS) : null;
300
+ const actualRedeemFee = redeemArgs ? Number(redeemArgs.feeBPS) : null;
301
+
302
+ console.log(` ${label} event fees:`);
303
+ console.log(` Mint: ${actualMintFee} BPS (expected ${expectedMint})`);
304
+ console.log(` Redeem: ${actualRedeemFee} BPS (expected ${expectedRedeem})`);
305
+ expect(actualMintFee).toBe(expectedMint);
306
+ expect(actualRedeemFee).toBe(expectedRedeem);
307
+
308
+ // 2) Verify actual treasury income from Transfer events
309
+ const mintIncome = parseFeeIncome(mintReceipt);
310
+ const redeemIncome = parseFeeIncome(redeemReceipt);
311
+
312
+ // Fee denomination depends on operation and is identified by fi.token:
313
+ // Mint: fee deducted from input LST → fi.token = LST address
314
+ // apUSD redeem: fee deducted from input apUSD → fi.token = apUSD address
315
+ // xBNB redeem: fee deducted from input xBNB → fi.token = xBNB address
316
+ const computeExpected = (fi: { token: Address; amount: bigint }, args: any, feeBPS: number): { expected: bigint; unit: string } | null => {
317
+ const feeToken = fi.token.toLowerCase();
318
+ const fee = BigInt(feeBPS);
319
+ if (feeToken === TOKENS.apUSD.toLowerCase() && args?.apUSDAmount) {
320
+ return { expected: args.apUSDAmount * fee / 10000n, unit: "apUSD" };
321
+ }
322
+ if (feeToken === TOKENS.xBNB.toLowerCase() && args?.xBNBAmount) {
323
+ return { expected: args.xBNBAmount * fee / 10000n, unit: "xBNB" };
324
+ }
325
+ if (args?.lstAmount) {
326
+ return { expected: args.lstAmount * fee / 10000n, unit: "LST" };
327
+ }
328
+ return null;
329
+ };
330
+
331
+ const verifyIncome = (income: Array<{ token: Address; amount: bigint }>, args: any, feeBPS: number, label: string) => {
332
+ if (feeBPS === 0) {
333
+ expect(income.length).toBe(0);
334
+ console.log(` ${label} treasury: $0 (0% fee, correctly no transfer)`);
335
+ return;
336
+ }
337
+ expect(income.length).toBeGreaterThan(0);
338
+ for (const fi of income) {
339
+ const calc = computeExpected(fi, args, feeBPS);
340
+ if (!calc) continue;
341
+ console.log(` ${label} treasury: +${formatAmount(fi.amount)} ${tokenName(fi.token)} (expected ≈${formatAmount(calc.expected)} ${calc.unit})`);
342
+ const diff = fi.amount > calc.expected ? fi.amount - calc.expected : calc.expected - fi.amount;
343
+ const tolerance = calc.expected / 10n + 1n;
344
+ expect(diff).toBeLessThanOrEqual(tolerance);
345
+ console.log(` ✓ ${label} treasury income matches (within 10%)`);
346
+ }
347
+ };
348
+
349
+ if (actualMintFee !== null) verifyIncome(mintIncome, mintArgs, actualMintFee, "Mint");
350
+ if (actualRedeemFee !== null) verifyIncome(redeemIncome, redeemArgs, actualRedeemFee, "Redeem");
351
+
352
+ console.log(` ✓ Fees verified`);
353
+ };
354
+
355
+ const mintApUSDWithBNB = async (bnbAmount: bigint) => {
356
+ const hash = await routerWrite.stakeAndMint({
357
+ targetLST: TOKENS.slisBNB, isXBNB: false, minMintOut: 0n, deadline: dl(), value: bnbAmount,
358
+ });
359
+ return waitTx(hash);
360
+ };
361
+
362
+ const mintXBNBWithBNB = async (bnbAmount: bigint) => {
363
+ const hash = await routerWrite.stakeAndMint({
364
+ targetLST: TOKENS.slisBNB, isXBNB: true, minMintOut: 0n, deadline: dl(), value: bnbAmount,
365
+ });
366
+ return waitTx(hash);
367
+ };
368
+
369
+ const fundFromWhale = async (token: Address, whale: Address, amount: bigint) => {
370
+ if (!IS_FORK) throw new Error("fundFromWhale is only available in fork mode");
371
+ await testClient.impersonateAccount({ address: whale });
372
+ await testClient.setBalance({ address: whale, value: parseEther("1") });
373
+ const whaleWallet = createWalletClient({ account: whale, chain: bsc, transport: http(RPC_URL) });
374
+ const hash = await whaleWallet.writeContract({
375
+ address: token, abi: ERC20_ABI, functionName: "transfer", args: [account.address, amount],
376
+ });
377
+ await publicClient.waitForTransactionReceipt({ hash });
378
+ await testClient.stopImpersonatingAccount({ address: whale });
379
+ };
380
+
381
+ const ensureUSDT = async (needed: bigint) => {
382
+ if (IS_FORK) {
383
+ await fundFromWhale(TOKENS.USDT, USDT_WHALE, needed);
384
+ } else {
385
+ const bal = await getBalance(TOKENS.USDT);
386
+ if (bal < needed) throw new Error(`Insufficient USDT: have ${formatAmount(bal)}, need ${formatAmount(needed)}`);
387
+ }
388
+ };
389
+
390
+ const mintApUSDWithUSDT = async (usdtAmount: bigint) => {
391
+ await ensureUSDT(usdtAmount);
392
+ await approve(TOKENS.USDT, ROUTER, usdtAmount);
393
+ const hash = await routerWrite.swapAndMintDefault({
394
+ inputToken: TOKENS.USDT, inputAmount: usdtAmount, mintXBNB: false, minMintOut: 0n, deadline: dl(),
395
+ });
396
+ return waitTx(hash);
397
+ };
398
+
399
+ const mintXBNBWithUSDT = async (usdtAmount: bigint) => {
400
+ await ensureUSDT(usdtAmount);
401
+ await approve(TOKENS.USDT, ROUTER, usdtAmount);
402
+ const hash = await routerWrite.swapAndMintDefault({
403
+ inputToken: TOKENS.USDT, inputAmount: usdtAmount, mintXBNB: true, minMintOut: 0n, deadline: dl(),
404
+ });
405
+ return waitTx(hash);
406
+ };
407
+
408
+ const redeemToUSDT = async (amount: bigint, isXBNB: boolean) => {
409
+ const token = isXBNB ? TOKENS.xBNB : TOKENS.apUSD;
410
+ await approve(token, ROUTER, amount);
411
+ const path = encodeV3Path([TOKENS.slisBNB, TOKENS.WBNB, TOKENS.USDT], [500, 500]);
412
+ const hash = await routerWrite.redeemAndSwap({
413
+ lst: TOKENS.slisBNB, redeemXBNB: isXBNB, amount, path, minOut: 0n, deadline: dl(), unwrapBNB: false,
414
+ });
415
+ return waitTx(hash);
416
+ };
417
+
418
+ /**
419
+ * Lower CR by minting apUSD (adds equal TVL and supply, pushing CR toward 100%).
420
+ * Fork mode: uses BNB via stakeAndMint.
421
+ * Live mode: uses USDT via swapAndMintDefault (no BNB needed).
422
+ *
423
+ * Math: new_CR = (TVL + V) / (supply + V), solve for V: V = supply × (CR − target) / (target − 10000)
424
+ */
425
+ /**
426
+ * Lower CR by:
427
+ * Live mode: 1) redeem xBNB → USDT (removes TVL, lowers CR, recovers USDT)
428
+ * 2) if xBNB exhausted, mint apUSD with USDT (adds supply)
429
+ * Fork mode: mint apUSD with BNB (whale-funded)
430
+ */
431
+ const lowerCRTo = async (targetBPS: bigint) => {
432
+ // Max 3 attempts: calculate exact amount → send 1 tx → verify → small correction if needed
433
+ for (let i = 0; i < 3; i++) {
434
+ if (i > 0 && IS_LIVE) await sleep(3000);
435
+ const { cr, tvlBNB, tvlUSD, supply } = await readProtocolState();
436
+ console.log(` lowerCR[${i}] read: CR=${formatCR(cr)}, target=${formatCR(targetBPS)}, TVL=$${formatAmount(tvlUSD)}, supply=${formatAmount(supply)}`);
437
+ if ((cr <= targetBPS && cr > 0n) || cr === 0n) return;
438
+
439
+ if (IS_LIVE && !(await ensureGas("lowerCR"))) {
440
+ throw new Error(`Out of gas at CR=${formatCR(cr)}, target=${formatCR(targetBPS)}`);
441
+ }
442
+
443
+ if (IS_LIVE) {
444
+ // Strategy 1: redeem xBNB (removes TVL, supply unchanged)
445
+ // Exact math: neededTVLRemoval = TVL - target * supply / 10000
446
+ const neededTVLRemoval = tvlUSD - targetBPS * supply / 10000n;
447
+ const xBNBBalance = await getBalance(TOKENS.xBNB);
448
+
449
+ if (xBNBBalance > parseAmount("0.0001") && neededTVLRemoval > 0n) {
450
+ const xBNBSupply = await diamondRead.getXBNBSupply();
451
+ let redeemAmount = xBNBSupply > 0n
452
+ ? neededTVLRemoval * xBNBSupply / tvlUSD
453
+ : xBNBBalance;
454
+ // +10% buffer for redeem fee + swap slippage; +20% on retries
455
+ redeemAmount = redeemAmount * (i === 0 ? 11n : 12n) / 10n;
456
+ if (redeemAmount > xBNBBalance) redeemAmount = xBNBBalance;
457
+ if (redeemAmount < parseAmount("0.0001")) redeemAmount = parseAmount("0.0001");
458
+
459
+ console.log(` lowerCR[${i}]: redeem ${formatAmount(redeemAmount)} xBNB (CR ${formatCR(cr)} → ${formatCR(targetBPS)}, need $${formatAmount(neededTVLRemoval)} TVL removal)`);
460
+ try {
461
+ await redeemToUSDT(redeemAmount, true);
462
+ continue;
463
+ } catch (err: any) {
464
+ console.log(` lowerCR: xBNB redeem failed, trying apUSD mint`);
465
+ }
466
+ }
467
+
468
+ // Strategy 2: mint apUSD (adds equal TVL+supply → pushes CR toward 100%)
469
+ // Exact math: X = (TVL*10000 - target*supply) / (target - 10000)
470
+ const usdtBal = await getBalance(TOKENS.USDT);
471
+ if (usdtBal < parseAmount("0.1")) throw new Error(`Insufficient USDT to lower CR: ${formatAmount(usdtBal)}`);
472
+ let chunk: bigint;
473
+ if (tvlUSD > 0n && supply > 0n && targetBPS > 10000n) {
474
+ chunk = (tvlUSD * 10000n - targetBPS * supply) / (targetBPS - 10000n);
475
+ chunk = chunk * (i === 0 ? 11n : 12n) / 10n;
476
+ } else {
477
+ chunk = parseAmount("0.5");
478
+ }
479
+ if (chunk < parseAmount("0.1")) chunk = parseAmount("0.1");
480
+ if (chunk > usdtBal) chunk = usdtBal;
481
+ if (chunk > parseAmount("10")) chunk = parseAmount("10");
482
+
483
+ console.log(` lowerCR[${i}]: mint apUSD with ${formatAmount(chunk)} USDT (CR ${formatCR(cr)} → ${formatCR(targetBPS)})`);
484
+ try {
485
+ await mintApUSDWithUSDT(chunk);
486
+ } catch {
487
+ const cur = await getCR();
488
+ if (cur <= targetBPS && cur > 0n) return;
489
+ throw new Error(`Cannot lower CR further: stuck at ${formatCR(cur)}`);
490
+ }
491
+ } else {
492
+ let chunk: bigint;
493
+ if (tvlUSD > 0n && supply > 0n && targetBPS > 10000n) {
494
+ const neededUSD = supply * (cr - targetBPS) / (targetBPS - 10000n);
495
+ chunk = neededUSD * tvlBNB / tvlUSD * 11n / 10n;
496
+ } else {
497
+ chunk = parseEther("0.01");
498
+ }
499
+ if (chunk < parseEther("0.001")) chunk = parseEther("0.001");
500
+ if (chunk > parseEther("5")) chunk = parseEther("5");
501
+ try {
502
+ await mintApUSDWithBNB(chunk);
503
+ } catch {
504
+ const cur = await getCR();
505
+ if (cur <= targetBPS && cur > 0n) return;
506
+ throw new Error(`Cannot lower CR further: stuck at ${formatCR(cur)}`);
507
+ }
508
+ }
509
+ }
510
+ if (IS_LIVE) await sleep(3000);
511
+ const finalCR = await getCR();
512
+ if (finalCR <= targetBPS && finalCR > 0n) return;
513
+ throw new Error(`Failed to lower CR to ${formatCR(targetBPS)} (stuck at ${formatCR(finalCR)})`);
514
+ };
515
+
516
+ /**
517
+ * Raise CR by:
518
+ * Live mode: 1) redeem apUSD → USDT (reduces supply, raises CR, recovers USDT)
519
+ * 2) if apUSD exhausted, mint xBNB with USDT (adds TVL)
520
+ * Fork mode: mint xBNB with USDT (whale-funded)
521
+ *
522
+ * Redeem math: new_CR = (TVL − X) / (supply − X)
523
+ * → X = supply × (target − CR) / (target − 10000)
524
+ */
525
+ const raiseCRTo = async (targetBPS: bigint) => {
526
+ // Max 3 attempts: calculate exact amount → send 1 tx → verify → small correction if needed
527
+ for (let i = 0; i < 3; i++) {
528
+ if (i > 0 && IS_LIVE) await sleep(3000);
529
+ const { cr, tvlUSD, supply } = await readProtocolState();
530
+ console.log(` raiseCR[${i}] read: CR=${formatCR(cr)}, target=${formatCR(targetBPS)}, TVL=$${formatAmount(tvlUSD)}, supply=${formatAmount(supply)}`);
531
+ if (cr >= targetBPS) return;
532
+
533
+ if (IS_LIVE && !(await ensureGas("raiseCR"))) {
534
+ throw new Error(`Out of gas at CR=${formatCR(cr)}, target=${formatCR(targetBPS)}`);
535
+ }
536
+
537
+ if (IS_LIVE) {
538
+ // Strategy 1: redeem apUSD (reduces both TVL and supply equally → CR rises when CR > 100%)
539
+ // Exact math: X = (target*supply - TVL*10000) / (target - 10000)
540
+ const apUSDBalance = await getBalance(TOKENS.apUSD);
541
+ if (apUSDBalance > parseAmount("0.05") && targetBPS > 10000n) {
542
+ const neededRedeem = (targetBPS * supply - tvlUSD * 10000n) / (targetBPS - 10000n);
543
+ let redeemAmount = neededRedeem > 0n ? neededRedeem : parseAmount("0.1");
544
+ redeemAmount = redeemAmount * (i === 0 ? 11n : 12n) / 10n;
545
+ if (redeemAmount > apUSDBalance) redeemAmount = apUSDBalance;
546
+ if (redeemAmount < parseAmount("0.05")) redeemAmount = parseAmount("0.05");
547
+
548
+ console.log(` raiseCR[${i}]: redeem ${formatAmount(redeemAmount)} apUSD (CR ${formatCR(cr)} → ${formatCR(targetBPS)})`);
549
+ try {
550
+ await redeemToUSDT(redeemAmount, false);
551
+ continue;
552
+ } catch (err: any) {
553
+ console.log(` raiseCR: apUSD redeem failed, trying xBNB mint`);
554
+ }
555
+ }
556
+
557
+ // Strategy 2: mint xBNB (adds TVL, supply unchanged)
558
+ // Exact math: X = target * supply / 10000 - TVL
559
+ const usdtBal = await getBalance(TOKENS.USDT);
560
+ if (usdtBal < parseAmount("0.1")) throw new Error(`Insufficient USDT to raise CR: ${formatAmount(usdtBal)}`);
561
+ let chunk: bigint;
562
+ if (supply > 0n && targetBPS > 0n) {
563
+ const neededTVL = targetBPS * supply / 10000n - tvlUSD;
564
+ chunk = neededTVL > 0n ? neededTVL : parseAmount("0.1");
565
+ chunk = chunk * (i === 0 ? 11n : 12n) / 10n;
566
+ } else {
567
+ chunk = parseAmount("0.5");
568
+ }
569
+ if (chunk < parseAmount("0.1")) chunk = parseAmount("0.1");
570
+ if (chunk > usdtBal) chunk = usdtBal;
571
+ if (chunk > parseAmount("10")) chunk = parseAmount("10");
572
+
573
+ console.log(` raiseCR[${i}]: mint xBNB with ${formatAmount(chunk)} USDT (CR ${formatCR(cr)} → ${formatCR(targetBPS)})`);
574
+ try {
575
+ await mintXBNBWithUSDT(chunk);
576
+ } catch {
577
+ const cur = await getCR();
578
+ if (cur >= targetBPS) return;
579
+ throw new Error(`Cannot raise CR further: stuck at ${formatCR(cur)}`);
580
+ }
581
+ } else {
582
+ let chunk: bigint;
583
+ if (tvlUSD > 0n && supply > 0n && targetBPS > 0n) {
584
+ const neededTVL = targetBPS * supply / 10000n - tvlUSD;
585
+ chunk = neededTVL > 0n ? neededTVL * 11n / 10n : parseAmount("0.5");
586
+ } else {
587
+ chunk = parseAmount("0.5");
588
+ }
589
+ if (chunk < parseAmount("0.5")) chunk = parseAmount("0.5");
590
+ if (chunk > parseAmount("100")) chunk = parseAmount("100");
591
+
592
+ try {
593
+ await mintXBNBWithUSDT(chunk);
594
+ } catch {
595
+ const cur = await getCR();
596
+ if (cur >= targetBPS) return;
597
+ throw new Error(`Cannot raise CR further: stuck at ${formatCR(cur)}`);
598
+ }
599
+ }
600
+ }
601
+ if (IS_LIVE) await sleep(3000);
602
+ const finalCR = await getCR();
603
+ if (finalCR >= targetBPS) return;
604
+ throw new Error(`Failed to raise CR to ${formatCR(targetBPS)} (stuck at ${formatCR(finalCR)})`);
605
+ };
606
+
607
+ // ============ Setup ============
608
+
609
+ beforeAll(async () => {
610
+ account = privateKeyToAccount(PRIVATE_KEY_HEX);
611
+ publicClient = createPublicClient({ chain: bsc, transport: http(RPC_URL) });
612
+ walletClient = createWalletClient({ account, chain: bsc, transport: http(RPC_URL) });
613
+
614
+ if (IS_FORK) {
615
+ testClient = createTestClient({ chain: bsc, transport: http(RPC_URL), mode: "anvil" });
616
+ await testClient.setBalance({ address: account.address, value: parseEther("500000") });
617
+ }
618
+
619
+ diamondRead = new AspanReadClient({ diamondAddress: DIAMOND, chain: bsc, rpcUrl: RPC_URL });
620
+ diamondWrite = new AspanClient({ diamondAddress: DIAMOND, account, chain: bsc, rpcUrl: RPC_URL });
621
+ routerRead = new AspanRouterReadClient({ routerAddress: ROUTER, chain: bsc, rpcUrl: RPC_URL });
622
+ routerWrite = new AspanRouterClient({ routerAddress: ROUTER, account, chain: bsc, rpcUrl: RPC_URL });
623
+
624
+ // Read actual fee tiers from contract (source of truth)
625
+ const tierCount = await diamondRead.getFeeTierCount();
626
+ const tiers: FeeTier[] = [];
627
+ for (let i = 0n; i < tierCount; i++) {
628
+ tiers.push(await diamondRead.getFeeTier(i));
629
+ }
630
+ tiers.sort((a, b) => (a.minCR > b.minCR ? -1 : a.minCR < b.minCR ? 1 : 0));
631
+
632
+ const labels = ["A", "B", "C", "D", "E", "F"];
633
+ tiers.forEach((tier, idx) => {
634
+ const label = labels[idx] || `Z${idx}`;
635
+ ZONE_FEES[label] = {
636
+ xBNBMintFee: tier.xBNBMintFee,
637
+ xBNBRedeemFee: tier.xBNBRedeemFee,
638
+ apUSDMintFee: tier.apUSDMintFee,
639
+ apUSDRedeemFee: tier.apUSDRedeemFee,
640
+ apUSDMintDisabled: tier.apUSDMintDisabled,
641
+ };
642
+ if (tier.minCR > 0n) {
643
+ ZONE_CR[`${label}_MIN`] = tier.minCR;
644
+ }
645
+ });
646
+
647
+ ZONE_LAST_LABEL = labels[tiers.length - 1] || "D";
648
+
649
+ console.log(`📍 Wallet: ${account.address}`);
650
+ console.log(`📊 Fee tiers (${tierCount} from contract):`);
651
+ tiers.forEach((t, i) => {
652
+ console.log(` ${labels[i]}: minCR=${formatCR(t.minCR)} xBNBMint=${t.xBNBMintFee} xBNBRedeem=${t.xBNBRedeemFee} apUSDMint=${t.apUSDMintFee} apUSDRedeem=${t.apUSDRedeemFee} disabled=${t.apUSDMintDisabled}`);
653
+ });
654
+ await verifyCR("Initial");
655
+ });
656
+
657
+ // ================================================================
658
+ // FC-01: Fee Tier Configuration (consistency check)
659
+ // ================================================================
660
+
661
+ describe("FC-01: Fee Tier Configuration", () => {
662
+ it("FC-01: getCurrentFees matches fee tier for current CR", async () => {
663
+ const fees = await getFees();
664
+ const zone = getZone(fees.currentCR);
665
+ const expected = ZONE_FEES[zone];
666
+ console.log(` Current zone: ${zone}, CR: ${formatCR(fees.currentCR)}`);
667
+ console.log(` Fees: xBNBMint=${fees.xBNBMintFee} xBNBRedeem=${fees.xBNBRedeemFee} apUSDMint=${fees.apUSDMintFee} apUSDRedeem=${fees.apUSDRedeemFee} disabled=${fees.apUSDMintDisabled}`);
668
+
669
+ expect(fees.xBNBMintFee).toBe(expected.xBNBMintFee);
670
+ expect(fees.xBNBRedeemFee).toBe(expected.xBNBRedeemFee);
671
+ expect(fees.apUSDMintFee).toBe(expected.apUSDMintFee);
672
+ expect(fees.apUSDRedeemFee).toBe(expected.apUSDRedeemFee);
673
+ expect(fees.apUSDMintDisabled).toBe(expected.apUSDMintDisabled);
674
+ });
675
+
676
+ it("CR independent verification: TVL / apUSD_Supply matches contract CR", async () => {
677
+ const { contractCR, calculatedCR, tvlInUSD, apUSDSupply } = await verifyCR("CR Check");
678
+
679
+ if (apUSDSupply === 0n) {
680
+ console.log(` ⚠️ No apUSD supply yet, CR check N/A`);
681
+ return;
682
+ }
683
+
684
+ const diff = contractCR > calculatedCR
685
+ ? contractCR - calculatedCR
686
+ : calculatedCR - contractCR;
687
+ console.log(` Contract CR: ${formatCR(contractCR)}, Calculated CR: ${formatCR(calculatedCR)}, Diff: ${diff} BPS`);
688
+ expect(diff).toBeLessThanOrEqual(1n);
689
+ console.log(` ✓ Contract CR matches independent calculation (TVL * 10000 / apUSD_Supply)`);
690
+ });
691
+ });
692
+
693
+ // ================================================================
694
+ // Zone A: FC-01-01, P-01-01, P-02-01, P-03-01
695
+ // ================================================================
696
+
697
+ describe.skipIf(!IS_FORK)("Zone A (CR > 170%) [Fork]", () => {
698
+ let snap: `0x${string}`;
699
+ beforeAll(async () => { snap = await testClient.snapshot(); });
700
+ afterAll(async () => { await testClient.revert({ id: snap }); });
701
+
702
+ it("FC-01-01: Zone A fee + CR verification", async () => {
703
+ const cr = await getCR();
704
+ if (cr < ZONE_CR.A_MIN) {
705
+ console.log(` CR=${formatCR(cr)}, raising to Zone A...`);
706
+ try {
707
+ await raiseCRTo(ZONE_CR.A_MIN + 500n);
708
+ } catch (err: any) {
709
+ console.log(` ⚠️ Could not raise CR to Zone A: ${err.message?.slice(0, 80)}`);
710
+ console.log(` Verifying fees for current zone instead`);
711
+ }
712
+ }
713
+ const { zone } = await verifyCR("Zone A CR");
714
+ const fees = await getFees();
715
+ const actualZone = getZone(fees.currentCR);
716
+ verifyFees(fees, actualZone, `Zone ${actualZone}`);
717
+ if (actualZone !== "A") {
718
+ console.log(` ⚠️ Target was Zone A but CR is in Zone ${actualZone}`);
719
+ }
720
+ }, 600000);
721
+
722
+ it("P-01-01: USDT → apUSD → USDT (Zone A, Mint=0%, Redeem=1%)", async () => {
723
+ console.log(`\n[P-01-01] USDT → apUSD → USDT`);
724
+ const AMOUNT = parseAmount("1");
725
+
726
+ await fundFromWhale(TOKENS.USDT, USDT_WHALE, AMOUNT);
727
+ await approve(TOKENS.USDT, ROUTER, AMOUNT);
728
+
729
+ const apUSDBefore = await getBalance(TOKENS.apUSD);
730
+ let mintReceipt: any;
731
+ try {
732
+ mintReceipt = await waitTx(await routerWrite.swapAndMintDefault({
733
+ inputToken: TOKENS.USDT, inputAmount: AMOUNT, mintXBNB: false, minMintOut: 0n, deadline: dl(),
734
+ }));
735
+ } catch (err: any) {
736
+ console.log(` ⚠️ apUSD minting failed: ${err.shortMessage || err.message?.slice(0, 80)}`);
737
+ return;
738
+ }
739
+ const apUSDMinted = (await getBalance(TOKENS.apUSD)) - apUSDBefore;
740
+ console.log(` Minted: ${formatAmount(apUSDMinted)} apUSD`);
741
+ expect(apUSDMinted).toBeGreaterThan(0n);
742
+
743
+ await approve(TOKENS.apUSD, ROUTER, apUSDMinted);
744
+ const usdtBefore = await getBalance(TOKENS.USDT);
745
+ const path = encodeV3Path([TOKENS.slisBNB, TOKENS.WBNB, TOKENS.USDT], [500, 500]);
746
+ let redeemReceipt: any;
747
+ try {
748
+ redeemReceipt = await waitTx(await routerWrite.redeemAndSwap({
749
+ lst: TOKENS.slisBNB, redeemXBNB: false, amount: apUSDMinted, path, minOut: 0n, deadline: dl(), unwrapBNB: false,
750
+ }));
751
+ } catch (err: any) {
752
+ console.log(` ⚠️ Redeem swap failed (PancakeSwap liquidity shifted after CR manipulation): ${err.shortMessage || err.message?.slice(0, 80)}`);
753
+ return;
754
+ }
755
+ const usdtReceived = (await getBalance(TOKENS.USDT)) - usdtBefore;
756
+ console.log(` Received: ${formatAmount(usdtReceived)} USDT`);
757
+ expect(usdtReceived).toBeGreaterThan(0n);
758
+
759
+ const fees = await getFees();
760
+ const zone = getZone(fees.currentCR);
761
+ verifyEventFees(mintReceipt, redeemReceipt, false, zone, `apUSD Zone ${zone}`);
762
+ }, 180000);
763
+
764
+ it("P-02-01: USDT → xBNB → USDT (Zone A)", async () => {
765
+ console.log(`\n[P-02-01] USDT → xBNB → USDT`);
766
+ const AMOUNT = parseAmount("1");
767
+
768
+ const xBNBBefore = await getBalance(TOKENS.xBNB);
769
+ let mintReceipt: any;
770
+ try {
771
+ mintReceipt = await mintXBNBWithUSDT(AMOUNT);
772
+ } catch (err: any) {
773
+ console.log(` ⚠️ xBNB minting failed: ${err.shortMessage || err.message?.slice(0, 80)}`);
774
+ return;
775
+ }
776
+ const xBNBMinted = (await getBalance(TOKENS.xBNB)) - xBNBBefore;
777
+ console.log(` Minted: ${formatAmount(xBNBMinted, 8)} xBNB`);
778
+ expect(xBNBMinted).toBeGreaterThan(0n);
779
+
780
+ const usdtBefore = await getBalance(TOKENS.USDT);
781
+ let redeemReceipt: any;
782
+ try {
783
+ redeemReceipt = await redeemToUSDT(xBNBMinted, true);
784
+ } catch (err: any) {
785
+ console.log(` ⚠️ xBNB redeem swap failed (PancakeSwap liquidity): ${err.shortMessage || err.message?.slice(0, 80)}`);
786
+ return;
787
+ }
788
+ const usdtReceived = (await getBalance(TOKENS.USDT)) - usdtBefore;
789
+ console.log(` Received: ${formatAmount(usdtReceived)} USDT`);
790
+ expect(usdtReceived).toBeGreaterThan(0n);
791
+
792
+ const fees = await getFees();
793
+ const zone = getZone(fees.currentCR);
794
+ verifyEventFees(mintReceipt, redeemReceipt, true, zone, `xBNB Zone ${zone}`);
795
+ }, 180000);
796
+
797
+ it("P-03-01: USDT → xBNB → USDT Zap round-trip (Zone A)", async () => {
798
+ console.log(`\n[P-03-01] USDT → xBNB → USDT Zap`);
799
+ const AMOUNT = parseAmount("1");
800
+
801
+ const xBNBBefore = await getBalance(TOKENS.xBNB);
802
+ let mintReceipt: any;
803
+ try {
804
+ mintReceipt = await mintXBNBWithUSDT(AMOUNT);
805
+ } catch (err: any) {
806
+ console.log(` ⚠️ xBNB minting failed: ${err.shortMessage || err.message?.slice(0, 80)}`);
807
+ return;
808
+ }
809
+ const xBNBMinted = (await getBalance(TOKENS.xBNB)) - xBNBBefore;
810
+ console.log(` Minted: ${formatAmount(xBNBMinted, 8)} xBNB, Gas: ${mintReceipt.gasUsed}`);
811
+ expect(xBNBMinted).toBeGreaterThan(0n);
812
+
813
+ const usdtBefore = await getBalance(TOKENS.USDT);
814
+ let redeemReceipt: any;
815
+ try {
816
+ redeemReceipt = await redeemToUSDT(xBNBMinted, true);
817
+ } catch (err: any) {
818
+ console.log(` ⚠️ xBNB redeem swap failed (PancakeSwap liquidity): ${err.shortMessage || err.message?.slice(0, 80)}`);
819
+ return;
820
+ }
821
+ const usdtReceived = (await getBalance(TOKENS.USDT)) - usdtBefore;
822
+ console.log(` Received: ${formatAmount(usdtReceived)} USDT`);
823
+ expect(usdtReceived).toBeGreaterThan(0n);
824
+
825
+ const fees = await getFees();
826
+ const zone = getZone(fees.currentCR);
827
+ verifyEventFees(mintReceipt, redeemReceipt, true, zone, `xBNB Zap Zone ${zone}`);
828
+ }, 180000);
829
+ });
830
+
831
+ // ================================================================
832
+ // Zone B: FC-01-02, P-01-02, P-02-02, P-03-02
833
+ // ================================================================
834
+
835
+ describe.skipIf(!IS_FORK)("Zone B (150-170%) [Fork]", () => {
836
+ let snap: `0x${string}`;
837
+ beforeAll(async () => {
838
+ snap = await testClient.snapshot();
839
+ console.log(` Lowering CR to Zone B...`);
840
+ await lowerCRTo(ZONE_CR.B_MIN + 500n);
841
+ }, 600000);
842
+ afterAll(async () => { await testClient.revert({ id: snap }); });
843
+
844
+ it("FC-01-02: Zone B fee + CR verification", async () => {
845
+ await verifyCR("Zone B CR");
846
+ const fees = await getFees();
847
+ const actualZone = getZone(fees.currentCR);
848
+ verifyFees(fees, actualZone, `Zone ${actualZone}`);
849
+ if (actualZone !== "B") {
850
+ console.log(` ⚠️ Target was Zone B but CR is in Zone ${actualZone} (CR=${formatCR(fees.currentCR)})`);
851
+ }
852
+ });
853
+
854
+ it("P-01-02: USDC → apUSD → USDT (Zone B)", async () => {
855
+ console.log(`\n[P-01-02] USDC → apUSD → USDT`);
856
+ const fees = await getFees();
857
+ if (fees.apUSDMintDisabled) {
858
+ console.log(` ⚠️ apUSD minting disabled in Zone B, skipping`);
859
+ return;
860
+ }
861
+ const AMOUNT = parseAmount("1");
862
+
863
+ try {
864
+ await fundFromWhale(TOKENS.USDC, USDC_WHALE, AMOUNT);
865
+ } catch {
866
+ console.log(` ⚠️ USDC whale has insufficient balance, skipping`);
867
+ return;
868
+ }
869
+ await approve(TOKENS.USDC, ROUTER, AMOUNT);
870
+
871
+ const apUSDBefore = await getBalance(TOKENS.apUSD);
872
+ const mintReceipt = await waitTx(await routerWrite.swapAndMintDefault({
873
+ inputToken: TOKENS.USDC, inputAmount: AMOUNT, mintXBNB: false, minMintOut: 0n, deadline: dl(),
874
+ }));
875
+ const apUSDMinted = (await getBalance(TOKENS.apUSD)) - apUSDBefore;
876
+ console.log(` Minted: ${formatAmount(apUSDMinted)} apUSD`);
877
+ expect(apUSDMinted).toBeGreaterThan(0n);
878
+
879
+ await approve(TOKENS.apUSD, ROUTER, apUSDMinted);
880
+ const usdtBefore = await getBalance(TOKENS.USDT);
881
+ const path = encodeV3Path([TOKENS.slisBNB, TOKENS.WBNB, TOKENS.USDT], [500, 500]);
882
+ const redeemReceipt = await waitTx(await routerWrite.redeemAndSwap({
883
+ lst: TOKENS.slisBNB, redeemXBNB: false, amount: apUSDMinted, path, minOut: 0n, deadline: dl(), unwrapBNB: false,
884
+ }));
885
+ const usdtReceived = (await getBalance(TOKENS.USDT)) - usdtBefore;
886
+ console.log(` Received: ${formatAmount(usdtReceived)} USDT`);
887
+ expect(usdtReceived).toBeGreaterThan(0n);
888
+
889
+ const curFees = await getFees();
890
+ const curZone = getZone(curFees.currentCR);
891
+ verifyEventFees(mintReceipt, redeemReceipt, false, curZone, `apUSD Zone ${curZone}`);
892
+ }, 180000);
893
+
894
+ it("P-02-02: USDT → xBNB → USDT (Zone B)", async () => {
895
+ console.log(`\n[P-02-02] USDT → xBNB → USDT`);
896
+ const AMOUNT = parseAmount("1");
897
+
898
+ const xBNBBefore = await getBalance(TOKENS.xBNB);
899
+ let mintReceipt: any;
900
+ try {
901
+ mintReceipt = await mintXBNBWithUSDT(AMOUNT);
902
+ } catch (err: any) {
903
+ console.log(` ⚠️ xBNB minting failed: ${err.shortMessage || err.message?.slice(0, 80)}`);
904
+ return;
905
+ }
906
+ const xBNBMinted = (await getBalance(TOKENS.xBNB)) - xBNBBefore;
907
+ console.log(` Minted: ${formatAmount(xBNBMinted, 8)} xBNB`);
908
+ expect(xBNBMinted).toBeGreaterThan(0n);
909
+
910
+ const usdtBefore = await getBalance(TOKENS.USDT);
911
+ let redeemReceipt: any;
912
+ try {
913
+ redeemReceipt = await redeemToUSDT(xBNBMinted, true);
914
+ } catch (err: any) {
915
+ console.log(` ⚠️ xBNB redeem swap failed (PancakeSwap liquidity): ${err.shortMessage || err.message?.slice(0, 80)}`);
916
+ return;
917
+ }
918
+ const usdtReceived = (await getBalance(TOKENS.USDT)) - usdtBefore;
919
+ console.log(` Received: ${formatAmount(usdtReceived)} USDT`);
920
+ expect(usdtReceived).toBeGreaterThan(0n);
921
+
922
+ const fees = await getFees();
923
+ const zone = getZone(fees.currentCR);
924
+ verifyEventFees(mintReceipt, redeemReceipt, true, zone, `xBNB Zone ${zone}`);
925
+ }, 180000);
926
+
927
+ it("P-03-02: USDT → apUSD → USDT (Zone B)", async () => {
928
+ console.log(`\n[P-03-02] USDT → apUSD → USDT`);
929
+ const fees = await getFees();
930
+ if (fees.apUSDMintDisabled) {
931
+ console.log(` ⚠️ apUSD minting disabled, skipping`);
932
+ return;
933
+ }
934
+ const AMOUNT = parseAmount("1");
935
+
936
+ const apUSDBefore = await getBalance(TOKENS.apUSD);
937
+ let mintReceipt: any;
938
+ try {
939
+ mintReceipt = await mintApUSDWithUSDT(AMOUNT);
940
+ } catch (err: any) {
941
+ console.log(` ⚠️ apUSD minting failed: ${err.shortMessage || err.message?.slice(0, 80)}`);
942
+ return;
943
+ }
944
+ const apUSDMinted = (await getBalance(TOKENS.apUSD)) - apUSDBefore;
945
+ console.log(` Minted: ${formatAmount(apUSDMinted)} apUSD`);
946
+ expect(apUSDMinted).toBeGreaterThan(0n);
947
+
948
+ const usdtBefore = await getBalance(TOKENS.USDT);
949
+ let redeemReceipt: any;
950
+ try {
951
+ redeemReceipt = await redeemToUSDT(apUSDMinted, false);
952
+ } catch (err: any) {
953
+ console.log(` ⚠️ Redeem swap failed: ${err.shortMessage || err.message?.slice(0, 80)}`);
954
+ return;
955
+ }
956
+ const usdtReceived = (await getBalance(TOKENS.USDT)) - usdtBefore;
957
+ console.log(` Received: ${formatAmount(usdtReceived)} USDT`);
958
+ expect(usdtReceived).toBeGreaterThan(0n);
959
+
960
+ const zone = getZone(fees.currentCR);
961
+ verifyEventFees(mintReceipt, redeemReceipt, false, zone, `apUSD Zone ${zone}`);
962
+ }, 180000);
963
+ });
964
+
965
+ // ================================================================
966
+ // Zone C: FC-01-03, P-01-03, P-02-03
967
+ // ================================================================
968
+
969
+ describe.skipIf(!IS_FORK)("Zone C (130-150%) [Fork]", () => {
970
+ let snap: `0x${string}`;
971
+ beforeAll(async () => {
972
+ snap = await testClient.snapshot();
973
+ console.log(` Lowering CR to Zone C...`);
974
+ await lowerCRTo(ZONE_CR.C_MIN + 500n);
975
+ }, 600000);
976
+ afterAll(async () => { await testClient.revert({ id: snap }); });
977
+
978
+ it("FC-01-03: Zone C fee + CR verification (high xBNB redeem penalty)", async () => {
979
+ await verifyCR("Zone C CR");
980
+ const fees = await getFees();
981
+ const actualZone = getZone(fees.currentCR);
982
+ verifyFees(fees, actualZone, `Zone ${actualZone}`);
983
+ if (actualZone !== "C") {
984
+ console.log(` ⚠️ Target was Zone C but CR is in Zone ${actualZone} (CR=${formatCR(fees.currentCR)})`);
985
+ }
986
+ });
987
+
988
+ it("P-01-03: USDT → apUSD → USDC (Zone C)", async () => {
989
+ console.log(`\n[P-01-03] USDT → apUSD → USDC`);
990
+ const currentFees = await getFees();
991
+ if (currentFees.apUSDMintDisabled) {
992
+ console.log(` ⚠️ apUSD minting disabled in Zone C, skipping`);
993
+ return;
994
+ }
995
+ const AMOUNT = parseAmount("1");
996
+
997
+ await fundFromWhale(TOKENS.USDT, USDT_WHALE, AMOUNT);
998
+ await approve(TOKENS.USDT, ROUTER, AMOUNT);
999
+
1000
+ const apUSDBefore = await getBalance(TOKENS.apUSD);
1001
+ const mintReceipt = await waitTx(await routerWrite.swapAndMintDefault({
1002
+ inputToken: TOKENS.USDT, inputAmount: AMOUNT, mintXBNB: false, minMintOut: 0n, deadline: dl(),
1003
+ }));
1004
+ const apUSDMinted = (await getBalance(TOKENS.apUSD)) - apUSDBefore;
1005
+ console.log(` Minted: ${formatAmount(apUSDMinted)} apUSD`);
1006
+ expect(apUSDMinted).toBeGreaterThan(0n);
1007
+
1008
+ await approve(TOKENS.apUSD, ROUTER, apUSDMinted);
1009
+ const usdcBefore = await getBalance(TOKENS.USDC);
1010
+ const path = encodeV3Path([TOKENS.slisBNB, TOKENS.WBNB, TOKENS.USDC], [500, 500]);
1011
+ const redeemReceipt = await waitTx(await routerWrite.redeemAndSwap({
1012
+ lst: TOKENS.slisBNB, redeemXBNB: false, amount: apUSDMinted, path, minOut: 0n, deadline: dl(), unwrapBNB: false,
1013
+ }));
1014
+ const usdcReceived = (await getBalance(TOKENS.USDC)) - usdcBefore;
1015
+ console.log(` Received: ${formatAmount(usdcReceived)} USDC`);
1016
+ expect(usdcReceived).toBeGreaterThan(0n);
1017
+
1018
+ const postFees = await getFees();
1019
+ const zone = getZone(postFees.currentCR);
1020
+ verifyEventFees(mintReceipt, redeemReceipt, false, zone, `apUSD Zone ${zone}`);
1021
+ }, 180000);
1022
+
1023
+ it("P-02-03: USDT → xBNB → USDT (Zone C)", async () => {
1024
+ console.log(`\n[P-02-03] USDT → xBNB → USDT`);
1025
+ const AMOUNT = parseAmount("1");
1026
+
1027
+ let mintReceipt: any;
1028
+ const xBNBBefore = await getBalance(TOKENS.xBNB);
1029
+ try {
1030
+ mintReceipt = await mintXBNBWithUSDT(AMOUNT);
1031
+ } catch (err: any) {
1032
+ console.log(` ⚠️ xBNB minting failed in Zone C (swap/liquidity issue), skipping: ${err.shortMessage || err.message?.slice(0, 80)}`);
1033
+ return;
1034
+ }
1035
+ const xBNBMinted = (await getBalance(TOKENS.xBNB)) - xBNBBefore;
1036
+ console.log(` Minted: ${formatAmount(xBNBMinted, 8)} xBNB`);
1037
+ expect(xBNBMinted).toBeGreaterThan(0n);
1038
+
1039
+ const usdtBefore = await getBalance(TOKENS.USDT);
1040
+ let redeemReceipt: any;
1041
+ try {
1042
+ redeemReceipt = await redeemToUSDT(xBNBMinted, true);
1043
+ } catch (err: any) {
1044
+ console.log(` ⚠️ xBNB redeem swap failed: ${err.shortMessage || err.message?.slice(0, 80)}`);
1045
+ return;
1046
+ }
1047
+ const usdtReceived = (await getBalance(TOKENS.USDT)) - usdtBefore;
1048
+ console.log(` Received: ${formatAmount(usdtReceived)} USDT`);
1049
+ expect(usdtReceived).toBeGreaterThan(0n);
1050
+
1051
+ const fees = await getFees();
1052
+ const zone = getZone(fees.currentCR);
1053
+ verifyEventFees(mintReceipt, redeemReceipt, true, zone, `xBNB Zone ${zone}`);
1054
+ }, 180000);
1055
+ });
1056
+
1057
+ // ================================================================
1058
+ // Zone D: FC-01-04~06, P-01-04, P-02-04
1059
+ // ================================================================
1060
+
1061
+ describe.skipIf(!IS_FORK)("Zone D (CR < 130%) [Fork]", () => {
1062
+ let snap: `0x${string}`;
1063
+ beforeAll(async () => {
1064
+ snap = await testClient.snapshot();
1065
+ console.log(` Lowering CR to Zone D...`);
1066
+ await lowerCRTo(ZONE_CR.C_MIN - 100n);
1067
+ }, 600000);
1068
+ afterAll(async () => { await testClient.revert({ id: snap }); });
1069
+
1070
+ it("FC-01-04: Zone D fee + CR verification (emergency lockdown)", async () => {
1071
+ await verifyCR("Zone D CR");
1072
+ const fees = await getFees();
1073
+ const actualZone = getZone(fees.currentCR);
1074
+ verifyFees(fees, actualZone, `Zone ${actualZone}`);
1075
+ if (actualZone !== "D") {
1076
+ console.log(` ⚠️ Target was Zone D but CR is in Zone ${actualZone}`);
1077
+ }
1078
+ });
1079
+
1080
+ it("FC-01-05: Zone D apUSD minting is suspended", async () => {
1081
+ console.log(`\n[FC-01-05] apUSD mint in Zone D`);
1082
+ const fees = await getFees();
1083
+ expect(fees.apUSDMintDisabled).toBe(true);
1084
+
1085
+ await expect(mintApUSDWithUSDT(parseAmount("1"))).rejects.toThrow();
1086
+ console.log(` ✓ apUSD minting correctly blocked`);
1087
+ }, 60000);
1088
+
1089
+ it("FC-01-06: Zone D xBNB redeem fee (USDT round-trip)", async () => {
1090
+ console.log(`\n[FC-01-06] USDT → xBNB → USDT in Zone D`);
1091
+ const AMOUNT = parseAmount("1");
1092
+
1093
+ let mintReceipt: any;
1094
+ const xBNBBefore = await getBalance(TOKENS.xBNB);
1095
+ try {
1096
+ mintReceipt = await mintXBNBWithUSDT(AMOUNT);
1097
+ } catch (err: any) {
1098
+ console.log(` ⚠️ xBNB minting failed in Zone D (swap/liquidity issue), skipping: ${err.shortMessage || err.message?.slice(0, 80)}`);
1099
+ return;
1100
+ }
1101
+ const xBNBMinted = (await getBalance(TOKENS.xBNB)) - xBNBBefore;
1102
+ console.log(` Minted: ${formatAmount(xBNBMinted, 8)} xBNB`);
1103
+ expect(xBNBMinted).toBeGreaterThan(0n);
1104
+
1105
+ const usdtBefore = await getBalance(TOKENS.USDT);
1106
+ let redeemReceipt: any;
1107
+ try {
1108
+ redeemReceipt = await redeemToUSDT(xBNBMinted, true);
1109
+ } catch (err: any) {
1110
+ console.log(` ⚠️ xBNB redeem swap failed: ${err.shortMessage || err.message?.slice(0, 80)}`);
1111
+ return;
1112
+ }
1113
+ const usdtReceived = (await getBalance(TOKENS.USDT)) - usdtBefore;
1114
+ console.log(` Received: ${formatAmount(usdtReceived)} USDT`);
1115
+ expect(usdtReceived).toBeGreaterThan(0n);
1116
+
1117
+ const fees = await getFees();
1118
+ const zone = getZone(fees.currentCR);
1119
+ verifyEventFees(mintReceipt, redeemReceipt, true, zone, `xBNB Zone ${zone}`);
1120
+ }, 180000);
1121
+
1122
+ it("P-01-04: apUSD minting blocked in Zone D, existing holders can redeem at 0%", async () => {
1123
+ console.log(`\n[P-01-04] Zone D: mint blocked, redeem 0%`);
1124
+ const fees = await getFees();
1125
+ expect(fees.apUSDMintDisabled).toBe(true);
1126
+ console.log(` apUSD redeem fee: ${fees.apUSDRedeemFee} BPS`);
1127
+
1128
+ const apUSDBalance = await getBalance(TOKENS.apUSD);
1129
+ if (apUSDBalance > 0n) {
1130
+ const redeemAmount = apUSDBalance > parseAmount("1") ? parseAmount("1") : apUSDBalance;
1131
+ console.log(` Existing apUSD holder: ${formatAmount(apUSDBalance)} apUSD, redeeming ${formatAmount(redeemAmount)}`);
1132
+ try {
1133
+ const usdtBefore = await getBalance(TOKENS.USDT);
1134
+ await redeemToUSDT(redeemAmount, false);
1135
+ const usdtReceived = (await getBalance(TOKENS.USDT)) - usdtBefore;
1136
+ console.log(` Received: ${formatAmount(usdtReceived)} USDT ✓`);
1137
+ expect(usdtReceived).toBeGreaterThan(0n);
1138
+ } catch (err: any) {
1139
+ console.log(` ⚠️ Redeem swap failed: ${err.shortMessage || err.message?.slice(0, 80)}`);
1140
+ }
1141
+ } else {
1142
+ console.log(` No apUSD balance to test redeem (normal in Zone D fork)`);
1143
+ }
1144
+ }, 180000);
1145
+
1146
+ it("P-02-04: xBNB extreme fees in Zone D", async () => {
1147
+ console.log(`\n[P-02-04] Zone D extreme fees`);
1148
+ const fees = await getFees();
1149
+ const zone = getZone(fees.currentCR);
1150
+ const expected = ZONE_FEES[zone];
1151
+ console.log(` Zone ${zone}: xBNBMint=${fees.xBNBMintFee}BPS xBNBRedeem=${fees.xBNBRedeemFee}BPS`);
1152
+ expect(fees.xBNBMintFee).toBe(expected.xBNBMintFee);
1153
+ expect(fees.xBNBRedeemFee).toBe(expected.xBNBRedeemFee);
1154
+ console.log(` ✓ xBNB fees match zone ${zone} configuration`);
1155
+ });
1156
+ });
1157
+
1158
+ // ================================================================
1159
+ // FC-01-07: CR Recovery (Zone D → Zone A)
1160
+ // ================================================================
1161
+
1162
+ describe.skipIf(!IS_FORK)("FC-01-07: CR Recovery [Fork]", () => {
1163
+ let snap: `0x${string}`;
1164
+ beforeAll(async () => { snap = await testClient.snapshot(); });
1165
+ afterAll(async () => { await testClient.revert({ id: snap }); });
1166
+
1167
+ it("FC-01-07: Fees restore to Zone A after CR recovery", async () => {
1168
+ console.log(`\n[FC-01-07] Zone D → Zone A recovery`);
1169
+
1170
+ // First lower to Zone D
1171
+ console.log(` Step 1: Lower CR to Zone D...`);
1172
+ await lowerCRTo(ZONE_CR.C_MIN - 100n);
1173
+ let fees = await getFees();
1174
+ let zone = getZone(fees.currentCR);
1175
+ verifyFees(fees, zone, `${zone} (before recovery)`);
1176
+
1177
+ // Then raise back to Zone A by minting xBNB
1178
+ console.log(` Step 2: Raise CR to Zone A...`);
1179
+ try {
1180
+ await raiseCRTo(ZONE_CR.A_MIN + 500n);
1181
+ } catch (err: any) {
1182
+ console.log(` ⚠️ Could not raise to Zone A: ${err.message?.slice(0, 80)}`);
1183
+ console.log(` Verifying fees auto-switch at whatever CR we reached`);
1184
+ }
1185
+ fees = await getFees();
1186
+ zone = getZone(fees.currentCR);
1187
+ verifyFees(fees, zone, `${zone} (after recovery)`);
1188
+
1189
+ const crAfter = fees.currentCR;
1190
+ console.log(` Final CR: ${formatCR(crAfter)}, Zone: ${zone}`);
1191
+ if (zone === "A") {
1192
+ console.log(` ✓ Fee auto-recovery to Zone A confirmed`);
1193
+ } else {
1194
+ console.log(` ⚠️ Only recovered to Zone ${zone}`);
1195
+ }
1196
+ }, 1200000);
1197
+ });
1198
+
1199
+ // ================================================================
1200
+ // FC-02 + P-04: Stability Pool Intervention
1201
+ // ================================================================
1202
+
1203
+ describe.skipIf(!IS_FORK)("FC-02 + P-04: Stability Pool Intervention [Fork]", () => {
1204
+ let snap: `0x${string}`;
1205
+ beforeAll(async () => { snap = await testClient.snapshot(); });
1206
+ afterAll(async () => { await testClient.revert({ id: snap }); });
1207
+
1208
+ let depositedApUSD = 0n;
1209
+
1210
+ it("Setup: Mint apUSD and deposit to stability pool", async () => {
1211
+ console.log(`\n[FC-02 Setup] Mint and deposit to stability pool`);
1212
+
1213
+ const fees = await getFees();
1214
+ const cr = fees.currentCR;
1215
+ if (fees.apUSDMintDisabled || cr < ZONE_CR.B_MIN) {
1216
+ console.log(` CR=${formatCR(cr)} too low for apUSD minting, raising to Zone B...`);
1217
+ await raiseCRTo(ZONE_CR.B_MIN + 500n);
1218
+ }
1219
+
1220
+ await mintApUSDWithUSDT(parseAmount("1"));
1221
+ depositedApUSD = await getBalance(TOKENS.apUSD);
1222
+ console.log(` Minted: ${formatAmount(depositedApUSD)} apUSD`);
1223
+ expect(depositedApUSD).toBeGreaterThan(0n);
1224
+
1225
+ await approve(TOKENS.apUSD, DIAMOND, depositedApUSD);
1226
+ try {
1227
+ await waitTx(await diamondWrite.deposit({ apUSDAmount: depositedApUSD }));
1228
+ } catch (err: any) {
1229
+ console.log(` ⚠️ Deposit failed (AccessControl or not configured): ${err.shortMessage || err.message?.slice(0, 100)}`);
1230
+ console.log(` Skipping remaining FC-02 tests`);
1231
+ return;
1232
+ }
1233
+
1234
+ const shares = await diamondRead.getShares(account.address);
1235
+ const stats = await diamondRead.getStabilityPoolStats();
1236
+ console.log(` Shares: ${formatAmount(shares)}`);
1237
+ console.log(` Pool total: ${formatAmount(stats.totalStaked)}, Rate: ${formatAmount(stats.exchangeRate)}`);
1238
+ expect(shares).toBeGreaterThan(0n);
1239
+ }, 120000);
1240
+
1241
+ it("FC-02-01: SM2 not triggerable above 130%", async () => {
1242
+ const sm2 = await diamondRead.canTriggerStabilityMode2();
1243
+ const cr = await getCR();
1244
+ console.log(` CR: ${formatCR(cr)}, canTrigger: ${sm2.canTrigger}`);
1245
+ if (cr >= ZONE_CR.C_MIN) {
1246
+ expect(sm2.canTrigger).toBe(false);
1247
+ console.log(` ✓ SM2 correctly not triggerable`);
1248
+ }
1249
+ });
1250
+
1251
+ it("FC-02-01b: SM2 becomes triggerable below 130%", async () => {
1252
+ const shares = await diamondRead.getShares(account.address);
1253
+ if (shares === 0n) {
1254
+ console.log(` ⚠️ No shares deposited (setup failed), skipping SM2 tests`);
1255
+ return;
1256
+ }
1257
+ console.log(` Lowering CR below 130%...`);
1258
+ await lowerCRTo(12500n);
1259
+ const sm2 = await diamondRead.canTriggerStabilityMode2();
1260
+ console.log(` CR: ${formatCR(sm2.currentCR)}, canTrigger: ${sm2.canTrigger}, conversion: ${formatAmount(sm2.potentialConversion)} apUSD`);
1261
+ expect(sm2.canTrigger).toBe(true);
1262
+ expect(sm2.potentialConversion).toBeGreaterThan(0n);
1263
+ }, 600000);
1264
+
1265
+ it("FC-02-02 + FC-02-03: SM2 execution with 40% burn cap", async () => {
1266
+ console.log(`\n[FC-02-02/03] SM2 execution`);
1267
+ const shares = await diamondRead.getShares(account.address);
1268
+ if (shares === 0n) {
1269
+ console.log(` ⚠️ No shares deposited, skipping`);
1270
+ return;
1271
+ }
1272
+
1273
+ const poolBefore = await diamondRead.getTotalStaked();
1274
+ console.log(` Pool before: ${formatAmount(poolBefore)} apUSD`);
1275
+
1276
+ try {
1277
+ await waitTx(await diamondWrite.triggerStabilityMode2());
1278
+ } catch (err: any) {
1279
+ console.log(` ⚠️ SM2 trigger failed: ${err.shortMessage || err.message?.slice(0, 100)}`);
1280
+ return;
1281
+ }
1282
+
1283
+ const poolAfter = await diamondRead.getTotalStaked();
1284
+ const burned = poolBefore - poolAfter;
1285
+ console.log(` Pool after: ${formatAmount(poolAfter)}, Burned: ${formatAmount(burned)}`);
1286
+
1287
+ if (poolBefore > 0n) {
1288
+ const burnPct = Number(burned * 10000n / poolBefore) / 100;
1289
+ console.log(` Burn ratio: ${burnPct.toFixed(1)}%`);
1290
+ expect(burnPct).toBeLessThanOrEqual(40.5);
1291
+ }
1292
+ console.log(` ✓ SM2 executed within 40% cap`);
1293
+ }, 120000);
1294
+
1295
+ it("FC-02-07: exchangeRate after SM2", async () => {
1296
+ const shares = await diamondRead.getShares(account.address);
1297
+ if (shares === 0n) {
1298
+ console.log(` ⚠️ No shares (SM2 tests skipped), checking exchange rate only`);
1299
+ }
1300
+ const rate = await diamondRead.getExchangeRate();
1301
+ const ONE = 10n ** 18n;
1302
+ console.log(` Exchange rate: ${formatAmount(rate)} (1.0 = ${formatAmount(ONE)})`);
1303
+ if (rate < ONE) {
1304
+ console.log(` ✓ Exchange rate shrunk as expected`);
1305
+ } else {
1306
+ console.log(` ⚠️ Exchange rate >= 1.0 (SM2 may not have executed)`);
1307
+ }
1308
+ });
1309
+
1310
+ it("FC-02-04: SM2 cooldown period - second trigger reverts", async () => {
1311
+ console.log(`\n[FC-02-04] Cooldown test`);
1312
+ try {
1313
+ await diamondWrite.triggerStabilityMode2();
1314
+ console.log(` ⚠️ SM2 did not revert (may not have been triggered previously)`);
1315
+ } catch {
1316
+ console.log(` ✓ SM2 correctly rejected (cooldown or not triggerable)`);
1317
+ }
1318
+ }, 30000);
1319
+
1320
+ it("FC-02-06 + P-04-01: Dirty pool withdraw returns dual assets (apUSD + xBNB)", async () => {
1321
+ console.log(`\n[FC-02-06] Dirty pool withdrawal + value verification`);
1322
+
1323
+ const currentShares = await diamondRead.getShares(account.address);
1324
+ if (currentShares === 0n) {
1325
+ console.log(` ⚠️ No shares to withdraw, skipping`);
1326
+ return;
1327
+ }
1328
+
1329
+ // Advance time past minimum deposit period
1330
+ try {
1331
+ const minPeriod = await diamondRead.getMinDepositPeriod();
1332
+ console.log(` Advancing time by ${minPeriod}s (minDepositPeriod)...`);
1333
+ await testClient.increaseTime({ seconds: Number(minPeriod) + 60 });
1334
+ await testClient.mine({ blocks: 1 });
1335
+ } catch {
1336
+ console.log(` Advancing time by 1 hour (fallback)...`);
1337
+ await testClient.increaseTime({ seconds: 3600 });
1338
+ await testClient.mine({ blocks: 1 });
1339
+ }
1340
+
1341
+ const ONE = 10n ** 18n;
1342
+ const [exchangeRate, xBNBPriceUSD] = await Promise.all([
1343
+ diamondRead.getExchangeRate(),
1344
+ diamondRead.getXBNBPriceUSD(),
1345
+ ]);
1346
+ console.log(` Exchange rate: ${formatAmount(exchangeRate)}`);
1347
+ console.log(` xBNB price: $${formatAmount(xBNBPriceUSD)}`);
1348
+
1349
+ const xBNBBefore = await getBalance(TOKENS.xBNB);
1350
+ const apUSDBefore = await getBalance(TOKENS.apUSD);
1351
+
1352
+ console.log(` Withdrawing ${formatAmount(currentShares)} shares...`);
1353
+ try {
1354
+ await waitTx(await diamondWrite.withdraw({ shares: currentShares }));
1355
+ } catch (err: any) {
1356
+ console.log(` ⚠️ Withdraw failed: ${err.shortMessage || err.message?.slice(0, 100)}`);
1357
+ return;
1358
+ }
1359
+
1360
+ const apUSDReceived = (await getBalance(TOKENS.apUSD)) - apUSDBefore;
1361
+ const xBNBReceived = (await getBalance(TOKENS.xBNB)) - xBNBBefore;
1362
+ console.log(` Received: ${formatAmount(apUSDReceived)} apUSD + ${formatAmount(xBNBReceived, 8)} xBNB`);
1363
+
1364
+ expect(apUSDReceived).toBeGreaterThanOrEqual(0n);
1365
+
1366
+ // Value verification: sApUSD × exchangeRate ≈ apUSD × $1 + xBNB × xBNBPrice
1367
+ // expectedValue = shares × exchangeRate / 1e18 (in apUSD units, $1 each)
1368
+ // actualValue = apUSD + xBNB × xBNBPrice / 1e18
1369
+ const expectedValue = currentShares * exchangeRate / ONE;
1370
+ const apUSDValue = apUSDReceived;
1371
+ const xBNBValue = xBNBPriceUSD > 0n ? xBNBReceived * xBNBPriceUSD / ONE : 0n;
1372
+ const actualValue = apUSDValue + xBNBValue;
1373
+
1374
+ console.log(` Expected value: $${formatAmount(expectedValue)} (${formatAmount(currentShares)} shares × ${formatAmount(exchangeRate)} rate)`);
1375
+ console.log(` Actual value: $${formatAmount(actualValue)} (apUSD=$${formatAmount(apUSDValue)} + xBNB=$${formatAmount(xBNBValue)})`);
1376
+
1377
+ if (xBNBReceived > 0n) {
1378
+ console.log(` ✓ Dual-asset return confirmed (pool was dirty)`);
1379
+ } else {
1380
+ console.log(` ⚠️ Only apUSD received — pool may not be dirty`);
1381
+ }
1382
+
1383
+ if (expectedValue > 0n) {
1384
+ const minAcceptable = expectedValue * 95n / 100n;
1385
+ console.log(` Min acceptable: $${formatAmount(minAcceptable)} (95% of expected)`);
1386
+ expect(actualValue).toBeGreaterThanOrEqual(minAcceptable);
1387
+ if (actualValue >= expectedValue) {
1388
+ console.log(` ✓ Value check passed: received >= expected (${(Number(actualValue * 10000n / expectedValue) / 100).toFixed(2)}%)`);
1389
+ } else {
1390
+ const lossPct = Number((expectedValue - actualValue) * 10000n / expectedValue) / 100;
1391
+ console.log(` ✓ Value check passed: loss ${lossPct.toFixed(2)}% (within 5% tolerance)`);
1392
+ }
1393
+ }
1394
+ }, 120000);
1395
+ });
1396
+
1397
+ // ================================================================
1398
+ // FC-04: Extreme Scenarios
1399
+ // ================================================================
1400
+
1401
+ describe.skipIf(!IS_FORK)("FC-04: Extreme Scenarios [Fork]", () => {
1402
+ let snap: `0x${string}`;
1403
+ beforeAll(async () => { snap = await testClient.snapshot(); });
1404
+ afterAll(async () => { await testClient.revert({ id: snap }); });
1405
+
1406
+ it("FC-04-03 + FC-04-04: Oracle staleness → operations rejected", async () => {
1407
+ console.log(`\n[FC-04-03/04] Oracle freeze mode`);
1408
+
1409
+ let maxPriceAge: bigint;
1410
+ try {
1411
+ maxPriceAge = await diamondRead.getMaxPriceAge();
1412
+ } catch {
1413
+ console.log(` ⚠️ getMaxPriceAge not available on this Diamond deployment, skipping`);
1414
+ return;
1415
+ }
1416
+ console.log(` Max oracle price age: ${maxPriceAge}s`);
1417
+
1418
+ const fastForward = Number(maxPriceAge) + 7200;
1419
+ console.log(` Fast-forwarding ${fastForward}s past oracle freshness window...`);
1420
+ await testClient.increaseTime({ seconds: fastForward });
1421
+ await testClient.mine({ blocks: 1 });
1422
+
1423
+ // All minting should fail due to stale oracle
1424
+ let apUSDBlocked = false;
1425
+ try {
1426
+ await mintApUSDWithUSDT(parseAmount("1"));
1427
+ console.log(` ⚠️ apUSD mint succeeded (oracle might have fallback)`);
1428
+ } catch {
1429
+ apUSDBlocked = true;
1430
+ console.log(` ✓ apUSD mint rejected (stale oracle)`);
1431
+ }
1432
+
1433
+ let xBNBBlocked = false;
1434
+ try {
1435
+ await mintXBNBWithUSDT(parseAmount("1"));
1436
+ console.log(` ⚠️ xBNB mint succeeded (oracle might have fallback)`);
1437
+ } catch {
1438
+ xBNBBlocked = true;
1439
+ console.log(` ✓ xBNB mint rejected (stale oracle)`);
1440
+ }
1441
+
1442
+ expect(apUSDBlocked || xBNBBlocked).toBe(true);
1443
+
1444
+ // These may also fail when oracle is stale (contract checks oracle internally)
1445
+ try {
1446
+ const mode = await diamondRead.getStabilityMode();
1447
+ const paused = await diamondRead.isPaused();
1448
+ console.log(` Stability mode: ${mode.mode}, Paused: ${paused}`);
1449
+ } catch {
1450
+ console.log(` ⚠️ getStabilityMode/isPaused also reverted due to stale oracle (expected in strict oracle mode)`);
1451
+ }
1452
+ }, 120000);
1453
+ });
1454
+
1455
+ // ================================================================
1456
+ // Live Mode: Current Zone Path Tests
1457
+ // Runs on real BSC with PRIVATE_KEY, testing fees + round-trips
1458
+ // at whatever the current on-chain CR zone is.
1459
+ // ================================================================
1460
+
1461
+ describe.skipIf(!IS_LIVE)("Live Mode: Current Zone Path Tests", () => {
1462
+ it("L-01: Fee tier + CR verification at current zone", async () => {
1463
+ const fees = await getFees();
1464
+ const zone = getZone(fees.currentCR);
1465
+ const { contractCR, calculatedCR, tvlInUSD, apUSDSupply } = await verifyCR("Live CR");
1466
+ console.log(` Current zone: ${zone}, CR: ${formatCR(fees.currentCR)}`);
1467
+ verifyFees(fees, zone, `Live Zone ${zone}`);
1468
+
1469
+ if (apUSDSupply > 0n) {
1470
+ const diff = contractCR > calculatedCR ? contractCR - calculatedCR : calculatedCR - contractCR;
1471
+ expect(diff).toBeLessThanOrEqual(1n);
1472
+ }
1473
+ });
1474
+
1475
+ it("L-02: USDT → apUSD → USDT (current zone)", async () => {
1476
+ const fees = await getFees();
1477
+ if (fees.apUSDMintDisabled) {
1478
+ console.log(` ⚠️ apUSD minting disabled at current CR, skipping`);
1479
+ return;
1480
+ }
1481
+
1482
+ const AMOUNT = parseAmount("0.1");
1483
+ const usdtBal = await getBalance(TOKENS.USDT);
1484
+ if (usdtBal < AMOUNT) {
1485
+ console.log(` ⚠️ Insufficient USDT (${formatAmount(usdtBal)}), skipping`);
1486
+ return;
1487
+ }
1488
+
1489
+ await approve(TOKENS.USDT, ROUTER, AMOUNT);
1490
+ const apUSDBefore = await getBalance(TOKENS.apUSD);
1491
+ let mintReceipt: any;
1492
+ try {
1493
+ const mintHash = await routerWrite.swapAndMintDefault({
1494
+ inputToken: TOKENS.USDT, inputAmount: AMOUNT, mintXBNB: false, minMintOut: 0n, deadline: dl(),
1495
+ });
1496
+ mintReceipt = await publicClient.waitForTransactionReceipt({ hash: mintHash });
1497
+ } catch (err: any) {
1498
+ console.log(` ⚠️ apUSD mint tx failed: ${err.shortMessage || err.message?.slice(0, 120)}`);
1499
+ return;
1500
+ }
1501
+ if (mintReceipt.status !== "success") {
1502
+ console.log(` ⚠️ apUSD mint reverted on chain (possible liquidity/routing issue), skipping`);
1503
+ return;
1504
+ }
1505
+ const apUSDAfter = await getBalance(TOKENS.apUSD);
1506
+ const apUSDMinted = apUSDAfter - apUSDBefore;
1507
+ console.log(` Minted ${formatAmount(apUSDMinted)} apUSD from ${formatAmount(AMOUNT)} USDT`);
1508
+ if (apUSDMinted === 0n) {
1509
+ console.log(` ⚠️ Minted 0 apUSD (Zone restriction or routing issue), skipping redeem`);
1510
+ return;
1511
+ }
1512
+
1513
+ // Verify mint fee against zone at mint time
1514
+ const mintFees = await getFees();
1515
+ const mintZone = getZone(mintFees.currentCR);
1516
+ const mintArgs = parseDiamondEvent(mintReceipt, "ApUSDMinted");
1517
+ if (mintArgs) {
1518
+ const actualMintFee = Number(mintArgs.feeBPS);
1519
+ console.log(` Mint fee: ${actualMintFee} BPS (Zone ${mintZone} expects ${ZONE_FEES[mintZone].apUSDMintFee})`);
1520
+ expect(actualMintFee).toBe(ZONE_FEES[mintZone].apUSDMintFee);
1521
+ }
1522
+
1523
+ const redeemAmount = apUSDMinted / 2n;
1524
+ if (redeemAmount === 0n) {
1525
+ console.log(` ⚠️ Minted amount too small to redeem`);
1526
+ return;
1527
+ }
1528
+ const usdtBefore = await getBalance(TOKENS.USDT);
1529
+ let redeemReceipt: any;
1530
+ try {
1531
+ redeemReceipt = await redeemToUSDT(redeemAmount, false);
1532
+ } catch (err: any) {
1533
+ console.log(` ⚠️ Redeem failed: ${err.shortMessage || err.message?.slice(0, 80)}`);
1534
+ return;
1535
+ }
1536
+ const usdtAfter = await getBalance(TOKENS.USDT);
1537
+ console.log(` Redeemed ${formatAmount(redeemAmount)} apUSD → ${formatAmount(usdtAfter - usdtBefore)} USDT`);
1538
+ expect(usdtAfter).toBeGreaterThan(usdtBefore);
1539
+
1540
+ // Verify redeem fee against zone at redeem time (may differ from mint zone in small pools)
1541
+ const redeemFees = await getFees();
1542
+ const redeemZone = getZone(redeemFees.currentCR);
1543
+ const redeemArgs = parseDiamondEvent(redeemReceipt, "ApUSDRedeemed");
1544
+ if (redeemArgs) {
1545
+ const actualRedeemFee = Number(redeemArgs.feeBPS);
1546
+ console.log(` Redeem fee: ${actualRedeemFee} BPS (Zone ${redeemZone} expects ${ZONE_FEES[redeemZone].apUSDRedeemFee})`);
1547
+ expect(actualRedeemFee).toBe(ZONE_FEES[redeemZone].apUSDRedeemFee);
1548
+ }
1549
+ if (mintZone !== redeemZone) {
1550
+ console.log(` ⚠️ Zone shifted during test: mint in Zone ${mintZone}, redeem in Zone ${redeemZone}`);
1551
+ }
1552
+ }, 180000);
1553
+
1554
+ it("L-03: USDT → xBNB → USDT (current zone)", async () => {
1555
+ const AMOUNT = parseAmount("0.1");
1556
+ const usdtBal = await getBalance(TOKENS.USDT);
1557
+ if (usdtBal < AMOUNT) {
1558
+ console.log(` ⚠️ Insufficient USDT (${formatAmount(usdtBal)}), skipping`);
1559
+ return;
1560
+ }
1561
+
1562
+ await approve(TOKENS.USDT, ROUTER, AMOUNT);
1563
+ const xBNBBefore = await getBalance(TOKENS.xBNB);
1564
+ let mintReceipt: any;
1565
+ try {
1566
+ const mintHash = await routerWrite.swapAndMintDefault({
1567
+ inputToken: TOKENS.USDT, inputAmount: AMOUNT, mintXBNB: true, minMintOut: 0n, deadline: dl(),
1568
+ });
1569
+ mintReceipt = await publicClient.waitForTransactionReceipt({ hash: mintHash });
1570
+ } catch (err: any) {
1571
+ console.log(` ⚠️ xBNB mint tx failed: ${err.shortMessage || err.message?.slice(0, 120)}`);
1572
+ return;
1573
+ }
1574
+ if (mintReceipt.status !== "success") {
1575
+ console.log(` ⚠️ xBNB mint reverted on chain (possible liquidity/routing issue), skipping`);
1576
+ return;
1577
+ }
1578
+ const xBNBAfter = await getBalance(TOKENS.xBNB);
1579
+ const xBNBMinted = xBNBAfter - xBNBBefore;
1580
+ console.log(` Minted ${formatAmount(xBNBMinted)} xBNB from ${formatAmount(AMOUNT)} USDT`);
1581
+ if (xBNBMinted === 0n) {
1582
+ console.log(` ⚠️ Minted 0 xBNB (routing issue), skipping redeem`);
1583
+ return;
1584
+ }
1585
+
1586
+ // Verify mint fee against zone at mint time
1587
+ const mintFees = await getFees();
1588
+ const mintZone = getZone(mintFees.currentCR);
1589
+ const xMintArgs = parseDiamondEvent(mintReceipt, "XBNBMinted");
1590
+ if (xMintArgs) {
1591
+ const actualMintFee = Number(xMintArgs.feeBPS);
1592
+ console.log(` Mint fee: ${actualMintFee} BPS (Zone ${mintZone} expects ${ZONE_FEES[mintZone].xBNBMintFee})`);
1593
+ expect(actualMintFee).toBe(ZONE_FEES[mintZone].xBNBMintFee);
1594
+ }
1595
+
1596
+ const redeemAmount = xBNBMinted / 2n;
1597
+ if (redeemAmount === 0n) {
1598
+ console.log(` ⚠️ Minted amount too small to redeem`);
1599
+ return;
1600
+ }
1601
+ const usdtBefore = await getBalance(TOKENS.USDT);
1602
+ let redeemReceipt: any;
1603
+ try {
1604
+ redeemReceipt = await redeemToUSDT(redeemAmount, true);
1605
+ } catch (err: any) {
1606
+ console.log(` ⚠️ Redeem failed: ${err.shortMessage || err.message?.slice(0, 80)}`);
1607
+ return;
1608
+ }
1609
+ const usdtAfter = await getBalance(TOKENS.USDT);
1610
+ console.log(` Redeemed ${formatAmount(redeemAmount)} xBNB → ${formatAmount(usdtAfter - usdtBefore)} USDT`);
1611
+ expect(usdtAfter).toBeGreaterThan(usdtBefore);
1612
+
1613
+ // Verify redeem fee against zone at redeem time
1614
+ const redeemFees = await getFees();
1615
+ const redeemZone = getZone(redeemFees.currentCR);
1616
+ const xRedeemArgs = parseDiamondEvent(redeemReceipt, "XBNBRedeemed");
1617
+ if (xRedeemArgs) {
1618
+ const actualRedeemFee = Number(xRedeemArgs.feeBPS);
1619
+ console.log(` Redeem fee: ${actualRedeemFee} BPS (Zone ${redeemZone} expects ${ZONE_FEES[redeemZone].xBNBRedeemFee})`);
1620
+ expect(actualRedeemFee).toBe(ZONE_FEES[redeemZone].xBNBRedeemFee);
1621
+ }
1622
+ if (mintZone !== redeemZone) {
1623
+ console.log(` ⚠️ Zone shifted during test: mint in Zone ${mintZone}, redeem in Zone ${redeemZone}`);
1624
+ }
1625
+ }, 180000);
1626
+
1627
+ it("L-04: BNB → xBNB (stakeAndMint, current zone)", async () => {
1628
+ const BNB_AMOUNT = parseEther("0.0005");
1629
+ const bnbBal = await publicClient.getBalance({ address: account.address });
1630
+ const minRequired = BNB_AMOUNT + parseEther("0.0005");
1631
+ if (bnbBal < minRequired) {
1632
+ console.log(` ⚠️ Insufficient BNB: have ${formatAmount(bnbBal)}, need ${formatAmount(minRequired)} (value+gas), skipping`);
1633
+ return;
1634
+ }
1635
+ const xBNBBefore = await getBalance(TOKENS.xBNB);
1636
+ try {
1637
+ const hash = await routerWrite.stakeAndMint({
1638
+ targetLST: TOKENS.slisBNB, isXBNB: true, minMintOut: 0n, deadline: dl(), value: BNB_AMOUNT,
1639
+ });
1640
+ const receipt = await waitTx(hash);
1641
+ const xBNBAfter = await getBalance(TOKENS.xBNB);
1642
+ console.log(` Staked ${formatAmount(BNB_AMOUNT)} BNB → ${formatAmount(xBNBAfter - xBNBBefore)} xBNB`);
1643
+ expect(xBNBAfter).toBeGreaterThan(xBNBBefore);
1644
+ } catch (err: any) {
1645
+ console.log(` ⚠️ stakeAndMint failed: ${err.shortMessage || err.message?.slice(0, 80)}`);
1646
+ }
1647
+ }, 120000);
1648
+
1649
+ it("L-05: BNB → apUSD (stakeAndMint, current zone)", async () => {
1650
+ const fees = await getFees();
1651
+ if (fees.apUSDMintDisabled) {
1652
+ console.log(` ⚠️ apUSD minting disabled at current CR, skipping`);
1653
+ return;
1654
+ }
1655
+
1656
+ const BNB_AMOUNT = parseEther("0.0005");
1657
+ const bnbBal = await publicClient.getBalance({ address: account.address });
1658
+ const minRequired = BNB_AMOUNT + parseEther("0.0005");
1659
+ if (bnbBal < minRequired) {
1660
+ console.log(` ⚠️ Insufficient BNB: have ${formatAmount(bnbBal)}, need ${formatAmount(minRequired)} (value+gas), skipping`);
1661
+ return;
1662
+ }
1663
+ const apUSDBefore = await getBalance(TOKENS.apUSD);
1664
+ try {
1665
+ const hash = await routerWrite.stakeAndMint({
1666
+ targetLST: TOKENS.slisBNB, isXBNB: false, minMintOut: 0n, deadline: dl(), value: BNB_AMOUNT,
1667
+ });
1668
+ const receipt = await waitTx(hash);
1669
+ const apUSDAfter = await getBalance(TOKENS.apUSD);
1670
+ console.log(` Staked ${formatAmount(BNB_AMOUNT)} BNB → ${formatAmount(apUSDAfter - apUSDBefore)} apUSD`);
1671
+ expect(apUSDAfter).toBeGreaterThan(apUSDBefore);
1672
+ } catch (err: any) {
1673
+ console.log(` ⚠️ stakeAndMint failed: ${err.shortMessage || err.message?.slice(0, 80)}`);
1674
+ }
1675
+ }, 120000);
1676
+
1677
+ it("L-06: Read-only protocol stats", async () => {
1678
+ const [cr, tvlBNB, tvlUSD, supply, xBNBSupply, lsts] = await Promise.all([
1679
+ getCR(),
1680
+ diamondRead.getTVLInBNB(),
1681
+ diamondRead.getTVLInUSD(),
1682
+ diamondRead.getApUSDSupply(),
1683
+ diamondRead.getXBNBSupply(),
1684
+ diamondRead.getSupportedLSTs(),
1685
+ ]);
1686
+
1687
+ console.log(` CR: ${formatCR(cr)}`);
1688
+ console.log(` TVL(BNB): ${formatAmount(tvlBNB)} | TVL(USD): $${formatAmount(tvlUSD)}`);
1689
+ console.log(` apUSD supply: ${formatAmount(supply)} | xBNB supply: ${formatAmount(xBNBSupply)}`);
1690
+ console.log(` Supported LSTs: ${lsts.length}`);
1691
+
1692
+ expect(cr).toBeGreaterThan(0n);
1693
+ expect(tvlBNB).toBeGreaterThan(0n);
1694
+ expect(tvlUSD).toBeGreaterThan(0n);
1695
+ expect(supply).toBeGreaterThan(0n);
1696
+ });
1697
+
1698
+ // Zone traversal: A → B → C → D, verify fees at each zone
1699
+ // Uses real USDT to manipulate CR. Requires ~15 USDT.
1700
+ // Slower tx wait (3s) so operator can check bot alerts at each zone transition.
1701
+
1702
+ it("L-07: Raise to Zone A → verify fees + real mint/redeem", async () => {
1703
+ txWaitMs = 3000;
1704
+ if (!(await ensureGas("L-07"))) return;
1705
+ const usdtBal = await getBalance(TOKENS.USDT);
1706
+ if (usdtBal < parseAmount("3")) {
1707
+ console.log(` ⚠️ Insufficient USDT (${formatAmount(usdtBal)}) for zone traversal, skipping`);
1708
+ return;
1709
+ }
1710
+
1711
+ const crBefore = await getCR();
1712
+ console.log(` CR before: ${formatCR(crBefore)}`);
1713
+
1714
+ try {
1715
+ await raiseCRTo(ZONE_CR.A_MIN + 500n);
1716
+ } catch (err: any) {
1717
+ console.log(` ⚠️ Could not raise to Zone A: ${err.message?.slice(0, 80)}`);
1718
+ const cur = await getCR();
1719
+ const zone = getZone(cur);
1720
+ console.log(` Verifying fees for current zone ${zone} instead`);
1721
+ const fees = await getFees();
1722
+ verifyFees(fees, zone, `Zone ${zone}`);
1723
+ return;
1724
+ }
1725
+
1726
+ if (IS_LIVE) {
1727
+ console.log(` ⏳ Zone A reached, waiting 30s for bot alert check...`);
1728
+ await sleep(30000);
1729
+ }
1730
+
1731
+ const { zone } = await verifyCR("Zone A");
1732
+ const fees = await getFees();
1733
+ verifyFees(fees, zone, `Zone ${zone}`);
1734
+
1735
+ const TEST_AMT = parseAmount("0.1");
1736
+
1737
+ // apUSD round-trip: USDT → apUSD → USDT
1738
+ if (!fees.apUSDMintDisabled) {
1739
+ console.log(` [Zone ${zone}] apUSD round-trip (0.1 USDT)...`);
1740
+ const apUSDBefore = await getBalance(TOKENS.apUSD);
1741
+ let mintReceipt: any;
1742
+ try {
1743
+ mintReceipt = await mintApUSDWithUSDT(TEST_AMT);
1744
+ } catch (err: any) {
1745
+ console.log(` ⚠️ apUSD mint failed: ${err.shortMessage || err.message?.slice(0, 80)}`);
1746
+ mintReceipt = null;
1747
+ }
1748
+ if (mintReceipt) {
1749
+ const apUSDMinted = (await getBalance(TOKENS.apUSD)) - apUSDBefore;
1750
+ console.log(` Minted: ${formatAmount(apUSDMinted)} apUSD`);
1751
+ if (apUSDMinted > 0n) {
1752
+ const usdtBefore = await getBalance(TOKENS.USDT);
1753
+ try {
1754
+ const redeemReceipt = await redeemToUSDT(apUSDMinted, false);
1755
+ const usdtReceived = (await getBalance(TOKENS.USDT)) - usdtBefore;
1756
+ console.log(` Redeemed: ${formatAmount(usdtReceived)} USDT`);
1757
+ const curFees = await getFees();
1758
+ const curZone = getZone(curFees.currentCR);
1759
+ verifyEventFees(mintReceipt, redeemReceipt, false, curZone, `Zone ${curZone} apUSD`);
1760
+ } catch (err: any) {
1761
+ console.log(` ⚠️ apUSD redeem failed: ${err.shortMessage || err.message?.slice(0, 80)}`);
1762
+ }
1763
+ }
1764
+ }
1765
+ } else {
1766
+ console.log(` [Zone ${zone}] apUSD minting disabled, skipping apUSD round-trip`);
1767
+ }
1768
+
1769
+ // xBNB round-trip: USDT → xBNB → USDT
1770
+ console.log(` [Zone ${zone}] xBNB round-trip (0.1 USDT)...`);
1771
+ const xBNBBefore = await getBalance(TOKENS.xBNB);
1772
+ let xMintReceipt: any;
1773
+ try {
1774
+ xMintReceipt = await mintXBNBWithUSDT(TEST_AMT);
1775
+ } catch (err: any) {
1776
+ console.log(` ⚠️ xBNB mint failed: ${err.shortMessage || err.message?.slice(0, 80)}`);
1777
+ xMintReceipt = null;
1778
+ }
1779
+ if (xMintReceipt) {
1780
+ const xBNBMinted = (await getBalance(TOKENS.xBNB)) - xBNBBefore;
1781
+ console.log(` Minted: ${formatAmount(xBNBMinted, 8)} xBNB`);
1782
+ if (xBNBMinted > 0n) {
1783
+ const usdtBefore = await getBalance(TOKENS.USDT);
1784
+ try {
1785
+ const redeemReceipt = await redeemToUSDT(xBNBMinted, true);
1786
+ const usdtReceived = (await getBalance(TOKENS.USDT)) - usdtBefore;
1787
+ console.log(` Redeemed: ${formatAmount(usdtReceived)} USDT`);
1788
+ const curFees = await getFees();
1789
+ const curZone = getZone(curFees.currentCR);
1790
+ verifyEventFees(xMintReceipt, redeemReceipt, true, curZone, `Zone ${curZone} xBNB`);
1791
+ } catch (err: any) {
1792
+ console.log(` ⚠️ xBNB redeem failed: ${err.shortMessage || err.message?.slice(0, 80)}`);
1793
+ }
1794
+ }
1795
+ }
1796
+
1797
+ console.log(` USDT remaining: ${formatAmount(await getBalance(TOKENS.USDT))}`);
1798
+ }, 600000);
1799
+
1800
+ it("L-08: Lower to Zone B → verify fees + real mint/redeem", async () => {
1801
+ if (!(await ensureGas("L-08"))) return;
1802
+ const cr = await getCR();
1803
+ if (!ZONE_CR.B_MIN || cr < ZONE_CR.B_MIN) {
1804
+ console.log(` ⚠️ Already below Zone B (CR=${formatCR(cr)}), skipping`);
1805
+ return;
1806
+ }
1807
+
1808
+ try {
1809
+ await lowerCRTo(ZONE_CR.B_MIN + 500n);
1810
+ } catch (err: any) {
1811
+ console.log(` ⚠️ Could not lower to Zone B: ${err.message?.slice(0, 80)}`);
1812
+ }
1813
+
1814
+ if (IS_LIVE) {
1815
+ console.log(` ⏳ Zone B reached, waiting 30s for bot alert check...`);
1816
+ await sleep(30000);
1817
+ }
1818
+
1819
+ const { zone } = await verifyCR("Zone B");
1820
+ const fees = await getFees();
1821
+ const actualZone = getZone(fees.currentCR);
1822
+ verifyFees(fees, actualZone, `Zone ${actualZone}`);
1823
+
1824
+ const TEST_AMT = parseAmount("0.1");
1825
+
1826
+ // apUSD round-trip: USDT → apUSD → USDT
1827
+ if (!fees.apUSDMintDisabled) {
1828
+ console.log(` [Zone ${actualZone}] apUSD round-trip (0.1 USDT)...`);
1829
+ const apUSDBefore = await getBalance(TOKENS.apUSD);
1830
+ let mintReceipt: any;
1831
+ try {
1832
+ mintReceipt = await mintApUSDWithUSDT(TEST_AMT);
1833
+ } catch (err: any) {
1834
+ console.log(` ⚠️ apUSD mint failed: ${err.shortMessage || err.message?.slice(0, 80)}`);
1835
+ mintReceipt = null;
1836
+ }
1837
+ if (mintReceipt) {
1838
+ const apUSDMinted = (await getBalance(TOKENS.apUSD)) - apUSDBefore;
1839
+ console.log(` Minted: ${formatAmount(apUSDMinted)} apUSD`);
1840
+ if (apUSDMinted > 0n) {
1841
+ const usdtBefore = await getBalance(TOKENS.USDT);
1842
+ try {
1843
+ const redeemReceipt = await redeemToUSDT(apUSDMinted, false);
1844
+ const usdtReceived = (await getBalance(TOKENS.USDT)) - usdtBefore;
1845
+ console.log(` Redeemed: ${formatAmount(usdtReceived)} USDT`);
1846
+ const curFees = await getFees();
1847
+ const curZone = getZone(curFees.currentCR);
1848
+ verifyEventFees(mintReceipt, redeemReceipt, false, curZone, `Zone ${curZone} apUSD`);
1849
+ } catch (err: any) {
1850
+ console.log(` ⚠️ apUSD redeem failed: ${err.shortMessage || err.message?.slice(0, 80)}`);
1851
+ }
1852
+ }
1853
+ }
1854
+ } else {
1855
+ console.log(` [Zone ${actualZone}] apUSD minting disabled, skipping apUSD round-trip`);
1856
+ }
1857
+
1858
+ // xBNB round-trip: USDT → xBNB → USDT
1859
+ console.log(` [Zone ${actualZone}] xBNB round-trip (0.1 USDT)...`);
1860
+ const xBNBBefore = await getBalance(TOKENS.xBNB);
1861
+ let xMintReceipt: any;
1862
+ try {
1863
+ xMintReceipt = await mintXBNBWithUSDT(TEST_AMT);
1864
+ } catch (err: any) {
1865
+ console.log(` ⚠️ xBNB mint failed: ${err.shortMessage || err.message?.slice(0, 80)}`);
1866
+ xMintReceipt = null;
1867
+ }
1868
+ if (xMintReceipt) {
1869
+ const xBNBMinted = (await getBalance(TOKENS.xBNB)) - xBNBBefore;
1870
+ console.log(` Minted: ${formatAmount(xBNBMinted, 8)} xBNB`);
1871
+ if (xBNBMinted > 0n) {
1872
+ const usdtBefore = await getBalance(TOKENS.USDT);
1873
+ try {
1874
+ const redeemReceipt = await redeemToUSDT(xBNBMinted, true);
1875
+ const usdtReceived = (await getBalance(TOKENS.USDT)) - usdtBefore;
1876
+ console.log(` Redeemed: ${formatAmount(usdtReceived)} USDT`);
1877
+ const curFees = await getFees();
1878
+ const curZone = getZone(curFees.currentCR);
1879
+ verifyEventFees(xMintReceipt, redeemReceipt, true, curZone, `Zone ${curZone} xBNB`);
1880
+ } catch (err: any) {
1881
+ console.log(` ⚠️ xBNB redeem failed: ${err.shortMessage || err.message?.slice(0, 80)}`);
1882
+ }
1883
+ }
1884
+ }
1885
+
1886
+ console.log(` USDT remaining: ${formatAmount(await getBalance(TOKENS.USDT))}`);
1887
+ }, 600000);
1888
+
1889
+ it("L-09: Lower to Zone C → verify fees + real mint/redeem", async () => {
1890
+ if (!(await ensureGas("L-09"))) return;
1891
+ const cr = await getCR();
1892
+ if (!ZONE_CR.C_MIN || cr < ZONE_CR.C_MIN) {
1893
+ console.log(` ⚠️ Already below Zone C (CR=${formatCR(cr)}), skipping`);
1894
+ return;
1895
+ }
1896
+
1897
+ try {
1898
+ await lowerCRTo(ZONE_CR.C_MIN + 500n);
1899
+ } catch (err: any) {
1900
+ console.log(` ⚠️ Could not lower to Zone C: ${err.message?.slice(0, 80)}`);
1901
+ }
1902
+
1903
+ if (IS_LIVE) {
1904
+ console.log(` ⏳ Zone C reached, waiting 30s for bot alert check...`);
1905
+ await sleep(30000);
1906
+ }
1907
+
1908
+ const { zone } = await verifyCR("Zone C");
1909
+ const fees = await getFees();
1910
+ const actualZone = getZone(fees.currentCR);
1911
+ verifyFees(fees, actualZone, `Zone ${actualZone}`);
1912
+
1913
+ const TEST_AMT = parseAmount("0.1");
1914
+
1915
+ // apUSD round-trip: USDT → apUSD → USDT
1916
+ if (!fees.apUSDMintDisabled) {
1917
+ console.log(` [Zone ${actualZone}] apUSD round-trip (0.1 USDT)...`);
1918
+ const apUSDBefore = await getBalance(TOKENS.apUSD);
1919
+ let mintReceipt: any;
1920
+ try {
1921
+ mintReceipt = await mintApUSDWithUSDT(TEST_AMT);
1922
+ } catch (err: any) {
1923
+ console.log(` ⚠️ apUSD mint failed: ${err.shortMessage || err.message?.slice(0, 80)}`);
1924
+ mintReceipt = null;
1925
+ }
1926
+ if (mintReceipt) {
1927
+ const apUSDMinted = (await getBalance(TOKENS.apUSD)) - apUSDBefore;
1928
+ console.log(` Minted: ${formatAmount(apUSDMinted)} apUSD`);
1929
+ if (apUSDMinted > 0n) {
1930
+ const usdtBefore = await getBalance(TOKENS.USDT);
1931
+ try {
1932
+ const redeemReceipt = await redeemToUSDT(apUSDMinted, false);
1933
+ const usdtReceived = (await getBalance(TOKENS.USDT)) - usdtBefore;
1934
+ console.log(` Redeemed: ${formatAmount(usdtReceived)} USDT`);
1935
+ const curFees = await getFees();
1936
+ const curZone = getZone(curFees.currentCR);
1937
+ verifyEventFees(mintReceipt, redeemReceipt, false, curZone, `Zone ${curZone} apUSD`);
1938
+ } catch (err: any) {
1939
+ console.log(` ⚠️ apUSD redeem failed: ${err.shortMessage || err.message?.slice(0, 80)}`);
1940
+ }
1941
+ }
1942
+ }
1943
+ } else {
1944
+ console.log(` [Zone ${actualZone}] apUSD minting disabled, skipping apUSD round-trip`);
1945
+ }
1946
+
1947
+ // xBNB round-trip: USDT → xBNB → USDT
1948
+ console.log(` [Zone ${actualZone}] xBNB round-trip (0.1 USDT)...`);
1949
+ const xBNBBefore = await getBalance(TOKENS.xBNB);
1950
+ let xMintReceipt: any;
1951
+ try {
1952
+ xMintReceipt = await mintXBNBWithUSDT(TEST_AMT);
1953
+ } catch (err: any) {
1954
+ console.log(` ⚠️ xBNB mint failed: ${err.shortMessage || err.message?.slice(0, 80)}`);
1955
+ xMintReceipt = null;
1956
+ }
1957
+ if (xMintReceipt) {
1958
+ const xBNBMinted = (await getBalance(TOKENS.xBNB)) - xBNBBefore;
1959
+ console.log(` Minted: ${formatAmount(xBNBMinted, 8)} xBNB`);
1960
+ if (xBNBMinted > 0n) {
1961
+ const usdtBefore = await getBalance(TOKENS.USDT);
1962
+ try {
1963
+ const redeemReceipt = await redeemToUSDT(xBNBMinted, true);
1964
+ const usdtReceived = (await getBalance(TOKENS.USDT)) - usdtBefore;
1965
+ console.log(` Redeemed: ${formatAmount(usdtReceived)} USDT`);
1966
+ const curFees = await getFees();
1967
+ const curZone = getZone(curFees.currentCR);
1968
+ verifyEventFees(xMintReceipt, redeemReceipt, true, curZone, `Zone ${curZone} xBNB`);
1969
+ } catch (err: any) {
1970
+ console.log(` ⚠️ xBNB redeem failed: ${err.shortMessage || err.message?.slice(0, 80)}`);
1971
+ }
1972
+ }
1973
+ }
1974
+
1975
+ console.log(` USDT remaining: ${formatAmount(await getBalance(TOKENS.USDT))}`);
1976
+ }, 600000);
1977
+
1978
+ it("L-10: Lower to Zone D → verify apUSD disabled + fees + real xBNB mint/redeem", async () => {
1979
+ if (!(await ensureGas("L-10"))) return;
1980
+ const cr = await getCR();
1981
+ if (!ZONE_CR.C_MIN || cr < 11000n) {
1982
+ console.log(` ⚠️ CR already very low (${formatCR(cr)}), skipping`);
1983
+ return;
1984
+ }
1985
+
1986
+ // Target: middle of Zone D. Use D_MIN if available, otherwise C_MIN - 1500
1987
+ const zoneDTarget = ZONE_CR.D_MIN
1988
+ ? ZONE_CR.D_MIN + (ZONE_CR.C_MIN - ZONE_CR.D_MIN) / 2n
1989
+ : ZONE_CR.C_MIN - 1500n;
1990
+ console.log(` Lowering CR to Zone D target: ${formatCR(zoneDTarget)} (current: ${formatCR(cr)})`);
1991
+
1992
+ try {
1993
+ await lowerCRTo(zoneDTarget);
1994
+ } catch (err: any) {
1995
+ console.log(` ⚠️ Could not lower to Zone D: ${err.message?.slice(0, 80)}`);
1996
+ }
1997
+
1998
+ if (IS_LIVE) {
1999
+ const crNow = await getCR();
2000
+ console.log(` ⏳ Zone D reached (CR=${formatCR(crNow)}), waiting 60s for bot alert check...`);
2001
+ await sleep(60000);
2002
+ }
2003
+
2004
+ const fees = await getFees();
2005
+ const actualZone = getZone(fees.currentCR);
2006
+ console.log(` Zone ${actualZone}: CR=${formatCR(fees.currentCR)} disabled=${fees.apUSDMintDisabled}`);
2007
+ verifyFees(fees, actualZone, `Zone ${actualZone}`);
2008
+
2009
+ if (actualZone === ZONE_LAST_LABEL) {
2010
+ expect(fees.apUSDMintDisabled).toBe(true);
2011
+ console.log(` ✓ apUSD minting is disabled in Zone ${actualZone} as expected`);
2012
+
2013
+ // Verify apUSD mint is actually blocked on-chain
2014
+ try {
2015
+ await mintApUSDWithUSDT(parseAmount("0.1"));
2016
+ console.log(` ⚠️ apUSD mint unexpectedly succeeded in Zone ${actualZone}`);
2017
+ } catch {
2018
+ console.log(` ✓ apUSD mint correctly rejected in Zone ${actualZone}`);
2019
+ }
2020
+ }
2021
+
2022
+ // xBNB round-trip: USDT → xBNB → USDT (extreme fees in Zone D)
2023
+ const TEST_AMT = parseAmount("0.1");
2024
+ console.log(` [Zone ${actualZone}] xBNB round-trip (0.1 USDT, extreme fees)...`);
2025
+ const xBNBBefore = await getBalance(TOKENS.xBNB);
2026
+ let xMintReceipt: any;
2027
+ try {
2028
+ xMintReceipt = await mintXBNBWithUSDT(TEST_AMT);
2029
+ } catch (err: any) {
2030
+ console.log(` ⚠️ xBNB mint failed: ${err.shortMessage || err.message?.slice(0, 80)}`);
2031
+ xMintReceipt = null;
2032
+ }
2033
+ if (xMintReceipt) {
2034
+ const xBNBMinted = (await getBalance(TOKENS.xBNB)) - xBNBBefore;
2035
+ console.log(` Minted: ${formatAmount(xBNBMinted, 8)} xBNB`);
2036
+ if (xBNBMinted > 0n) {
2037
+ const usdtBefore = await getBalance(TOKENS.USDT);
2038
+ try {
2039
+ const redeemReceipt = await redeemToUSDT(xBNBMinted, true);
2040
+ const usdtReceived = (await getBalance(TOKENS.USDT)) - usdtBefore;
2041
+ console.log(` Redeemed: ${formatAmount(usdtReceived)} USDT`);
2042
+ const curFees = await getFees();
2043
+ const curZone = getZone(curFees.currentCR);
2044
+ verifyEventFees(xMintReceipt, redeemReceipt, true, curZone, `Zone ${curZone} xBNB`);
2045
+ } catch (err: any) {
2046
+ console.log(` ⚠️ xBNB redeem failed: ${err.shortMessage || err.message?.slice(0, 80)}`);
2047
+ }
2048
+ }
2049
+ }
2050
+
2051
+ console.log(` USDT remaining: ${formatAmount(await getBalance(TOKENS.USDT))}`);
2052
+ }, 600000);
2053
+
2054
+ it("L-11: Restore CR to safe zone (Zone B/C)", async () => {
2055
+ txWaitMs = 500;
2056
+ if (!(await ensureGas("L-11"))) return;
2057
+ const cr = await getCR();
2058
+ const targetCR = ZONE_CR.B_MIN ? ZONE_CR.B_MIN + 500n : 15500n;
2059
+ if (cr >= targetCR) {
2060
+ console.log(` CR already at ${formatCR(cr)}, no restore needed`);
2061
+ return;
2062
+ }
2063
+
2064
+ const usdtBal = await getBalance(TOKENS.USDT);
2065
+ if (usdtBal < parseAmount("0.5")) {
2066
+ console.log(` ⚠️ Not enough USDT to restore CR (${formatAmount(usdtBal)})`);
2067
+ console.log(` ⚠️ Current CR: ${formatCR(cr)} — you may need to manually mint xBNB to raise CR`);
2068
+ return;
2069
+ }
2070
+
2071
+ console.log(` Restoring CR from ${formatCR(cr)} to ${formatCR(targetCR)}...`);
2072
+ try {
2073
+ await raiseCRTo(targetCR);
2074
+ } catch (err: any) {
2075
+ console.log(` ⚠️ Could not fully restore CR: ${err.message?.slice(0, 80)}`);
2076
+ }
2077
+
2078
+ const finalCR = await getCR();
2079
+ const finalZone = getZone(finalCR);
2080
+ console.log(` Final CR: ${formatCR(finalCR)} (Zone ${finalZone})`);
2081
+ console.log(` USDT remaining: ${formatAmount(await getBalance(TOKENS.USDT))}`);
2082
+ }, 600000);
2083
+ });
2084
+ });