@strkfarm/sdk 1.1.34 → 1.1.35

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.
@@ -60,6 +60,7 @@ export class AvnuWrapper {
60
60
  throw new Error('no quotes found')
61
61
  }
62
62
 
63
+ logger.verbose(`${AvnuWrapper.name}: getQuotes => filteredQuotes: ${JSON.stringify(filteredQuotes)}`);
63
64
  return filteredQuotes[0];
64
65
  }
65
66
 
@@ -14,6 +14,7 @@ import { ENDPOINTS } from "../constants";
14
14
  import VesuMultiplyAbi from '@/data/vesu-multiple.abi.json';
15
15
  import { EkuboPoolKey } from "../ekubo-cl-vault";
16
16
  import VesuPoolV2Abi from '@/data/vesu-pool-v2.abi.json';
17
+ import VesuExtensionAbi from '@/data/vesu-extension.abi.json';
17
18
 
18
19
  interface VesuPoolsInfo { pools: any[]; isErrorPoolsAPI: boolean };
19
20
 
@@ -56,6 +57,23 @@ export interface VesuAdapterConfig {
56
57
  id: string
57
58
  }
58
59
 
60
+ type InterestRateConfig = {
61
+ target_utilization: bigint;
62
+ zero_utilization_rate: bigint;
63
+ target_rate_percent: bigint;
64
+
65
+ min_target_utilization: bigint;
66
+ max_target_utilization: bigint;
67
+ rate_half_life: bigint;
68
+ min_full_utilization_rate: bigint;
69
+ max_full_utilization_rate: bigint;
70
+ };
71
+
72
+ const SCALE = BigInt(1e18);
73
+ const UTILIZATION_SCALE = 100_000n;
74
+ const UTILIZATION_SCALE_TO_SCALE = BigInt(1e13);
75
+
76
+
59
77
  export interface TokenAmount {
60
78
  token: ContractAddr,
61
79
  amount: Web3Number, // i129
@@ -217,6 +235,9 @@ export const VesuPools = {
217
235
  Re7xBTC: ContractAddr.from('0x3a8416bf20d036df5b1cf3447630a2e1cb04685f6b0c3a70ed7fb1473548ecf')
218
236
  }
219
237
 
238
+ export const extensionMap: {[key: string]: ContractAddr} = {};
239
+ extensionMap[VesuPools.Re7xSTRK.address] = ContractAddr.from('0x04e06e04b8d624d039aa1c3ca8e0aa9e21dc1ccba1d88d0d650837159e0ee054');
240
+
220
241
  export function getVesuSingletonAddress(vesuPool: ContractAddr) {
221
242
  if (vesuPool.eq(VesuPools.Genesis) ||
222
243
  vesuPool.eq(VesuPools.Re7xSTRK)) {
@@ -501,6 +522,67 @@ export class VesuAdapter extends BaseAdapter {
501
522
  }
502
523
  }
503
524
 
525
+ async getDebtCap(config: IConfig) {
526
+ const { contract, isV2 } = await this.getVesuSingletonContract(config, this.config.poolId);
527
+ if (!isV2) {
528
+ const extensionAddr = extensionMap[this.config.poolId.address];
529
+ if (!extensionAddr) {
530
+ throw new Error('Extension address not found');
531
+ }
532
+ const extensionContract = new Contract({abi: VesuExtensionAbi, address: extensionAddr.address, providerOrAccount: config.provider});
533
+ const output: any = await extensionContract.call('debt_caps', [this.config.poolId.address, this.config.collateral.address.address, this.config.debt.address.address]);
534
+ logger.verbose(`${this.config.debt.symbol}::VesuAdapter::getDebtCap debt_cap: ${output.toString()}`);
535
+ return Web3Number.fromWei(output.toString(), this.config.debt.decimals);
536
+ }
537
+ const output: any = await contract.call('pair_config', [this.config.collateral.address.address, this.config.debt.address.address]);
538
+ logger.verbose(`${this.config.debt.symbol}::VesuAdapter::getDebtCap debt_cap: ${output.debt_cap.toString()}`);
539
+ return Web3Number.fromWei(output.debt_cap.toString(), this.config.debt.decimals);
540
+ }
541
+
542
+ async getMaxBorrowableByInterestRate(config: IConfig, asset: TokenInfo, maxBorrowAPY: number) {
543
+ const { contract, isV2 } = await this.getVesuSingletonContract(config, this.config.poolId);
544
+ let interestRateConfigContract = contract;
545
+ if (!isV2) {
546
+ const extensionAddr = extensionMap[this.config.poolId.address];
547
+ if (!extensionAddr) {
548
+ throw new Error('Extension address not found');
549
+ }
550
+ interestRateConfigContract = new Contract({abi: VesuExtensionAbi, address: extensionAddr.address, providerOrAccount: config.provider});
551
+
552
+ }
553
+ const _interestRateConfig: any = await interestRateConfigContract.call(
554
+ 'interest_rate_config',
555
+ isV2 ? [this.config.debt.address.address] : [this.config.poolId.address, this.config.debt.address.address]
556
+ );
557
+ const interestRateConfig: InterestRateConfig = {
558
+ target_utilization: _interestRateConfig.target_utilization,
559
+ zero_utilization_rate: _interestRateConfig.zero_utilization_rate,
560
+ target_rate_percent: _interestRateConfig.target_rate_percent,
561
+ min_target_utilization: _interestRateConfig.min_target_utilization,
562
+ max_target_utilization: _interestRateConfig.max_target_utilization,
563
+ rate_half_life: _interestRateConfig.rate_half_life,
564
+ min_full_utilization_rate: _interestRateConfig.min_full_utilization_rate,
565
+ max_full_utilization_rate: _interestRateConfig.max_full_utilization_rate,
566
+ };
567
+
568
+ const _assetConfig: any = await contract.call(
569
+ isV2 ? 'asset_config' : 'asset_config_unsafe',
570
+ isV2 ? [asset.address.address] : [this.config.poolId.address, asset.address.address]
571
+ );
572
+ const assetConfig = isV2 ? _assetConfig : _assetConfig['0'];
573
+ const timeDelta = assetConfig.last_updated;
574
+ const lastFullUtilizationRate = assetConfig.last_full_utilization_rate;
575
+ const totalSupply = (new Web3Number((Number(assetConfig.total_nominal_debt) / 1e18).toFixed(9), asset.decimals)).plus(Web3Number.fromWei(assetConfig.reserve, asset.decimals));
576
+
577
+ const ratePerSecond = BigInt(Math.round(maxBorrowAPY / 365 / 24 / 60 / 60 * Number(SCALE)));
578
+ const maxUtilisation = this.getMaxUtilizationGivenRatePerSecond(interestRateConfig, ratePerSecond, timeDelta, lastFullUtilizationRate);
579
+ logger.verbose(`${asset.symbol}::VesuAdapter::getMaxBorrowableByInterestRate maxUtilisation: ${Number(maxUtilisation) / 1e18}, totalSupply: ${totalSupply.toString()}`);
580
+
581
+ const maxDebtToHave = totalSupply.multipliedBy(Number(maxUtilisation) / 1e18);
582
+ const currentDebt = new Web3Number((Number(assetConfig.total_nominal_debt) / 1e18).toFixed(9), asset.decimals);
583
+ return maxDebtToHave.minus(currentDebt);
584
+ }
585
+
504
586
  async getLTVConfig(config: IConfig) {
505
587
  const CACHE_KEY = 'ltv_config';
506
588
  const cacheData = this.getCache<number>(CACHE_KEY);
@@ -516,6 +598,9 @@ export class VesuAdapter extends BaseAdapter {
516
598
  const output: any = await contract.call('ltv_config', [this.config.poolId.address, this.config.collateral.address.address, this.config.debt.address.address]);
517
599
  ltv = Number(output.max_ltv) / 1e18;
518
600
  }
601
+ if (ltv == 0) {
602
+ throw new Error('LTV is 0');
603
+ }
519
604
  this.setCache(CACHE_KEY, ltv, 300000); // ttl: 5min
520
605
  return this.getCache<number>(CACHE_KEY) as number;
521
606
  }
@@ -673,4 +758,120 @@ export class VesuAdapter extends BaseAdapter {
673
758
  Global.setGlobalCache(CACHE_KEY, { pools, isErrorPoolsAPI }, 300000); // cache for 5 minutes
674
759
  return { pools, isErrorPoolsAPI };
675
760
  }
761
+
762
+
763
+ fullUtilizationRate(
764
+ interestRateConfig: InterestRateConfig,
765
+ timeDelta: bigint,
766
+ utilization: bigint,
767
+ fullUtilizationRate: bigint
768
+ ): bigint {
769
+ const {
770
+ min_target_utilization,
771
+ max_target_utilization,
772
+ rate_half_life,
773
+ min_full_utilization_rate,
774
+ max_full_utilization_rate,
775
+ } = interestRateConfig;
776
+
777
+ const halfLifeScaled = rate_half_life * SCALE;
778
+
779
+ let nextFullUtilizationRate: bigint;
780
+
781
+ if (utilization < min_target_utilization) {
782
+ const utilizationDelta =
783
+ ((min_target_utilization - utilization) * SCALE) / min_target_utilization;
784
+ const decay = halfLifeScaled + utilizationDelta * timeDelta;
785
+ nextFullUtilizationRate = (fullUtilizationRate * halfLifeScaled) / decay;
786
+ } else if (utilization > max_target_utilization) {
787
+ const utilizationDelta =
788
+ ((utilization - max_target_utilization) * SCALE) /
789
+ (UTILIZATION_SCALE - max_target_utilization);
790
+ const growth = halfLifeScaled + utilizationDelta * timeDelta;
791
+ nextFullUtilizationRate = (fullUtilizationRate * growth) / halfLifeScaled;
792
+ } else {
793
+ nextFullUtilizationRate = fullUtilizationRate;
794
+ }
795
+
796
+ if (nextFullUtilizationRate > max_full_utilization_rate) {
797
+ return max_full_utilization_rate;
798
+ } else if (nextFullUtilizationRate < min_full_utilization_rate) {
799
+ return min_full_utilization_rate;
800
+ } else {
801
+ return nextFullUtilizationRate;
802
+ }
803
+ }
804
+
805
+ /**
806
+ * Calculates new interest rate per second and next full utilization rate.
807
+ */
808
+ calculateInterestRate(
809
+ interestRateConfig: InterestRateConfig,
810
+ utilization: bigint,
811
+ timeDelta: bigint,
812
+ lastFullUtilizationRate: bigint
813
+ ): { newRatePerSecond: bigint; nextFullUtilizationRate: bigint } {
814
+ const scaledUtilization = utilization / UTILIZATION_SCALE_TO_SCALE;
815
+
816
+ const {
817
+ target_utilization,
818
+ zero_utilization_rate,
819
+ target_rate_percent,
820
+ } = interestRateConfig;
821
+
822
+ const nextFullUtilizationRate = this.fullUtilizationRate(
823
+ interestRateConfig,
824
+ timeDelta,
825
+ scaledUtilization,
826
+ lastFullUtilizationRate
827
+ );
828
+
829
+ const targetRate =
830
+ (((nextFullUtilizationRate - zero_utilization_rate) * target_rate_percent) / SCALE) +
831
+ zero_utilization_rate;
832
+
833
+ let newRatePerSecond: bigint;
834
+
835
+ if (scaledUtilization < target_utilization) {
836
+ newRatePerSecond =
837
+ zero_utilization_rate +
838
+ (scaledUtilization * (targetRate - zero_utilization_rate)) /
839
+ target_utilization;
840
+ } else {
841
+ newRatePerSecond =
842
+ targetRate +
843
+ ((scaledUtilization - target_utilization) *
844
+ (nextFullUtilizationRate - targetRate)) /
845
+ (UTILIZATION_SCALE - target_utilization);
846
+ }
847
+
848
+ return { newRatePerSecond, nextFullUtilizationRate };
849
+ }
850
+
851
+ /**
852
+ * Calculates utilization given a specific rate per second.
853
+ * This is an inverse function of the piecewise interest rate formula above.
854
+ */
855
+ getMaxUtilizationGivenRatePerSecond(
856
+ interestRateConfig: InterestRateConfig,
857
+ ratePerSecond: bigint,
858
+ timeDelta: bigint,
859
+ last_full_utilization_rate: bigint
860
+ ): bigint {
861
+ logger.verbose(`VesuAdapter::getMaxUtilizationGivenRatePerSecond ratePerSecond: ${Number(ratePerSecond) / 1e18}, timeDelta: ${Number(timeDelta) / 1e18}, last_full_utilization_rate: ${Number(last_full_utilization_rate) / 1e18}`);
862
+ // use binary search to find the max utilization
863
+ // vary utlisation from 0 to SCALE, answer is the utilisation where the next step > ratePerSecond and the previous step < ratePerSecond
864
+ // Step is vary by SCALE / 100
865
+ let utilization = 0n;
866
+ let nextUtilization = SCALE / 100n;
867
+ while (utilization <= SCALE) {
868
+ logger.verbose(`VesuAdapter::getMaxUtilizationGivenRatePerSecond utilization: ${Number(utilization) / 1e18}, nextUtilization: ${Number(nextUtilization) / 1e18}`);
869
+ const { newRatePerSecond } = this.calculateInterestRate(interestRateConfig, utilization, timeDelta, last_full_utilization_rate);
870
+ if (newRatePerSecond > ratePerSecond) {
871
+ return utilization;
872
+ }
873
+ utilization += nextUtilization;
874
+ }
875
+ throw new Error('Max utilization not found');
876
+ }
676
877
  }
@@ -1,4 +1,4 @@
1
- import { FAQ, getNoRiskTags, highlightTextWithLinks, IConfig, IStrategyMetadata, Protocols, RiskFactor, RiskType } from "@/interfaces";
1
+ import { FAQ, getNoRiskTags, highlightTextWithLinks, IConfig, IStrategyMetadata, Protocols, RiskFactor, RiskType, TokenInfo } from "@/interfaces";
2
2
  import { AUMTypes, getContractDetails, UNIVERSAL_ADAPTERS, UNIVERSAL_MANAGE_IDS, UniversalManageCall, UniversalStrategy, UniversalStrategySettings } from "./universal-strategy";
3
3
  import { PricerBase } from "@/modules/pricerBase";
4
4
  import { ContractAddr, Web3Number } from "@/dataTypes";
@@ -12,12 +12,15 @@ import { SingleTokenInfo } from "./base-strategy";
12
12
  import { Call, Contract, uint256 } from "starknet";
13
13
  import ERC4626Abi from "@/data/erc4626.abi.json";
14
14
 
15
+ export interface HyperLSTStrategySettings extends UniversalStrategySettings {
16
+ borrowable_assets: TokenInfo[];
17
+ }
15
18
 
16
- export class UniversalLstMultiplierStrategy extends UniversalStrategy<UniversalStrategySettings> {
19
+ export class UniversalLstMultiplierStrategy extends UniversalStrategy<HyperLSTStrategySettings> {
17
20
 
18
21
  private quoteAmountToFetchPrice = new Web3Number(1, 18);
19
22
 
20
- constructor(config: IConfig, pricer: PricerBase, metadata: IStrategyMetadata<UniversalStrategySettings>) {
23
+ constructor(config: IConfig, pricer: PricerBase, metadata: IStrategyMetadata<HyperLSTStrategySettings>) {
21
24
  super(config, pricer, metadata);
22
25
 
23
26
  const STRKToken = Global.getDefaultTokens().find(token => token.symbol === 'STRK')!;
@@ -39,14 +42,32 @@ export class UniversalLstMultiplierStrategy extends UniversalStrategy<UniversalS
39
42
  return `${UniversalLstMultiplierStrategy.name}:${this.metadata.name}`;
40
43
  }
41
44
 
45
+ // Vesu adapter with LST and base token match
46
+ getVesuSameTokenAdapter() {
47
+ const baseAdapter = this.getAdapter(UNIVERSAL_ADAPTERS.VESU_LEG1) as VesuAdapter;
48
+ baseAdapter.networkConfig = this.config;
49
+ baseAdapter.pricer = this.pricer;
50
+ return baseAdapter;
51
+ }
52
+
42
53
  // only one leg is used
43
54
  // todo support lending assets of underlying as well (like if xSTRK looping is not viable, simply supply STRK)
44
55
  getVesuAdapters() {
45
- const vesuAdapter1 = this.getAdapter(UNIVERSAL_ADAPTERS.VESU_LEG1) as VesuAdapter;
46
- vesuAdapter1.pricer = this.pricer;
47
- vesuAdapter1.networkConfig = this.config;
48
-
49
- return [vesuAdapter1];
56
+ const adapters: VesuAdapter[] = [];
57
+ const baseAdapter = this.getVesuSameTokenAdapter();
58
+ for (const asset of this.metadata.additionalInfo.borrowable_assets) {
59
+ const vesuAdapter1 = new VesuAdapter({
60
+ poolId: baseAdapter.config.poolId,
61
+ collateral: this.asset(),
62
+ debt: asset,
63
+ vaultAllocator: this.metadata.additionalInfo.vaultAllocator,
64
+ id: ''
65
+ })
66
+ vesuAdapter1.pricer = this.pricer;
67
+ vesuAdapter1.networkConfig = this.config;
68
+ adapters.push(vesuAdapter1);
69
+ }
70
+ return adapters;
50
71
  }
51
72
 
52
73
  // not applicable for this strategy
@@ -84,7 +105,7 @@ export class UniversalLstMultiplierStrategy extends UniversalStrategy<UniversalS
84
105
  // TODO use a varibale for 1.02
85
106
  return this._getAvnuDepositSwapLegCall({
86
107
  ...params,
87
- minHF: 1.02
108
+ minHF: 1.1 // undo
88
109
  });
89
110
  }
90
111
 
@@ -123,6 +144,7 @@ export class UniversalLstMultiplierStrategy extends UniversalStrategy<UniversalS
123
144
  const totalDebtAmount = totalCollateral.multipliedBy(collateralPrice).multipliedBy(legLTV).dividedBy(debtPrice).dividedBy(params.minHF);
124
145
  logger.verbose(`${this.getTag()}::_getAvnuDepositSwapLegCall totalDebtAmount: ${totalDebtAmount}`);
125
146
  const debtAmount = totalDebtAmount.minus(existingDebtInfo.amount);
147
+ logger.verbose(`${this.getTag()}::_getAvnuDepositSwapLegCall debtAmount: ${debtAmount}`);
126
148
  if (debtAmount.lt(0)) {
127
149
  // this is to unwind the position to optimal HF.
128
150
  const lstDEXPrice = await this.getLSTDexPrice();
@@ -131,10 +153,9 @@ export class UniversalLstMultiplierStrategy extends UniversalStrategy<UniversalS
131
153
  isDeposit: false,
132
154
  leg1DepositAmount: debtAmountInLST
133
155
  })
134
- assert(calls.length == 1, 'Expected 1 call for unwind');
156
+ assert(calls.length == 1, `Expected 1 call for unwind, got ${calls.length}`);
135
157
  return calls[0];
136
158
  }
137
- logger.verbose(`${this.getTag()}::_getAvnuDepositSwapLegCall debtAmount: ${debtAmount}`);
138
159
  const STEP0 = UNIVERSAL_MANAGE_IDS.APPROVE_TOKEN1;
139
160
  const manage0Info = this.getProofs<ApproveCallParams>(STEP0);
140
161
  const manageCall0 = manage0Info.callConstructor({
@@ -261,11 +282,42 @@ export class UniversalLstMultiplierStrategy extends UniversalStrategy<UniversalS
261
282
  }
262
283
  }
263
284
 
285
+ protected async getVesuAUM(adapter: VesuAdapter) {
286
+ const legAUM = await adapter.getPositions(this.config);
287
+ const underlying = this.asset();
288
+ // assert its an LST of Endur
289
+ assert(underlying.symbol.startsWith('x'), 'Underlying is not an LST of Endur');
290
+
291
+ let vesuAum = Web3Number.fromWei("0", underlying.decimals);
292
+
293
+ let tokenUnderlyingPrice = await this.getLSTExchangeRate();
294
+ // to offset for usual DEX lag, we multiply by 0.998 (i.e. 0.2% loss)
295
+ tokenUnderlyingPrice = tokenUnderlyingPrice * 0.998;
296
+ logger.verbose(`${this.getTag()} tokenUnderlyingPrice: ${tokenUnderlyingPrice}`);
297
+
298
+ // handle collateral
299
+ if (legAUM[0].token.address.eq(underlying.address)) {
300
+ vesuAum = vesuAum.plus(legAUM[0].amount);
301
+ } else {
302
+ vesuAum = vesuAum.plus(legAUM[0].amount.dividedBy(tokenUnderlyingPrice));
303
+ }
304
+
305
+ // handle debt
306
+ if (legAUM[1].token.address.eq(underlying.address)) {
307
+ vesuAum = vesuAum.minus(legAUM[1].amount);
308
+ } else {
309
+ vesuAum = vesuAum.minus(legAUM[1].amount.dividedBy(tokenUnderlyingPrice));
310
+ };
311
+
312
+ logger.verbose(`${this.getTag()} Vesu AUM: ${vesuAum}, legCollateral: ${legAUM[0].amount.toNumber()}, legDebt: ${legAUM[1].amount.toNumber()}`);
313
+ return vesuAum;
314
+ }
315
+
264
316
  //
265
317
  private async _getMinOutputAmountLSTBuy(amountInUnderlying: Web3Number) {
266
318
  const lstTruePrice = await this.getLSTExchangeRate();
267
319
  // during buy, the purchase should always be <= true LST price.
268
- const minOutputAmount = amountInUnderlying.dividedBy(lstTruePrice);
320
+ const minOutputAmount = amountInUnderlying.dividedBy(lstTruePrice).multipliedBy(0.99979); // minus 0.021% to account for avnu fees
269
321
  return minOutputAmount;
270
322
  }
271
323
 
@@ -292,6 +344,18 @@ export class UniversalLstMultiplierStrategy extends UniversalStrategy<UniversalS
292
344
  const legLTV = await vesuAdapter1.getLTVConfig(this.config);
293
345
  logger.verbose(`${this.getTag()}::getVesuMultiplyCall legLTV: ${legLTV}`);
294
346
 
347
+ if (!params.isDeposit) {
348
+ // try using unused balance to unwind.
349
+ // no need to unwind.
350
+ const unusedBalance = await this.getUnusedBalance();
351
+ logger.verbose(`${this.getTag()}::getVesuMultiplyCall unusedBalance: ${unusedBalance.amount.toString()}, required: ${params.leg1DepositAmount.toString()}`);
352
+ // undo
353
+ // if (unusedBalance.amount.gte(params.leg1DepositAmount)) {
354
+ // return [];
355
+ // }
356
+ // throw new Error('Unused balance is less than the amount to unwind');
357
+ }
358
+
295
359
  const existingPositions = await vesuAdapter1.getPositions(this.config);
296
360
  const collateralisation = await vesuAdapter1.getCollateralization(this.config);
297
361
  const existingCollateralInfo = existingPositions[0];
@@ -350,17 +414,37 @@ export class UniversalLstMultiplierStrategy extends UniversalStrategy<UniversalS
350
414
  }
351
415
 
352
416
  getLSTUnderlyingTokenInfo() {
353
- const [vesuAdapter1] = this.getVesuAdapters();
417
+ const vesuAdapter1 = this.getVesuSameTokenAdapter();
354
418
  return vesuAdapter1.config.debt;
355
419
  }
356
420
 
421
+ async getMaxBorrowableAmount() {
422
+ const vesuAdapters = this.getVesuAdapters();
423
+ let netMaxBorrowableAmount = Web3Number.fromWei("0", this.getLSTUnderlyingTokenInfo().decimals);
424
+ const maxBorrowables: {amount: Web3Number, borrowableAsset: TokenInfo}[] = [];
425
+ const lstAPY = await this.getLSTAPR(this.getLSTUnderlyingTokenInfo().address);
426
+ const maxInterestRate = lstAPY * 0.8;
427
+ for (const vesuAdapter of vesuAdapters) {
428
+ const maxBorrowableAmount = await vesuAdapter.getMaxBorrowableByInterestRate(this.config, vesuAdapter.config.debt, maxInterestRate);
429
+ const debtCap = await vesuAdapter.getDebtCap(this.config);
430
+ maxBorrowables.push({amount: maxBorrowableAmount.minimum(debtCap), borrowableAsset: vesuAdapter.config.debt});
431
+ }
432
+ maxBorrowables.sort((a, b) => b.amount.toNumber() - a.amount.toNumber());
433
+ netMaxBorrowableAmount = maxBorrowables.reduce((acc, curr) => acc.plus(curr.amount), Web3Number.fromWei("0", this.getLSTUnderlyingTokenInfo().decimals));
434
+ return {netMaxBorrowableAmount, maxBorrowables};
435
+ }
436
+
437
+ // todo how much to unwind to get back healthy APY zone again
438
+ // if net APY < LST APR + 0.5%, we need to unwind to get back to LST APR + 1% atleast or 0 vesu position
439
+ // For xSTRK, simply deposit in Vesu if looping is not viable
440
+
357
441
  /**
358
442
  * Gets LST APR for the strategy's underlying asset from Endur API
359
443
  * @returns Promise<number> The LST APR (not divided by 1e18)
360
444
  */
361
445
  async getLSTAPR(_address: ContractAddr): Promise<number> {
362
446
  try {
363
- const vesuAdapter1 = this.getVesuAdapters()[0];
447
+ const vesuAdapter1 = this.getVesuSameTokenAdapter();
364
448
  const apr = await LSTAPRService.getLSTAPR(vesuAdapter1.config.debt.address);
365
449
  if (!apr) {
366
450
  throw new Error('Failed to get LST APR');
@@ -374,22 +458,50 @@ export class UniversalLstMultiplierStrategy extends UniversalStrategy<UniversalS
374
458
 
375
459
  // todo undo this
376
460
  async netAPY(): Promise<{ net: number; splits: { apy: number; id: string; }[]; }> {
377
- const { net, splits } = await super.netAPY();
378
- let _net = net;
379
- if (this.asset().symbol == 'xWBTC') {
380
- const debtToken = this.getVesuAdapters()[0].config.debt;
381
- const lstAPY = await this.getLSTAPR(debtToken.address);
382
- _net = lstAPY * 5;
461
+ const unusedBalance = await this.getUnusedBalance();
462
+ const maxNewDeposits = await this.maxNewDeposits();
463
+
464
+ // if unused balance is > max servicable from loan, we are limited by the max borrowing we can do
465
+ // we also allow accepting little higher deposits (1.5x) to have room for future looping when theres more liquidity or debt cap rises
466
+ if (maxNewDeposits * 1.5 < unusedBalance.amount.toNumber()) {
467
+ // we have excess, just use real APY
468
+ return super.netAPY();
469
+ } else {
470
+ // we have little bit room to accept more deposits, we use theoretical max APY
471
+ const { positions, baseAPYs, rewardAPYs } = await this.getVesuAPYs();
472
+ const weights = positions.map((p, index) => p.usdValue * (index % 2 == 0 ? 1 : -1));
473
+ return this.returnNetAPY(baseAPYs, rewardAPYs, weights);
383
474
  }
384
- return {
385
- net: _net,
386
- splits: splits
475
+ }
476
+
477
+ async maxNewDeposits() {
478
+ const maxBorrowableAmounts = await this.getMaxBorrowableAmount();
479
+
480
+ let ltv: number | undefined = undefined;
481
+ for (let adapter of this.getVesuAdapters()) {
482
+ const maxBorrowableAmount = maxBorrowableAmounts.maxBorrowables.find(b => b.borrowableAsset.address.eq(adapter.config.debt.address))?.amount;
483
+ if (!maxBorrowableAmount) {
484
+ throw new Error(`Max borrowable amount not found for adapter: ${adapter.config.debt.symbol}`);
485
+ }
486
+ const maxLTV = await adapter.getLTVConfig(this.config);
487
+ if (!ltv) {
488
+ ltv = maxLTV;
489
+ } else if (ltv != maxLTV) {
490
+ throw new Error(`LTV mismatch for adapter: ${adapter.config.debt.symbol}`);
491
+ }
387
492
  }
493
+ if (!ltv) {
494
+ throw new Error('LTV not found');
495
+ }
496
+ // for simplicity, we assume 1 underlying = 1 LST
497
+ const numerator = this.metadata.additionalInfo.targetHealthFactor * maxBorrowableAmounts.netMaxBorrowableAmount.toNumber() / (ltv)
498
+ return numerator - maxBorrowableAmounts.netMaxBorrowableAmount.toNumber();
388
499
  }
389
500
 
501
+ // todo revisit cases where 0th adapters is used
390
502
  protected async getUnusedBalanceAPY() {
391
503
  const unusedBalance = await this.getUnusedBalance();
392
- const vesuAdapter = this.getVesuAdapters()[0];
504
+ const vesuAdapter = this.getVesuSameTokenAdapter();
393
505
  const underlying = vesuAdapter.config.debt;
394
506
  const lstAPY = await this.getLSTAPR(underlying.address);
395
507
  return {
@@ -398,7 +510,7 @@ export class UniversalLstMultiplierStrategy extends UniversalStrategy<UniversalS
398
510
  }
399
511
 
400
512
  async getLSTExchangeRate() {
401
- const [vesuAdapter1] = this.getVesuAdapters();
513
+ const vesuAdapter1 = this.getVesuSameTokenAdapter();
402
514
  const lstTokenInfo = vesuAdapter1.config.collateral;
403
515
  const lstABI = new Contract({
404
516
  abi: ERC4626Abi,
@@ -425,7 +537,7 @@ export class UniversalLstMultiplierStrategy extends UniversalStrategy<UniversalS
425
537
  logger.verbose(`${this.getTag()}::getModifyLeverCall marginAmount: ${params.marginAmount}, debtAmount: ${params.debtAmount}, lstDexPriceInUnderlying: ${params.lstDexPriceInUnderlying}, isIncrease: ${params.isIncrease}`);
426
538
  assert(!params.marginAmount.isZero() || !params.debtAmount.isZero(), 'Deposit/debt must be non-0');
427
539
 
428
- const [vesuAdapter1] = this.getVesuAdapters();
540
+ const vesuAdapter1 = this.getVesuSameTokenAdapter();
429
541
  const lstTokenInfo = this.asset();
430
542
  const lstUnderlyingTokenInfo = vesuAdapter1.config.debt;
431
543
 
@@ -604,7 +716,7 @@ enum LST_MULTIPLIER_MANAGE_IDS {
604
716
  function getLooperSettings(
605
717
  lstSymbol: string,
606
718
  underlyingSymbol: string,
607
- vaultSettings: UniversalStrategySettings,
719
+ vaultSettings: HyperLSTStrategySettings,
608
720
  pool1: ContractAddr,
609
721
  ) {
610
722
  vaultSettings.leafAdapters = [];
@@ -730,7 +842,11 @@ const _riskFactor: RiskFactor[] = [
730
842
  {type: RiskType.DEPEG_RISK, value: DepegRiskLevel.GENERALLY_STABLE, weight: 25, reason: "Generally stable pegged assets" },
731
843
  ];
732
844
 
733
- const hyperxSTRK: UniversalStrategySettings = {
845
+ const borrowableAssets = [
846
+ 'WBTC', 'tBTC', 'LBTC', 'solvBTC'
847
+ ]
848
+
849
+ const hyperxSTRK: HyperLSTStrategySettings = {
734
850
  vaultAddress: ContractAddr.from('0x46c7a54c82b1fe374353859f554a40b8bd31d3e30f742901579e7b57b1b5960'),
735
851
  manager: ContractAddr.from('0x5d499cd333757f461a0bedaca3dfc4d77320c773037e0aa299f22a6dbfdc03a'),
736
852
  vaultAllocator: ContractAddr.from('0x511d07953a09bc7c505970891507c5a2486d2ea22752601a14db092186d7caa'),
@@ -739,10 +855,11 @@ const hyperxSTRK: UniversalStrategySettings = {
739
855
  leafAdapters: [],
740
856
  adapters: [],
741
857
  targetHealthFactor: 1.1,
742
- minHealthFactor: 1.05
858
+ minHealthFactor: 1.05,
859
+ borrowable_assets: Global.getDefaultTokens().filter(token => token.symbol === 'STRK'),
743
860
  }
744
861
 
745
- const hyperxWBTC: UniversalStrategySettings = {
862
+ const hyperxWBTC: HyperLSTStrategySettings = {
746
863
  vaultAddress: ContractAddr.from('0x2da9d0f96a46b453f55604313785dc866424240b1c6811d13bef594343db818'),
747
864
  manager: ContractAddr.from('0x75866db44c81e6986f06035206ee9c7d15833ddb22d6a22c016cfb5c866a491'),
748
865
  vaultAllocator: ContractAddr.from('0x57b5c1bb457b5e840a2714ae53ada87d77be2f3fd33a59b4fe709ef20c020c1'),
@@ -751,10 +868,11 @@ const hyperxWBTC: UniversalStrategySettings = {
751
868
  leafAdapters: [],
752
869
  adapters: [],
753
870
  targetHealthFactor: 1.1,
754
- minHealthFactor: 1.05
871
+ minHealthFactor: 1.05,
872
+ borrowable_assets: borrowableAssets.map(asset => Global.getDefaultTokens().find(token => token.symbol === asset)!),
755
873
  }
756
874
 
757
- const hyperxtBTC: UniversalStrategySettings = {
875
+ const hyperxtBTC: HyperLSTStrategySettings = {
758
876
  vaultAddress: ContractAddr.from('0x47d5f68477e5637ce0e56436c6b5eee5a354e6828995dae106b11a48679328'),
759
877
  manager: ContractAddr.from('0xc4cc3e08029a0ae076f5fdfca70575abb78d23c5cd1c49a957f7e697885401'),
760
878
  vaultAllocator: ContractAddr.from('0x50bbd4fe69f841ecb13b2619fe50ebfa4e8944671b5d0ebf7868fd80c61b31e'),
@@ -763,10 +881,11 @@ const hyperxtBTC: UniversalStrategySettings = {
763
881
  leafAdapters: [],
764
882
  adapters: [],
765
883
  targetHealthFactor: 1.1,
766
- minHealthFactor: 1.05
884
+ minHealthFactor: 1.05,
885
+ borrowable_assets: borrowableAssets.map(asset => Global.getDefaultTokens().find(token => token.symbol === asset)!),
767
886
  }
768
887
 
769
- const hyperxsBTC: UniversalStrategySettings = {
888
+ const hyperxsBTC: HyperLSTStrategySettings = {
770
889
  vaultAddress: ContractAddr.from('0x437ef1e7d0f100b2e070b7a65cafec0b2be31b0290776da8b4112f5473d8d9'),
771
890
  manager: ContractAddr.from('0xc9ac023090625b0be3f6532ca353f086746f9c09f939dbc1b2613f09e5f821'),
772
891
  vaultAllocator: ContractAddr.from('0x60c2d856936b975459a5b4eb28b8672d91f757bd76cebb6241f8d670185dc01'),
@@ -775,10 +894,11 @@ const hyperxsBTC: UniversalStrategySettings = {
775
894
  leafAdapters: [],
776
895
  adapters: [],
777
896
  targetHealthFactor: 1.1,
778
- minHealthFactor: 1.05
897
+ minHealthFactor: 1.05,
898
+ borrowable_assets: borrowableAssets.map(asset => Global.getDefaultTokens().find(token => token.symbol === asset)!),
779
899
  }
780
900
 
781
- const hyperxLBTC: UniversalStrategySettings = {
901
+ const hyperxLBTC: HyperLSTStrategySettings = {
782
902
  vaultAddress: ContractAddr.from('0x64cf24d4883fe569926419a0569ab34497c6956a1a308fa883257f7486d7030'),
783
903
  manager: ContractAddr.from('0x203530a4022a99b8f4b406aaf33b0849d43ad7422c1d5cc14ff8c667abec6c0'),
784
904
  vaultAllocator: ContractAddr.from('0x7dbc8ccd4eabce6ea6c19e0e5c9ccca3a93bd510303b9e071cbe25fc508546e'),
@@ -787,7 +907,8 @@ const hyperxLBTC: UniversalStrategySettings = {
787
907
  leafAdapters: [],
788
908
  adapters: [],
789
909
  targetHealthFactor: 1.1,
790
- minHealthFactor: 1.05
910
+ minHealthFactor: 1.05,
911
+ borrowable_assets: borrowableAssets.map(asset => Global.getDefaultTokens().find(token => token.symbol === asset)!),
791
912
  }
792
913
 
793
914
  function getInvestmentSteps(lstSymbol: string, underlyingSymbol: string) {
@@ -800,7 +921,7 @@ function getInvestmentSteps(lstSymbol: string, underlyingSymbol: string) {
800
921
  ]
801
922
  }
802
923
 
803
- function getStrategySettings(lstSymbol: string, underlyingSymbol: string, addresses: UniversalStrategySettings, isPreview: boolean = false): IStrategyMetadata<UniversalStrategySettings> {
924
+ function getStrategySettings(lstSymbol: string, underlyingSymbol: string, addresses: HyperLSTStrategySettings, isPreview: boolean = false): IStrategyMetadata<HyperLSTStrategySettings> {
804
925
  return {
805
926
  name: `Hyper ${lstSymbol}`,
806
927
  description: getDescription(lstSymbol, underlyingSymbol),
@@ -822,11 +943,12 @@ function getStrategySettings(lstSymbol: string, underlyingSymbol: string, addres
822
943
  contractDetails: getContractDetails(addresses),
823
944
  faqs: getFAQs(lstSymbol, underlyingSymbol),
824
945
  investmentSteps: getInvestmentSteps(lstSymbol, underlyingSymbol),
825
- isPreview: isPreview
946
+ isPreview: isPreview,
947
+ apyMethodology: 'Current annualized APY in terms of base asset of the LST'
826
948
  }
827
949
  }
828
950
 
829
- export const HyperLSTStrategies: IStrategyMetadata<UniversalStrategySettings>[] =
951
+ export const HyperLSTStrategies: IStrategyMetadata<HyperLSTStrategySettings>[] =
830
952
  [
831
953
  getStrategySettings('xSTRK', 'STRK', hyperxSTRK, false),
832
954
  getStrategySettings('xWBTC', 'WBTC', hyperxWBTC, false),