@strkfarm/sdk 1.0.40 → 1.0.42

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.
@@ -62,6 +62,11 @@ export interface CLVaultStrategySettings {
62
62
  lstContract?: ContractAddr;
63
63
  truePrice?: number; // useful for pools where price is known (e.g. USDC/USDT as 1)
64
64
  feeBps: number;
65
+ rebalanceConditions: {
66
+ minWaitHours: number; // number of hours out of range to rebalance
67
+ direction: "any" | "uponly"; // any for pools like USDC/USDT, uponly for pools like xSTRK/STRK
68
+ customShouldRebalance: (currentPoolPrice: number) => Promise<boolean>; // any additional logic for deciding factor to rebalance or not based on pools
69
+ };
65
70
  }
66
71
 
67
72
  export class EkuboCLVault extends BaseStrategy<
@@ -297,7 +302,10 @@ export class EkuboCLVault extends BaseStrategy<
297
302
  ? (await this.config.provider.getBlockWithTxs(blockIdentifier))
298
303
  .timestamp
299
304
  : new Date().getTime() / 1000;
300
- const blockBefore = Math.max(blockNow - sinceBlocks, this.metadata.launchBlock);
305
+ const blockBefore = Math.max(
306
+ blockNow - sinceBlocks,
307
+ this.metadata.launchBlock
308
+ );
301
309
  const adjustedSupplyNow = supplyNow.minus(
302
310
  await this.getHarvestRewardShares(blockBefore, blockNow)
303
311
  );
@@ -369,7 +377,7 @@ export class EkuboCLVault extends BaseStrategy<
369
377
  blockIdentifier: BlockIdentifier = "pending"
370
378
  ): Promise<Web3Number> {
371
379
  let bal = await this.contract.call("balance_of", [user.address], {
372
- blockIdentifier
380
+ blockIdentifier,
373
381
  });
374
382
  return Web3Number.fromWei(bal.toString(), 18);
375
383
  }
@@ -807,6 +815,19 @@ export class EkuboCLVault extends BaseStrategy<
807
815
  .minus(availableAmount0)
808
816
  .dividedBy(ratio.plus(1 / price));
809
817
  const x = y.dividedBy(price);
818
+ logger.verbose(
819
+ `${
820
+ EkuboCLVault.name
821
+ }: _solveExpectedAmountsEq => x: ${x.toString()}, y: ${y.toString()}, amount0: ${availableAmount0.toString()}, amount1: ${availableAmount1.toString()}`
822
+ );
823
+
824
+ if (ratio.eq(0)) {
825
+ return {
826
+ amount0: Web3Number.fromWei("0", availableAmount0.decimals),
827
+ amount1: availableAmount1.minus(y),
828
+ ratio: 0,
829
+ };
830
+ }
810
831
  return {
811
832
  amount0: availableAmount0.plus(x),
812
833
  amount1: availableAmount1.minus(y),
@@ -814,10 +835,8 @@ export class EkuboCLVault extends BaseStrategy<
814
835
  };
815
836
  }
816
837
 
817
- async getSwapInfoToHandleUnused(considerRebalance: boolean = true) {
818
- const poolKey = await this.getPoolKey();
819
-
820
- // fetch current unused balances of vault
838
+ async unusedBalances(_poolKey?: EkuboPoolKey) {
839
+ const poolKey = _poolKey || (await this.getPoolKey());
821
840
  const erc20Mod = new ERC20(this.config);
822
841
  const token0Info = await Global.getTokenInfoFromAddr(poolKey.token0);
823
842
  const token1Info = await Global.getTokenInfoFromAddr(poolKey.token1);
@@ -831,6 +850,7 @@ export class EkuboCLVault extends BaseStrategy<
831
850
  this.address.address,
832
851
  token1Info.decimals
833
852
  );
853
+
834
854
  logger.verbose(
835
855
  `${
836
856
  EkuboCLVault.name
@@ -841,16 +861,41 @@ export class EkuboCLVault extends BaseStrategy<
841
861
  const token1Price = await this.pricer.getPrice(token1Info.symbol);
842
862
  const token0PriceUsd = token0Price.price * Number(token0Bal1.toFixed(13));
843
863
  const token1PriceUsd = token1Price.price * Number(token1Bal1.toFixed(13));
844
- if (token0PriceUsd > 1 && token1PriceUsd > 1) {
845
- // the swap is designed to handle one token only.
846
- // i.e. all balance should be in one token
847
- // except small amount of dust
848
- // so we need to call handle_fees first, which will atleast use
849
- // most of one token
850
- throw new Error(
851
- "Both tokens are non-zero and above $1, call handle_fees first"
852
- );
853
- }
864
+
865
+ return {
866
+ token0: {
867
+ amount: token0Bal1,
868
+ tokenInfo: token0Info,
869
+ usdValue: token0PriceUsd,
870
+ },
871
+ token1: {
872
+ amount: token1Bal1,
873
+ tokenInfo: token1Info,
874
+ usdValue: token1PriceUsd,
875
+ },
876
+ };
877
+ }
878
+
879
+ async getSwapInfoToHandleUnused(considerRebalance: boolean = true) {
880
+ const poolKey = await this.getPoolKey();
881
+
882
+ // fetch current unused balances of vault
883
+ const unusedBalances = await this.unusedBalances(poolKey);
884
+ const { amount: token0Bal1, usdValue: token0PriceUsd } =
885
+ unusedBalances.token0;
886
+ const { amount: token1Bal1, usdValue: token1PriceUsd } =
887
+ unusedBalances.token1;
888
+
889
+ // if (token0PriceUsd > 1 && token1PriceUsd > 1) {
890
+ // // the swap is designed to handle one token only.
891
+ // // i.e. all balance should be in one token
892
+ // // except small amount of dust
893
+ // // so we need to call handle_fees first, which will atleast use
894
+ // // most of one token
895
+ // throw new Error(
896
+ // "Both tokens are non-zero and above $1, call handle_fees first"
897
+ // );
898
+ // }
854
899
 
855
900
  let token0Bal = token0Bal1;
856
901
  let token1Bal = token1Bal1;
@@ -875,16 +920,21 @@ export class EkuboCLVault extends BaseStrategy<
875
920
  );
876
921
 
877
922
  // get expected amounts for liquidity
878
- const newBounds = await this.getNewBounds();
923
+ let ekuboBounds: EkuboBounds;
924
+ if (considerRebalance) {
925
+ ekuboBounds = await this.getNewBounds();
926
+ } else {
927
+ ekuboBounds = await this.getCurrentBounds();
928
+ }
879
929
  logger.verbose(
880
- `${EkuboCLVault.name}: getSwapInfoToHandleUnused => newBounds: ${newBounds.lowerTick}, ${newBounds.upperTick}`
930
+ `${EkuboCLVault.name}: getSwapInfoToHandleUnused => newBounds: ${ekuboBounds.lowerTick}, ${ekuboBounds.upperTick}`
881
931
  );
882
932
 
883
933
  return await this.getSwapInfoGivenAmounts(
884
934
  poolKey,
885
935
  token0Bal,
886
936
  token1Bal,
887
- newBounds
937
+ ekuboBounds
888
938
  );
889
939
  }
890
940
 
@@ -894,6 +944,13 @@ export class EkuboCLVault extends BaseStrategy<
894
944
  token1Bal: Web3Number,
895
945
  bounds: EkuboBounds
896
946
  ): Promise<SwapInfo> {
947
+ logger.verbose(
948
+ `${
949
+ EkuboCLVault.name
950
+ }: getSwapInfoGivenAmounts::pre => token0Bal: ${token0Bal.toString()}, token1Bal: ${token1Bal.toString()}`
951
+ );
952
+
953
+ // Compute the expected amounts of token0 and token1 for the given liquidity bounds
897
954
  let expectedAmounts = await this._getExpectedAmountsForLiquidity(
898
955
  token0Bal,
899
956
  token1Bal,
@@ -902,17 +959,19 @@ export class EkuboCLVault extends BaseStrategy<
902
959
  logger.verbose(
903
960
  `${
904
961
  EkuboCLVault.name
905
- }: getSwapInfoToHandleUnused => expectedAmounts: ${expectedAmounts.amount0.toString()}, ${expectedAmounts.amount1.toString()}`
962
+ }: getSwapInfoToHandleUnused => expectedAmounts2: ${expectedAmounts.amount0.toString()}, ${expectedAmounts.amount1.toString()}`
906
963
  );
907
964
 
908
- // get swap info
909
- // fetch avnu routes to ensure expected amounts
910
965
  let retry = 0;
911
966
  const maxRetry = 10;
912
- while (retry < maxRetry) {
913
- retry++;
914
- // assert one token is increased and other is decreased
915
967
 
968
+ // Helper to check for invalid states:
969
+ // Throws if both tokens are decreased or both are increased, which is not expected
970
+ function assertValidAmounts(
971
+ expectedAmounts: any,
972
+ token0Bal: Web3Number,
973
+ token1Bal: Web3Number
974
+ ) {
916
975
  if (
917
976
  expectedAmounts.amount0.lessThan(token0Bal) &&
918
977
  expectedAmounts.amount1.lessThan(token1Bal)
@@ -925,58 +984,75 @@ export class EkuboCLVault extends BaseStrategy<
925
984
  ) {
926
985
  throw new Error("Both tokens are increased, something is wrong");
927
986
  }
987
+ }
928
988
 
989
+ // Helper to determine which token to sell, which to buy, and the amounts to use
990
+ function getSwapParams(
991
+ expectedAmounts: any,
992
+ poolKey: EkuboPoolKey,
993
+ token0Bal: Web3Number,
994
+ token1Bal: Web3Number
995
+ ) {
996
+ // Decide which token to sell based on which expected amount is less than the balance
929
997
  const tokenToSell = expectedAmounts.amount0.lessThan(token0Bal)
930
998
  ? poolKey.token0
931
999
  : poolKey.token1;
1000
+ // The other token is the one to buy
932
1001
  const tokenToBuy =
933
1002
  tokenToSell == poolKey.token0 ? poolKey.token1 : poolKey.token0;
934
- let amountToSell =
1003
+ // Calculate how much to sell
1004
+ const amountToSell =
935
1005
  tokenToSell == poolKey.token0
936
1006
  ? token0Bal.minus(expectedAmounts.amount0)
937
1007
  : token1Bal.minus(expectedAmounts.amount1);
1008
+ // The remaining amount of the sold token after swap
938
1009
  const remainingSellAmount =
939
1010
  tokenToSell == poolKey.token0
940
1011
  ? expectedAmounts.amount0
941
1012
  : expectedAmounts.amount1;
942
- const tokenToBuyInfo = await Global.getTokenInfoFromAddr(tokenToBuy);
943
- const expectedRatio = expectedAmounts.ratio;
1013
+ return { tokenToSell, tokenToBuy, amountToSell, remainingSellAmount };
1014
+ }
944
1015
 
1016
+ // Main retry loop: attempts to find a swap that matches the expected ratio within tolerance
1017
+ while (retry < maxRetry) {
1018
+ retry++;
945
1019
  logger.verbose(
946
- `${EkuboCLVault.name}: getSwapInfoToHandleUnused => tokenToSell: ${
947
- tokenToSell.address
948
- }, tokenToBuy: ${
949
- tokenToBuy.address
950
- }, amountToSell: ${amountToSell.toWei()}`
951
- );
952
- logger.verbose(
953
- `${
954
- EkuboCLVault.name
955
- }: getSwapInfoToHandleUnused => remainingSellAmount: ${remainingSellAmount.toString()}`
956
- );
957
- logger.verbose(
958
- `${EkuboCLVault.name}: getSwapInfoToHandleUnused => expectedRatio: ${expectedRatio}`
1020
+ `getSwapInfoGivenAmounts::Retry attempt: ${retry}/${maxRetry}`
959
1021
  );
960
1022
 
1023
+ // Ensure the expected amounts are valid for swap logic
1024
+ assertValidAmounts(expectedAmounts, token0Bal, token1Bal);
1025
+
1026
+ // Get swap parameters for this iteration
1027
+ const { tokenToSell, tokenToBuy, amountToSell, remainingSellAmount } =
1028
+ getSwapParams(expectedAmounts, poolKey, token0Bal, token1Bal);
1029
+
1030
+ const tokenToBuyInfo = await Global.getTokenInfoFromAddr(tokenToBuy);
1031
+ const expectedRatio = expectedAmounts.ratio;
1032
+
1033
+ logger.verbose(
1034
+ `${EkuboCLVault.name}: getSwapInfoToHandleUnused => iteration info: ${JSON.stringify({
1035
+ tokenToSell: tokenToSell.address,
1036
+ tokenToBuy: tokenToBuy.address,
1037
+ amountToSell: amountToSell.toString(),
1038
+ remainingSellAmount: remainingSellAmount.toString(),
1039
+ expectedRatio: expectedRatio
1040
+ })}`);
1041
+
1042
+ // If nothing to sell, return a zero swap
961
1043
  if (amountToSell.eq(0)) {
962
- return {
963
- token_from_address: tokenToSell.address,
964
- token_from_amount: uint256.bnToUint256(0),
965
- token_to_address: tokenToSell.address,
966
- token_to_amount: uint256.bnToUint256(0),
967
- token_to_min_amount: uint256.bnToUint256(0),
968
- beneficiary: this.address.address,
969
- integrator_fee_amount_bps: 0,
970
- integrator_fee_recipient: this.address.address,
971
- routes: [],
972
- };
1044
+ return AvnuWrapper.buildZeroSwap(tokenToSell, this.address.address);
973
1045
  }
1046
+
1047
+ // Get a quote for swapping the calculated amount
974
1048
  const quote = await this.avnu.getQuotes(
975
1049
  tokenToSell.address,
976
1050
  tokenToBuy.address,
977
1051
  amountToSell.toWei(),
978
1052
  this.address.address
979
1053
  );
1054
+
1055
+ // If all of the token is to be swapped, return the swap info directly
980
1056
  if (remainingSellAmount.eq(0)) {
981
1057
  const minAmountOut = Web3Number.fromWei(
982
1058
  quote.buyAmount.toString(),
@@ -991,36 +1067,34 @@ export class EkuboCLVault extends BaseStrategy<
991
1067
  );
992
1068
  }
993
1069
 
1070
+ // Calculate the actual output and price from the quote
994
1071
  const amountOut = Web3Number.fromWei(
995
1072
  quote.buyAmount.toString(),
996
1073
  tokenToBuyInfo.decimals
997
1074
  );
1075
+ // Calculate the swap price depending on which token is being sold
998
1076
  const swapPrice =
999
1077
  tokenToSell == poolKey.token0
1000
1078
  ? amountOut.dividedBy(amountToSell)
1001
1079
  : amountToSell.dividedBy(amountOut);
1080
+ // Calculate the new ratio after the swap
1002
1081
  const newRatio =
1003
1082
  tokenToSell == poolKey.token0
1004
1083
  ? remainingSellAmount.dividedBy(token1Bal.plus(amountOut))
1005
1084
  : token0Bal.plus(amountOut).dividedBy(remainingSellAmount);
1006
- logger.verbose(
1007
- `${
1008
- EkuboCLVault.name
1009
- }: getSwapInfoToHandleUnused => amountOut: ${amountOut.toString()}`
1010
- );
1011
- logger.verbose(
1012
- `${
1013
- EkuboCLVault.name
1014
- }: getSwapInfoToHandleUnused => swapPrice: ${swapPrice.toString()}`
1015
- );
1016
- logger.verbose(
1017
- `${
1018
- EkuboCLVault.name
1019
- }: getSwapInfoToHandleUnused => newRatio: ${newRatio.toString()}`
1085
+
1086
+ logger.verbose(`${EkuboCLVault.name} getSwapInfoToHandleUnused => iter post calc: ${JSON.stringify({
1087
+ amountOut: amountOut.toString(),
1088
+ swapPrice: swapPrice.toString(),
1089
+ newRatio: newRatio.toString(),
1090
+ })}`
1020
1091
  );
1092
+
1093
+ // If the new ratio is not within tolerance, adjust expected amounts and retry
1094
+ const expectedPrecision = Math.min(7, tokenToBuyInfo.decimals - 2);
1021
1095
  if (
1022
- Number(newRatio.toString()) > expectedRatio * 1.0000001 ||
1023
- Number(newRatio.toString()) < expectedRatio * 0.9999999
1096
+ Number(newRatio.toString()) > expectedRatio * (1 + 1 / 10 ** expectedPrecision) ||
1097
+ Number(newRatio.toString()) < expectedRatio * (1 - 1 / 10 ** expectedPrecision)
1024
1098
  ) {
1025
1099
  expectedAmounts = await this._solveExpectedAmountsEq(
1026
1100
  token0Bal,
@@ -1034,6 +1108,7 @@ export class EkuboCLVault extends BaseStrategy<
1034
1108
  }: getSwapInfoToHandleUnused => expectedAmounts: ${expectedAmounts.amount0.toString()}, ${expectedAmounts.amount1.toString()}`
1035
1109
  );
1036
1110
  } else {
1111
+ // Otherwise, return the swap info with a slippage buffer
1037
1112
  const minAmountOut = Web3Number.fromWei(
1038
1113
  quote.buyAmount.toString(),
1039
1114
  tokenToBuyInfo.decimals
@@ -1046,10 +1121,10 @@ export class EkuboCLVault extends BaseStrategy<
1046
1121
  minAmountOut.toWei()
1047
1122
  );
1048
1123
  }
1049
-
1050
1124
  retry++;
1051
1125
  }
1052
1126
 
1127
+ // If no suitable swap found after max retries, throw error
1053
1128
  throw new Error("Failed to get swap info");
1054
1129
  }
1055
1130
 
@@ -1266,6 +1341,9 @@ export class EkuboCLVault extends BaseStrategy<
1266
1341
  const token0Info = await Global.getTokenInfoFromAddr(poolKey.token0);
1267
1342
  const token1Info = await Global.getTokenInfoFromAddr(poolKey.token1);
1268
1343
  const bounds = await this.getCurrentBounds();
1344
+ logger.verbose(
1345
+ `${EkuboCLVault.name}: harvest => unClaimedRewards: ${unClaimedRewards.length}`
1346
+ );
1269
1347
  const calls: Call[] = [];
1270
1348
  for (let claim of unClaimedRewards) {
1271
1349
  const fee = claim.claim.amount
@@ -1279,6 +1357,8 @@ export class EkuboCLVault extends BaseStrategy<
1279
1357
  EkuboCLVault.name
1280
1358
  }: harvest => Processing claim, isToken1: ${isToken1} amount: ${postFeeAmount.toWei()}`
1281
1359
  );
1360
+
1361
+ // todo what if the claim token is neither token0 or token1
1282
1362
  const token0Amt = isToken1
1283
1363
  ? new Web3Number(0, token0Info.decimals)
1284
1364
  : postFeeAmount;
@@ -1308,14 +1388,30 @@ export class EkuboCLVault extends BaseStrategy<
1308
1388
  const harvestEstimateCall = async (swapInfo1: SwapInfo) => {
1309
1389
  const swap1Amount = Web3Number.fromWei(
1310
1390
  uint256.uint256ToBN(swapInfo1.token_from_amount).toString(),
1311
- 18, // cause its always STRK?
1391
+ 18 // cause its always STRK?
1392
+ );
1393
+ logger.verbose(
1394
+ `${EkuboCLVault.name}: harvest => swap1Amount: ${swap1Amount}`
1312
1395
  );
1313
1396
  const remainingAmount = postFeeAmount.minus(swap1Amount);
1397
+ logger.verbose(
1398
+ `${EkuboCLVault.name}: harvest => remainingAmount: ${remainingAmount}`
1399
+ );
1314
1400
  const swapInfo2 = {
1315
1401
  ...swapInfo,
1316
1402
  token_from_amount: uint256.bnToUint256(remainingAmount.toWei()),
1317
1403
  };
1318
1404
  swapInfo2.token_to_address = token1Info.address.address;
1405
+ logger.verbose(
1406
+ `${EkuboCLVault.name}: harvest => swapInfo: ${JSON.stringify(
1407
+ swapInfo
1408
+ )}`
1409
+ );
1410
+ logger.verbose(
1411
+ `${EkuboCLVault.name}: harvest => swapInfo2: ${JSON.stringify(
1412
+ swapInfo2
1413
+ )}`
1414
+ );
1319
1415
  const calldata = [
1320
1416
  claim.rewardsContract.address,
1321
1417
  {
@@ -1440,9 +1536,20 @@ const faqs: FAQ[] = [
1440
1536
  },
1441
1537
  {
1442
1538
  question: "Is the strategy audited?",
1443
- answer:
1444
- <div>Yes, the strategy has been audited. You can review the audit report in our docs <a href="https://docs.strkfarm.com/p/ekubo-cl-vaults#technical-details" style={{textDecoration: 'underline', marginLeft: '5px'}}>Here</a>.</div>
1445
- }
1539
+ answer: (
1540
+ <div>
1541
+ Yes, the strategy has been audited. You can review the audit report in
1542
+ our docs{" "}
1543
+ <a
1544
+ href="https://docs.strkfarm.com/p/ekubo-cl-vaults#technical-details"
1545
+ style={{ textDecoration: "underline", marginLeft: "5px" }}
1546
+ >
1547
+ Here
1548
+ </a>
1549
+ .
1550
+ </div>
1551
+ ),
1552
+ },
1446
1553
  ];
1447
1554
  /**
1448
1555
  * Represents the Vesu Rebalance Strategies.
@@ -1504,6 +1611,11 @@ export const EkuboCLVaultStrategies: IStrategyMetadata<CLVaultStrategySettings>[
1504
1611
  "0x028d709c875c0ceac3dce7065bec5328186dc89fe254527084d1689910954b0a"
1505
1612
  ),
1506
1613
  feeBps: 1000,
1614
+ rebalanceConditions: {
1615
+ customShouldRebalance: async (currentPrice: number) => true,
1616
+ minWaitHours: 24,
1617
+ direction: "uponly",
1618
+ },
1507
1619
  },
1508
1620
  faqs: [
1509
1621
  ...faqs,
@@ -1512,7 +1624,7 @@ export const EkuboCLVaultStrategies: IStrategyMetadata<CLVaultStrategySettings>[
1512
1624
  answer:
1513
1625
  "A negative APY can occur when xSTRK's price drops on DEXes. This is usually temporary and tends to recover within a few days or a week.",
1514
1626
  },
1515
- ]
1627
+ ],
1516
1628
  },
1517
1629
  {
1518
1630
  name: "Ekubo USDC/USDT",
@@ -1549,8 +1661,10 @@ export const EkuboCLVaultStrategies: IStrategyMetadata<CLVaultStrategySettings>[
1549
1661
  risk: {
1550
1662
  riskFactor: _riskFactorStable,
1551
1663
  netRisk:
1552
- _riskFactorStable.reduce((acc, curr) => acc + curr.value * curr.weight, 0) /
1553
- _riskFactorStable.reduce((acc, curr) => acc + curr.weight, 0),
1664
+ _riskFactorStable.reduce(
1665
+ (acc, curr) => acc + curr.value * curr.weight,
1666
+ 0
1667
+ ) / _riskFactorStable.reduce((acc, curr) => acc + curr.weight, 0),
1554
1668
  notARisks: getNoRiskTags(_riskFactorStable),
1555
1669
  },
1556
1670
  apyMethodology:
@@ -1562,9 +1676,13 @@ export const EkuboCLVaultStrategies: IStrategyMetadata<CLVaultStrategySettings>[
1562
1676
  },
1563
1677
  truePrice: 1,
1564
1678
  feeBps: 1000,
1679
+ rebalanceConditions: {
1680
+ customShouldRebalance: async (currentPrice: number) =>
1681
+ currentPrice > 0.99 && currentPrice < 1.01,
1682
+ minWaitHours: 6,
1683
+ direction: "any",
1684
+ },
1565
1685
  },
1566
- faqs: [
1567
- ...faqs,
1568
- ]
1686
+ faqs: [...faqs],
1569
1687
  },
1570
1688
  ];