@strkfarm/sdk 2.0.0-dev.4 → 2.0.0-dev.41

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.
Files changed (78) hide show
  1. package/dist/cli.js +190 -36
  2. package/dist/cli.mjs +188 -34
  3. package/dist/index.browser.global.js +116250 -90801
  4. package/dist/index.browser.mjs +13050 -10957
  5. package/dist/index.d.ts +2232 -1933
  6. package/dist/index.js +13380 -11084
  7. package/dist/index.mjs +13280 -11007
  8. package/package.json +6 -7
  9. package/src/data/avnu.abi.json +840 -0
  10. package/src/data/ekubo-price-fethcer.abi.json +265 -0
  11. package/src/data/redeem-request-nft.abi.json +752 -0
  12. package/src/data/universal-vault.abi.json +8 -7
  13. package/src/dataTypes/_bignumber.ts +13 -4
  14. package/src/dataTypes/bignumber.browser.ts +10 -1
  15. package/src/dataTypes/bignumber.node.ts +10 -1
  16. package/src/dataTypes/index.ts +3 -2
  17. package/src/dataTypes/mynumber.ts +141 -0
  18. package/src/global.ts +93 -36
  19. package/src/index.browser.ts +2 -1
  20. package/src/interfaces/common.tsx +218 -5
  21. package/src/modules/apollo-client-config.ts +28 -0
  22. package/src/modules/avnu.ts +21 -12
  23. package/src/modules/ekubo-pricer.ts +79 -0
  24. package/src/modules/ekubo-quoter.ts +48 -30
  25. package/src/modules/erc20.ts +17 -0
  26. package/src/modules/harvests.ts +43 -29
  27. package/src/modules/index.ts +2 -1
  28. package/src/modules/pragma.ts +23 -8
  29. package/src/modules/pricer-avnu-api.ts +114 -0
  30. package/src/modules/pricer-from-api.ts +156 -15
  31. package/src/modules/pricer-lst.ts +1 -1
  32. package/src/modules/pricer.ts +94 -40
  33. package/src/modules/pricerBase.ts +2 -1
  34. package/src/node/deployer.ts +36 -1
  35. package/src/node/pricer-redis.ts +3 -1
  36. package/src/strategies/base-strategy.ts +168 -16
  37. package/src/strategies/constants.ts +8 -3
  38. package/src/strategies/ekubo-cl-vault.tsx +1047 -351
  39. package/src/strategies/factory.ts +199 -0
  40. package/src/strategies/index.ts +5 -3
  41. package/src/strategies/registry.ts +262 -0
  42. package/src/strategies/sensei.ts +353 -9
  43. package/src/strategies/svk-strategy.ts +283 -31
  44. package/src/strategies/token-boosted-xstrk-carry-strategy.tsx +1262 -0
  45. package/src/strategies/types.ts +4 -0
  46. package/src/strategies/universal-adapters/adapter-utils.ts +4 -1
  47. package/src/strategies/universal-adapters/avnu-adapter.ts +196 -272
  48. package/src/strategies/universal-adapters/baseAdapter.ts +263 -251
  49. package/src/strategies/universal-adapters/common-adapter.ts +206 -203
  50. package/src/strategies/universal-adapters/index.ts +10 -8
  51. package/src/strategies/universal-adapters/svk-troves-adapter.ts +511 -0
  52. package/src/strategies/universal-adapters/token-transfer-adapter.ts +200 -0
  53. package/src/strategies/universal-adapters/vesu-adapter.ts +120 -82
  54. package/src/strategies/universal-adapters/vesu-modify-position-adapter.ts +525 -0
  55. package/src/strategies/universal-adapters/vesu-multiply-adapter.ts +1098 -712
  56. package/src/strategies/universal-adapters/vesu-position-common.ts +258 -0
  57. package/src/strategies/universal-adapters/vesu-supply-only-adapter.ts +18 -3
  58. package/src/strategies/universal-lst-muliplier-strategy.tsx +631 -414
  59. package/src/strategies/universal-strategy.tsx +1331 -1173
  60. package/src/strategies/vesu-rebalance.tsx +252 -152
  61. package/src/strategies/yoloVault.ts +1087 -0
  62. package/src/utils/cacheClass.ts +11 -2
  63. package/src/utils/health-factor-math.ts +33 -1
  64. package/src/utils/index.ts +3 -1
  65. package/src/utils/logger.browser.ts +22 -4
  66. package/src/utils/logger.node.ts +259 -24
  67. package/src/utils/starknet-call-parser.ts +1036 -0
  68. package/src/utils/strategy-utils.ts +61 -0
  69. package/src/modules/ExtendedWrapperSDk/index.ts +0 -62
  70. package/src/modules/ExtendedWrapperSDk/types.ts +0 -311
  71. package/src/modules/ExtendedWrapperSDk/wrapper.ts +0 -395
  72. package/src/strategies/universal-adapters/extended-adapter.ts +0 -661
  73. package/src/strategies/universal-adapters/unused-balance-adapter.ts +0 -109
  74. package/src/strategies/vesu-extended-strategy/services/operationService.ts +0 -34
  75. package/src/strategies/vesu-extended-strategy/utils/config.runtime.ts +0 -77
  76. package/src/strategies/vesu-extended-strategy/utils/constants.ts +0 -49
  77. package/src/strategies/vesu-extended-strategy/utils/helper.ts +0 -372
  78. package/src/strategies/vesu-extended-strategy/vesu-extended-strategy.tsx +0 -1140
@@ -0,0 +1,1262 @@
1
+ import { ContractAddr, Web3Number } from "@/dataTypes";
2
+ import { Global } from "@/global";
3
+ import {
4
+ AccessControlType,
5
+ AuditStatus,
6
+ FAQ,
7
+ getMainnetConfig,
8
+ getNoRiskTags,
9
+ IConfig,
10
+ InstantWithdrawalVault,
11
+ IStrategyMetadata,
12
+ Protocols,
13
+ RiskFactor,
14
+ RiskType,
15
+ SourceCodeType,
16
+ StrategyLiveStatus,
17
+ StrategyTag,
18
+ TokenInfo,
19
+ VaultPosition,
20
+ VaultType,
21
+ } from "@/interfaces";
22
+ import {
23
+ CounterpartyRiskLevel,
24
+ DepegRiskLevel,
25
+ LiquidationRiskLevel,
26
+ LowLiquidityRiskLevel,
27
+ MarketRiskLevel,
28
+ OracleRiskLevel,
29
+ SmartContractRiskLevel,
30
+ TechnicalRiskLevel,
31
+ } from "@/interfaces/risks";
32
+ import { ERC20, PricerFromApi, TokenMarketData } from "@/modules";
33
+ import { PricerBase } from "@/modules/pricerBase";
34
+ import { logger, assert } from "@/utils";
35
+ import { BlockIdentifier, Call, uint256, num } from "starknet";
36
+ import {
37
+ SingleTokenInfo,
38
+ UserPositionCard,
39
+ UserPositionCardsInput,
40
+ } from "./base-strategy";
41
+ import { SVKStrategy } from "./svk-strategy";
42
+ import {
43
+ APYType,
44
+ AvnuAdapter,
45
+ BaseAdapterConfig,
46
+ CommonAdapter,
47
+ PositionInfo,
48
+ TokenTransferAdapter,
49
+ VesuModifyPositionAdapter,
50
+ } from "./universal-adapters";
51
+ import { AdapterOptimizer } from "./universal-adapters/adapter-optimizer";
52
+ import {
53
+ AVNU_EXCHANGE,
54
+ AVNU_QUOTE_URL,
55
+ } from "./universal-adapters/adapter-utils";
56
+ import { SvkTrovesAdapter } from "./universal-adapters/svk-troves-adapter";
57
+ import { VesuPools, VesuAdapter } from "./universal-adapters/vesu-adapter";
58
+ import {
59
+ AUMTypes,
60
+ getContractDetails,
61
+ UNIVERSAL_MANAGE_IDS,
62
+ UniversalStrategySettings,
63
+ } from "./universal-strategy";
64
+
65
+ export interface BoostedxSTRKCarryStrategySettings extends UniversalStrategySettings {
66
+ depositToken: TokenInfo; // USDC, WBTC, etc. - the collateral token
67
+ debtToken: TokenInfo; // STRK, etc. - the debt token
68
+ lstHyperToken: TokenInfo; // xSTRK, etc. - the LST token deposited into hyper vault
69
+ vesuPoolId: ContractAddr;
70
+ maxLTV: number; // e.g. 0.66
71
+ targetLTV: number;
72
+ hyperLstVaultAddress: ContractAddr; // Address of the hyper vault for LST
73
+ hyperLstRedeemNFT: ContractAddr; // NFT contract for pending hyper redeems
74
+ trovesStrategyId: string; // e.g. "hyper_xstrk" - used for APY fetching
75
+ // TODO: will refac later on to a better approach where we dont need flags
76
+ hasBtcFiRewards: boolean; // Whether this strategy has BTC.Fi rewards (affects reporting flow)
77
+ // Adapter IDs computed from token symbols
78
+ adapterIds?: {
79
+ vesu: string;
80
+ avnu: string;
81
+ hyper: string;
82
+ transfer: string;
83
+ };
84
+ }
85
+
86
+ export class BoostedxSTRKCarryStrategy<
87
+ S extends BoostedxSTRKCarryStrategySettings,
88
+ > extends SVKStrategy<S> {
89
+ constructor(
90
+ config: IConfig,
91
+ pricer: PricerBase,
92
+ metadata: IStrategyMetadata<S>,
93
+ ) {
94
+ super(config, pricer, metadata);
95
+
96
+ this.metadata.additionalInfo.adapters.forEach((adapter) => {
97
+ adapter.adapter.config.networkConfig = this.config;
98
+ adapter.adapter.config.pricer = this.pricer;
99
+ if ((adapter.adapter as any)._vesuAdapter) {
100
+ (adapter.adapter as any)._vesuAdapter.networkConfig = this.config;
101
+ (adapter.adapter as any)._vesuAdapter.pricer = this.pricer;
102
+ }
103
+ });
104
+ }
105
+
106
+ getTag(): string {
107
+ return `${BoostedxSTRKCarryStrategy.name}:${this.metadata.name}`;
108
+ }
109
+
110
+ private getAdapterById<T>(id: string): T {
111
+ const entry = this.metadata.additionalInfo.adapters.find(
112
+ (a) => a.id === id,
113
+ );
114
+ if (!entry) {
115
+ throw new Error(
116
+ `${this.getTag()}::getAdapterById: adapter not found for id "${id}"`,
117
+ );
118
+ }
119
+ return entry.adapter as T;
120
+ }
121
+
122
+ private async getTruePriceForToken(token: TokenInfo): Promise<number> {
123
+ return new TokenMarketData(this.pricer, this.config).getTruePrice(token);
124
+ }
125
+
126
+ async getMinOutputAmountLSTBuy(amountInUnderlying: Web3Number) {
127
+ const lst = this.metadata.additionalInfo.lstHyperToken;
128
+ const lstTruePrice = await this.getTruePriceForToken(lst);
129
+ const minOutputAmount = amountInUnderlying
130
+ .dividedBy(lstTruePrice)
131
+ .multipliedBy(0.99979); // 0.21 % avnu fees
132
+ return new Web3Number(minOutputAmount.toString(), lst.decimals);
133
+ }
134
+
135
+ async getMinOutputAmountLSTSell(amountInLST: Web3Number) {
136
+ const lst = this.metadata.additionalInfo.lstHyperToken;
137
+ const lstTruePrice = await this.getTruePriceForToken(lst);
138
+ return amountInLST.multipliedBy(lstTruePrice).multipliedBy(0.995);
139
+ }
140
+
141
+ async getVesuModifyPositionCall(params: {
142
+ isDeposit: boolean;
143
+ leg1DepositAmount: Web3Number;
144
+ debtAmount?: Web3Number;
145
+ }): Promise<Call> {
146
+ logger.verbose(
147
+ `${this.getTag()}::getVesuModifyPositionCall isDeposit=${params.isDeposit}, amount=${params.leg1DepositAmount}, debtAmount=${params.debtAmount?.toNumber()}`,
148
+ );
149
+ return this.buildManageCallFromAdapter(
150
+ this.getAdapterById<VesuModifyPositionAdapter>(
151
+ this.metadata.additionalInfo.adapterIds!.vesu,
152
+ ),
153
+ params.isDeposit,
154
+ params.leg1DepositAmount,
155
+ params.debtAmount ? { debtAmount: params.debtAmount } : undefined,
156
+ );
157
+ }
158
+
159
+ async getVesuPositions(): Promise<VaultPosition[]> {
160
+ const positions = await this.getAdapterById<VesuModifyPositionAdapter>(
161
+ this.metadata.additionalInfo.adapterIds!.vesu,
162
+ ).getPositions();
163
+ return positions.map((p) => ({
164
+ amount: p.amount,
165
+ usdValue: p.usdValue,
166
+ remarks: p.remarks ?? "",
167
+ token: p.tokenInfo,
168
+ protocol: p.protocol,
169
+ }));
170
+ }
171
+
172
+ async getVesuHealthFactor(
173
+ blockNumber: BlockIdentifier = "latest",
174
+ ): Promise<number> {
175
+ const vesuAdapter = this.getAdapterById<VesuModifyPositionAdapter>(
176
+ this.metadata.additionalInfo.adapterIds!.vesu,
177
+ );
178
+ return await vesuAdapter._vesuAdapter.getHealthFactor(blockNumber);
179
+ }
180
+
181
+ // TODO: will have to still modify for instant withdrawals as of now
182
+ async getModifyHyperPositionCall(params: {
183
+ isDeposit: boolean;
184
+ amount: Web3Number;
185
+ }): Promise<Call> {
186
+ logger.verbose(
187
+ `${this.getTag()}::getModifyHyperPositionCall isDeposit=${params.isDeposit}, amount=${params.amount}`,
188
+ );
189
+ return this.buildManageCallFromAdapter(
190
+ this.getAdapterById<SvkTrovesAdapter>(
191
+ this.metadata.additionalInfo.adapterIds!.hyper,
192
+ ),
193
+ params.isDeposit,
194
+ params.amount,
195
+ );
196
+ }
197
+
198
+ async getAvnuSwapCall(params: {
199
+ isDeposit: boolean;
200
+ amount: Web3Number;
201
+ minAmount?: Web3Number;
202
+ }): Promise<Call> {
203
+ logger.verbose(
204
+ `${this.getTag()}::getAvnuSwapCall isDeposit=${params.isDeposit}, amount=${params.amount}, minAmount=${params.minAmount?.toNumber()}`,
205
+ );
206
+ return this.buildManageCallFromAdapter(
207
+ this.getAdapterById<AvnuAdapter>(
208
+ this.metadata.additionalInfo.adapterIds!.avnu,
209
+ ),
210
+ params.isDeposit,
211
+ params.amount,
212
+ params.minAmount ? { minAmount: params.minAmount } : undefined,
213
+ );
214
+ }
215
+
216
+ /**
217
+ * Returns the deposit token balance sitting idle inside the vault contract itself.
218
+ * This balance can offset the required liquidity during withdrawal processing.
219
+ */
220
+ async getUnusedBalanceVault(): Promise<Web3Number> {
221
+ const depositToken = this.metadata.additionalInfo.depositToken;
222
+ return new ERC20(this.config).balanceOf(
223
+ depositToken.address,
224
+ this.metadata.additionalInfo.vaultAddress,
225
+ depositToken.decimals,
226
+ );
227
+ }
228
+
229
+ /**
230
+ * Returns the current borrow token balance sitting unused in the vault allocator.
231
+ * This covers borrow token from prior borrow cycles that hasn't been swapped yet.
232
+ */
233
+ // Technically we can use this or we can even use the avnu-adapter to get the unused debt
234
+ async getUnusedDebt(): Promise<Web3Number> {
235
+ const debtToken = this.metadata.additionalInfo.debtToken;
236
+ return new ERC20(this.config).balanceOf(
237
+ debtToken.address,
238
+ this.metadata.additionalInfo.vaultAllocator,
239
+ debtToken.decimals,
240
+ );
241
+ }
242
+
243
+ /**
244
+ * Returns the LST balance sitting in the vault allocator.
245
+ * This is non-zero when the previous cycle's request_redeem on the HyperPosition
246
+ * has been settled — the redeemed LST lands here and is ready to be swapped.
247
+ */
248
+ async getLstInVaultAllocator(): Promise<Web3Number> {
249
+ const lstToken = this.metadata.additionalInfo.lstHyperToken;
250
+ return new ERC20(this.config).balanceOf(
251
+ lstToken.address.address,
252
+ this.metadata.additionalInfo.vaultAllocator,
253
+ lstToken.decimals,
254
+ );
255
+ }
256
+
257
+ /**
258
+ * Simulates depositing `depositAmount` on Vesu and returns the
259
+ * incremental borrow token that would be borrowed. Needed to size the AVNU swap
260
+ * call that is batched together with the Vesu call in the same transaction.
261
+ */
262
+ async computeVesuDepositBorrowAmount(
263
+ depositAmount: Web3Number,
264
+ ): Promise<Web3Number> {
265
+ return this.getAdapterById<VesuModifyPositionAdapter>(
266
+ this.metadata.additionalInfo.adapterIds!.vesu,
267
+ ).getExpectedDepositDebtDelta(depositAmount);
268
+ }
269
+
270
+ async computeVesuWithdrawDebtDelta(
271
+ withdrawAmount: Web3Number,
272
+ ): Promise<Web3Number> {
273
+ return this.getAdapterById<VesuModifyPositionAdapter>(
274
+ this.metadata.additionalInfo.adapterIds!.vesu,
275
+ ).getExpectedWithdrawDebtDelta(withdrawAmount);
276
+ }
277
+
278
+ async getPendingHyperAssets(): Promise<Web3Number> {
279
+ const lstToken = this.metadata.additionalInfo.lstHyperToken;
280
+ const hyperLstRedeemNFT = this.metadata.additionalInfo.hyperLstRedeemNFT;
281
+ return this.getAdapterById<SvkTrovesAdapter>(
282
+ this.metadata.additionalInfo.adapterIds!.hyper,
283
+ ).getPendingAssetsFromOwnerNFTMethod(
284
+ hyperLstRedeemNFT,
285
+ this.metadata.additionalInfo.vaultAllocator,
286
+ lstToken.decimals,
287
+ );
288
+ }
289
+
290
+ // TODO: rn we are just making these functions in the strategy itself but
291
+ // later on we will move them to the SVKStrategy for generalization
292
+
293
+ async getUserTVL(
294
+ user: ContractAddr,
295
+ blockIdentifier: BlockIdentifier = "latest",
296
+ ) {
297
+ const shares: any = await this.contract.call("balanceOf", [user.address], {
298
+ blockIdentifier,
299
+ });
300
+ const assets: any = await this.contract.call(
301
+ "convert_to_assets",
302
+ [uint256.bnToUint256(shares)],
303
+ { blockIdentifier },
304
+ );
305
+ const amount = Web3Number.fromWei(
306
+ assets.toString(),
307
+ this.metadata.depositTokens[0].decimals,
308
+ );
309
+
310
+ const blockNumber =
311
+ typeof blockIdentifier === "number" || typeof blockIdentifier === "bigint"
312
+ ? Number(blockIdentifier)
313
+ : undefined;
314
+
315
+ const price = await this.pricer.getPrice(
316
+ this.metadata.depositTokens[0].symbol,
317
+ blockNumber,
318
+ );
319
+ const usdValue = Number(amount.toFixed(6)) * price.price;
320
+ return {
321
+ tokenInfo: this.asset(),
322
+ amount,
323
+ usdValue,
324
+ };
325
+ }
326
+
327
+ async getUserPositionCards(
328
+ input: UserPositionCardsInput,
329
+ ): Promise<UserPositionCard[]> {
330
+ const cards = await super.getUserPositionCards(input);
331
+ const realizedApyRaw = await this.getUserRealizedAPY().catch((error) => {
332
+ logger.warn(
333
+ `${this.getTag()}::getUserPositionCards realized APY fallback`,
334
+ error,
335
+ );
336
+ return null;
337
+ });
338
+
339
+ const realizedApy =
340
+ typeof realizedApyRaw === "number"
341
+ ? this.formatPercentForCard(realizedApyRaw)
342
+ : "N/A";
343
+
344
+ cards.push({
345
+ title: "Realized APY",
346
+ tooltip:
347
+ this.metadata.realizedApyMethodology ||
348
+ "Realized APY is based on past 14 days performance",
349
+ value: realizedApy,
350
+ });
351
+
352
+ return cards;
353
+ }
354
+
355
+ async getAUM(): Promise<{
356
+ net: SingleTokenInfo;
357
+ prevAum: Web3Number;
358
+ splits: PositionInfo[];
359
+ }> {
360
+ const underlying = this.asset(); // Deposit token (USDC, WBTC, etc.)
361
+ const depositTokenPrice = await this.pricer.getPrice(underlying.symbol);
362
+
363
+ const allPositions = await this.getVaultPositions();
364
+
365
+ let netAUM = new Web3Number(0, underlying.decimals);
366
+ for (const position of allPositions) {
367
+ if (position.token.address.eq(underlying.address)) {
368
+ // Same token as underlying - add amount directly
369
+ netAUM = netAUM.plus(position.amount);
370
+ } else {
371
+ // Different token - convert USD value to underlying token amount
372
+ const amountInUnderlying = new Web3Number(
373
+ position.usdValue.toString(),
374
+ underlying.decimals,
375
+ ).dividedBy(depositTokenPrice.price);
376
+ netAUM = netAUM.plus(amountInUnderlying);
377
+ }
378
+ }
379
+
380
+ const prevAum = await this.getPrevAUM();
381
+ logger.verbose(`${this.getTag()} Actual AUM: ${netAUM}`);
382
+
383
+ // Calculate BTCFI rewards contribution
384
+ const rewardAssets = await this.getRewardsAUM(prevAum);
385
+ logger.verbose(`${this.getTag()} Rewards AUM: ${rewardAssets}`);
386
+
387
+ // Sum up total AUM
388
+ const totalAUM = netAUM.plus(rewardAssets);
389
+ logger.verbose(`${this.getTag()} Total AUM: ${totalAUM}`);
390
+
391
+ const realAUM: PositionInfo = {
392
+ tokenInfo: underlying,
393
+ amount: netAUM,
394
+ usdValue: netAUM.toNumber() * depositTokenPrice.price,
395
+ apy: { apy: 0, type: APYType.BASE },
396
+ remarks: AUMTypes.FINALISED,
397
+ protocol: Protocols.NONE,
398
+ };
399
+
400
+ const estimatedAUMDelta: PositionInfo = {
401
+ tokenInfo: underlying,
402
+ amount: rewardAssets,
403
+ usdValue: rewardAssets.toNumber() * depositTokenPrice.price,
404
+ apy: { apy: 0, type: APYType.BASE },
405
+ remarks: AUMTypes.BTCFI,
406
+ protocol: Protocols.NONE,
407
+ };
408
+
409
+ return {
410
+ net: {
411
+ tokenInfo: underlying,
412
+ amount: totalAUM,
413
+ usdValue: totalAUM.toNumber() * depositTokenPrice.price,
414
+ },
415
+ prevAum,
416
+ splits: [realAUM, estimatedAUMDelta],
417
+ };
418
+ }
419
+
420
+ /**
421
+ * Get Vesu APYs for collateral and debt positions
422
+ */
423
+ async getVesuAPYs(): Promise<{
424
+ baseAPYs: number[];
425
+ rewardAPYs: number[];
426
+ positions: VaultPosition[];
427
+ }> {
428
+ const vesuAdapter = this.getAdapterById<VesuModifyPositionAdapter>(
429
+ this.metadata.additionalInfo.adapterIds!.vesu,
430
+ );
431
+
432
+ // Get Vesu pool data
433
+ const allVesuPools = await VesuAdapter.getVesuPools();
434
+ const pool = allVesuPools.pools.find((p: any) =>
435
+ vesuAdapter.config.poolId.eqString(num.getHexString(p.id)),
436
+ );
437
+
438
+ if (!pool) {
439
+ throw new Error(
440
+ `${this.getTag()}::getVesuAPYs: Pool not found for ${vesuAdapter.config.poolId.address}`,
441
+ );
442
+ }
443
+
444
+ // logger.verbose(
445
+ // `${this.getTag()}::getVesuAPYs: vesu-pool: ${JSON.stringify(pool)}`,
446
+ // );
447
+
448
+ // Get positions
449
+ const positions = await this.getVesuPositions();
450
+ logger.verbose(
451
+ `${this.getTag()}::getVesuAPYs: positions: ${JSON.stringify(positions)}`,
452
+ );
453
+
454
+ const baseAPYs: number[] = [];
455
+ const rewardAPYs: number[] = [];
456
+
457
+ // Find collateral and debt asset stats
458
+ const collateralAsset = pool.assets.find(
459
+ (a: any) =>
460
+ a.symbol.toLowerCase() ===
461
+ vesuAdapter.config.collateral.symbol.toLowerCase(),
462
+ )?.stats!;
463
+ const debtAsset = pool.assets.find(
464
+ (a: any) =>
465
+ a.symbol.toLowerCase() === vesuAdapter.config.debt.symbol.toLowerCase(),
466
+ )?.stats!;
467
+
468
+ if (!collateralAsset || !debtAsset) {
469
+ throw new Error(
470
+ `${this.getTag()}::getVesuAPYs: Collateral or debt asset stats not found`,
471
+ );
472
+ }
473
+
474
+ const supplyApy = Number(collateralAsset.supplyApy.value || 0) / 1e18;
475
+ const borrowApr = Number(debtAsset.borrowApr.value) / 1e18;
476
+ const collateralRewardsApr = this.metadata.additionalInfo.hasBtcFiRewards
477
+ ? Number(collateralAsset.btcFiSupplyApr?.value || "0") / 1e18
478
+ : 0;
479
+
480
+ logger.verbose(
481
+ `${this.getTag()}::getVesuAPYs: collateralRewardsApr: ${collateralRewardsApr}`,
482
+ );
483
+ logger.verbose(
484
+ `${this.getTag()}::getVesuAPYs: collateralAsset: ${JSON.stringify(collateralAsset)}`,
485
+ );
486
+ logger.verbose(
487
+ `${this.getTag()}::getVesuAPYs: supplyApy=${supplyApy}, borrowApr=${borrowApr}, collateralRewards=${collateralRewardsApr}`,
488
+ );
489
+
490
+ // Collateral: supply APY, Debt: borrow APR (cost)
491
+ baseAPYs.push(supplyApy, borrowApr);
492
+ rewardAPYs.push(collateralRewardsApr, 0);
493
+
494
+ logger.verbose(
495
+ `${this.getTag()}::getVesuAPYs: baseAPYs: ${JSON.stringify(baseAPYs)}, rewardAPYs: ${JSON.stringify(rewardAPYs)}`,
496
+ );
497
+
498
+ assert(
499
+ baseAPYs.length === positions.length,
500
+ "APYs and positions length mismatch",
501
+ );
502
+
503
+ return { baseAPYs, rewardAPYs, positions };
504
+ }
505
+
506
+ /**
507
+ * Get APY from the Hyper LST vault position
508
+ */
509
+ async getHyperVaultAPY(): Promise<{ apy: number; weight: number }> {
510
+ try {
511
+ const hyperAdapter = this.getAdapterById<SvkTrovesAdapter>(
512
+ this.metadata.additionalInfo.adapterIds!.hyper,
513
+ );
514
+ const positions = await hyperAdapter.getPositions();
515
+
516
+ if (positions.length === 0 || positions[0].amount.isZero()) {
517
+ return { apy: 0, weight: 0 };
518
+ }
519
+
520
+ const position = positions[0];
521
+ const apy = position.apy.apy;
522
+ const weight = position.usdValue;
523
+
524
+ logger.verbose(
525
+ `${this.getTag()}::getHyperVaultAPY: apy=${apy}, weight=${weight}`,
526
+ );
527
+
528
+ return { apy, weight };
529
+ } catch (error) {
530
+ logger.warn(
531
+ `${this.getTag()}::getHyperVaultAPY: Error getting Hyper vault APY: ${error}`,
532
+ );
533
+ return { apy: 0, weight: 0 };
534
+ }
535
+ }
536
+
537
+ /**
538
+ * Get APY for unused balance (idle funds in vault allocator)
539
+ */
540
+ protected async getUnusedBalanceAPY() {
541
+ return {
542
+ apy: 0,
543
+ weight: 0,
544
+ };
545
+ }
546
+
547
+ /**
548
+ * Compute weighted APY from individual APYs, weights, and total AUM
549
+ */
550
+ private computeAPY(
551
+ apys: number[],
552
+ weights: number[],
553
+ totalWeight: number,
554
+ ): number {
555
+ assert(apys.length === weights.length, "APYs and weights length mismatch");
556
+ const weightedSum = apys.reduce((acc, apy, i) => acc + apy * weights[i], 0);
557
+ logger.verbose(
558
+ `${this.getTag()} computeAPY: apys: ${JSON.stringify(apys)}, weights: ${JSON.stringify(weights)}, weightedSum: ${weightedSum}, totalWeight: ${totalWeight}`,
559
+ );
560
+ return weightedSum / totalWeight;
561
+ }
562
+
563
+ /**
564
+ * Calculate net APY from base and reward APYs
565
+ */
566
+ protected async returnNetAPY(
567
+ baseAPYs: number[],
568
+ rewardAPYs: number[],
569
+ weights: number[],
570
+ totalWeightUSD: number,
571
+ ) {
572
+ // If no positions, return 0
573
+ if (weights.every((p) => p == 0)) {
574
+ return {
575
+ net: 0,
576
+ splits: [
577
+ {
578
+ apy: 0,
579
+ id: "base",
580
+ },
581
+ {
582
+ apy: 0,
583
+ id: "btcfi",
584
+ },
585
+ ],
586
+ };
587
+ }
588
+
589
+ const baseAPY = this.computeAPY(baseAPYs, weights, totalWeightUSD);
590
+ const rewardAPY = this.computeAPY(rewardAPYs, weights, totalWeightUSD);
591
+ const netAPY = baseAPY + rewardAPY;
592
+ logger.verbose(
593
+ `${this.getTag()}::netAPY: net: ${netAPY}, baseAPY: ${baseAPY}, rewardAPY: ${rewardAPY}`,
594
+ );
595
+ return {
596
+ net: netAPY,
597
+ splits: [
598
+ {
599
+ apy: baseAPY,
600
+ id: "base",
601
+ },
602
+ {
603
+ apy: rewardAPY,
604
+ id: "btcfi",
605
+ },
606
+ ],
607
+ };
608
+ }
609
+
610
+ /**
611
+ * Calculate net APY across all positions (Vesu, Hyper vault, unused balance)
612
+ * weighted by USD value
613
+ */
614
+ // TODO: will be reviewing this later on, we only need BTCFI APY for now for AUM
615
+ async netAPY(): Promise<{
616
+ net: number;
617
+ splits: { apy: number; id: string }[];
618
+ }> {
619
+ try {
620
+ // Get Vesu APYs and positions
621
+ const { positions, baseAPYs, rewardAPYs } = await this.getVesuAPYs();
622
+
623
+ // Get Hyper vault APY
624
+ const hyperAPY = await this.getHyperVaultAPY();
625
+ baseAPYs.push(hyperAPY.apy);
626
+ rewardAPYs.push(0); // Hyper vault rewards already included in its APY
627
+
628
+ // Get unused balance APY
629
+ const unusedBalanceAPY = await this.getUnusedBalanceAPY();
630
+ baseAPYs.push(unusedBalanceAPY.apy);
631
+ rewardAPYs.push(0);
632
+
633
+ // Compute weights - debt usdValue is already negative from adapter
634
+ const weights = positions.map((p) => p.usdValue);
635
+ weights.push(hyperAPY.weight);
636
+ weights.push(unusedBalanceAPY.weight);
637
+
638
+ logger.verbose(
639
+ `${this.getTag()}::netAPY: weights: ${JSON.stringify(weights)}`,
640
+ );
641
+
642
+ // Calculate total weight (current total AUM in USD)
643
+ const totalWeightUSD = weights.reduce((sum, weight) => sum + weight, 0);
644
+
645
+ logger.verbose(
646
+ `${this.getTag()}::netAPY: totalWeightUSD: ${totalWeightUSD}`,
647
+ );
648
+
649
+ return this.returnNetAPY(baseAPYs, rewardAPYs, weights, totalWeightUSD);
650
+ } catch (error: any) {
651
+ logger.error(
652
+ `${this.getTag()}::netAPY: Error calculating net APY: ${error?.message || error}`,
653
+ );
654
+ return {
655
+ net: 0,
656
+ splits: [
657
+ {
658
+ apy: 0,
659
+ id: "base",
660
+ },
661
+ {
662
+ apy: 0,
663
+ id: "btcfi",
664
+ },
665
+ ],
666
+ };
667
+ }
668
+ }
669
+
670
+ /**
671
+ * Calculate future rewards (e.g. BTCFI rewards) contribution to AUM
672
+ * Similar to universal-strategy.tsx approach
673
+ */
674
+ protected async getRewardsAUM(prevAum: Web3Number): Promise<Web3Number> {
675
+ if (!this.metadata.additionalInfo.hasBtcFiRewards) {
676
+ return Web3Number.fromWei("0", this.asset().decimals);
677
+ }
678
+
679
+ try {
680
+ const lastReportTime = await this.contract.call(
681
+ "last_report_timestamp",
682
+ [],
683
+ );
684
+
685
+ // Calculate estimated growth from rewards
686
+ const netAPY = await this.netAPY();
687
+ // Account only 80% of value for conservative estimate
688
+ const btcfiAPY =
689
+ (netAPY.splits.find((s) => s.id === "btcfi")?.apy || 0) * 0.8;
690
+
691
+ if (!btcfiAPY) {
692
+ logger.verbose(
693
+ `${this.getTag()} No BTCFI APY found, returning 0 rewards`,
694
+ );
695
+ return Web3Number.fromWei("0", this.asset().decimals);
696
+ }
697
+
698
+ // Compute rewards contribution to AUM
699
+ const timeDiff = Math.round(Date.now() / 1000) - Number(lastReportTime);
700
+ const growthRate = (timeDiff * btcfiAPY) / (365 * 24 * 60 * 60);
701
+ const rewardAssets = prevAum.multipliedBy(growthRate);
702
+
703
+ logger.verbose(
704
+ `${this.getTag()} BtcFi AUM time difference: ${timeDiff}s`,
705
+ );
706
+ logger.verbose(`${this.getTag()} Previous AUM: ${prevAum.toString()}`);
707
+ logger.verbose(`${this.getTag()} BtcFi APY: ${btcfiAPY}`);
708
+ logger.verbose(`${this.getTag()} Growth rate: ${growthRate}`);
709
+ logger.verbose(
710
+ `${this.getTag()} Rewards AUM: ${rewardAssets.toString()}`,
711
+ );
712
+
713
+ return rewardAssets;
714
+ } catch (error: any) {
715
+ logger.warn(
716
+ `${this.getTag()} Error calculating rewards AUM: ${error?.message || error}`,
717
+ );
718
+ return Web3Number.fromWei("0", this.asset().decimals);
719
+ }
720
+ }
721
+
722
+ // TODO: can refactor later but seems redundant since not being used ANYWHERE
723
+ // Most of the fund management done through adapters only
724
+
725
+ async getFundManagementCall(params: {
726
+ isDeposit: boolean;
727
+ leg1DepositAmount: Web3Number;
728
+ }) {
729
+ logger.verbose(
730
+ `${this.getTag()}::getFundManagementCall params: ${JSON.stringify(params)}`,
731
+ );
732
+ const allAdapters = this.metadata.additionalInfo.adapters.map(
733
+ (a) => a.adapter,
734
+ );
735
+
736
+ if (!params.isDeposit) {
737
+ const unusedBalance = await this.getUnusedBalance();
738
+ logger.verbose(
739
+ `${this.getTag()}::getFundManagementCall unusedBalance: ${unusedBalance.amount}, required: ${params.leg1DepositAmount}`,
740
+ );
741
+ if (unusedBalance.amount.gte(params.leg1DepositAmount)) {
742
+ return null;
743
+ }
744
+
745
+ const adapters = await AdapterOptimizer.getAdapterToUse(
746
+ allAdapters,
747
+ false,
748
+ params.leg1DepositAmount,
749
+ );
750
+ if (adapters.length > 0) {
751
+ const proofsInfo = adapters.map((adapter) =>
752
+ adapter.getProofs(false, this.getMerkleTree()),
753
+ );
754
+ const calls: Call[] = [];
755
+ for (const info of proofsInfo) {
756
+ const manageCalls = await info.callConstructor({
757
+ amount: params.leg1DepositAmount,
758
+ });
759
+ const call = this.getManageCall(
760
+ this.getProofGroupsForManageCalls(manageCalls),
761
+ manageCalls,
762
+ );
763
+ calls.push(call);
764
+ }
765
+ return calls;
766
+ }
767
+
768
+ throw new Error(
769
+ `${this.getTag()}::getFundManagementCall: no adapters for withdraw: ${unusedBalance.amount}`,
770
+ );
771
+ }
772
+
773
+ const adapters = await AdapterOptimizer.getAdapterToUse(
774
+ allAdapters,
775
+ true,
776
+ params.leg1DepositAmount,
777
+ );
778
+ if (adapters.length > 0) {
779
+ const proofsInfo = adapters.map((adapter) =>
780
+ adapter.getProofs(true, this.getMerkleTree()),
781
+ );
782
+ const calls: Call[] = [];
783
+ for (const info of proofsInfo) {
784
+ const manageCalls = await info.callConstructor({
785
+ amount: params.leg1DepositAmount,
786
+ });
787
+ const call = this.getManageCall(
788
+ this.getProofGroupsForManageCalls(manageCalls),
789
+ manageCalls,
790
+ );
791
+ calls.push(call);
792
+ }
793
+ return calls;
794
+ }
795
+
796
+ throw new Error(
797
+ `${this.getTag()}::getFundManagementCall: no adapters for deposit: ${params.leg1DepositAmount}`,
798
+ );
799
+ }
800
+ }
801
+
802
+ function getBoostedxSTRKCarrySettings(
803
+ vaultSettings: BoostedxSTRKCarryStrategySettings,
804
+ ) {
805
+ vaultSettings.leafAdapters = [];
806
+
807
+ // Use metadata-driven token configuration
808
+ const depositToken = vaultSettings.depositToken;
809
+ const debtToken = vaultSettings.debtToken;
810
+ const lstToken = vaultSettings.lstHyperToken;
811
+
812
+ // Build dynamic adapter IDs based on token symbols
813
+ const adapterIds = {
814
+ vesu: `vesu_${depositToken.symbol.toLowerCase()}_${debtToken.symbol.toLowerCase()}`,
815
+ avnu: `avnu_${debtToken.symbol.toLowerCase()}_${lstToken.symbol.toLowerCase()}`,
816
+ hyper: `hyper_${lstToken.symbol.toLowerCase()}`,
817
+ transfer: `${depositToken.symbol.toLowerCase()}_transfer`,
818
+ };
819
+ vaultSettings.adapterIds = adapterIds;
820
+
821
+ const baseAdapterConfig: BaseAdapterConfig = {
822
+ baseToken: depositToken,
823
+ supportedPositions: [{ asset: depositToken, isDebt: false }],
824
+ networkConfig: getMainnetConfig(),
825
+ pricer: new PricerFromApi(getMainnetConfig(), Global.getDefaultTokens()),
826
+ vaultAllocator: vaultSettings.vaultAllocator,
827
+ vaultAddress: vaultSettings.vaultAddress,
828
+ };
829
+
830
+ // ── 1. VesuModifyPositionAdapter (deposit token collateral / borrow token debt) ──
831
+
832
+ const vesuModifyPositionAdapter = new VesuModifyPositionAdapter({
833
+ poolId: vaultSettings.vesuPoolId,
834
+ collateral: depositToken,
835
+ debt: debtToken,
836
+ targetLtv: vaultSettings.targetLTV,
837
+ maxLtv: vaultSettings.maxLTV,
838
+ ...baseAdapterConfig,
839
+ supportedPositions: [
840
+ { asset: depositToken, isDebt: false },
841
+ { asset: debtToken, isDebt: true },
842
+ ],
843
+ });
844
+
845
+ // ── 2. AvnuAdapter (borrow token ↔ LST swaps) ──
846
+ const avnuAdapter = new AvnuAdapter({
847
+ baseUrl: AVNU_QUOTE_URL,
848
+ avnuContract: AVNU_EXCHANGE,
849
+ slippage: 0.01,
850
+ minimumExtendedPriceDifferenceForSwapOpen: 0,
851
+ maximumExtendedPriceDifferenceForSwapClosing: 0,
852
+ ...baseAdapterConfig,
853
+ baseToken: debtToken,
854
+ supportedPositions: [
855
+ { asset: debtToken, isDebt: false },
856
+ { asset: lstToken, isDebt: false },
857
+ ],
858
+ });
859
+
860
+ // ── 3. SvkTrovesAdapter (deposit LST into / withdraw from Hyper vault) ──
861
+ const svkTrovesAdapter = new SvkTrovesAdapter({
862
+ ...baseAdapterConfig,
863
+ baseToken: lstToken,
864
+ supportedPositions: [{ asset: lstToken, isDebt: false }],
865
+ strategyVault: vaultSettings.hyperLstVaultAddress,
866
+ trovesStrategyId: vaultSettings.trovesStrategyId,
867
+ redeemRequestNFT: vaultSettings.hyperLstRedeemNFT,
868
+ });
869
+
870
+ // ── 5. CommonAdapter (approve + bring liquidity) ──
871
+ const commonAdapter = new CommonAdapter({
872
+ id: UNIVERSAL_MANAGE_IDS.FLASH_LOAN,
873
+ vaultAddress: vaultSettings.vaultAddress,
874
+ vaultAllocator: vaultSettings.vaultAllocator,
875
+ manager: vaultSettings.manager,
876
+ asset: depositToken.address,
877
+ });
878
+
879
+ // ── Register adapters for position tracking ──
880
+ vaultSettings.adapters.push(
881
+ { id: adapterIds.vesu, adapter: vesuModifyPositionAdapter },
882
+ // Used to track swapped funds in vaultAllocator
883
+ { id: adapterIds.avnu, adapter: avnuAdapter },
884
+ { id: adapterIds.hyper, adapter: svkTrovesAdapter },
885
+ );
886
+
887
+ // ── Register leaf adapters for merkle tree ──
888
+ // Vesu modify position
889
+ vaultSettings.leafAdapters.push(() =>
890
+ vesuModifyPositionAdapter.getDepositLeaf(),
891
+ );
892
+ vaultSettings.leafAdapters.push(() =>
893
+ vesuModifyPositionAdapter.getWithdrawLeaf(),
894
+ );
895
+
896
+ // Avnu swaps (borrow token ↔ LST)
897
+ vaultSettings.leafAdapters.push(() => avnuAdapter.getDepositLeaf());
898
+ vaultSettings.leafAdapters.push(() => avnuAdapter.getWithdrawLeaf());
899
+
900
+ // Hyper LST vault deposit / withdraw
901
+ vaultSettings.leafAdapters.push(() => svkTrovesAdapter.getDepositLeaf());
902
+ vaultSettings.leafAdapters.push(() => svkTrovesAdapter.getWithdrawLeaf());
903
+
904
+ // Bring liquidity back to vault
905
+ vaultSettings.leafAdapters.push(
906
+ commonAdapter
907
+ .getApproveAdapter(
908
+ depositToken.address,
909
+ vaultSettings.vaultAddress,
910
+ UNIVERSAL_MANAGE_IDS.APPROVE_BRING_LIQUIDITY,
911
+ )
912
+ .bind(commonAdapter),
913
+ );
914
+ vaultSettings.leafAdapters.push(
915
+ commonAdapter
916
+ .getBringLiquidityAdapter(UNIVERSAL_MANAGE_IDS.BRING_LIQUIDITY)
917
+ .bind(commonAdapter),
918
+ );
919
+
920
+ return vaultSettings;
921
+ }
922
+
923
+ const boostedxSTRKCarrySettings: BoostedxSTRKCarryStrategySettings = {
924
+ vaultAddress: ContractAddr.from(
925
+ "0x02490bd7fa730790e220b40a3cb7bec14ddf030e15c9813610aabbe391f781c4",
926
+ ),
927
+ manager: ContractAddr.from(
928
+ "0x011cd83d2a8c4da86da7b2e2351e9d073568b2fe0fef00dbbdab03ebfc7923b9",
929
+ ),
930
+ vaultAllocator: ContractAddr.from(
931
+ "0x0671f407da7daeb57a9ecfea17bd61b8fcb987d8d0c43855e5f07c2f08a424be",
932
+ ),
933
+ redeemRequestNFT: ContractAddr.from(
934
+ "0x0669d0af171fb4b5d93a2e51f897ae3a2526b0011120fd30e20922de6aa204a1",
935
+ ),
936
+ // Not there for USDC but will be there for WBTC
937
+ aumOracle: ContractAddr.from("0x0"),
938
+ leafAdapters: [],
939
+ adapters: [],
940
+ // Calc using the maxLTV / targetLTV (0.5)
941
+ targetHealthFactor: 1.36,
942
+ // Calc using the maxLTV / maxAcceptableLTV (0.55)
943
+ minHealthFactor: 1.3,
944
+ vesuPoolId: VesuPools.Prime,
945
+ // New metadata-driven token configuration
946
+ depositToken: Global.getDefaultTokens().find((t) => t.symbol === "USDC")!,
947
+ debtToken: Global.getDefaultTokens().find((t) => t.symbol === "STRK")!,
948
+ lstHyperToken: Global.getDefaultTokens().find((t) => t.symbol === "xSTRK")!,
949
+ maxLTV: 0.68,
950
+ targetLTV: 0.5,
951
+ // BTC.Fi rewards flag - false for USDC (uses old report flow)
952
+ hasBtcFiRewards: false,
953
+ // New fields
954
+ hyperLstVaultAddress: ContractAddr.from(
955
+ "0x46c7a54c82b1fe374353859f554a40b8bd31d3e30f742901579e7b57b1b5960",
956
+ ),
957
+ hyperLstRedeemNFT: ContractAddr.from(
958
+ "0x51e40b839dc0c2feca923f863072673b94abfa2483345be3b30b457a90d095",
959
+ ),
960
+ trovesStrategyId: "hyper_xstrk",
961
+ };
962
+
963
+ const wbtcBoostedSettings: BoostedxSTRKCarryStrategySettings = {
964
+ vaultAddress: ContractAddr.from(
965
+ "0x00a4c7c06313a3e06e408dd19a57e633d26cec5b9c4483223bca5d5b467f4ff6",
966
+ ),
967
+ manager: ContractAddr.from(
968
+ "0x002aef32849cd9c011fe5fa09940d818b5895370d0a5076d9e0b95fee91bbad8",
969
+ ),
970
+ vaultAllocator: ContractAddr.from(
971
+ "0x006b7896f559084862c7a2e6606a9aecadc1367b61b0264225e5890401031337",
972
+ ),
973
+ redeemRequestNFT: ContractAddr.from(
974
+ "0x05502bd98af66c3d1227aa4ac7107eb6867b1686c02a62e2edbda91a686be1e3",
975
+ ),
976
+ aumOracle: ContractAddr.from(
977
+ "0x03bd63a5e03de8af1fc28d80c016511d739303eacec67767d50608b1fee6a7df",
978
+ ),
979
+ leafAdapters: [],
980
+ adapters: [],
981
+ targetHealthFactor: 1.4,
982
+ minHealthFactor: 1.35,
983
+ vesuPoolId: VesuPools.Prime,
984
+ depositToken: Global.getDefaultTokens().find((t) => t.symbol === "WBTC")!,
985
+ debtToken: Global.getDefaultTokens().find((t) => t.symbol === "STRK")!,
986
+ lstHyperToken: Global.getDefaultTokens().find((t) => t.symbol === "xSTRK")!,
987
+ maxLTV: 0.7,
988
+ targetLTV: 0.5,
989
+ hasBtcFiRewards: true,
990
+ hyperLstVaultAddress: ContractAddr.from(
991
+ "0x46c7a54c82b1fe374353859f554a40b8bd31d3e30f742901579e7b57b1b5960",
992
+ ),
993
+ hyperLstRedeemNFT: ContractAddr.from(
994
+ "0x51e40b839dc0c2feca923f863072673b94abfa2483345be3b30b457a90d095",
995
+ ),
996
+ trovesStrategyId: "hyper_xstrk",
997
+ };
998
+
999
+ // Risk weights drive the displayed net risk score (weighted average of factor levels).
1000
+ // Highest weights go to risks that can directly impair principal or block exits; lower weights
1001
+ // for mitigated or secondary drivers. Weights sum to 100 for easy reasoning about share of total.
1002
+ const boostedCarryRiskFactors: RiskFactor[] = [
1003
+ {
1004
+ type: RiskType.SMART_CONTRACT_RISK,
1005
+ value: SmartContractRiskLevel.WELL_AUDITED,
1006
+ // 20% — core trust assumption; SVK is audited but still multi-contract (vault, manager, adapters).
1007
+ weight: 20,
1008
+ reason:
1009
+ "Built on the Starknet Vault Kit (SVK), with audits under the Sherlock-audited branch.",
1010
+ },
1011
+ {
1012
+ type: RiskType.LIQUIDATION_RISK,
1013
+ value: LiquidationRiskLevel.MODERATE_PROBABILITY,
1014
+ // 20% — tied with smart contract; uncorrelated collateral/debt is the main principal risk here.
1015
+ weight: 20,
1016
+ reason:
1017
+ "Collateral (USDC or WBTC) and STRK debt are not price-correlated. We target ~50% LTV and rebalance above ~55%, with a health-factor buffer, but extreme moves or prolonged monitoring gaps can still trigger liquidation.",
1018
+ },
1019
+ {
1020
+ type: RiskType.LOW_LIQUIDITY_RISK,
1021
+ value: LowLiquidityRiskLevel.MODERATE_CONCERNS,
1022
+ // 15% — below liquidation but above ops risks; every unwind/withdraw depends on xSTRK→STRK depth.
1023
+ weight: 15,
1024
+ reason:
1025
+ "Closing the loop requires selling xSTRK to STRK on DEXes to repay Vesu debt. xSTRK liquidity can be thin at fair prices; larger withdrawals or rebalances may take longer or complete in smaller chunks.",
1026
+ },
1027
+ {
1028
+ type: RiskType.TECHNICAL_RISK,
1029
+ value: TechnicalRiskLevel.SOME_COMPLEXITY,
1030
+ // 13% — monitoring/rebalancing stack matters, but usually recoverable without instant loss.
1031
+ weight: 13,
1032
+ reason:
1033
+ "Yield routes across Vesu, Avnu, and Hyper xSTRK with automated monitoring. Technical failures can delay rebalancing.",
1034
+ },
1035
+ {
1036
+ type: RiskType.DEPEG_RISK,
1037
+ value: DepegRiskLevel.OCCASIONAL_DEPEG,
1038
+ // 12% — affects yield and swap notionals; USDC/WBTC depeg is rarer than xSTRK/STRK drift.
1039
+ weight: 12,
1040
+ reason:
1041
+ "xSTRK yield depends on its exchange rate vs STRK; USDC and WBTC carry standard stablecoin and BTC market risks.",
1042
+ },
1043
+ {
1044
+ type: RiskType.ORACLE_RISK,
1045
+ value: OracleRiskLevel.SINGLE_RELIABLE,
1046
+ // 10% — Vesu HF depends on feeds; Starknet oracles are established but not redundant here.
1047
+ weight: 10,
1048
+ reason: "Vesu collateral and debt valuations rely on Starknet price feeds.",
1049
+ },
1050
+ {
1051
+ type: RiskType.COUNTERPARTY_RISK,
1052
+ value: CounterpartyRiskLevel.REPUTABLE_COUNTERPARTY,
1053
+ // 10% — same tier as oracle; Vesu/Endur/Troves are mature but still protocol counterparty risk.
1054
+ weight: 10,
1055
+ reason:
1056
+ "Exposure to Vesu lending, Endur (xSTRK), and Troves Hyper vault infrastructure.",
1057
+ },
1058
+ {
1059
+ type: RiskType.MARKET_RISK,
1060
+ value: MarketRiskLevel.MODERATE_VOLATILITY,
1061
+ // 5% — largely captured by liquidation + depeg factors; kept small to avoid double-counting vol.
1062
+ weight: 5,
1063
+ reason:
1064
+ "STRK, BTC, and stablecoin volatility can move loan-to-value and swap costs.",
1065
+ },
1066
+ ];
1067
+
1068
+ function getBoostedCarryRisk() {
1069
+ const netRisk =
1070
+ boostedCarryRiskFactors.reduce(
1071
+ (acc, curr) => acc + curr.value * curr.weight,
1072
+ 0,
1073
+ ) / boostedCarryRiskFactors.reduce((acc, curr) => acc + curr.weight, 0);
1074
+ return {
1075
+ riskFactor: boostedCarryRiskFactors,
1076
+ netRisk,
1077
+ notARisks: getNoRiskTags(boostedCarryRiskFactors),
1078
+ };
1079
+ }
1080
+
1081
+ function getBoostedCarryFAQs(
1082
+ depositSymbol: string,
1083
+ debtSymbol: string,
1084
+ lstSymbol: string,
1085
+ ): FAQ[] {
1086
+ return [
1087
+ {
1088
+ question: `What is ${depositSymbol} Boosted?`,
1089
+ answer: `${depositSymbol} Boosted is a carry-style vault: your ${depositSymbol} is supplied as collateral on Vesu, ${debtSymbol} is borrowed, swapped to ${lstSymbol}, and deposited into Hyper-${lstSymbol} for additional yield on top of the lending loop.`,
1090
+ },
1091
+ {
1092
+ question: "How does this strategy generate yield?",
1093
+ answer: `Yield comes from several layers: ${lstSymbol} staking yield inside Hyper-${lstSymbol}, the spread between borrowing ${debtSymbol} and holding ${lstSymbol}, and efficient routing via Avnu. The vault targets about 50% loan-to-value and rebalances when LTV rises above ~55% so risk and yield stay in balance.`,
1094
+ },
1095
+ {
1096
+ question: "Which protocols are used?",
1097
+ answer: (
1098
+ <span>
1099
+ <strong>Vesu</strong> for collateral and borrowing,{" "}
1100
+ <strong>Avnu</strong> for swaps, <strong>Endur</strong> for{" "}
1101
+ {lstSymbol}, and the Troves <strong>Hyper-{lstSymbol}</strong> vault
1102
+ for the boosted leg.
1103
+ </span>
1104
+ ),
1105
+ },
1106
+ {
1107
+ question: "Is there liquidation risk?",
1108
+ answer: `Yes. Your ${depositSymbol} collateral and ${debtSymbol} debt can move in different directions, which affects your health factor on Vesu. We actively monitor positions and rebalance automatically through our systems to keep loan-to-value near target. However, unexpected technical failures or very sharp market moves can still lead to liquidation. Under normal conditions we aim for a buffer of roughly 30% adverse price movement before liquidation becomes likely — but if monitoring is unavailable for many hours during a trending market, that buffer can be consumed. In extreme scenarios, liquidation remains possible.`,
1109
+ },
1110
+ {
1111
+ question: "What do I receive when I deposit?",
1112
+ answer:
1113
+ "You receive vault tokens representing your share of the vault. They reflect both principal and accrued yield.",
1114
+ },
1115
+ {
1116
+ question: "How long do withdrawals take?",
1117
+ answer:
1118
+ "Withdrawals typically take 1–2 hours. You receive an NFT for your request; funds are sent to the NFT owner once the vault unwinds its positions. The vault must sell xSTRK back to STRK to repay debt — if DEX liquidity is thin or your withdrawal is large, unwinding can take longer or happen in smaller steps to limit slippage.",
1119
+ },
1120
+ {
1121
+ question: "Is this vault non-custodial?",
1122
+ answer:
1123
+ "Yes. The vault runs on-chain. You hold your vault tokens, and positions are transparent via on-chain contracts.",
1124
+ },
1125
+ {
1126
+ question: "Are there any fees?",
1127
+ answer:
1128
+ "Troves charges a 15% performance fee on yield only — not on your deposited principal. The APY shown is already net of this fee. When you withdraw, a 0.1% redemption fee applies. That small fee helps keep the vault efficient for long-term depositors, very short stays create extra swap and rebalancing costs for everyone. If you stay for a reasonable period, your yield will typically far outweigh this one-time charge.",
1129
+ },
1130
+ {
1131
+ question: "Is the vault audited?",
1132
+ answer: (
1133
+ <span>
1134
+ Yes. The strategy is built on the audited Starknet Vault Kit. See the
1135
+ security details beside the strategy name and the{" "}
1136
+ <a
1137
+ href="https://github.com/trovesfi/starknet_vault_kit/tree/sherlock-audited"
1138
+ target="_blank"
1139
+ rel="noopener noreferrer"
1140
+ style={{ textDecoration: "underline" }}
1141
+ >
1142
+ open-source repo
1143
+ </a>
1144
+ .
1145
+ </span>
1146
+ ),
1147
+ },
1148
+ ];
1149
+ }
1150
+
1151
+ function getStrategySettings(
1152
+ settings: BoostedxSTRKCarryStrategySettings,
1153
+ meta: { id: string; name: string; launchBlock: number },
1154
+ ): IStrategyMetadata<BoostedxSTRKCarryStrategySettings> {
1155
+ const depositToken = settings.depositToken;
1156
+ const debtToken = settings.debtToken;
1157
+ const lstToken = settings.lstHyperToken;
1158
+
1159
+ return {
1160
+ id: meta.id,
1161
+ name: meta.name,
1162
+ description: `Deposits ${depositToken.symbol} as collateral on Vesu, borrows ${debtToken.symbol}, swaps to ${lstToken.symbol}, and deposits into Hyper-${lstToken.symbol} for boosted yield. Target LTV is about 50%, with rebalances happening when LTV is above 55%.`,
1163
+ address: settings.vaultAddress,
1164
+ launchBlock: meta.launchBlock,
1165
+ type: "ERC4626" as const,
1166
+ vaultType: {
1167
+ type: VaultType.META_VAULT,
1168
+ description: `Deposits ${depositToken.symbol} as collateral on Vesu, borrows ${debtToken.symbol}, swaps to ${lstToken.symbol}, and deposits into Hyper-${lstToken.symbol} for boosted yield`,
1169
+ },
1170
+ depositTokens: [depositToken],
1171
+ additionalInfo: getBoostedxSTRKCarrySettings(settings),
1172
+ risk: getBoostedCarryRisk(),
1173
+ protocols: [Protocols.VESU, Protocols.ENDUR, Protocols.TROVES],
1174
+ curator: {
1175
+ name: "Unwrap Labs",
1176
+ logo: "https://assets.troves.fi/integrations/unwraplabs/white.png",
1177
+ },
1178
+ settings: {
1179
+ maxTVL: Web3Number.fromWei(0, depositToken.decimals),
1180
+ liveStatus: StrategyLiveStatus.HOT,
1181
+ isPaused: false,
1182
+ isAudited: true,
1183
+ isInstantWithdrawal: false,
1184
+ hideHarvestInfo: true,
1185
+ quoteToken: depositToken,
1186
+ alerts: [
1187
+ {
1188
+ tab: "withdraw" as const,
1189
+ text: "On withdrawal, you will receive an NFT representing your withdrawal request. The funds will be automatically sent to your wallet (NFT owner) in 1-2 hours. You can monitor the status in transactions tab.",
1190
+ type: "info" as const,
1191
+ },
1192
+ ],
1193
+ },
1194
+ contractDetails: getContractDetails(settings),
1195
+ feeBps: {
1196
+ performanceFeeBps: 1500,
1197
+ },
1198
+ faqs: getBoostedCarryFAQs(
1199
+ depositToken.symbol,
1200
+ debtToken.symbol,
1201
+ lstToken.symbol,
1202
+ ),
1203
+ apyMethodology:
1204
+ "APY reflects net returns performance fee on yield (not on principal). Underlying xSTRK appreciation and Vesu borrow costs can move over time; displayed APY is an estimate.",
1205
+ investmentSteps: [
1206
+ `Deposit ${depositToken.symbol} into the vault`,
1207
+ `${depositToken.symbol} is supplied as collateral on Vesu, ${debtToken.symbol} is borrowed`,
1208
+ `Borrowed ${debtToken.symbol} is swapped to ${lstToken.symbol} via Avnu`,
1209
+ `${lstToken.symbol} is deposited into the Hyper-${lstToken.symbol} vault for additional yield`,
1210
+ `Actively monitored and rebalanced to maintain optimal LTV and yield.`,
1211
+ ],
1212
+ // TODO: config later
1213
+ tags: [StrategyTag.META_VAULT],
1214
+ security: {
1215
+ auditStatus: AuditStatus.AUDITED,
1216
+ sourceCode: {
1217
+ type: SourceCodeType.OPEN_SOURCE,
1218
+ contractLink:
1219
+ "https://github.com/trovesfi/starknet_vault_kit/tree/sherlock-audited",
1220
+ },
1221
+ // TODO
1222
+ accessControl: {
1223
+ type: AccessControlType.STANDARD_ACCOUNT,
1224
+ addresses: [ContractAddr.from("0x0")],
1225
+ timeLock: "0 Days",
1226
+ },
1227
+ },
1228
+ redemptionInfo: {
1229
+ instantWithdrawalVault: InstantWithdrawalVault.NO,
1230
+ redemptionsInfo: [
1231
+ {
1232
+ title: "Typical Duration",
1233
+ description: "1-2 hours",
1234
+ },
1235
+ ],
1236
+ alerts: [
1237
+ {
1238
+ type: "info",
1239
+ text: "Redemption times are estimates and may vary based on network conditions and liquidity requirements.",
1240
+ tab: "withdraw",
1241
+ },
1242
+ ],
1243
+ },
1244
+ usualTimeToEarnings: "2 weeks",
1245
+ usualTimeToEarningsDescription:
1246
+ "This strategy depends on Hyper xSTRK's yield, which depends on the price of xSTRK on DEX appreciating. It may be possible the increase is not continuous and generally rebases atleast once every 2 weeks.",
1247
+ };
1248
+ }
1249
+
1250
+ export const BoostedxSTRKCarryStrategies: IStrategyMetadata<BoostedxSTRKCarryStrategySettings>[] =
1251
+ [
1252
+ getStrategySettings(boostedxSTRKCarrySettings, {
1253
+ id: "usdc_boosted",
1254
+ name: "USDC Boosted",
1255
+ launchBlock: 10502825,
1256
+ }),
1257
+ getStrategySettings(wbtcBoostedSettings, {
1258
+ id: "wbtc_boosted",
1259
+ name: "WBTC Boosted",
1260
+ launchBlock: 10502758,
1261
+ }),
1262
+ ];