@strkfarm/sdk 1.0.28 → 1.0.30

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.
@@ -1,8 +1,8 @@
1
1
  import { ContractAddr, Web3Number } from "@/dataTypes";
2
- import { getNoRiskTags, IConfig, IProtocol, IStrategyMetadata, RiskFactor, RiskType } from "@/interfaces";
2
+ import { FlowChartColors, getNoRiskTags, IConfig, IInvestmentFlow, IProtocol, IStrategyMetadata, RiskFactor, RiskType } from "@/interfaces";
3
3
  import { PricerBase } from "@/modules/pricerBase";
4
4
  import { assert } from "@/utils";
5
- import { Call, Contract, uint256 } from "starknet";
5
+ import { Account, BlockIdentifier, Call, Contract, num, uint256 } from "starknet";
6
6
  import CLVaultAbi from '@/data/cl-vault.abi.json';
7
7
  import EkuboPositionsAbi from '@/data/ekubo-positions.abi.json';
8
8
  import EkuboMathAbi from '@/data/ekubo-math.abi.json';
@@ -13,6 +13,7 @@ import { BaseStrategy } from "./base-strategy";
13
13
  import { DualActionAmount } from "./base-strategy";
14
14
  import { DualTokenInfo } from "./base-strategy";
15
15
  import { log } from "winston";
16
+ import { EkuboHarvests } from "@/modules/harvests";
16
17
 
17
18
  export interface EkuboPoolKey {
18
19
  token0: ContractAddr,
@@ -40,7 +41,8 @@ export interface CLVaultStrategySettings {
40
41
  upper: number
41
42
  },
42
43
  // to get true price
43
- lstContract: ContractAddr
44
+ lstContract: ContractAddr,
45
+ feeBps: number
44
46
  }
45
47
 
46
48
  export class EkuboCLVault extends BaseStrategy<DualTokenInfo, DualActionAmount> {
@@ -87,12 +89,76 @@ export class EkuboCLVault extends BaseStrategy<DualTokenInfo, DualActionAmount>
87
89
  this.avnu = new AvnuWrapper();
88
90
  }
89
91
 
90
- depositCall(amountInfo: DualActionAmount, receiver: ContractAddr): Call[] {
91
- return []
92
+ async matchInputAmounts(amountInfo: DualActionAmount): Promise<DualActionAmount> {
93
+ const bounds = await this.getCurrentBounds();
94
+ const res = await this._getExpectedAmountsForLiquidity(
95
+ amountInfo.token0.amount,
96
+ amountInfo.token1.amount,
97
+ bounds,
98
+ false
99
+ );
100
+ return {
101
+ token0: {
102
+ tokenInfo: amountInfo.token0.tokenInfo,
103
+ amount: res.amount0
104
+ },
105
+ token1: {
106
+ tokenInfo: amountInfo.token1.tokenInfo,
107
+ amount: res.amount1
108
+ }
109
+ }
110
+ }
111
+
112
+ /** Returns minimum amounts give given two amounts based on what can be added for liq */
113
+ async getMinDepositAmounts(amountInfo: DualActionAmount): Promise<DualActionAmount> {
114
+ const shares = await this.tokensToShares(amountInfo);
115
+
116
+ // get actual amounts now
117
+ const { amount0, amount1 }: any = await this.contract.call('convert_to_assets', [uint256.bnToUint256(shares.toWei())])
118
+
119
+ // todo use user balances to compute what is required to be swapped
120
+ return {
121
+ token0: {
122
+ tokenInfo: amountInfo.token0.tokenInfo,
123
+ amount: Web3Number.fromWei(amount0.toString(), amountInfo.token0.tokenInfo.decimals)
124
+ },
125
+ token1: {
126
+ tokenInfo: amountInfo.token1.tokenInfo,
127
+ amount: Web3Number.fromWei(amount1.toString(), amountInfo.token1.tokenInfo.decimals)
128
+ },
129
+ }
130
+ }
131
+
132
+ async depositCall(amountInfo: DualActionAmount, receiver: ContractAddr): Promise<Call[]> {
133
+ const updateAmountInfo = await this.getMinDepositAmounts(amountInfo);
134
+ // Technically its not erc4626 abi, but we just need approve call
135
+ // so, its ok to use it
136
+ const token0Contract = new Contract(ERC4626Abi, amountInfo.token0.tokenInfo.address.address, this.config.provider);
137
+ const token1Contract = new Contract(ERC4626Abi, amountInfo.token1.tokenInfo.address.address, this.config.provider);
138
+ const call1 = token0Contract.populate('approve', [this.address.address, uint256.bnToUint256(updateAmountInfo.token0.amount.toWei())])
139
+ const call2 = token1Contract.populate('approve', [this.address.address, uint256.bnToUint256(updateAmountInfo.token1.amount.toWei())])
140
+ const call3 = this.contract.populate('deposit', [uint256.bnToUint256(updateAmountInfo.token0.amount.toWei()), uint256.bnToUint256(updateAmountInfo.token1.amount.toWei()), receiver.address]);
141
+ const calls: Call[] = [];
142
+ if (updateAmountInfo.token0.amount.greaterThan(0)) calls.push(call1);
143
+ if (updateAmountInfo.token1.amount.greaterThan(0)) calls.push(call2);
144
+ return [...calls, call3];
92
145
  }
93
146
 
94
- withdrawCall(amountInfo: DualActionAmount, receiver: ContractAddr, owner: ContractAddr): Call[] {
95
- return []
147
+ async tokensToShares(amountInfo: DualActionAmount) {
148
+ const shares = await this.contract.call('convert_to_shares', [
149
+ uint256.bnToUint256(amountInfo.token0.amount.toWei()),
150
+ uint256.bnToUint256(amountInfo.token1.amount.toWei())
151
+ ])
152
+ return Web3Number.fromWei(shares.toString(), 18);
153
+ }
154
+
155
+ async withdrawCall(amountInfo: DualActionAmount, receiver: ContractAddr, owner: ContractAddr): Promise<Call[]> {
156
+ const shares = await this.tokensToShares(amountInfo);
157
+ logger.verbose(`${EkuboCLVault.name}: withdrawCall: shares=${shares.toString()}`);
158
+ return [this.contract.populate('withdraw', [
159
+ uint256.bnToUint256(shares.toWei()),
160
+ receiver.address
161
+ ])];
96
162
  }
97
163
 
98
164
  rebalanceCall(newBounds: EkuboBounds, swapParams: SwapInfo): Call[] {
@@ -115,23 +181,136 @@ export class EkuboCLVault extends BaseStrategy<DualTokenInfo, DualActionAmount>
115
181
  return [this.contract.populate('handle_fees', [])]
116
182
  }
117
183
 
118
- async getUserTVL(user: ContractAddr): Promise<DualTokenInfo> {
119
- throw new Error('Not implemented');
184
+ /**
185
+ * Calculates assets before and now in a given token of TVL per share to observe growth
186
+ * @returns {Promise<number>} The weighted average APY across all pools
187
+ */
188
+ async netAPY(blockIdentifier: BlockIdentifier = 'pending', sinceBlocks = 20000): Promise<number> {
189
+ // no special provisions required to account for defi spring rewards
190
+ // or strategy fees, bcz this returns realisitic apy based on 7day performance
191
+
192
+ const tvlNow = await this._getTVL(blockIdentifier);
193
+ const supplyNow = await this.totalSupply(blockIdentifier);
194
+ const priceNow = await this.getCurrentPrice(blockIdentifier);
195
+ let blockNow = typeof blockIdentifier == 'number' ? blockIdentifier : (await this.config.provider.getBlockLatestAccepted()).block_number;
196
+ const blockNowTime = typeof blockIdentifier == 'number' ? (await this.config.provider.getBlockWithTxs(blockIdentifier)).timestamp : new Date().getTime() / 1000;
197
+ const blockBefore = blockNow - sinceBlocks;
198
+ const adjustedSupplyNow = supplyNow.minus(await this.getHarvestRewardShares(blockBefore, blockNow))
199
+ let blockBeforeInfo = await this.config.provider.getBlockWithTxs(blockBefore);
200
+ const tvlBefore = await this._getTVL(blockBefore);
201
+ const supplyBefore = await this.totalSupply(blockBefore);
202
+ const priceBefore = await this.getCurrentPrice(blockBefore);
203
+
204
+ const tvlInToken0Now = tvlNow.amount0.multipliedBy(priceNow.price).plus(tvlNow.amount1);
205
+ const tvlPerShareNow = tvlInToken0Now.multipliedBy(1e18).dividedBy(adjustedSupplyNow)
206
+ const tvlInToken0Bf = tvlBefore.amount0.multipliedBy(priceBefore.price).plus(tvlBefore.amount1);
207
+ const tvlPerShareBf = tvlInToken0Bf.multipliedBy(1e18).dividedBy(supplyBefore)
208
+ const timeDiffSeconds = blockNowTime - blockBeforeInfo.timestamp;
209
+ logger.verbose(`tvlInToken0Now: ${tvlInToken0Now.toString()}`);
210
+ logger.verbose(`tvlInToken0Bf: ${tvlInToken0Bf.toString()}`);
211
+ logger.verbose(`tvlPerShareNow: ${tvlPerShareNow.toString()}`);
212
+ logger.verbose(`tvlPerShareBf: ${tvlPerShareBf.toString()}`);
213
+ logger.verbose(`Price before: ${priceBefore.price.toString()}`);
214
+ logger.verbose(`Price now: ${priceNow.price.toString()}`);
215
+ logger.verbose(`Supply before: ${supplyBefore.toString()}`);
216
+ logger.verbose(`Supply now: ${adjustedSupplyNow.toString()}`);
217
+ logger.verbose(`Time diff in seconds: ${timeDiffSeconds}`);
218
+ const apyForGivenBlocks = Number((tvlPerShareNow.minus(tvlPerShareBf).multipliedBy(10000).dividedBy(tvlPerShareBf))) / 10000;
219
+ return apyForGivenBlocks * (365 * 24 * 3600) / (timeDiffSeconds)
120
220
  }
121
221
 
122
- async getTVL(): Promise<DualTokenInfo> {
123
- const result = await this.contract.call('total_liquidity', []);
124
- const bounds = await this.getCurrentBounds();
125
- const {amount0, amount1} = await this.getLiquidityToAmounts(Web3Number.fromWei(result.toString(), 18), bounds);
126
- const poolKey = await this.getPoolKey();
222
+ async getHarvestRewardShares(fromBlock: number, toBlock: number) {
223
+ const len = Number(await this.contract.call('get_total_rewards'));
224
+ let shares = Web3Number.fromWei(0, 18);
225
+ for (let i = len - 1; i > 0; --i) {
226
+ let record: any = await this.contract.call('get_rewards_info', [i]);
227
+ logger.verbose(`${EkuboCLVault.name}: getHarvestRewardShares: ${i}`);
228
+ console.log(record)
229
+ const block = Number(record.block_number);
230
+ if (block < fromBlock) {
231
+ return shares
232
+ } else if (block > toBlock) {
233
+ continue;
234
+ } else {
235
+ shares = shares.plus(Web3Number.fromWei(record.shares.toString(), 18));
236
+ }
237
+ logger.verbose(`${EkuboCLVault.name}: getHarvestRewardShares: ${i} => ${shares.toWei()}`);
238
+ }
239
+ return shares;
240
+ }
241
+
242
+ async balanceOf(user: ContractAddr, blockIdentifier: BlockIdentifier = 'pending'): Promise<Web3Number> {
243
+ let bal = await this.contract.call('balance_of', [user.address]);
244
+ return Web3Number.fromWei(bal.toString(), 18);
245
+ }
246
+
247
+ async getUserTVL(user: ContractAddr, blockIdentifier: BlockIdentifier = 'pending'): Promise<DualTokenInfo> {
248
+ let bal = await this.balanceOf(user, blockIdentifier);
249
+ const assets: any = await this.contract.call('convert_to_assets', [uint256.bnToUint256(bal.toWei())], {
250
+ blockIdentifier
251
+ })
252
+ const poolKey = await this.getPoolKey(blockIdentifier);
253
+ this.assertValidDepositTokens(poolKey);
127
254
  const token0Info = await Global.getTokenInfoFromAddr(poolKey.token0);
128
255
  const token1Info = await Global.getTokenInfoFromAddr(poolKey.token1);
256
+ const amount0 = Web3Number.fromWei(assets.amount0.toString(), token0Info.decimals);
257
+ const amount1 = Web3Number.fromWei(assets.amount1.toString(), token1Info.decimals);
129
258
  const P0 = await this.pricer.getPrice(token0Info.symbol);
130
259
  const P1 = await this.pricer.getPrice(token1Info.symbol);
131
260
  const token0Usd = Number(amount0.toFixed(13)) * P0.price;
132
261
  const token1Usd = Number(amount1.toFixed(13)) * P1.price;
262
+
133
263
  return {
134
- netUsdValue: token0Usd + token1Usd,
264
+ usdValue: token0Usd + token1Usd,
265
+ token0: {
266
+ tokenInfo: token0Info,
267
+ amount: amount0,
268
+ usdValue: token0Usd
269
+ },
270
+ token1: {
271
+ tokenInfo: token1Info,
272
+ amount: amount1,
273
+ usdValue: token1Usd
274
+ }
275
+ }
276
+ }
277
+
278
+ async _getTVL(blockIdentifier: BlockIdentifier = 'pending') {
279
+ const result = await this.contract.call('total_liquidity', [], {
280
+ blockIdentifier
281
+ });
282
+ const bounds = await this.getCurrentBounds(blockIdentifier);
283
+ const {amount0, amount1} = await this.getLiquidityToAmounts(Web3Number.fromWei(result.toString(), 18), bounds, blockIdentifier);
284
+
285
+ return { amount0, amount1 }
286
+ }
287
+
288
+ async totalSupply(blockIdentifier: BlockIdentifier = 'pending'): Promise<Web3Number> {
289
+ const res = await this.contract.call('total_supply', [], {
290
+ blockIdentifier
291
+ });
292
+ return Web3Number.fromWei(res.toString(), 18);
293
+ }
294
+
295
+ assertValidDepositTokens(poolKey: EkuboPoolKey) {
296
+ // given this is called by UI, if wrong config is done, it will throw error;
297
+ assert(poolKey.token0.eq(this.metadata.depositTokens[0].address), 'Expected token0 in depositTokens[0]');
298
+ assert(poolKey.token1.eq(this.metadata.depositTokens[1].address), 'Expected token1 in depositTokens[1]');
299
+ }
300
+
301
+ async getTVL(blockIdentifier: BlockIdentifier = 'pending'): Promise<DualTokenInfo> {
302
+ const { amount0, amount1 } = await this._getTVL(blockIdentifier);
303
+ const poolKey = await this.getPoolKey(blockIdentifier);
304
+ this.assertValidDepositTokens(poolKey);
305
+
306
+ const token0Info = await Global.getTokenInfoFromAddr(poolKey.token0);
307
+ const token1Info = await Global.getTokenInfoFromAddr(poolKey.token1);
308
+ const P0 = await this.pricer.getPrice(token0Info.symbol);
309
+ const P1 = await this.pricer.getPrice(token1Info.symbol);
310
+ const token0Usd = Number(amount0.toFixed(13)) * P0.price;
311
+ const token1Usd = Number(amount1.toFixed(13)) * P1.price;
312
+ return {
313
+ usdValue: token0Usd + token1Usd,
135
314
  token0: {
136
315
  tokenInfo: token0Info,
137
316
  amount: amount0,
@@ -172,7 +351,7 @@ export class EkuboCLVault extends BaseStrategy<DualTokenInfo, DualActionAmount>
172
351
  const token0Usd = Number(token0Web3.toFixed(13)) * P0.price;
173
352
  const token1Usd = Number(token1Web3.toFixed(13)) * P1.price;
174
353
  return {
175
- netUsdValue: token0Usd + token1Usd,
354
+ usdValue: token0Usd + token1Usd,
176
355
  token0: {
177
356
  tokenInfo: token0Info,
178
357
  amount: token0Web3,
@@ -197,12 +376,12 @@ export class EkuboCLVault extends BaseStrategy<DualTokenInfo, DualActionAmount>
197
376
  return truePrice;
198
377
  }
199
378
 
200
- async getCurrentPrice() {
201
- const poolKey = await this.getPoolKey();
202
- return this._getCurrentPrice(poolKey);
379
+ async getCurrentPrice(blockIdentifier: BlockIdentifier = 'pending') {
380
+ const poolKey = await this.getPoolKey(blockIdentifier);
381
+ return this._getCurrentPrice(poolKey, blockIdentifier);
203
382
  }
204
383
 
205
- private async _getCurrentPrice(poolKey: EkuboPoolKey) {
384
+ private async _getCurrentPrice(poolKey: EkuboPoolKey, blockIdentifier: BlockIdentifier = 'pending') {
206
385
  const priceInfo: any = await this.ekuboPositionsContract.call('get_pool_price', [
207
386
  {
208
387
  token0: poolKey.token0.address,
@@ -211,18 +390,25 @@ export class EkuboCLVault extends BaseStrategy<DualTokenInfo, DualActionAmount>
211
390
  tick_spacing: poolKey.tick_spacing,
212
391
  extension: poolKey.extension
213
392
  }
214
- ])
393
+ ], {
394
+ blockIdentifier
395
+ })
215
396
  const sqrtRatio = EkuboCLVault.div2Power128(BigInt(priceInfo.sqrt_ratio.toString()));
397
+ console.log(`EkuboCLVault: getCurrentPrice: blockIdentifier: ${blockIdentifier}, sqrtRatio: ${sqrtRatio}, ${priceInfo.sqrt_ratio.toString()}`);
216
398
  const price = sqrtRatio * sqrtRatio;
217
399
  const tick = EkuboCLVault.priceToTick(price, true, Number(poolKey.tick_spacing));
400
+ console.log(`EkuboCLVault: getCurrentPrice: blockIdentifier: ${blockIdentifier}, price: ${price}, tick: ${tick.mag}, ${tick.sign}`);
218
401
  return {
219
402
  price,
220
- tick: tick.mag * (tick.sign == 0 ? 1 : -1)
403
+ tick: tick.mag * (tick.sign == 0 ? 1 : -1),
404
+ sqrtRatio: priceInfo.sqrt_ratio.toString()
221
405
  }
222
406
  }
223
407
 
224
- async getCurrentBounds(): Promise<EkuboBounds> {
225
- const result: any = await this.contract.call('get_position_key', []);
408
+ async getCurrentBounds(blockIdentifier: BlockIdentifier = 'pending'): Promise<EkuboBounds> {
409
+ const result: any = await this.contract.call('get_position_key', [], {
410
+ blockIdentifier
411
+ })
226
412
  return {
227
413
  lowerTick: EkuboCLVault.i129ToNumber(result.bounds.lower),
228
414
  upperTick: EkuboCLVault.i129ToNumber(result.bounds.upper)
@@ -239,11 +425,13 @@ export class EkuboCLVault extends BaseStrategy<DualTokenInfo, DualActionAmount>
239
425
  return this.tickToi129(tick);
240
426
  }
241
427
 
242
- async getPoolKey(): Promise<EkuboPoolKey> {
428
+ async getPoolKey(blockIdentifier: BlockIdentifier = 'pending'): Promise<EkuboPoolKey> {
243
429
  if (this.poolKey) {
244
430
  return this.poolKey;
245
431
  }
246
- const result: any = await this.contract.call('get_settings', []);
432
+ const result: any = await this.contract.call('get_settings', [], {
433
+ blockIdentifier
434
+ });
247
435
  const poolKey: EkuboPoolKey = {
248
436
  token0: ContractAddr.from(result.pool_key.token0.toString()),
249
437
  token1: ContractAddr.from(result.pool_key.token1.toString()),
@@ -281,19 +469,17 @@ export class EkuboCLVault extends BaseStrategy<DualTokenInfo, DualActionAmount>
281
469
  private async _getExpectedAmountsForLiquidity(
282
470
  amount0: Web3Number,
283
471
  amount1: Web3Number,
284
- bounds: EkuboBounds
472
+ bounds: EkuboBounds,
473
+ justUseInputAmount = true
285
474
  ) {
286
475
  assert(amount0.greaterThan(0) || amount1.greaterThan(0), 'Amount is 0');
287
476
 
288
- // token is token0 or token1
289
- const poolKey = await this.getPoolKey();
290
-
291
477
  // get amount ratio for 1e18 liquidity
292
- const sampleLiq = 1e18;
478
+ const sampleLiq = 1e20;
293
479
  const {amount0: sampleAmount0, amount1: sampleAmount1} = await this.getLiquidityToAmounts(Web3Number.fromWei(sampleLiq.toString(), 18), bounds);
294
480
  logger.verbose(`${EkuboCLVault.name}: _getExpectedAmountsForLiquidity => sampleAmount0: ${sampleAmount0.toString()}, sampleAmount1: ${sampleAmount1.toString()}`);
295
481
 
296
- assert(!sampleAmount0.eq(0) && !sampleAmount1.eq(0), 'Sample amount is 0');
482
+ assert(!sampleAmount0.eq(0) || !sampleAmount1.eq(0), 'Sample amount is 0');
297
483
 
298
484
  // notation: P = P0 / P1
299
485
  const price = await (await this.getCurrentPrice()).price;
@@ -304,13 +490,13 @@ export class EkuboCLVault extends BaseStrategy<DualTokenInfo, DualActionAmount>
304
490
  if (sampleAmount1.eq(0)) {
305
491
  return {
306
492
  amount0: amount0,
307
- amount1: Web3Number.fromWei('0', 18),
493
+ amount1: Web3Number.fromWei('0', amount1.decimals),
308
494
  ratio: Infinity
309
495
  }
310
496
  } else if (sampleAmount0.eq(0)) {
311
497
  // swap all to token1
312
498
  return {
313
- amount0: Web3Number.fromWei('0', 18),
499
+ amount0: Web3Number.fromWei('0', amount0.decimals),
314
500
  amount1: amount0.multipliedBy(price),
315
501
  ratio: 0
316
502
  }
@@ -318,7 +504,7 @@ export class EkuboCLVault extends BaseStrategy<DualTokenInfo, DualActionAmount>
318
504
  } else if (amount0.eq(0) && amount1.greaterThan(0)) {
319
505
  if (sampleAmount0.eq(0)) {
320
506
  return {
321
- amount0: Web3Number.fromWei('0', 18),
507
+ amount0: Web3Number.fromWei('0', amount0.decimals),
322
508
  amount1: amount1,
323
509
  ratio: 0
324
510
  }
@@ -326,16 +512,45 @@ export class EkuboCLVault extends BaseStrategy<DualTokenInfo, DualActionAmount>
326
512
  // swap all to token0
327
513
  return {
328
514
  amount0: amount1.dividedBy(price),
329
- amount1: Web3Number.fromWei('0', 18),
515
+ amount1: Web3Number.fromWei('0', amount1.decimals),
330
516
  ratio: Infinity
331
517
  }
332
518
  }
333
519
  }
334
520
 
335
- const ratio = (sampleAmount0.multipliedBy(1e18).dividedBy(sampleAmount1.toString())).dividedBy(1e18);
521
+ // must make it general later
522
+ assert(sampleAmount0.decimals == sampleAmount1.decimals, 'Sample amounts have different decimals');
523
+ const ratioWeb3Number = (sampleAmount0.multipliedBy(1e18).dividedBy(sampleAmount1.toString())).dividedBy(1e18);
524
+ const ratio: number = Number(ratioWeb3Number.toFixed(18));
336
525
  logger.verbose(`${EkuboCLVault.name}: ${this.metadata.name} => ratio: ${ratio.toString()}`);
337
526
 
338
- return this._solveExpectedAmountsEq(amount0, amount1, ratio, price);
527
+ if (justUseInputAmount)
528
+ return this._solveExpectedAmountsEq(amount0, amount1, ratioWeb3Number, price);
529
+
530
+ // we are at liberty to propose amounts outside the propsed amount
531
+ // assuming amount0 and amount1 as independent values, compute other amounts
532
+ // Also, if code came till here, it means both sample amounts are non-zero
533
+ if (amount1.eq(0) && amount0.greaterThan(0)) {
534
+ // use amount0 as base and compute amount1 using ratio
535
+ const _amount1 = amount0.dividedBy(ratioWeb3Number);
536
+ return {
537
+ amount0: amount0,
538
+ amount1: _amount1,
539
+ ratio
540
+ };
541
+ } else if (amount0.eq(0) && amount1.greaterThan(0)) {
542
+ // use amount1 as base and compute amount0 using ratio
543
+ const _amount0 = amount1.multipliedBy(ratio);
544
+ return {
545
+ amount0: _amount0,
546
+ amount1: amount1,
547
+ ratio,
548
+ };
549
+ } else {
550
+ // ambiguous case
551
+ // can lead to diverging results
552
+ throw new Error('Both amounts are non-zero, cannot compute expected amounts');
553
+ }
339
554
  }
340
555
 
341
556
  private _solveExpectedAmountsEq(availableAmount0: Web3Number, availableAmount1: Web3Number, ratio: Web3Number, price: number) {
@@ -360,12 +575,12 @@ export class EkuboCLVault extends BaseStrategy<DualTokenInfo, DualActionAmount>
360
575
 
361
576
  // fetch current unused balances of vault
362
577
  const erc20Mod = new ERC20(this.config);
363
- const token0Bal1 = await erc20Mod.balanceOf(poolKey.token0, this.address.address, 18);
364
- const token1Bal1 = await erc20Mod.balanceOf(poolKey.token1, this.address.address, 18);
365
-
366
- // if both tokens are non-zero and above $1 throw error
367
578
  const token0Info = await Global.getTokenInfoFromAddr(poolKey.token0);
368
- const token1Info = await Global.getTokenInfoFromAddr(poolKey.token1);
579
+ const token1Info = await Global.getTokenInfoFromAddr(poolKey.token1);
580
+ const token0Bal1 = await erc20Mod.balanceOf(poolKey.token0, this.address.address, token0Info.decimals);
581
+ const token1Bal1 = await erc20Mod.balanceOf(poolKey.token1, this.address.address, token1Info.decimals);
582
+ logger.verbose(`${EkuboCLVault.name}: getSwapInfoToHandleUnused => token0Bal1: ${token0Bal1.toString()}, token1Bal1: ${token1Bal1.toString()}`);
583
+ // if both tokens are non-zero and above $1 throw error
369
584
  const token0Price = await this.pricer.getPrice(token0Info.symbol);
370
585
  const token1Price = await this.pricer.getPrice(token1Info.symbol);
371
586
  const token0PriceUsd = token0Price.price * Number(token0Bal1.toFixed(13));
@@ -385,6 +600,7 @@ export class EkuboCLVault extends BaseStrategy<DualTokenInfo, DualActionAmount>
385
600
 
386
601
  // if rebalancing, consider whole TVL as available
387
602
  if (considerRebalance) {
603
+ logger.verbose(`${EkuboCLVault.name}: getSwapInfoToHandleUnused => considerRebalance: true`);
388
604
  const tvl = await this.getTVL();
389
605
  token0Bal = token0Bal.plus(tvl.token0.amount.toString());
390
606
  token1Bal = token1Bal.plus(tvl.token1.amount.toString());
@@ -396,7 +612,12 @@ export class EkuboCLVault extends BaseStrategy<DualTokenInfo, DualActionAmount>
396
612
  // get expected amounts for liquidity
397
613
  const newBounds = await this.getNewBounds();
398
614
  logger.verbose(`${EkuboCLVault.name}: getSwapInfoToHandleUnused => newBounds: ${newBounds.lowerTick}, ${newBounds.upperTick}`);
399
- let expectedAmounts = await this._getExpectedAmountsForLiquidity(token0Bal, token1Bal, newBounds);
615
+
616
+ return await this.getSwapInfoGivenAmounts(poolKey, token0Bal, token1Bal, newBounds)
617
+ }
618
+
619
+ async getSwapInfoGivenAmounts(poolKey: EkuboPoolKey, token0Bal: Web3Number, token1Bal: Web3Number, bounds: EkuboBounds): Promise<SwapInfo> {
620
+ let expectedAmounts = await this._getExpectedAmountsForLiquidity(token0Bal, token1Bal, bounds);
400
621
  logger.verbose(`${EkuboCLVault.name}: getSwapInfoToHandleUnused => expectedAmounts: ${expectedAmounts.amount0.toString()}, ${expectedAmounts.amount1.toString()}`);
401
622
 
402
623
  // get swap info
@@ -406,6 +627,7 @@ export class EkuboCLVault extends BaseStrategy<DualTokenInfo, DualActionAmount>
406
627
  while (retry < maxRetry) {
407
628
  retry++;
408
629
  // assert one token is increased and other is decreased
630
+
409
631
  if (expectedAmounts.amount0.lessThan(token0Bal) && expectedAmounts.amount1.lessThan(token1Bal)) {
410
632
  throw new Error('Both tokens are decreased, something is wrong');
411
633
  }
@@ -424,6 +646,19 @@ export class EkuboCLVault extends BaseStrategy<DualTokenInfo, DualActionAmount>
424
646
  logger.verbose(`${EkuboCLVault.name}: getSwapInfoToHandleUnused => remainingSellAmount: ${remainingSellAmount.toString()}`);
425
647
  logger.verbose(`${EkuboCLVault.name}: getSwapInfoToHandleUnused => expectedRatio: ${expectedRatio}`);
426
648
 
649
+ if (amountToSell.eq(0)) {
650
+ return {
651
+ token_from_address: tokenToSell.address,
652
+ token_from_amount: uint256.bnToUint256(0),
653
+ token_to_address: tokenToSell.address,
654
+ token_to_amount: uint256.bnToUint256(0),
655
+ token_to_min_amount: uint256.bnToUint256(0),
656
+ beneficiary: this.address.address,
657
+ integrator_fee_amount_bps: 0,
658
+ integrator_fee_recipient: this.address.address,
659
+ routes: []
660
+ }
661
+ }
427
662
  const quote = await this.avnu.getQuotes(tokenToSell.address, tokenToBuy.address, amountToSell.toWei(), this.address.address);
428
663
  if (remainingSellAmount.eq(0)) {
429
664
  const minAmountOut = Web3Number.fromWei(quote.buyAmount.toString(), tokenToBuyInfo.decimals).multipliedBy(0.9999);
@@ -450,6 +685,95 @@ export class EkuboCLVault extends BaseStrategy<DualTokenInfo, DualActionAmount>
450
685
  throw new Error('Failed to get swap info');
451
686
  }
452
687
 
688
+ /**
689
+ * Attempts to rebalance the vault by iteratively adjusting swap amounts if initial attempt fails.
690
+ * Uses binary search approach to find optimal swap amount.
691
+ *
692
+ * @param newBounds - The new tick bounds to rebalance to
693
+ * @param swapInfo - Initial swap parameters for rebalancing
694
+ * @param acc - Account to estimate gas fees with
695
+ * @param retry - Current retry attempt number (default 0)
696
+ * @param adjustmentFactor - Percentage to adjust swap amount by (default 1)
697
+ * @param isToken0Deficit - Whether token0 balance needs increasing (default true)
698
+ * @returns Array of contract calls needed for rebalancing
699
+ * @throws Error if max retries reached without successful rebalance
700
+ */
701
+ async rebalanceIter(
702
+ swapInfo: SwapInfo,
703
+ acc: Account,
704
+ estimateCall: (swapInfo: SwapInfo) => Promise<Call[]>,
705
+ retry = 0,
706
+ adjustmentFactor = 1,
707
+ isToken0Deficit = true
708
+ ): Promise<Call[]> {
709
+ const MAX_RETRIES = 20;
710
+ const MIN_ADJUSTMENT = 0.001; // Minimum adjustment factor
711
+
712
+ logger.verbose(
713
+ `Rebalancing ${this.metadata.name}: ` +
714
+ `retry=${retry}, adjustment=${adjustmentFactor}%, token0Deficit=${isToken0Deficit}`
715
+ );
716
+
717
+ const fromAmount = uint256.uint256ToBN(swapInfo.token_from_amount);
718
+ logger.verbose(
719
+ `Selling ${fromAmount.toString()} of token ${swapInfo.token_from_address}`
720
+ );
721
+
722
+ try {
723
+ const calls = await estimateCall(swapInfo);
724
+ await acc.estimateInvokeFee(calls);
725
+ return calls;
726
+ } catch(err: any) {
727
+ if (retry >= MAX_RETRIES) {
728
+ logger.error(`Rebalance failed after ${MAX_RETRIES} retries`);
729
+ throw err;
730
+ }
731
+
732
+ if (adjustmentFactor < MIN_ADJUSTMENT) {
733
+ logger.error('Adjustment factor too small, likely oscillating');
734
+ throw new Error('Failed to converge on valid swap amount');
735
+ }
736
+
737
+ logger.error(`Rebalance attempt ${retry + 1} failed, adjusting swap amount...`);
738
+
739
+ const newSwapInfo = { ...swapInfo };
740
+ const currentAmount = Web3Number.fromWei(fromAmount.toString(), 18); // 18 is ok, as its toWei eventually anyways
741
+
742
+ if (err.message.includes('invalid token0 balance') || err.message.includes('invalid token0 amount')) {
743
+ // Too much token0, decrease swap amount
744
+ logger.verbose('Reducing swap amount - excess token0');
745
+ newSwapInfo.token_from_amount = uint256.bnToUint256(
746
+ currentAmount.multipliedBy((100 - adjustmentFactor)/100).toWei()
747
+ );
748
+ adjustmentFactor = isToken0Deficit ? adjustmentFactor * 2: adjustmentFactor / 2;
749
+ isToken0Deficit = true;
750
+
751
+ } else if (err.message.includes('invalid token1 balance') || err.message.includes('invalid token1 amount')) {
752
+ // Too much token1, increase swap amount
753
+ logger.verbose('Increasing swap amount - excess token1');
754
+ newSwapInfo.token_from_amount = uint256.bnToUint256(
755
+ currentAmount.multipliedBy((100 + adjustmentFactor)/100).toWei()
756
+ );
757
+ adjustmentFactor = isToken0Deficit ? adjustmentFactor / 2 : adjustmentFactor * 2;
758
+ isToken0Deficit = false;
759
+
760
+ } else {
761
+ logger.error('Unexpected error:', err);
762
+ }
763
+
764
+ newSwapInfo.token_to_min_amount = uint256.bnToUint256('0');
765
+
766
+ return this.rebalanceIter(
767
+ newSwapInfo,
768
+ acc,
769
+ estimateCall,
770
+ retry + 1,
771
+ adjustmentFactor,
772
+ isToken0Deficit
773
+ );
774
+ }
775
+ }
776
+
453
777
  static tickToi129(tick: number) {
454
778
  if (tick < 0) {
455
779
  return {
@@ -476,21 +800,31 @@ export class EkuboCLVault extends BaseStrategy<DualTokenInfo, DualActionAmount>
476
800
  return Math.pow(1.000001, Number(tick));
477
801
  }
478
802
 
479
- async getLiquidityToAmounts(liquidity: Web3Number, bounds: EkuboBounds) {
480
- const currentPrice = await this.getCurrentPrice();
481
- const lowerPrice = await EkuboCLVault.tickToPrice(bounds.lowerTick);
482
- const upperPrice = await EkuboCLVault.tickToPrice(bounds.upperTick);
803
+ async getLiquidityToAmounts(
804
+ liquidity: Web3Number,
805
+ bounds: EkuboBounds,
806
+ blockIdentifier: BlockIdentifier = 'pending',
807
+ _poolKey: EkuboPoolKey | null = null,
808
+ _currentPrice: {
809
+ price: number, tick: number, sqrtRatio: string
810
+ } | null = null
811
+ ) {
812
+ const currentPrice = _currentPrice || await this.getCurrentPrice(blockIdentifier);
813
+ const lowerPrice = EkuboCLVault.tickToPrice(bounds.lowerTick);
814
+ const upperPrice = EkuboCLVault.tickToPrice(bounds.upperTick);
483
815
  logger.verbose(`${EkuboCLVault.name}: getLiquidityToAmounts => currentPrice: ${currentPrice.price}, lowerPrice: ${lowerPrice}, upperPrice: ${upperPrice}`);
484
816
  const result: any = await this.ekuboMathContract.call('liquidity_delta_to_amount_delta', [
485
- uint256.bnToUint256(EkuboCLVault.priceToSqrtRatio(currentPrice.price).toString()),
817
+ uint256.bnToUint256(currentPrice.sqrtRatio),
486
818
  {
487
819
  mag: liquidity.toWei(),
488
820
  sign: 0
489
821
  },
490
822
  uint256.bnToUint256(EkuboCLVault.priceToSqrtRatio(lowerPrice).toString()),
491
823
  uint256.bnToUint256(EkuboCLVault.priceToSqrtRatio(upperPrice).toString())
492
- ] as any);
493
- const poolKey = await this.getPoolKey();
824
+ ] as any, {
825
+ blockIdentifier
826
+ });
827
+ const poolKey = _poolKey || await this.getPoolKey(blockIdentifier);
494
828
  const token0Info = await Global.getTokenInfoFromAddr(poolKey.token0);
495
829
  const token1Info = await Global.getTokenInfoFromAddr(poolKey.token1);
496
830
  const amount0 = Web3Number.fromWei(EkuboCLVault.i129ToNumber(result.amount0).toString(), token0Info.decimals);
@@ -500,37 +834,125 @@ export class EkuboCLVault extends BaseStrategy<DualTokenInfo, DualActionAmount>
500
834
  amount0, amount1
501
835
  }
502
836
  }
837
+
838
+ async harvest(acc: Account) {
839
+ const ekuboHarvests = new EkuboHarvests(this.config);
840
+ const unClaimedRewards = await ekuboHarvests.getUnHarvestedRewards(this.address);
841
+ const poolKey = await this.getPoolKey();
842
+ const token0Info = await Global.getTokenInfoFromAddr(poolKey.token0);
843
+ const token1Info = await Global.getTokenInfoFromAddr(poolKey.token1);
844
+ const bounds = await this.getCurrentBounds();
845
+ const calls: Call[] = [];
846
+ for (let claim of unClaimedRewards) {
847
+ const fee = claim.claim.amount.multipliedBy(this.metadata.additionalInfo.feeBps).dividedBy(10000);
848
+ const postFeeAmount = claim.claim.amount.minus(fee);
849
+
850
+ const isToken1 = claim.token.eq(poolKey.token1);
851
+ logger.verbose(`${EkuboCLVault.name}: harvest => Processing claim, isToken1: ${isToken1} amount: ${postFeeAmount.toWei()}`);
852
+ const token0Amt = isToken1 ? new Web3Number(0, token0Info.decimals) : postFeeAmount;
853
+ const token1Amt = isToken1 ? postFeeAmount : new Web3Number(0, token0Info.decimals);
854
+ logger.verbose(`${EkuboCLVault.name}: harvest => token0Amt: ${token0Amt.toString()}, token1Amt: ${token1Amt.toString()}`);
855
+
856
+ const swapInfo = await this.getSwapInfoGivenAmounts(poolKey, token0Amt, token1Amt, bounds);
857
+ swapInfo.token_to_address = token0Info.address.address;
858
+ logger.verbose(`${EkuboCLVault.name}: harvest => swapInfo: ${JSON.stringify(swapInfo)}`);
859
+
860
+ logger.verbose(`${EkuboCLVault.name}: harvest => claim: ${JSON.stringify(claim)}`);
861
+ const harvestEstimateCall = async (swapInfo1: SwapInfo) => {
862
+ const swap1Amount = Web3Number.fromWei(uint256.uint256ToBN(swapInfo1.token_from_amount).toString(), 18);
863
+ const remainingAmount = postFeeAmount.minus(swap1Amount);
864
+ const swapInfo2 = {...swapInfo, token_from_amount: uint256.bnToUint256(remainingAmount.toWei()) }
865
+ swapInfo2.token_to_address = token1Info.address.address;
866
+ const calldata = [
867
+ claim.rewardsContract.address,
868
+ {
869
+ id: claim.claim.id,
870
+ amount: claim.claim.amount.toWei(),
871
+ claimee: claim.claim.claimee.address
872
+ },
873
+ claim.proof.map((p) => num.getDecimalString(p)),
874
+ swapInfo,
875
+ swapInfo2
876
+ ];
877
+ logger.verbose(`${EkuboCLVault.name}: harvest => calldata: ${JSON.stringify(calldata)}`);
878
+ return [this.contract.populate('harvest', calldata)]
879
+ }
880
+ const _callsFinal = await this.rebalanceIter(swapInfo, acc, harvestEstimateCall);
881
+ logger.verbose(`${EkuboCLVault.name}: harvest => _callsFinal: ${JSON.stringify(_callsFinal)}`);
882
+ calls.push(..._callsFinal);
883
+ }
884
+ return calls;
885
+ }
886
+
887
+ async getInvestmentFlows() {
888
+ const netYield = await this.netAPY();
889
+ const poolKey = await this.getPoolKey();
890
+
891
+ const linkedFlow: IInvestmentFlow = {
892
+ title: this.metadata.name,
893
+ subItems: [{key: "Pool", value: `${(EkuboCLVault.div2Power128(BigInt(poolKey.fee)) * 100).toFixed(2)}%, ${poolKey.tick_spacing} tick spacing`}],
894
+ linkedFlows: [],
895
+ style: {backgroundColor: FlowChartColors.Blue.valueOf()},
896
+ }
897
+
898
+ const baseFlow: IInvestmentFlow = {
899
+ id: 'base',
900
+ title: "Your Deposit",
901
+ subItems: [{key: `Net yield`, value: `${(netYield * 100).toFixed(2)}%`}, {key: `Performance Fee`, value: `${(this.metadata.additionalInfo.feeBps / 100).toFixed(2)}%`}],
902
+ linkedFlows: [linkedFlow],
903
+ style: {backgroundColor: FlowChartColors.Purple.valueOf()},
904
+ };
905
+
906
+ const rebalanceFlow: IInvestmentFlow = {
907
+ id: 'rebalance',
908
+ title: "Automated Rebalance",
909
+ subItems: [{
910
+ key: 'Range selection',
911
+ value: `${this.metadata.additionalInfo.newBounds.lower * Number(poolKey.tick_spacing)} to ${this.metadata.additionalInfo.newBounds.upper * Number(poolKey.tick_spacing)} ticks`
912
+ }],
913
+ linkedFlows: [linkedFlow],
914
+ style: {backgroundColor: FlowChartColors.Green.valueOf()},
915
+ }
916
+
917
+ return [baseFlow, rebalanceFlow];
918
+ }
503
919
  }
504
920
 
505
921
 
506
- const _description = 'Automatically rebalances liquidity near current price to maximize yield while reducing the necessity to manually rebalance positions frequently. Fees earn and Defi spring rewards are automatically re-invested.'
922
+ const _description = 'Deploys your {{POOL_NAME}} into an Ekubo liquidity pool, automatically rebalancing positions around the current price to optimize yield and reduce the need for manual adjustments. Trading fees and DeFi Spring rewards are automatically compounded back into the strategy. In return, you receive an ERC-20 token representing your share of the strategy. The APY is calculated based on 7-day historical performance.'
507
923
  const _protocol: IProtocol = {name: 'Ekubo', logo: 'https://app.ekubo.org/favicon.ico'}
508
924
  // need to fine tune better
509
925
  const _riskFactor: RiskFactor[] = [
510
926
  {type: RiskType.SMART_CONTRACT_RISK, value: 0.5, weight: 25},
511
927
  {type: RiskType.IMPERMANENT_LOSS, value: 1, weight: 75}
512
928
  ]
929
+ const AUDIT_URL = 'https://assets.strkfarm.com/strkfarm/audit_report_vesu_and_ekubo_strats.pdf';
930
+
513
931
  /**
514
932
  * Represents the Vesu Rebalance Strategies.
515
933
  */
516
934
  export const EkuboCLVaultStrategies: IStrategyMetadata<CLVaultStrategySettings>[] = [{
517
935
  name: 'Ekubo xSTRK/STRK',
518
- description: _description,
936
+ description: _description.replace('{{POOL_NAME}}', 'xSTRK/STRK'),
519
937
  address: ContractAddr.from('0x01f083b98674bc21effee29ef443a00c7b9a500fd92cf30341a3da12c73f2324'),
520
938
  type: 'Other',
521
- depositTokens: [Global.getDefaultTokens().find(t => t.symbol === 'STRK')!, Global.getDefaultTokens().find(t => t.symbol === 'xSTRK')!],
939
+ // must be same order as poolKey token0 and token1
940
+ depositTokens: [Global.getDefaultTokens().find(t => t.symbol === 'xSTRK')!, Global.getDefaultTokens().find(t => t.symbol === 'STRK')!],
522
941
  protocols: [_protocol],
942
+ auditUrl: AUDIT_URL,
523
943
  maxTVL: Web3Number.fromWei('0', 18),
524
944
  risk: {
525
945
  riskFactor: _riskFactor,
526
946
  netRisk: _riskFactor.reduce((acc, curr) => acc + curr.value * curr.weight, 0) / _riskFactor.reduce((acc, curr) => acc + curr.weight, 0),
527
947
  notARisks: getNoRiskTags(_riskFactor)
528
948
  },
949
+ apyMethodology: 'APY based on 7-day historical performance, including fees and rewards.',
529
950
  additionalInfo: {
530
951
  newBounds: {
531
952
  lower: -1,
532
953
  upper: 1
533
954
  },
534
- lstContract: ContractAddr.from('0x028d709c875c0ceac3dce7065bec5328186dc89fe254527084d1689910954b0a')
955
+ lstContract: ContractAddr.from('0x028d709c875c0ceac3dce7065bec5328186dc89fe254527084d1689910954b0a'),
956
+ feeBps: 1000
535
957
  }
536
958
  }]