@strkfarm/sdk 1.0.37 → 1.0.38

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,12 +1,29 @@
1
1
  import { ContractAddr, Web3Number } from "@/dataTypes";
2
- import { FlowChartColors, getNoRiskTags, IConfig, IInvestmentFlow, IProtocol, IStrategyMetadata, RiskFactor, RiskType } from "@/interfaces";
2
+ import {
3
+ FAQ,
4
+ FlowChartColors,
5
+ getNoRiskTags,
6
+ IConfig,
7
+ IInvestmentFlow,
8
+ IProtocol,
9
+ IStrategyMetadata,
10
+ RiskFactor,
11
+ RiskType,
12
+ } from "@/interfaces";
3
13
  import { PricerBase } from "@/modules/pricerBase";
4
14
  import { assert } from "@/utils";
5
- import { Account, BlockIdentifier, Call, Contract, num, uint256 } from "starknet";
6
- import CLVaultAbi from '@/data/cl-vault.abi.json';
7
- import EkuboPositionsAbi from '@/data/ekubo-positions.abi.json';
8
- import EkuboMathAbi from '@/data/ekubo-math.abi.json';
9
- import ERC4626Abi from '@/data/erc4626.abi.json';
15
+ import {
16
+ Account,
17
+ BlockIdentifier,
18
+ Call,
19
+ Contract,
20
+ num,
21
+ uint256,
22
+ } from "starknet";
23
+ import CLVaultAbi from "@/data/cl-vault.abi.json";
24
+ import EkuboPositionsAbi from "@/data/ekubo-positions.abi.json";
25
+ import EkuboMathAbi from "@/data/ekubo-math.abi.json";
26
+ import ERC4626Abi from "@/data/erc4626.abi.json";
10
27
  import { Global, logger } from "@/global";
11
28
  import { AvnuWrapper, ERC20, SwapInfo } from "@/modules";
12
29
  import { BaseStrategy } from "./base-strategy";
@@ -16,972 +33,1466 @@ import { log } from "winston";
16
33
  import { EkuboHarvests } from "@/modules/harvests";
17
34
 
18
35
  export interface EkuboPoolKey {
19
- token0: ContractAddr,
20
- token1: ContractAddr,
21
- fee: string,
22
- tick_spacing: string,
23
- extension: string
36
+ token0: ContractAddr;
37
+ token1: ContractAddr;
38
+ fee: string;
39
+ tick_spacing: string;
40
+ extension: string;
24
41
  }
25
42
 
26
43
  export interface EkuboBounds {
27
- lowerTick: bigint,
28
- upperTick: bigint
44
+ lowerTick: bigint;
45
+ upperTick: bigint;
29
46
  }
30
47
 
31
48
  /**
32
49
  * Settings for the CLVaultStrategy
33
- *
50
+ *
34
51
  * @property newBounds - The new bounds for the strategy
35
52
  * @property newBounds.lower - relative to the current tick
36
53
  * @property newBounds.upper - relative to the current tick
37
54
  */
38
55
  export interface CLVaultStrategySettings {
39
- newBounds: {
40
- lower: number,
41
- upper: number
42
- },
43
- // to get true price
44
- lstContract: ContractAddr,
45
- feeBps: number
56
+ newBounds: {
57
+ lower: number;
58
+ upper: number;
59
+ };
60
+ // to get true price
61
+ lstContract: ContractAddr;
62
+ feeBps: number;
46
63
  }
47
64
 
48
- export class EkuboCLVault extends BaseStrategy<DualTokenInfo, DualActionAmount> {
49
- /** Contract address of the strategy */
50
- readonly address: ContractAddr;
51
- /** Pricer instance for token price calculations */
52
- readonly pricer: PricerBase;
53
- /** Metadata containing strategy information */
54
- readonly metadata: IStrategyMetadata<CLVaultStrategySettings>
55
- /** Contract instance for interacting with the strategy */
56
- readonly contract: Contract;
57
- readonly BASE_WEIGHT = 10000; // 10000 bps = 100%
58
-
59
- readonly ekuboPositionsContract: Contract;
60
- readonly ekuboMathContract: Contract;
61
- readonly lstContract: Contract;
62
- poolKey: EkuboPoolKey | undefined;
63
- readonly avnu: AvnuWrapper;
64
-
65
- /**
66
- * Creates a new VesuRebalance strategy instance.
67
- * @param config - Configuration object containing provider and other settings
68
- * @param pricer - Pricer instance for token price calculations
69
- * @param metadata - Strategy metadata including deposit tokens and address
70
- * @throws {Error} If more than one deposit token is specified
71
- */
72
- constructor(config: IConfig, pricer: PricerBase, metadata: IStrategyMetadata<CLVaultStrategySettings>) {
73
- super(config);
74
- this.pricer = pricer;
75
-
76
- assert(metadata.depositTokens.length === 2, 'EkuboCL only supports 2 deposit token');
77
- this.metadata = metadata;
78
- this.address = metadata.address;
79
-
80
- this.contract = new Contract(CLVaultAbi, this.address.address, this.config.provider);
81
- this.lstContract = new Contract(ERC4626Abi, this.metadata.additionalInfo.lstContract.address, this.config.provider);
82
-
83
- // ekubo positions contract
84
- const EKUBO_POSITION = '0x02e0af29598b407c8716b17f6d2795eca1b471413fa03fb145a5e33722184067'
85
- this.ekuboPositionsContract = new Contract(EkuboPositionsAbi, EKUBO_POSITION, this.config.provider);
86
- const EKUBO_MATH = '0x04a72e9e166f6c0e9d800af4dc40f6b6fb4404b735d3f528d9250808b2481995';
87
- this.ekuboMathContract = new Contract(EkuboMathAbi, EKUBO_MATH, this.config.provider);
88
-
89
- this.avnu = new AvnuWrapper();
90
- }
91
-
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];
145
- }
146
-
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
- ])];
162
- }
163
-
164
- rebalanceCall(newBounds: EkuboBounds, swapParams: SwapInfo): Call[] {
165
- return [this.contract.populate('rebalance', [
166
- {
167
- lower: EkuboCLVault.tickToi129(Number(newBounds.lowerTick)),
168
- upper: EkuboCLVault.tickToi129(Number(newBounds.upperTick))
169
- },
170
- swapParams
171
- ])]
172
- }
173
-
174
- handleUnusedCall(swapParams: SwapInfo): Call[] {
175
- return [this.contract.populate('handle_unused', [
176
- swapParams
177
- ])]
178
- }
179
-
180
- handleFeesCall(): Call[] {
181
- return [this.contract.populate('handle_fees', [])]
182
- }
183
-
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)
220
- }
221
-
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
- }
65
+ export class EkuboCLVault extends BaseStrategy<
66
+ DualTokenInfo,
67
+ DualActionAmount
68
+ > {
69
+ /** Contract address of the strategy */
70
+ readonly address: ContractAddr;
71
+ /** Pricer instance for token price calculations */
72
+ readonly pricer: PricerBase;
73
+ /** Metadata containing strategy information */
74
+ readonly metadata: IStrategyMetadata<CLVaultStrategySettings>;
75
+ /** Contract instance for interacting with the strategy */
76
+ readonly contract: Contract;
77
+ readonly BASE_WEIGHT = 10000; // 10000 bps = 100%
78
+
79
+ readonly ekuboPositionsContract: Contract;
80
+ readonly ekuboMathContract: Contract;
81
+ readonly lstContract: Contract;
82
+ poolKey: EkuboPoolKey | undefined;
83
+ readonly avnu: AvnuWrapper;
84
+
85
+ /**
86
+ * Creates a new VesuRebalance strategy instance.
87
+ * @param config - Configuration object containing provider and other settings
88
+ * @param pricer - Pricer instance for token price calculations
89
+ * @param metadata - Strategy metadata including deposit tokens and address
90
+ * @throws {Error} If more than one deposit token is specified
91
+ */
92
+ constructor(
93
+ config: IConfig,
94
+ pricer: PricerBase,
95
+ metadata: IStrategyMetadata<CLVaultStrategySettings>
96
+ ) {
97
+ super(config);
98
+ this.pricer = pricer;
99
+
100
+ assert(
101
+ metadata.depositTokens.length === 2,
102
+ "EkuboCL only supports 2 deposit token"
103
+ );
104
+ this.metadata = metadata;
105
+ this.address = metadata.address;
106
+
107
+ this.contract = new Contract(
108
+ CLVaultAbi,
109
+ this.address.address,
110
+ this.config.provider
111
+ );
112
+ this.lstContract = new Contract(
113
+ ERC4626Abi,
114
+ this.metadata.additionalInfo.lstContract.address,
115
+ this.config.provider
116
+ );
117
+
118
+ // ekubo positions contract
119
+ const EKUBO_POSITION =
120
+ "0x02e0af29598b407c8716b17f6d2795eca1b471413fa03fb145a5e33722184067";
121
+ this.ekuboPositionsContract = new Contract(
122
+ EkuboPositionsAbi,
123
+ EKUBO_POSITION,
124
+ this.config.provider
125
+ );
126
+ const EKUBO_MATH =
127
+ "0x04a72e9e166f6c0e9d800af4dc40f6b6fb4404b735d3f528d9250808b2481995";
128
+ this.ekuboMathContract = new Contract(
129
+ EkuboMathAbi,
130
+ EKUBO_MATH,
131
+ this.config.provider
132
+ );
133
+
134
+ this.avnu = new AvnuWrapper();
135
+ }
136
+
137
+ async matchInputAmounts(
138
+ amountInfo: DualActionAmount
139
+ ): Promise<DualActionAmount> {
140
+ const bounds = await this.getCurrentBounds();
141
+ const res = await this._getExpectedAmountsForLiquidity(
142
+ amountInfo.token0.amount,
143
+ amountInfo.token1.amount,
144
+ bounds,
145
+ false
146
+ );
147
+ return {
148
+ token0: {
149
+ tokenInfo: amountInfo.token0.tokenInfo,
150
+ amount: res.amount0,
151
+ },
152
+ token1: {
153
+ tokenInfo: amountInfo.token1.tokenInfo,
154
+ amount: res.amount1,
155
+ },
156
+ };
157
+ }
158
+
159
+ /** Returns minimum amounts give given two amounts based on what can be added for liq */
160
+ async getMinDepositAmounts(
161
+ amountInfo: DualActionAmount
162
+ ): Promise<DualActionAmount> {
163
+ const shares = await this.tokensToShares(amountInfo);
164
+
165
+ // get actual amounts now
166
+ const { amount0, amount1 }: any = await this.contract.call(
167
+ "convert_to_assets",
168
+ [uint256.bnToUint256(shares.toWei())]
169
+ );
170
+
171
+ // todo use user balances to compute what is required to be swapped
172
+ return {
173
+ token0: {
174
+ tokenInfo: amountInfo.token0.tokenInfo,
175
+ amount: Web3Number.fromWei(
176
+ amount0.toString(),
177
+ amountInfo.token0.tokenInfo.decimals
178
+ ),
179
+ },
180
+ token1: {
181
+ tokenInfo: amountInfo.token1.tokenInfo,
182
+ amount: Web3Number.fromWei(
183
+ amount1.toString(),
184
+ amountInfo.token1.tokenInfo.decimals
185
+ ),
186
+ },
187
+ };
188
+ }
189
+
190
+ async depositCall(
191
+ amountInfo: DualActionAmount,
192
+ receiver: ContractAddr
193
+ ): Promise<Call[]> {
194
+ const updateAmountInfo = await this.getMinDepositAmounts(amountInfo);
195
+ // Technically its not erc4626 abi, but we just need approve call
196
+ // so, its ok to use it
197
+ const token0Contract = new Contract(
198
+ ERC4626Abi,
199
+ amountInfo.token0.tokenInfo.address.address,
200
+ this.config.provider
201
+ );
202
+ const token1Contract = new Contract(
203
+ ERC4626Abi,
204
+ amountInfo.token1.tokenInfo.address.address,
205
+ this.config.provider
206
+ );
207
+ const call1 = token0Contract.populate("approve", [
208
+ this.address.address,
209
+ uint256.bnToUint256(updateAmountInfo.token0.amount.toWei()),
210
+ ]);
211
+ const call2 = token1Contract.populate("approve", [
212
+ this.address.address,
213
+ uint256.bnToUint256(updateAmountInfo.token1.amount.toWei()),
214
+ ]);
215
+ const call3 = this.contract.populate("deposit", [
216
+ uint256.bnToUint256(updateAmountInfo.token0.amount.toWei()),
217
+ uint256.bnToUint256(updateAmountInfo.token1.amount.toWei()),
218
+ receiver.address,
219
+ ]);
220
+ const calls: Call[] = [];
221
+ if (updateAmountInfo.token0.amount.greaterThan(0)) calls.push(call1);
222
+ if (updateAmountInfo.token1.amount.greaterThan(0)) calls.push(call2);
223
+ return [...calls, call3];
224
+ }
225
+
226
+ async tokensToShares(amountInfo: DualActionAmount) {
227
+ const shares = await this.contract.call("convert_to_shares", [
228
+ uint256.bnToUint256(amountInfo.token0.amount.toWei()),
229
+ uint256.bnToUint256(amountInfo.token1.amount.toWei()),
230
+ ]);
231
+ return Web3Number.fromWei(shares.toString(), 18);
232
+ }
233
+
234
+ async withdrawCall(
235
+ amountInfo: DualActionAmount,
236
+ receiver: ContractAddr,
237
+ owner: ContractAddr
238
+ ): Promise<Call[]> {
239
+ const shares = await this.tokensToShares(amountInfo);
240
+ logger.verbose(
241
+ `${EkuboCLVault.name}: withdrawCall: shares=${shares.toString()}`
242
+ );
243
+ return [
244
+ this.contract.populate("withdraw", [
245
+ uint256.bnToUint256(shares.toWei()),
246
+ receiver.address,
247
+ ]),
248
+ ];
249
+ }
250
+
251
+ rebalanceCall(newBounds: EkuboBounds, swapParams: SwapInfo): Call[] {
252
+ return [
253
+ this.contract.populate("rebalance", [
254
+ {
255
+ lower: EkuboCLVault.tickToi129(Number(newBounds.lowerTick)),
256
+ upper: EkuboCLVault.tickToi129(Number(newBounds.upperTick)),
257
+ },
258
+ swapParams,
259
+ ]),
260
+ ];
261
+ }
262
+
263
+ handleUnusedCall(swapParams: SwapInfo): Call[] {
264
+ return [this.contract.populate("handle_unused", [swapParams])];
265
+ }
266
+
267
+ handleFeesCall(): Call[] {
268
+ return [this.contract.populate("handle_fees", [])];
269
+ }
270
+
271
+ /**
272
+ * Calculates assets before and now in a given token of TVL per share to observe growth
273
+ * @returns {Promise<number>} The weighted average APY across all pools
274
+ */
275
+ async netAPY(
276
+ blockIdentifier: BlockIdentifier = "pending",
277
+ sinceBlocks = 20000
278
+ ): Promise<number> {
279
+ // no special provisions required to account for defi spring rewards
280
+ // or strategy fees, bcz this returns realisitic apy based on 7day performance
281
+
282
+ const tvlNow = await this._getTVL(blockIdentifier);
283
+ const supplyNow = await this.totalSupply(blockIdentifier);
284
+ const priceNow = await this.getCurrentPrice(blockIdentifier);
285
+ let blockNow =
286
+ typeof blockIdentifier == "number"
287
+ ? blockIdentifier
288
+ : (await this.config.provider.getBlockLatestAccepted()).block_number;
289
+ const blockNowTime =
290
+ typeof blockIdentifier == "number"
291
+ ? (await this.config.provider.getBlockWithTxs(blockIdentifier))
292
+ .timestamp
293
+ : new Date().getTime() / 1000;
294
+ const blockBefore = blockNow - sinceBlocks;
295
+ const adjustedSupplyNow = supplyNow.minus(
296
+ await this.getHarvestRewardShares(blockBefore, blockNow)
297
+ );
298
+ let blockBeforeInfo = await this.config.provider.getBlockWithTxs(
299
+ blockBefore
300
+ );
301
+ const tvlBefore = await this._getTVL(blockBefore);
302
+ const supplyBefore = await this.totalSupply(blockBefore);
303
+ const priceBefore = await this.getCurrentPrice(blockBefore);
304
+
305
+ const tvlInToken0Now = tvlNow.amount0
306
+ .multipliedBy(priceNow.price)
307
+ .plus(tvlNow.amount1);
308
+ const tvlPerShareNow = tvlInToken0Now
309
+ .multipliedBy(1e18)
310
+ .dividedBy(adjustedSupplyNow);
311
+ const tvlInToken0Bf = tvlBefore.amount0
312
+ .multipliedBy(priceBefore.price)
313
+ .plus(tvlBefore.amount1);
314
+ const tvlPerShareBf = tvlInToken0Bf
315
+ .multipliedBy(1e18)
316
+ .dividedBy(supplyBefore);
317
+ const timeDiffSeconds = blockNowTime - blockBeforeInfo.timestamp;
318
+ logger.verbose(`tvlInToken0Now: ${tvlInToken0Now.toString()}`);
319
+ logger.verbose(`tvlInToken0Bf: ${tvlInToken0Bf.toString()}`);
320
+ logger.verbose(`tvlPerShareNow: ${tvlPerShareNow.toString()}`);
321
+ logger.verbose(`tvlPerShareBf: ${tvlPerShareBf.toString()}`);
322
+ logger.verbose(`Price before: ${priceBefore.price.toString()}`);
323
+ logger.verbose(`Price now: ${priceNow.price.toString()}`);
324
+ logger.verbose(`Supply before: ${supplyBefore.toString()}`);
325
+ logger.verbose(`Supply now: ${adjustedSupplyNow.toString()}`);
326
+ logger.verbose(`Time diff in seconds: ${timeDiffSeconds}`);
327
+ const apyForGivenBlocks =
328
+ Number(
329
+ tvlPerShareNow
330
+ .minus(tvlPerShareBf)
331
+ .multipliedBy(10000)
332
+ .dividedBy(tvlPerShareBf)
333
+ ) / 10000;
334
+ return (apyForGivenBlocks * (365 * 24 * 3600)) / timeDiffSeconds;
335
+ }
336
+
337
+ async getHarvestRewardShares(fromBlock: number, toBlock: number) {
338
+ const len = Number(await this.contract.call("get_total_rewards"));
339
+ let shares = Web3Number.fromWei(0, 18);
340
+ for (let i = len - 1; i > 0; --i) {
341
+ let record: any = await this.contract.call("get_rewards_info", [i]);
342
+ logger.verbose(`${EkuboCLVault.name}: getHarvestRewardShares: ${i}`);
343
+ console.log(record);
344
+ const block = Number(record.block_number);
345
+ if (block < fromBlock) {
239
346
  return shares;
347
+ } else if (block > toBlock) {
348
+ continue;
349
+ } else {
350
+ shares = shares.plus(Web3Number.fromWei(record.shares.toString(), 18));
351
+ }
352
+ logger.verbose(
353
+ `${
354
+ EkuboCLVault.name
355
+ }: getHarvestRewardShares: ${i} => ${shares.toWei()}`
356
+ );
240
357
  }
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);
358
+ return shares;
359
+ }
360
+
361
+ async balanceOf(
362
+ user: ContractAddr,
363
+ blockIdentifier: BlockIdentifier = "pending"
364
+ ): Promise<Web3Number> {
365
+ let bal = await this.contract.call("balance_of", [user.address]);
366
+ return Web3Number.fromWei(bal.toString(), 18);
367
+ }
368
+
369
+ async getUserTVL(
370
+ user: ContractAddr,
371
+ blockIdentifier: BlockIdentifier = "pending"
372
+ ): Promise<DualTokenInfo> {
373
+ let bal = await this.balanceOf(user, blockIdentifier);
374
+ const assets: any = await this.contract.call(
375
+ "convert_to_assets",
376
+ [uint256.bnToUint256(bal.toWei())],
377
+ {
378
+ blockIdentifier,
379
+ }
380
+ );
381
+ const poolKey = await this.getPoolKey(blockIdentifier);
382
+ this.assertValidDepositTokens(poolKey);
383
+ const token0Info = await Global.getTokenInfoFromAddr(poolKey.token0);
384
+ const token1Info = await Global.getTokenInfoFromAddr(poolKey.token1);
385
+ const amount0 = Web3Number.fromWei(
386
+ assets.amount0.toString(),
387
+ token0Info.decimals
388
+ );
389
+ const amount1 = Web3Number.fromWei(
390
+ assets.amount1.toString(),
391
+ token1Info.decimals
392
+ );
393
+ const P0 = await this.pricer.getPrice(token0Info.symbol);
394
+ const P1 = await this.pricer.getPrice(token1Info.symbol);
395
+ const token0Usd = Number(amount0.toFixed(13)) * P0.price;
396
+ const token1Usd = Number(amount1.toFixed(13)) * P1.price;
397
+
398
+ return {
399
+ usdValue: token0Usd + token1Usd,
400
+ token0: {
401
+ tokenInfo: token0Info,
402
+ amount: amount0,
403
+ usdValue: token0Usd,
404
+ },
405
+ token1: {
406
+ tokenInfo: token1Info,
407
+ amount: amount1,
408
+ usdValue: token1Usd,
409
+ },
410
+ };
411
+ }
412
+
413
+ async _getTVL(blockIdentifier: BlockIdentifier = "pending") {
414
+ const result = await this.contract.call("total_liquidity", [], {
415
+ blockIdentifier,
416
+ });
417
+ const bounds = await this.getCurrentBounds(blockIdentifier);
418
+ const { amount0, amount1 } = await this.getLiquidityToAmounts(
419
+ Web3Number.fromWei(result.toString(), 18),
420
+ bounds,
421
+ blockIdentifier
422
+ );
423
+
424
+ return { amount0, amount1 };
425
+ }
426
+
427
+ async totalSupply(
428
+ blockIdentifier: BlockIdentifier = "pending"
429
+ ): Promise<Web3Number> {
430
+ const res = await this.contract.call("total_supply", [], {
431
+ blockIdentifier,
432
+ });
433
+ return Web3Number.fromWei(res.toString(), 18);
434
+ }
435
+
436
+ assertValidDepositTokens(poolKey: EkuboPoolKey) {
437
+ // given this is called by UI, if wrong config is done, it will throw error;
438
+ assert(
439
+ poolKey.token0.eq(this.metadata.depositTokens[0].address),
440
+ "Expected token0 in depositTokens[0]"
441
+ );
442
+ assert(
443
+ poolKey.token1.eq(this.metadata.depositTokens[1].address),
444
+ "Expected token1 in depositTokens[1]"
445
+ );
446
+ }
447
+
448
+ async getTVL(
449
+ blockIdentifier: BlockIdentifier = "pending"
450
+ ): Promise<DualTokenInfo> {
451
+ const { amount0, amount1 } = await this._getTVL(blockIdentifier);
452
+ const poolKey = await this.getPoolKey(blockIdentifier);
453
+ this.assertValidDepositTokens(poolKey);
454
+
455
+ const token0Info = await Global.getTokenInfoFromAddr(poolKey.token0);
456
+ const token1Info = await Global.getTokenInfoFromAddr(poolKey.token1);
457
+ const P0 = await this.pricer.getPrice(token0Info.symbol);
458
+ const P1 = await this.pricer.getPrice(token1Info.symbol);
459
+ const token0Usd = Number(amount0.toFixed(13)) * P0.price;
460
+ const token1Usd = Number(amount1.toFixed(13)) * P1.price;
461
+ return {
462
+ usdValue: token0Usd + token1Usd,
463
+ token0: {
464
+ tokenInfo: token0Info,
465
+ amount: amount0,
466
+ usdValue: token0Usd,
467
+ },
468
+ token1: {
469
+ tokenInfo: token1Info,
470
+ amount: amount1,
471
+ usdValue: token1Usd,
472
+ },
473
+ };
474
+ }
475
+
476
+ async getUncollectedFees(): Promise<DualTokenInfo> {
477
+ const nftID = await this.getCurrentNFTID();
478
+ const poolKey = await this.getPoolKey();
479
+ const currentBounds = await this.getCurrentBounds();
480
+ const result: any = await this.ekuboPositionsContract.call(
481
+ "get_token_info",
482
+ [
483
+ nftID,
484
+ {
485
+ token0: poolKey.token0.address,
486
+ token1: poolKey.token1.address,
487
+ fee: poolKey.fee,
488
+ tick_spacing: poolKey.tick_spacing,
489
+ extension: poolKey.extension,
490
+ },
491
+ {
492
+ lower: EkuboCLVault.tickToi129(Number(currentBounds.lowerTick)),
493
+ upper: EkuboCLVault.tickToi129(Number(currentBounds.upperTick)),
494
+ },
495
+ ]
496
+ );
497
+ const token0Info = await Global.getTokenInfoFromAddr(poolKey.token0);
498
+ const token1Info = await Global.getTokenInfoFromAddr(poolKey.token1);
499
+ const P0 = await this.pricer.getPrice(token0Info.symbol);
500
+ const P1 = await this.pricer.getPrice(token1Info.symbol);
501
+ const token0Web3 = Web3Number.fromWei(
502
+ result.fees0.toString(),
503
+ token0Info.decimals
504
+ );
505
+ const token1Web3 = Web3Number.fromWei(
506
+ result.fees1.toString(),
507
+ token1Info.decimals
508
+ );
509
+ const token0Usd = Number(token0Web3.toFixed(13)) * P0.price;
510
+ const token1Usd = Number(token1Web3.toFixed(13)) * P1.price;
511
+ return {
512
+ usdValue: token0Usd + token1Usd,
513
+ token0: {
514
+ tokenInfo: token0Info,
515
+ amount: token0Web3,
516
+ usdValue: token0Usd,
517
+ },
518
+ token1: {
519
+ tokenInfo: token1Info,
520
+ amount: token1Web3,
521
+ usdValue: token1Usd,
522
+ },
523
+ };
524
+ }
525
+
526
+ async getCurrentNFTID(): Promise<number> {
527
+ const result: any = await this.contract.call("get_position_key", []);
528
+ return Number(result.salt.toString());
529
+ }
530
+
531
+ async truePrice() {
532
+ const result: any = await this.lstContract.call("convert_to_assets", [
533
+ uint256.bnToUint256(BigInt(1e18).toString()),
534
+ ]);
535
+ const truePrice =
536
+ Number((BigInt(result.toString()) * BigInt(1e9)) / BigInt(1e18)) / 1e9;
537
+ return truePrice;
538
+ }
539
+
540
+ async getCurrentPrice(blockIdentifier: BlockIdentifier = "pending") {
541
+ const poolKey = await this.getPoolKey(blockIdentifier);
542
+ return this._getCurrentPrice(poolKey, blockIdentifier);
543
+ }
544
+
545
+ private async _getCurrentPrice(
546
+ poolKey: EkuboPoolKey,
547
+ blockIdentifier: BlockIdentifier = "pending"
548
+ ) {
549
+ const priceInfo: any = await this.ekuboPositionsContract.call(
550
+ "get_pool_price",
551
+ [
552
+ {
553
+ token0: poolKey.token0.address,
554
+ token1: poolKey.token1.address,
555
+ fee: poolKey.fee,
556
+ tick_spacing: poolKey.tick_spacing,
557
+ extension: poolKey.extension,
558
+ },
559
+ ],
560
+ {
561
+ blockIdentifier,
562
+ }
563
+ );
564
+ const sqrtRatio = EkuboCLVault.div2Power128(
565
+ BigInt(priceInfo.sqrt_ratio.toString())
566
+ );
567
+ console.log(
568
+ `EkuboCLVault: getCurrentPrice: blockIdentifier: ${blockIdentifier}, sqrtRatio: ${sqrtRatio}, ${priceInfo.sqrt_ratio.toString()}`
569
+ );
570
+ const price = sqrtRatio * sqrtRatio;
571
+ const tick = EkuboCLVault.priceToTick(
572
+ price,
573
+ true,
574
+ Number(poolKey.tick_spacing)
575
+ );
576
+ console.log(
577
+ `EkuboCLVault: getCurrentPrice: blockIdentifier: ${blockIdentifier}, price: ${price}, tick: ${tick.mag}, ${tick.sign}`
578
+ );
579
+ return {
580
+ price,
581
+ tick: tick.mag * (tick.sign == 0 ? 1 : -1),
582
+ sqrtRatio: priceInfo.sqrt_ratio.toString(),
583
+ };
584
+ }
585
+
586
+ async getCurrentBounds(
587
+ blockIdentifier: BlockIdentifier = "pending"
588
+ ): Promise<EkuboBounds> {
589
+ const result: any = await this.contract.call("get_position_key", [], {
590
+ blockIdentifier,
591
+ });
592
+ return {
593
+ lowerTick: EkuboCLVault.i129ToNumber(result.bounds.lower),
594
+ upperTick: EkuboCLVault.i129ToNumber(result.bounds.upper),
595
+ };
596
+ }
597
+
598
+ static div2Power128(num: BigInt): number {
599
+ return (
600
+ Number((BigInt(num.toString()) * 1000000n) / BigInt(2 ** 128)) / 1000000
601
+ );
602
+ }
603
+
604
+ static priceToTick(price: number, isRoundDown: boolean, tickSpacing: number) {
605
+ const value = isRoundDown
606
+ ? Math.floor(Math.log(price) / Math.log(1.000001))
607
+ : Math.ceil(Math.log(price) / Math.log(1.000001));
608
+ const tick = Math.floor(value / tickSpacing) * tickSpacing;
609
+ return this.tickToi129(tick);
610
+ }
611
+
612
+ async getPoolKey(
613
+ blockIdentifier: BlockIdentifier = "pending"
614
+ ): Promise<EkuboPoolKey> {
615
+ if (this.poolKey) {
616
+ return this.poolKey;
245
617
  }
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);
254
- const token0Info = await Global.getTokenInfoFromAddr(poolKey.token0);
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);
258
- const P0 = await this.pricer.getPrice(token0Info.symbol);
259
- const P1 = await this.pricer.getPrice(token1Info.symbol);
260
- const token0Usd = Number(amount0.toFixed(13)) * P0.price;
261
- const token1Usd = Number(amount1.toFixed(13)) * P1.price;
262
-
618
+ const result: any = await this.contract.call("get_settings", [], {
619
+ blockIdentifier,
620
+ });
621
+ const poolKey: EkuboPoolKey = {
622
+ token0: ContractAddr.from(result.pool_key.token0.toString()),
623
+ token1: ContractAddr.from(result.pool_key.token1.toString()),
624
+ fee: result.pool_key.fee.toString(),
625
+ tick_spacing: result.pool_key.tick_spacing.toString(),
626
+ extension: result.pool_key.extension.toString(),
627
+ };
628
+ const token0Info = await Global.getTokenInfoFromAddr(poolKey.token0);
629
+ const token1Info = await Global.getTokenInfoFromAddr(poolKey.token1);
630
+ assert(
631
+ token0Info.decimals == token1Info.decimals,
632
+ "Tested only for equal decimals"
633
+ );
634
+ this.poolKey = poolKey;
635
+ return poolKey;
636
+ }
637
+
638
+ async getNewBounds(): Promise<EkuboBounds> {
639
+ const poolKey = await this.getPoolKey();
640
+ const currentPrice = await this._getCurrentPrice(poolKey);
641
+
642
+ const newLower =
643
+ currentPrice.tick +
644
+ Number(this.metadata.additionalInfo.newBounds.lower) *
645
+ Number(poolKey.tick_spacing);
646
+ const newUpper =
647
+ currentPrice.tick +
648
+ Number(this.metadata.additionalInfo.newBounds.upper) *
649
+ Number(poolKey.tick_spacing);
650
+
651
+ return {
652
+ lowerTick: BigInt(newLower),
653
+ upperTick: BigInt(newUpper),
654
+ };
655
+ }
656
+
657
+ /**
658
+ * Computes the expected amounts to fully utilize amount in
659
+ * to add liquidity to the pool
660
+ * @param amount0: amount of token0
661
+ * @param amount1: amount of token1
662
+ * @returns {amount0, amount1}
663
+ */
664
+ private async _getExpectedAmountsForLiquidity(
665
+ amount0: Web3Number,
666
+ amount1: Web3Number,
667
+ bounds: EkuboBounds,
668
+ justUseInputAmount = true
669
+ ) {
670
+ assert(amount0.greaterThan(0) || amount1.greaterThan(0), "Amount is 0");
671
+
672
+ // get amount ratio for 1e18 liquidity
673
+ const sampleLiq = 1e20;
674
+ const { amount0: sampleAmount0, amount1: sampleAmount1 } =
675
+ await this.getLiquidityToAmounts(
676
+ Web3Number.fromWei(sampleLiq.toString(), 18),
677
+ bounds
678
+ );
679
+ logger.verbose(
680
+ `${
681
+ EkuboCLVault.name
682
+ }: _getExpectedAmountsForLiquidity => sampleAmount0: ${sampleAmount0.toString()}, sampleAmount1: ${sampleAmount1.toString()}`
683
+ );
684
+
685
+ assert(!sampleAmount0.eq(0) || !sampleAmount1.eq(0), "Sample amount is 0");
686
+
687
+ // notation: P = P0 / P1
688
+ const price = await (await this.getCurrentPrice()).price;
689
+ logger.verbose(
690
+ `${EkuboCLVault.name}: _getExpectedAmountsForLiquidity => price: ${price}`
691
+ );
692
+ // Account for edge cases
693
+ // i.e. when liquidity is out of range
694
+ if (amount1.eq(0) && amount0.greaterThan(0)) {
695
+ if (sampleAmount1.eq(0)) {
263
696
  return {
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;
697
+ amount0: amount0,
698
+ amount1: Web3Number.fromWei("0", amount1.decimals),
699
+ ratio: Infinity,
700
+ };
701
+ } else if (sampleAmount0.eq(0)) {
702
+ // swap all to token1
312
703
  return {
313
- usdValue: token0Usd + token1Usd,
314
- token0: {
315
- tokenInfo: token0Info,
316
- amount: amount0,
317
- usdValue: token0Usd
318
- },
319
- token1: {
320
- tokenInfo: token1Info,
321
- amount: amount1,
322
- usdValue: token1Usd
323
- }
324
- }
325
- }
326
-
327
- async getUncollectedFees(): Promise<DualTokenInfo> {
328
- const nftID = await this.getCurrentNFTID();
329
- const poolKey = await this.getPoolKey();
330
- const currentBounds = await this.getCurrentBounds();
331
- const result: any = await this.ekuboPositionsContract.call('get_token_info', [
332
- nftID,
333
- {
334
- token0: poolKey.token0.address,
335
- token1: poolKey.token1.address,
336
- fee: poolKey.fee,
337
- tick_spacing: poolKey.tick_spacing,
338
- extension: poolKey.extension
339
- },
340
- {
341
- lower: EkuboCLVault.tickToi129(Number(currentBounds.lowerTick)),
342
- upper: EkuboCLVault.tickToi129(Number(currentBounds.upperTick))
343
- }
344
- ]);
345
- const token0Info = await Global.getTokenInfoFromAddr(poolKey.token0);
346
- const token1Info = await Global.getTokenInfoFromAddr(poolKey.token1);
347
- const P0 = await this.pricer.getPrice(token0Info.symbol);
348
- const P1 = await this.pricer.getPrice(token1Info.symbol);
349
- const token0Web3 = Web3Number.fromWei(result.fees0.toString(), token0Info.decimals);
350
- const token1Web3 = Web3Number.fromWei(result.fees1.toString(), token1Info.decimals);
351
- const token0Usd = Number(token0Web3.toFixed(13)) * P0.price;
352
- const token1Usd = Number(token1Web3.toFixed(13)) * P1.price;
704
+ amount0: Web3Number.fromWei("0", amount0.decimals),
705
+ amount1: amount0.multipliedBy(price),
706
+ ratio: 0,
707
+ };
708
+ }
709
+ } else if (amount0.eq(0) && amount1.greaterThan(0)) {
710
+ if (sampleAmount0.eq(0)) {
353
711
  return {
354
- usdValue: token0Usd + token1Usd,
355
- token0: {
356
- tokenInfo: token0Info,
357
- amount: token0Web3,
358
- usdValue: token0Usd
359
- },
360
- token1: {
361
- tokenInfo: token1Info,
362
- amount: token1Web3,
363
- usdValue: token1Usd
364
- }
365
- }
366
- }
367
-
368
- async getCurrentNFTID(): Promise<number> {
369
- const result: any = await this.contract.call('get_position_key', []);
370
- return Number(result.salt.toString());
371
- }
372
-
373
- async truePrice() {
374
- const result: any = await this.lstContract.call('convert_to_assets', [uint256.bnToUint256(BigInt(1e18).toString())]);
375
- const truePrice = Number(BigInt(result.toString()) * BigInt(1e9)/ BigInt(1e18)) / 1e9;
376
- return truePrice;
377
- }
378
-
379
- async getCurrentPrice(blockIdentifier: BlockIdentifier = 'pending') {
380
- const poolKey = await this.getPoolKey(blockIdentifier);
381
- return this._getCurrentPrice(poolKey, blockIdentifier);
382
- }
383
-
384
- private async _getCurrentPrice(poolKey: EkuboPoolKey, blockIdentifier: BlockIdentifier = 'pending') {
385
- const priceInfo: any = await this.ekuboPositionsContract.call('get_pool_price', [
386
- {
387
- token0: poolKey.token0.address,
388
- token1: poolKey.token1.address,
389
- fee: poolKey.fee,
390
- tick_spacing: poolKey.tick_spacing,
391
- extension: poolKey.extension
392
- }
393
- ], {
394
- blockIdentifier
395
- })
396
- const sqrtRatio = EkuboCLVault.div2Power128(BigInt(priceInfo.sqrt_ratio.toString()));
397
- console.log(`EkuboCLVault: getCurrentPrice: blockIdentifier: ${blockIdentifier}, sqrtRatio: ${sqrtRatio}, ${priceInfo.sqrt_ratio.toString()}`);
398
- const price = sqrtRatio * sqrtRatio;
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}`);
712
+ amount0: Web3Number.fromWei("0", amount0.decimals),
713
+ amount1: amount1,
714
+ ratio: 0,
715
+ };
716
+ } else if (sampleAmount1.eq(0)) {
717
+ // swap all to token0
401
718
  return {
402
- price,
403
- tick: tick.mag * (tick.sign == 0 ? 1 : -1),
404
- sqrtRatio: priceInfo.sqrt_ratio.toString()
405
- }
719
+ amount0: amount1.dividedBy(price),
720
+ amount1: Web3Number.fromWei("0", amount1.decimals),
721
+ ratio: Infinity,
722
+ };
723
+ }
406
724
  }
407
725
 
408
- async getCurrentBounds(blockIdentifier: BlockIdentifier = 'pending'): Promise<EkuboBounds> {
409
- const result: any = await this.contract.call('get_position_key', [], {
410
- blockIdentifier
411
- })
412
- return {
413
- lowerTick: EkuboCLVault.i129ToNumber(result.bounds.lower),
414
- upperTick: EkuboCLVault.i129ToNumber(result.bounds.upper)
415
- }
726
+ // must make it general later
727
+ assert(
728
+ sampleAmount0.decimals == sampleAmount1.decimals,
729
+ "Sample amounts have different decimals"
730
+ );
731
+ const ratioWeb3Number = sampleAmount0
732
+ .multipliedBy(1e18)
733
+ .dividedBy(sampleAmount1.toString())
734
+ .dividedBy(1e18);
735
+ const ratio: number = Number(ratioWeb3Number.toFixed(18));
736
+ logger.verbose(
737
+ `${EkuboCLVault.name}: ${
738
+ this.metadata.name
739
+ } => ratio: ${ratio.toString()}`
740
+ );
741
+
742
+ if (justUseInputAmount)
743
+ return this._solveExpectedAmountsEq(
744
+ amount0,
745
+ amount1,
746
+ ratioWeb3Number,
747
+ price
748
+ );
749
+
750
+ // we are at liberty to propose amounts outside the propsed amount
751
+ // assuming amount0 and amount1 as independent values, compute other amounts
752
+ // Also, if code came till here, it means both sample amounts are non-zero
753
+ if (amount1.eq(0) && amount0.greaterThan(0)) {
754
+ // use amount0 as base and compute amount1 using ratio
755
+ const _amount1 = amount0.dividedBy(ratioWeb3Number);
756
+ return {
757
+ amount0: amount0,
758
+ amount1: _amount1,
759
+ ratio,
760
+ };
761
+ } else if (amount0.eq(0) && amount1.greaterThan(0)) {
762
+ // use amount1 as base and compute amount0 using ratio
763
+ const _amount0 = amount1.multipliedBy(ratio);
764
+ return {
765
+ amount0: _amount0,
766
+ amount1: amount1,
767
+ ratio,
768
+ };
769
+ } else {
770
+ // ambiguous case
771
+ // can lead to diverging results
772
+ throw new Error(
773
+ "Both amounts are non-zero, cannot compute expected amounts"
774
+ );
416
775
  }
417
-
418
- static div2Power128(num: BigInt): number {
419
- return (Number(((BigInt(num.toString()) * 1000000n) / BigInt(2 ** 128))) / 1000000)
776
+ }
777
+
778
+ private _solveExpectedAmountsEq(
779
+ availableAmount0: Web3Number,
780
+ availableAmount1: Web3Number,
781
+ ratio: Web3Number,
782
+ price: number
783
+ ) {
784
+ // (amount0 + x) / (amount1 - y) = ratio
785
+ // x = y * Py / Px ---- (1)
786
+ // => (amount0 + y * Py / Px) / (amount1 - y) = ratio
787
+ // => amount0 + y * Py / Px = ratio * (amount1 - y)
788
+ // => amount0 + y * Py / Px = ratio * amount1 - ratio * y
789
+ // => y * (ratio + Py/Px) = ratio * amount1 - amount0
790
+ // => y = (ratio * amount1 - amount0) / (ratio + Py/Px) ---- (2)
791
+ const y = ratio
792
+ .multipliedBy(availableAmount1)
793
+ .minus(availableAmount0)
794
+ .dividedBy(ratio.plus(1 / price));
795
+ const x = y.dividedBy(price);
796
+ return {
797
+ amount0: availableAmount0.plus(x),
798
+ amount1: availableAmount1.minus(y),
799
+ ratio: Number(ratio.toString()),
800
+ };
801
+ }
802
+
803
+ async getSwapInfoToHandleUnused(considerRebalance: boolean = true) {
804
+ const poolKey = await this.getPoolKey();
805
+
806
+ // fetch current unused balances of vault
807
+ const erc20Mod = new ERC20(this.config);
808
+ const token0Info = await Global.getTokenInfoFromAddr(poolKey.token0);
809
+ const token1Info = await Global.getTokenInfoFromAddr(poolKey.token1);
810
+ const token0Bal1 = await erc20Mod.balanceOf(
811
+ poolKey.token0,
812
+ this.address.address,
813
+ token0Info.decimals
814
+ );
815
+ const token1Bal1 = await erc20Mod.balanceOf(
816
+ poolKey.token1,
817
+ this.address.address,
818
+ token1Info.decimals
819
+ );
820
+ logger.verbose(
821
+ `${
822
+ EkuboCLVault.name
823
+ }: getSwapInfoToHandleUnused => token0Bal1: ${token0Bal1.toString()}, token1Bal1: ${token1Bal1.toString()}`
824
+ );
825
+ // if both tokens are non-zero and above $1 throw error
826
+ const token0Price = await this.pricer.getPrice(token0Info.symbol);
827
+ const token1Price = await this.pricer.getPrice(token1Info.symbol);
828
+ const token0PriceUsd = token0Price.price * Number(token0Bal1.toFixed(13));
829
+ const token1PriceUsd = token1Price.price * Number(token1Bal1.toFixed(13));
830
+ if (token0PriceUsd > 1 && token1PriceUsd > 1) {
831
+ // the swap is designed to handle one token only.
832
+ // i.e. all balance should be in one token
833
+ // except small amount of dust
834
+ // so we need to call handle_fees first, which will atleast use
835
+ // most of one token
836
+ throw new Error(
837
+ "Both tokens are non-zero and above $1, call handle_fees first"
838
+ );
420
839
  }
421
840
 
422
- static priceToTick(price: number, isRoundDown: boolean, tickSpacing: number) {
423
- const value = isRoundDown ? Math.floor(Math.log(price) / Math.log(1.000001)) : Math.ceil(Math.log(price) / Math.log(1.000001));
424
- const tick = Math.floor(value / tickSpacing) * tickSpacing;
425
- return this.tickToi129(tick);
841
+ let token0Bal = token0Bal1;
842
+ let token1Bal = token1Bal1;
843
+
844
+ // if rebalancing, consider whole TVL as available
845
+ if (considerRebalance) {
846
+ logger.verbose(
847
+ `${EkuboCLVault.name}: getSwapInfoToHandleUnused => considerRebalance: true`
848
+ );
849
+ const tvl = await this.getTVL();
850
+ token0Bal = token0Bal.plus(tvl.token0.amount.toString());
851
+ token1Bal = token1Bal.plus(tvl.token1.amount.toString());
852
+ } else {
853
+ logger.verbose(
854
+ `${EkuboCLVault.name}: getSwapInfoToHandleUnused => considerRebalance: false`
855
+ );
426
856
  }
427
-
428
- async getPoolKey(blockIdentifier: BlockIdentifier = 'pending'): Promise<EkuboPoolKey> {
429
- if (this.poolKey) {
430
- return this.poolKey;
431
- }
432
- const result: any = await this.contract.call('get_settings', [], {
433
- blockIdentifier
434
- });
435
- const poolKey: EkuboPoolKey = {
436
- token0: ContractAddr.from(result.pool_key.token0.toString()),
437
- token1: ContractAddr.from(result.pool_key.token1.toString()),
438
- fee: result.pool_key.fee.toString(),
439
- tick_spacing: result.pool_key.tick_spacing.toString(),
440
- extension: result.pool_key.extension.toString()
857
+ logger.verbose(
858
+ `${
859
+ EkuboCLVault.name
860
+ }: getSwapInfoToHandleUnused => token0Bal: ${token0Bal.toString()}, token1Bal: ${token1Bal.toString()}`
861
+ );
862
+
863
+ // get expected amounts for liquidity
864
+ const newBounds = await this.getNewBounds();
865
+ logger.verbose(
866
+ `${EkuboCLVault.name}: getSwapInfoToHandleUnused => newBounds: ${newBounds.lowerTick}, ${newBounds.upperTick}`
867
+ );
868
+
869
+ return await this.getSwapInfoGivenAmounts(
870
+ poolKey,
871
+ token0Bal,
872
+ token1Bal,
873
+ newBounds
874
+ );
875
+ }
876
+
877
+ async getSwapInfoGivenAmounts(
878
+ poolKey: EkuboPoolKey,
879
+ token0Bal: Web3Number,
880
+ token1Bal: Web3Number,
881
+ bounds: EkuboBounds
882
+ ): Promise<SwapInfo> {
883
+ let expectedAmounts = await this._getExpectedAmountsForLiquidity(
884
+ token0Bal,
885
+ token1Bal,
886
+ bounds
887
+ );
888
+ logger.verbose(
889
+ `${
890
+ EkuboCLVault.name
891
+ }: getSwapInfoToHandleUnused => expectedAmounts: ${expectedAmounts.amount0.toString()}, ${expectedAmounts.amount1.toString()}`
892
+ );
893
+
894
+ // get swap info
895
+ // fetch avnu routes to ensure expected amounts
896
+ let retry = 0;
897
+ const maxRetry = 10;
898
+ while (retry < maxRetry) {
899
+ retry++;
900
+ // assert one token is increased and other is decreased
901
+
902
+ if (
903
+ expectedAmounts.amount0.lessThan(token0Bal) &&
904
+ expectedAmounts.amount1.lessThan(token1Bal)
905
+ ) {
906
+ throw new Error("Both tokens are decreased, something is wrong");
907
+ }
908
+ if (
909
+ expectedAmounts.amount0.greaterThan(token0Bal) &&
910
+ expectedAmounts.amount1.greaterThan(token1Bal)
911
+ ) {
912
+ throw new Error("Both tokens are increased, something is wrong");
913
+ }
914
+
915
+ const tokenToSell = expectedAmounts.amount0.lessThan(token0Bal)
916
+ ? poolKey.token0
917
+ : poolKey.token1;
918
+ const tokenToBuy =
919
+ tokenToSell == poolKey.token0 ? poolKey.token1 : poolKey.token0;
920
+ let amountToSell =
921
+ tokenToSell == poolKey.token0
922
+ ? token0Bal.minus(expectedAmounts.amount0)
923
+ : token1Bal.minus(expectedAmounts.amount1);
924
+ const remainingSellAmount =
925
+ tokenToSell == poolKey.token0
926
+ ? expectedAmounts.amount0
927
+ : expectedAmounts.amount1;
928
+ const tokenToBuyInfo = await Global.getTokenInfoFromAddr(tokenToBuy);
929
+ const expectedRatio = expectedAmounts.ratio;
930
+
931
+ logger.verbose(
932
+ `${EkuboCLVault.name}: getSwapInfoToHandleUnused => tokenToSell: ${
933
+ tokenToSell.address
934
+ }, tokenToBuy: ${
935
+ tokenToBuy.address
936
+ }, amountToSell: ${amountToSell.toWei()}`
937
+ );
938
+ logger.verbose(
939
+ `${
940
+ EkuboCLVault.name
941
+ }: getSwapInfoToHandleUnused => remainingSellAmount: ${remainingSellAmount.toString()}`
942
+ );
943
+ logger.verbose(
944
+ `${EkuboCLVault.name}: getSwapInfoToHandleUnused => expectedRatio: ${expectedRatio}`
945
+ );
946
+
947
+ if (amountToSell.eq(0)) {
948
+ return {
949
+ token_from_address: tokenToSell.address,
950
+ token_from_amount: uint256.bnToUint256(0),
951
+ token_to_address: tokenToSell.address,
952
+ token_to_amount: uint256.bnToUint256(0),
953
+ token_to_min_amount: uint256.bnToUint256(0),
954
+ beneficiary: this.address.address,
955
+ integrator_fee_amount_bps: 0,
956
+ integrator_fee_recipient: this.address.address,
957
+ routes: [],
441
958
  };
442
- const token0Info = await Global.getTokenInfoFromAddr(poolKey.token0);
443
- const token1Info = await Global.getTokenInfoFromAddr(poolKey.token1);
444
- assert(token0Info.decimals == token1Info.decimals, 'Tested only for equal decimals');
445
- this.poolKey = poolKey;
446
- return poolKey;
447
- }
448
-
449
- async getNewBounds(): Promise<EkuboBounds> {
450
- const poolKey = await this.getPoolKey();
451
- const currentPrice = await this._getCurrentPrice(poolKey);
452
-
453
- const newLower = currentPrice.tick + (Number(this.metadata.additionalInfo.newBounds.lower) * Number(poolKey.tick_spacing));
454
- const newUpper = currentPrice.tick + (Number(this.metadata.additionalInfo.newBounds.upper) * Number(poolKey.tick_spacing));
959
+ }
960
+ const quote = await this.avnu.getQuotes(
961
+ tokenToSell.address,
962
+ tokenToBuy.address,
963
+ amountToSell.toWei(),
964
+ this.address.address
965
+ );
966
+ if (remainingSellAmount.eq(0)) {
967
+ const minAmountOut = Web3Number.fromWei(
968
+ quote.buyAmount.toString(),
969
+ tokenToBuyInfo.decimals
970
+ ).multipliedBy(0.9999);
971
+ return await this.avnu.getSwapInfo(
972
+ quote,
973
+ this.address.address,
974
+ 0,
975
+ this.address.address,
976
+ minAmountOut.toWei()
977
+ );
978
+ }
979
+
980
+ const amountOut = Web3Number.fromWei(
981
+ quote.buyAmount.toString(),
982
+ tokenToBuyInfo.decimals
983
+ );
984
+ const swapPrice =
985
+ tokenToSell == poolKey.token0
986
+ ? amountOut.dividedBy(amountToSell)
987
+ : amountToSell.dividedBy(amountOut);
988
+ const newRatio =
989
+ tokenToSell == poolKey.token0
990
+ ? remainingSellAmount.dividedBy(token1Bal.plus(amountOut))
991
+ : token0Bal.plus(amountOut).dividedBy(remainingSellAmount);
992
+ logger.verbose(
993
+ `${
994
+ EkuboCLVault.name
995
+ }: getSwapInfoToHandleUnused => amountOut: ${amountOut.toString()}`
996
+ );
997
+ logger.verbose(
998
+ `${
999
+ EkuboCLVault.name
1000
+ }: getSwapInfoToHandleUnused => swapPrice: ${swapPrice.toString()}`
1001
+ );
1002
+ logger.verbose(
1003
+ `${
1004
+ EkuboCLVault.name
1005
+ }: getSwapInfoToHandleUnused => newRatio: ${newRatio.toString()}`
1006
+ );
1007
+ if (
1008
+ Number(newRatio.toString()) > expectedRatio * 1.0000001 ||
1009
+ Number(newRatio.toString()) < expectedRatio * 0.9999999
1010
+ ) {
1011
+ expectedAmounts = await this._solveExpectedAmountsEq(
1012
+ token0Bal,
1013
+ token1Bal,
1014
+ new Web3Number(Number(expectedRatio).toFixed(13), 18),
1015
+ Number(swapPrice.toString())
1016
+ );
1017
+ logger.verbose(
1018
+ `${
1019
+ EkuboCLVault.name
1020
+ }: getSwapInfoToHandleUnused => expectedAmounts: ${expectedAmounts.amount0.toString()}, ${expectedAmounts.amount1.toString()}`
1021
+ );
1022
+ } else {
1023
+ const minAmountOut = Web3Number.fromWei(
1024
+ quote.buyAmount.toString(),
1025
+ tokenToBuyInfo.decimals
1026
+ ).multipliedBy(0.9999);
1027
+ return await this.avnu.getSwapInfo(
1028
+ quote,
1029
+ this.address.address,
1030
+ 0,
1031
+ this.address.address,
1032
+ minAmountOut.toWei()
1033
+ );
1034
+ }
455
1035
 
456
- return {
457
- lowerTick: BigInt(newLower),
458
- upperTick: BigInt(newUpper)
459
- }
1036
+ retry++;
460
1037
  }
461
1038
 
462
- /**
463
- * Computes the expected amounts to fully utilize amount in
464
- * to add liquidity to the pool
465
- * @param amount0: amount of token0
466
- * @param amount1: amount of token1
467
- * @returns {amount0, amount1}
468
- */
469
- private async _getExpectedAmountsForLiquidity(
470
- amount0: Web3Number,
471
- amount1: Web3Number,
472
- bounds: EkuboBounds,
473
- justUseInputAmount = true
474
- ) {
475
- assert(amount0.greaterThan(0) || amount1.greaterThan(0), 'Amount is 0');
476
-
477
- // get amount ratio for 1e18 liquidity
478
- const sampleLiq = 1e20;
479
- const {amount0: sampleAmount0, amount1: sampleAmount1} = await this.getLiquidityToAmounts(Web3Number.fromWei(sampleLiq.toString(), 18), bounds);
480
- logger.verbose(`${EkuboCLVault.name}: _getExpectedAmountsForLiquidity => sampleAmount0: ${sampleAmount0.toString()}, sampleAmount1: ${sampleAmount1.toString()}`);
481
-
482
- assert(!sampleAmount0.eq(0) || !sampleAmount1.eq(0), 'Sample amount is 0');
483
-
484
- // notation: P = P0 / P1
485
- const price = await (await this.getCurrentPrice()).price;
486
- logger.verbose(`${EkuboCLVault.name}: _getExpectedAmountsForLiquidity => price: ${price}`);
487
- // Account for edge cases
488
- // i.e. when liquidity is out of range
489
- if (amount1.eq(0) && amount0.greaterThan(0)) {
490
- if (sampleAmount1.eq(0)) {
491
- return {
492
- amount0: amount0,
493
- amount1: Web3Number.fromWei('0', amount1.decimals),
494
- ratio: Infinity
495
- }
496
- } else if (sampleAmount0.eq(0)) {
497
- // swap all to token1
498
- return {
499
- amount0: Web3Number.fromWei('0', amount0.decimals),
500
- amount1: amount0.multipliedBy(price),
501
- ratio: 0
502
- }
503
- }
504
- } else if (amount0.eq(0) && amount1.greaterThan(0)) {
505
- if (sampleAmount0.eq(0)) {
506
- return {
507
- amount0: Web3Number.fromWei('0', amount0.decimals),
508
- amount1: amount1,
509
- ratio: 0
510
- }
511
- } else if (sampleAmount1.eq(0)) {
512
- // swap all to token0
513
- return {
514
- amount0: amount1.dividedBy(price),
515
- amount1: Web3Number.fromWei('0', amount1.decimals),
516
- ratio: Infinity
517
- }
518
- }
519
- }
520
-
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));
525
- logger.verbose(`${EkuboCLVault.name}: ${this.metadata.name} => ratio: ${ratio.toString()}`);
526
-
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
- };
1039
+ throw new Error("Failed to get swap info");
1040
+ }
1041
+
1042
+ /**
1043
+ * Attempts to rebalance the vault by iteratively adjusting swap amounts if initial attempt fails.
1044
+ * Uses binary search approach to find optimal swap amount.
1045
+ *
1046
+ * @param newBounds - The new tick bounds to rebalance to
1047
+ * @param swapInfo - Initial swap parameters for rebalancing
1048
+ * @param acc - Account to estimate gas fees with
1049
+ * @param retry - Current retry attempt number (default 0)
1050
+ * @param adjustmentFactor - Percentage to adjust swap amount by (default 1)
1051
+ * @param isToken0Deficit - Whether token0 balance needs increasing (default true)
1052
+ * @returns Array of contract calls needed for rebalancing
1053
+ * @throws Error if max retries reached without successful rebalance
1054
+ */
1055
+ async rebalanceIter(
1056
+ swapInfo: SwapInfo,
1057
+ acc: Account,
1058
+ estimateCall: (swapInfo: SwapInfo) => Promise<Call[]>,
1059
+ isSellTokenToken0 = true,
1060
+ retry = 0,
1061
+ lowerLimit = 0n,
1062
+ upperLimit = 0n
1063
+ ): Promise<Call[]> {
1064
+ const MAX_RETRIES = 40;
1065
+
1066
+ logger.verbose(
1067
+ `Rebalancing ${this.metadata.name}: ` +
1068
+ `retry=${retry}, lowerLimit=${lowerLimit}, upperLimit=${upperLimit}, isSellTokenToken0=${isSellTokenToken0}`
1069
+ );
1070
+
1071
+ const fromAmount = uint256.uint256ToBN(swapInfo.token_from_amount);
1072
+ logger.verbose(
1073
+ `Selling ${fromAmount.toString()} of token ${swapInfo.token_from_address}`
1074
+ );
1075
+
1076
+ try {
1077
+ const calls = await estimateCall(swapInfo);
1078
+ await acc.estimateInvokeFee(calls);
1079
+ return calls;
1080
+ } catch (err: any) {
1081
+ if (retry >= MAX_RETRIES) {
1082
+ logger.error(`Rebalance failed after ${MAX_RETRIES} retries`);
1083
+ throw err;
1084
+ }
1085
+
1086
+ logger.error(
1087
+ `Rebalance attempt ${retry + 1} failed, adjusting swap amount...`
1088
+ );
1089
+
1090
+ const newSwapInfo = { ...swapInfo };
1091
+ const currentAmount = Web3Number.fromWei(fromAmount.toString(), 18); // 18 is ok, as its toWei eventually anyways
1092
+ logger.verbose(`Current amount: ${currentAmount.toString()}`);
1093
+ if (
1094
+ err.message.includes("invalid token0 balance") ||
1095
+ err.message.includes("invalid token0 amount")
1096
+ ) {
1097
+ if (!isSellTokenToken0) {
1098
+ logger.verbose("Reducing swap amount - excess token0");
1099
+ let nextAmount = (fromAmount + lowerLimit) / 2n;
1100
+ upperLimit = fromAmount;
1101
+ if (nextAmount <= lowerLimit) {
1102
+ logger.error("Convergence failed: nextAmount <= lowerLimit");
1103
+ throw err;
1104
+ }
1105
+ newSwapInfo.token_from_amount = uint256.bnToUint256(nextAmount);
549
1106
  } else {
550
- // ambiguous case
551
- // can lead to diverging results
552
- throw new Error('Both amounts are non-zero, cannot compute expected amounts');
553
- }
554
- }
555
-
556
- private _solveExpectedAmountsEq(availableAmount0: Web3Number, availableAmount1: Web3Number, ratio: Web3Number, price: number) {
557
- // (amount0 + x) / (amount1 - y) = ratio
558
- // x = y * Py / Px ---- (1)
559
- // => (amount0 + y * Py / Px) / (amount1 - y) = ratio
560
- // => amount0 + y * Py / Px = ratio * (amount1 - y)
561
- // => amount0 + y * Py / Px = ratio * amount1 - ratio * y
562
- // => y * (ratio + Py/Px) = ratio * amount1 - amount0
563
- // => y = (ratio * amount1 - amount0) / (ratio + Py/Px) ---- (2)
564
- const y = ((ratio.multipliedBy(availableAmount1)).minus(availableAmount0)).dividedBy(ratio.plus(1 / price));
565
- const x = y.dividedBy(price);
566
- return {
567
- amount0: availableAmount0.plus(x),
568
- amount1: availableAmount1.minus(y),
569
- ratio: Number(ratio.toString())
570
- }
571
- }
572
-
573
- async getSwapInfoToHandleUnused(considerRebalance: boolean = true) {
574
- const poolKey = await this.getPoolKey();
575
-
576
- // fetch current unused balances of vault
577
- const erc20Mod = new ERC20(this.config);
578
- const token0Info = await Global.getTokenInfoFromAddr(poolKey.token0);
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
584
- const token0Price = await this.pricer.getPrice(token0Info.symbol);
585
- const token1Price = await this.pricer.getPrice(token1Info.symbol);
586
- const token0PriceUsd = token0Price.price * Number(token0Bal1.toFixed(13));
587
- const token1PriceUsd = token1Price.price * Number(token1Bal1.toFixed(13));
588
- if (token0PriceUsd > 1 && token1PriceUsd > 1) {
589
- // the swap is designed to handle one token only.
590
- // i.e. all balance should be in one token
591
- // except small amount of dust
592
- // so we need to call handle_fees first, which will atleast use
593
- // most of one token
594
- throw new Error('Both tokens are non-zero and above $1, call handle_fees first');
1107
+ logger.verbose("Increasing swap amount - deficit token0");
1108
+ let nextAmount = (fromAmount + upperLimit) / 2n;
1109
+ if (upperLimit == 0n) {
1110
+ nextAmount = fromAmount * 2n;
1111
+ }
1112
+ lowerLimit = fromAmount;
1113
+ if (upperLimit != 0n && nextAmount >= upperLimit) {
1114
+ logger.error("Convergence failed: nextAmount >= upperLimit");
1115
+ throw err;
1116
+ }
1117
+ newSwapInfo.token_from_amount = uint256.bnToUint256(nextAmount);
595
1118
  }
596
-
597
-
598
- let token0Bal = token0Bal1;
599
- let token1Bal = token1Bal1;
600
-
601
- // if rebalancing, consider whole TVL as available
602
- if (considerRebalance) {
603
- logger.verbose(`${EkuboCLVault.name}: getSwapInfoToHandleUnused => considerRebalance: true`);
604
- const tvl = await this.getTVL();
605
- token0Bal = token0Bal.plus(tvl.token0.amount.toString());
606
- token1Bal = token1Bal.plus(tvl.token1.amount.toString());
1119
+ } else if (
1120
+ err.message.includes("invalid token1 amount") ||
1121
+ err.message.includes("invalid token1 balance")
1122
+ ) {
1123
+ if (isSellTokenToken0) {
1124
+ logger.verbose("Reducing swap amount - excess token1");
1125
+ let nextAmount = (fromAmount + lowerLimit) / 2n;
1126
+ upperLimit = fromAmount;
1127
+ if (nextAmount <= lowerLimit) {
1128
+ logger.error("Convergence failed: nextAmount <= lowerLimit");
1129
+ throw err;
1130
+ }
1131
+ newSwapInfo.token_from_amount = uint256.bnToUint256(nextAmount);
607
1132
  } else {
608
- logger.verbose(`${EkuboCLVault.name}: getSwapInfoToHandleUnused => considerRebalance: false`);
1133
+ logger.verbose("Increasing swap amount - deficit token1");
1134
+ let nextAmount = (fromAmount + upperLimit) / 2n;
1135
+ if (upperLimit == 0n) {
1136
+ nextAmount = fromAmount * 2n;
1137
+ }
1138
+ lowerLimit = fromAmount;
1139
+ if (upperLimit != 0n && nextAmount >= upperLimit) {
1140
+ logger.error("Convergence failed: nextAmount >= upperLimit");
1141
+ throw err;
1142
+ }
1143
+ newSwapInfo.token_from_amount = uint256.bnToUint256(nextAmount);
609
1144
  }
610
- logger.verbose(`${EkuboCLVault.name}: getSwapInfoToHandleUnused => token0Bal: ${token0Bal.toString()}, token1Bal: ${token1Bal.toString()}`);
611
-
612
- // get expected amounts for liquidity
613
- const newBounds = await this.getNewBounds();
614
- logger.verbose(`${EkuboCLVault.name}: getSwapInfoToHandleUnused => newBounds: ${newBounds.lowerTick}, ${newBounds.upperTick}`);
615
-
616
- return await this.getSwapInfoGivenAmounts(poolKey, token0Bal, token1Bal, newBounds)
1145
+ } else {
1146
+ logger.error("Unexpected error:", err);
1147
+ throw err;
1148
+ }
1149
+ newSwapInfo.token_to_min_amount = uint256.bnToUint256("0");
1150
+ return this.rebalanceIter(
1151
+ newSwapInfo,
1152
+ acc,
1153
+ estimateCall,
1154
+ isSellTokenToken0,
1155
+ retry + 1,
1156
+ lowerLimit,
1157
+ upperLimit
1158
+ );
617
1159
  }
618
-
619
- async getSwapInfoGivenAmounts(poolKey: EkuboPoolKey, token0Bal: Web3Number, token1Bal: Web3Number, bounds: EkuboBounds): Promise<SwapInfo> {
620
- let expectedAmounts = await this._getExpectedAmountsForLiquidity(token0Bal, token1Bal, bounds);
621
- logger.verbose(`${EkuboCLVault.name}: getSwapInfoToHandleUnused => expectedAmounts: ${expectedAmounts.amount0.toString()}, ${expectedAmounts.amount1.toString()}`);
622
-
623
- // get swap info
624
- // fetch avnu routes to ensure expected amounts
625
- let retry = 0;
626
- const maxRetry = 10;
627
- while (retry < maxRetry) {
628
- retry++;
629
- // assert one token is increased and other is decreased
630
-
631
- if (expectedAmounts.amount0.lessThan(token0Bal) && expectedAmounts.amount1.lessThan(token1Bal)) {
632
- throw new Error('Both tokens are decreased, something is wrong');
633
- }
634
- if (expectedAmounts.amount0.greaterThan(token0Bal) && expectedAmounts.amount1.greaterThan(token1Bal)) {
635
- throw new Error('Both tokens are increased, something is wrong');
636
- }
637
-
638
- const tokenToSell = expectedAmounts.amount0.lessThan(token0Bal) ? poolKey.token0 : poolKey.token1;
639
- const tokenToBuy = tokenToSell == poolKey.token0 ? poolKey.token1 : poolKey.token0;
640
- let amountToSell = tokenToSell == poolKey.token0 ? token0Bal.minus(expectedAmounts.amount0) : token1Bal.minus(expectedAmounts.amount1);
641
- const remainingSellAmount = tokenToSell == poolKey.token0 ? expectedAmounts.amount0 : expectedAmounts.amount1;
642
- const tokenToBuyInfo = await Global.getTokenInfoFromAddr(tokenToBuy);
643
- const expectedRatio = expectedAmounts.ratio;
644
-
645
- logger.verbose(`${EkuboCLVault.name}: getSwapInfoToHandleUnused => tokenToSell: ${tokenToSell.address}, tokenToBuy: ${tokenToBuy.address}, amountToSell: ${amountToSell.toWei()}`);
646
- logger.verbose(`${EkuboCLVault.name}: getSwapInfoToHandleUnused => remainingSellAmount: ${remainingSellAmount.toString()}`);
647
- logger.verbose(`${EkuboCLVault.name}: getSwapInfoToHandleUnused => expectedRatio: ${expectedRatio}`);
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
- }
662
- const quote = await this.avnu.getQuotes(tokenToSell.address, tokenToBuy.address, amountToSell.toWei(), this.address.address);
663
- if (remainingSellAmount.eq(0)) {
664
- const minAmountOut = Web3Number.fromWei(quote.buyAmount.toString(), tokenToBuyInfo.decimals).multipliedBy(0.9999);
665
- return await this.avnu.getSwapInfo(quote, this.address.address, 0, this.address.address, minAmountOut.toWei());
666
- }
667
-
668
- const amountOut = Web3Number.fromWei(quote.buyAmount.toString(), tokenToBuyInfo.decimals);
669
- const swapPrice = tokenToSell == poolKey.token0 ? amountOut.dividedBy(amountToSell) : amountToSell.dividedBy(amountOut);
670
- const newRatio = tokenToSell == poolKey.token0 ? remainingSellAmount.dividedBy(token1Bal.plus(amountOut)) : token0Bal.plus(amountOut).dividedBy(remainingSellAmount);
671
- logger.verbose(`${EkuboCLVault.name}: getSwapInfoToHandleUnused => amountOut: ${amountOut.toString()}`);
672
- logger.verbose(`${EkuboCLVault.name}: getSwapInfoToHandleUnused => swapPrice: ${swapPrice.toString()}`);
673
- logger.verbose(`${EkuboCLVault.name}: getSwapInfoToHandleUnused => newRatio: ${newRatio.toString()}`);
674
- if (Number(newRatio.toString()) > expectedRatio * 1.0000001 || Number(newRatio.toString()) < expectedRatio * 0.9999999) {
675
- expectedAmounts = await this._solveExpectedAmountsEq(token0Bal, token1Bal, new Web3Number(Number(expectedRatio).toFixed(13), 18), Number(swapPrice.toString()));
676
- logger.verbose(`${EkuboCLVault.name}: getSwapInfoToHandleUnused => expectedAmounts: ${expectedAmounts.amount0.toString()}, ${expectedAmounts.amount1.toString()}`);
677
- } else {
678
- const minAmountOut = Web3Number.fromWei(quote.buyAmount.toString(), tokenToBuyInfo.decimals).multipliedBy(0.9999);
679
- return await this.avnu.getSwapInfo(quote, this.address.address, 0, this.address.address, minAmountOut.toWei());
680
- }
681
-
682
- retry++;
683
- }
684
-
685
- throw new Error('Failed to get swap info');
1160
+ }
1161
+
1162
+ static tickToi129(tick: number) {
1163
+ if (tick < 0) {
1164
+ return {
1165
+ mag: -tick,
1166
+ sign: 1,
1167
+ };
1168
+ } else {
1169
+ return {
1170
+ mag: tick,
1171
+ sign: 0,
1172
+ };
686
1173
  }
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
- isSellTokenToken0 = true,
706
- retry = 0,
707
- lowerLimit = 0n,
708
- upperLimit = 0n,
709
- ): Promise<Call[]> {
710
- const MAX_RETRIES = 40;
711
-
712
- logger.verbose(
713
- `Rebalancing ${this.metadata.name}: ` +
714
- `retry=${retry}, lowerLimit=${lowerLimit}, upperLimit=${upperLimit}, isSellTokenToken0=${isSellTokenToken0}`
1174
+ }
1175
+
1176
+ static priceToSqrtRatio(price: number) {
1177
+ return (
1178
+ (BigInt(Math.floor(Math.sqrt(price) * 10 ** 9)) * BigInt(2 ** 128)) /
1179
+ BigInt(1e9)
1180
+ );
1181
+ }
1182
+
1183
+ static i129ToNumber(i129: { mag: bigint; sign: number }) {
1184
+ return i129.mag * (i129.sign.toString() == "false" ? 1n : -1n);
1185
+ }
1186
+
1187
+ static tickToPrice(tick: bigint) {
1188
+ return Math.pow(1.000001, Number(tick));
1189
+ }
1190
+
1191
+ async getLiquidityToAmounts(
1192
+ liquidity: Web3Number,
1193
+ bounds: EkuboBounds,
1194
+ blockIdentifier: BlockIdentifier = "pending",
1195
+ _poolKey: EkuboPoolKey | null = null,
1196
+ _currentPrice: {
1197
+ price: number;
1198
+ tick: number;
1199
+ sqrtRatio: string;
1200
+ } | null = null
1201
+ ) {
1202
+ const currentPrice =
1203
+ _currentPrice || (await this.getCurrentPrice(blockIdentifier));
1204
+ const lowerPrice = EkuboCLVault.tickToPrice(bounds.lowerTick);
1205
+ const upperPrice = EkuboCLVault.tickToPrice(bounds.upperTick);
1206
+ logger.verbose(
1207
+ `${EkuboCLVault.name}: getLiquidityToAmounts => currentPrice: ${currentPrice.price}, lowerPrice: ${lowerPrice}, upperPrice: ${upperPrice}`
1208
+ );
1209
+ const result: any = await this.ekuboMathContract.call(
1210
+ "liquidity_delta_to_amount_delta",
1211
+ [
1212
+ uint256.bnToUint256(currentPrice.sqrtRatio),
1213
+ {
1214
+ mag: liquidity.toWei(),
1215
+ sign: 0,
1216
+ },
1217
+ uint256.bnToUint256(
1218
+ EkuboCLVault.priceToSqrtRatio(lowerPrice).toString()
1219
+ ),
1220
+ uint256.bnToUint256(
1221
+ EkuboCLVault.priceToSqrtRatio(upperPrice).toString()
1222
+ ),
1223
+ ] as any,
1224
+ {
1225
+ blockIdentifier,
1226
+ }
1227
+ );
1228
+ const poolKey = _poolKey || (await this.getPoolKey(blockIdentifier));
1229
+ const token0Info = await Global.getTokenInfoFromAddr(poolKey.token0);
1230
+ const token1Info = await Global.getTokenInfoFromAddr(poolKey.token1);
1231
+ const amount0 = Web3Number.fromWei(
1232
+ EkuboCLVault.i129ToNumber(result.amount0).toString(),
1233
+ token0Info.decimals
1234
+ );
1235
+ const amount1 = Web3Number.fromWei(
1236
+ EkuboCLVault.i129ToNumber(result.amount1).toString(),
1237
+ token1Info.decimals
1238
+ );
1239
+
1240
+ return {
1241
+ amount0,
1242
+ amount1,
1243
+ };
1244
+ }
1245
+
1246
+ async harvest(acc: Account) {
1247
+ const ekuboHarvests = new EkuboHarvests(this.config);
1248
+ const unClaimedRewards = await ekuboHarvests.getUnHarvestedRewards(
1249
+ this.address
1250
+ );
1251
+ const poolKey = await this.getPoolKey();
1252
+ const token0Info = await Global.getTokenInfoFromAddr(poolKey.token0);
1253
+ const token1Info = await Global.getTokenInfoFromAddr(poolKey.token1);
1254
+ const bounds = await this.getCurrentBounds();
1255
+ const calls: Call[] = [];
1256
+ for (let claim of unClaimedRewards) {
1257
+ const fee = claim.claim.amount
1258
+ .multipliedBy(this.metadata.additionalInfo.feeBps)
1259
+ .dividedBy(10000);
1260
+ const postFeeAmount = claim.claim.amount.minus(fee);
1261
+
1262
+ const isToken1 = claim.token.eq(poolKey.token1);
1263
+ logger.verbose(
1264
+ `${
1265
+ EkuboCLVault.name
1266
+ }: harvest => Processing claim, isToken1: ${isToken1} amount: ${postFeeAmount.toWei()}`
1267
+ );
1268
+ const token0Amt = isToken1
1269
+ ? new Web3Number(0, token0Info.decimals)
1270
+ : postFeeAmount;
1271
+ const token1Amt = isToken1
1272
+ ? postFeeAmount
1273
+ : new Web3Number(0, token0Info.decimals);
1274
+ logger.verbose(
1275
+ `${
1276
+ EkuboCLVault.name
1277
+ }: harvest => token0Amt: ${token0Amt.toString()}, token1Amt: ${token1Amt.toString()}`
1278
+ );
1279
+
1280
+ const swapInfo = await this.getSwapInfoGivenAmounts(
1281
+ poolKey,
1282
+ token0Amt,
1283
+ token1Amt,
1284
+ bounds
1285
+ );
1286
+ swapInfo.token_to_address = token0Info.address.address;
1287
+ logger.verbose(
1288
+ `${EkuboCLVault.name}: harvest => swapInfo: ${JSON.stringify(swapInfo)}`
1289
+ );
1290
+
1291
+ logger.verbose(
1292
+ `${EkuboCLVault.name}: harvest => claim: ${JSON.stringify(claim)}`
1293
+ );
1294
+ const harvestEstimateCall = async (swapInfo1: SwapInfo) => {
1295
+ const swap1Amount = Web3Number.fromWei(
1296
+ uint256.uint256ToBN(swapInfo1.token_from_amount).toString(),
1297
+ 18
715
1298
  );
716
-
717
- const fromAmount = uint256.uint256ToBN(swapInfo.token_from_amount);
1299
+ const remainingAmount = postFeeAmount.minus(swap1Amount);
1300
+ const swapInfo2 = {
1301
+ ...swapInfo,
1302
+ token_from_amount: uint256.bnToUint256(remainingAmount.toWei()),
1303
+ };
1304
+ swapInfo2.token_to_address = token1Info.address.address;
1305
+ const calldata = [
1306
+ claim.rewardsContract.address,
1307
+ {
1308
+ id: claim.claim.id,
1309
+ amount: claim.claim.amount.toWei(),
1310
+ claimee: claim.claim.claimee.address,
1311
+ },
1312
+ claim.proof.map((p) => num.getDecimalString(p)),
1313
+ swapInfo,
1314
+ swapInfo2,
1315
+ ];
718
1316
  logger.verbose(
719
- `Selling ${fromAmount.toString()} of token ${swapInfo.token_from_address}`
1317
+ `${EkuboCLVault.name}: harvest => calldata: ${JSON.stringify(
1318
+ calldata
1319
+ )}`
720
1320
  );
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
- logger.error(`Rebalance attempt ${retry + 1} failed, adjusting swap amount...`);
733
-
734
- const newSwapInfo = { ...swapInfo };
735
- const currentAmount = Web3Number.fromWei(fromAmount.toString(), 18); // 18 is ok, as its toWei eventually anyways
736
- logger.verbose(`Current amount: ${currentAmount.toString()}`);
737
- if (err.message.includes('invalid token0 balance') || err.message.includes('invalid token0 amount')) {
738
- if (!isSellTokenToken0) {
739
- logger.verbose('Reducing swap amount - excess token0');
740
- let nextAmount = (fromAmount + lowerLimit) / 2n;
741
- upperLimit = fromAmount;
742
- if (nextAmount <= lowerLimit) {
743
- logger.error('Convergence failed: nextAmount <= lowerLimit');
744
- throw err;
745
- }
746
- newSwapInfo.token_from_amount = uint256.bnToUint256(nextAmount);
747
- } else {
748
- logger.verbose('Increasing swap amount - deficit token0');
749
- let nextAmount = (fromAmount + upperLimit) / 2n;
750
- if (upperLimit == 0n) {
751
- nextAmount = fromAmount * 2n;
752
- }
753
- lowerLimit = fromAmount;
754
- if (upperLimit != 0n && nextAmount >= upperLimit) {
755
- logger.error('Convergence failed: nextAmount >= upperLimit');
756
- throw err;
757
- }
758
- newSwapInfo.token_from_amount = uint256.bnToUint256(nextAmount);
759
- }
760
- } else if (err.message.includes('invalid token1 amount') || err.message.includes('invalid token1 balance')) {
761
- if (isSellTokenToken0) {
762
- logger.verbose('Reducing swap amount - excess token1');
763
- let nextAmount = (fromAmount + lowerLimit) / 2n;
764
- upperLimit = fromAmount;
765
- if (nextAmount <= lowerLimit) {
766
- logger.error('Convergence failed: nextAmount <= lowerLimit');
767
- throw err;
768
- }
769
- newSwapInfo.token_from_amount = uint256.bnToUint256(nextAmount);
770
- } else {
771
- logger.verbose('Increasing swap amount - deficit token1');
772
- let nextAmount = (fromAmount + upperLimit) / 2n;
773
- if (upperLimit == 0n) {
774
- nextAmount = fromAmount * 2n;
775
- }
776
- lowerLimit = fromAmount;
777
- if (upperLimit != 0n && nextAmount >= upperLimit) {
778
- logger.error('Convergence failed: nextAmount >= upperLimit');
779
- throw err;
780
- }
781
- newSwapInfo.token_from_amount = uint256.bnToUint256(nextAmount);
782
- }
783
- } else {
784
- logger.error('Unexpected error:', err);
785
- throw err;
786
- }
787
- newSwapInfo.token_to_min_amount = uint256.bnToUint256('0');
788
- return this.rebalanceIter(
789
- newSwapInfo,
790
- acc,
791
- estimateCall,
792
- isSellTokenToken0,
793
- retry + 1,
794
- lowerLimit,
795
- upperLimit,
796
- );
797
- }
1321
+ return [this.contract.populate("harvest", calldata)];
1322
+ };
1323
+ const _callsFinal = await this.rebalanceIter(
1324
+ swapInfo,
1325
+ acc,
1326
+ harvestEstimateCall
1327
+ );
1328
+ logger.verbose(
1329
+ `${EkuboCLVault.name}: harvest => _callsFinal: ${JSON.stringify(
1330
+ _callsFinal
1331
+ )}`
1332
+ );
1333
+ calls.push(..._callsFinal);
798
1334
  }
1335
+ return calls;
1336
+ }
1337
+
1338
+ async getInvestmentFlows() {
1339
+ const netYield = await this.netAPY();
1340
+ const poolKey = await this.getPoolKey();
1341
+
1342
+ const linkedFlow: IInvestmentFlow = {
1343
+ title: this.metadata.name,
1344
+ subItems: [
1345
+ {
1346
+ key: "Pool",
1347
+ value: `${(
1348
+ EkuboCLVault.div2Power128(BigInt(poolKey.fee)) * 100
1349
+ ).toFixed(2)}%, ${poolKey.tick_spacing} tick spacing`,
1350
+ },
1351
+ ],
1352
+ linkedFlows: [],
1353
+ style: { backgroundColor: FlowChartColors.Blue.valueOf() },
1354
+ };
1355
+
1356
+ const baseFlow: IInvestmentFlow = {
1357
+ id: "base",
1358
+ title: "Your Deposit",
1359
+ subItems: [
1360
+ { key: `Net yield`, value: `${(netYield * 100).toFixed(2)}%` },
1361
+ {
1362
+ key: `Performance Fee`,
1363
+ value: `${(this.metadata.additionalInfo.feeBps / 100).toFixed(2)}%`,
1364
+ },
1365
+ ],
1366
+ linkedFlows: [linkedFlow],
1367
+ style: { backgroundColor: FlowChartColors.Purple.valueOf() },
1368
+ };
1369
+
1370
+ const rebalanceFlow: IInvestmentFlow = {
1371
+ id: "rebalance",
1372
+ title: "Automated Rebalance",
1373
+ subItems: [
1374
+ {
1375
+ key: "Range selection",
1376
+ value: `${
1377
+ this.metadata.additionalInfo.newBounds.lower *
1378
+ Number(poolKey.tick_spacing)
1379
+ } to ${
1380
+ this.metadata.additionalInfo.newBounds.upper *
1381
+ Number(poolKey.tick_spacing)
1382
+ } ticks`,
1383
+ },
1384
+ ],
1385
+ linkedFlows: [linkedFlow],
1386
+ style: { backgroundColor: FlowChartColors.Green.valueOf() },
1387
+ };
799
1388
 
800
- static tickToi129(tick: number) {
801
- if (tick < 0) {
802
- return {
803
- mag: -tick,
804
- sign: 1
805
- };
806
- } else {
807
- return {
808
- mag: tick,
809
- sign: 0
810
- };
811
- }
812
- }
813
-
814
- static priceToSqrtRatio(price: number) {
815
- return BigInt(Math.floor(Math.sqrt(price) * 10**9)) * BigInt(2 ** 128) / BigInt(1e9);
816
- }
817
-
818
- static i129ToNumber(i129: { mag: bigint, sign: number }) {
819
- return i129.mag * (i129.sign.toString() == "false" ? 1n : -1n);
820
- }
821
-
822
- static tickToPrice(tick: bigint) {
823
- return Math.pow(1.000001, Number(tick));
824
- }
825
-
826
- async getLiquidityToAmounts(
827
- liquidity: Web3Number,
828
- bounds: EkuboBounds,
829
- blockIdentifier: BlockIdentifier = 'pending',
830
- _poolKey: EkuboPoolKey | null = null,
831
- _currentPrice: {
832
- price: number, tick: number, sqrtRatio: string
833
- } | null = null
834
- ) {
835
- const currentPrice = _currentPrice || await this.getCurrentPrice(blockIdentifier);
836
- const lowerPrice = EkuboCLVault.tickToPrice(bounds.lowerTick);
837
- const upperPrice = EkuboCLVault.tickToPrice(bounds.upperTick);
838
- logger.verbose(`${EkuboCLVault.name}: getLiquidityToAmounts => currentPrice: ${currentPrice.price}, lowerPrice: ${lowerPrice}, upperPrice: ${upperPrice}`);
839
- const result: any = await this.ekuboMathContract.call('liquidity_delta_to_amount_delta', [
840
- uint256.bnToUint256(currentPrice.sqrtRatio),
841
- {
842
- mag: liquidity.toWei(),
843
- sign: 0
844
- },
845
- uint256.bnToUint256(EkuboCLVault.priceToSqrtRatio(lowerPrice).toString()),
846
- uint256.bnToUint256(EkuboCLVault.priceToSqrtRatio(upperPrice).toString())
847
- ] as any, {
848
- blockIdentifier
849
- });
850
- const poolKey = _poolKey || await this.getPoolKey(blockIdentifier);
851
- const token0Info = await Global.getTokenInfoFromAddr(poolKey.token0);
852
- const token1Info = await Global.getTokenInfoFromAddr(poolKey.token1);
853
- const amount0 = Web3Number.fromWei(EkuboCLVault.i129ToNumber(result.amount0).toString(), token0Info.decimals);
854
- const amount1 = Web3Number.fromWei(EkuboCLVault.i129ToNumber(result.amount1).toString(), token1Info.decimals);
855
-
856
- return {
857
- amount0, amount1
858
- }
859
- }
860
-
861
- async harvest(acc: Account) {
862
- const ekuboHarvests = new EkuboHarvests(this.config);
863
- const unClaimedRewards = await ekuboHarvests.getUnHarvestedRewards(this.address);
864
- const poolKey = await this.getPoolKey();
865
- const token0Info = await Global.getTokenInfoFromAddr(poolKey.token0);
866
- const token1Info = await Global.getTokenInfoFromAddr(poolKey.token1);
867
- const bounds = await this.getCurrentBounds();
868
- const calls: Call[] = [];
869
- for (let claim of unClaimedRewards) {
870
- const fee = claim.claim.amount.multipliedBy(this.metadata.additionalInfo.feeBps).dividedBy(10000);
871
- const postFeeAmount = claim.claim.amount.minus(fee);
872
-
873
- const isToken1 = claim.token.eq(poolKey.token1);
874
- logger.verbose(`${EkuboCLVault.name}: harvest => Processing claim, isToken1: ${isToken1} amount: ${postFeeAmount.toWei()}`);
875
- const token0Amt = isToken1 ? new Web3Number(0, token0Info.decimals) : postFeeAmount;
876
- const token1Amt = isToken1 ? postFeeAmount : new Web3Number(0, token0Info.decimals);
877
- logger.verbose(`${EkuboCLVault.name}: harvest => token0Amt: ${token0Amt.toString()}, token1Amt: ${token1Amt.toString()}`);
878
-
879
- const swapInfo = await this.getSwapInfoGivenAmounts(poolKey, token0Amt, token1Amt, bounds);
880
- swapInfo.token_to_address = token0Info.address.address;
881
- logger.verbose(`${EkuboCLVault.name}: harvest => swapInfo: ${JSON.stringify(swapInfo)}`);
882
-
883
- logger.verbose(`${EkuboCLVault.name}: harvest => claim: ${JSON.stringify(claim)}`);
884
- const harvestEstimateCall = async (swapInfo1: SwapInfo) => {
885
- const swap1Amount = Web3Number.fromWei(uint256.uint256ToBN(swapInfo1.token_from_amount).toString(), 18);
886
- const remainingAmount = postFeeAmount.minus(swap1Amount);
887
- const swapInfo2 = {...swapInfo, token_from_amount: uint256.bnToUint256(remainingAmount.toWei()) }
888
- swapInfo2.token_to_address = token1Info.address.address;
889
- const calldata = [
890
- claim.rewardsContract.address,
891
- {
892
- id: claim.claim.id,
893
- amount: claim.claim.amount.toWei(),
894
- claimee: claim.claim.claimee.address
895
- },
896
- claim.proof.map((p) => num.getDecimalString(p)),
897
- swapInfo,
898
- swapInfo2
899
- ];
900
- logger.verbose(`${EkuboCLVault.name}: harvest => calldata: ${JSON.stringify(calldata)}`);
901
- return [this.contract.populate('harvest', calldata)]
902
- }
903
- const _callsFinal = await this.rebalanceIter(swapInfo, acc, harvestEstimateCall);
904
- logger.verbose(`${EkuboCLVault.name}: harvest => _callsFinal: ${JSON.stringify(_callsFinal)}`);
905
- calls.push(..._callsFinal);
906
- }
907
- return calls;
908
- }
909
-
910
- async getInvestmentFlows() {
911
- const netYield = await this.netAPY();
912
- const poolKey = await this.getPoolKey();
913
-
914
- const linkedFlow: IInvestmentFlow = {
915
- title: this.metadata.name,
916
- subItems: [{key: "Pool", value: `${(EkuboCLVault.div2Power128(BigInt(poolKey.fee)) * 100).toFixed(2)}%, ${poolKey.tick_spacing} tick spacing`}],
917
- linkedFlows: [],
918
- style: {backgroundColor: FlowChartColors.Blue.valueOf()},
919
- }
920
-
921
- const baseFlow: IInvestmentFlow = {
922
- id: 'base',
923
- title: "Your Deposit",
924
- subItems: [{key: `Net yield`, value: `${(netYield * 100).toFixed(2)}%`}, {key: `Performance Fee`, value: `${(this.metadata.additionalInfo.feeBps / 100).toFixed(2)}%`}],
925
- linkedFlows: [linkedFlow],
926
- style: {backgroundColor: FlowChartColors.Purple.valueOf()},
927
- };
928
-
929
- const rebalanceFlow: IInvestmentFlow = {
930
- id: 'rebalance',
931
- title: "Automated Rebalance",
932
- subItems: [{
933
- key: 'Range selection',
934
- value: `${this.metadata.additionalInfo.newBounds.lower * Number(poolKey.tick_spacing)} to ${this.metadata.additionalInfo.newBounds.upper * Number(poolKey.tick_spacing)} ticks`
935
- }],
936
- linkedFlows: [linkedFlow],
937
- style: {backgroundColor: FlowChartColors.Green.valueOf()},
938
- }
939
-
940
- return [baseFlow, rebalanceFlow];
941
- }
1389
+ return [baseFlow, rebalanceFlow];
1390
+ }
942
1391
  }
943
1392
 
944
-
945
- 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.'
946
- const _protocol: IProtocol = {name: 'Ekubo', logo: 'https://app.ekubo.org/favicon.ico'}
1393
+ const _description =
1394
+ "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.";
1395
+ const _protocol: IProtocol = {
1396
+ name: "Ekubo",
1397
+ logo: "https://app.ekubo.org/favicon.ico",
1398
+ };
947
1399
  // need to fine tune better
948
1400
  const _riskFactor: RiskFactor[] = [
949
- {type: RiskType.SMART_CONTRACT_RISK, value: 0.5, weight: 25},
950
- {type: RiskType.IMPERMANENT_LOSS, value: 1, weight: 75}
951
- ]
952
- const AUDIT_URL = 'https://assets.strkfarm.com/strkfarm/audit_report_vesu_and_ekubo_strats.pdf';
953
-
1401
+ { type: RiskType.SMART_CONTRACT_RISK, value: 0.5, weight: 25 },
1402
+ { type: RiskType.IMPERMANENT_LOSS, value: 1, weight: 75 },
1403
+ ];
1404
+ const AUDIT_URL =
1405
+ "https://assets.strkfarm.com/strkfarm/audit_report_vesu_and_ekubo_strats.pdf";
1406
+
1407
+ const faqs: FAQ[] = [
1408
+ {
1409
+ question: "What is the Ekubo CL Vault strategy?",
1410
+ answer:
1411
+ "The Ekubo CL Vault strategy deploys your assets into an Ekubo liquidity pool, automatically rebalancing positions around the current price to optimize yield and reduce manual adjustments.",
1412
+ },
1413
+ {
1414
+ question: "How are trading fees and rewards handled?",
1415
+ answer:
1416
+ "Trading fees and DeFi Spring rewards are automatically compounded back into the strategy, increasing your overall returns.",
1417
+ },
1418
+ {
1419
+ question: "What happens during withdrawal?",
1420
+ answer:
1421
+ "During withdrawal, you may receive either or both tokens depending on market conditions and prevailing prices.",
1422
+ },
1423
+ {
1424
+ question: "Is the strategy audited?",
1425
+ answer:
1426
+ <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>
1427
+ }
1428
+ ];
954
1429
  /**
955
1430
  * Represents the Vesu Rebalance Strategies.
956
1431
  */
957
- export const EkuboCLVaultStrategies: IStrategyMetadata<CLVaultStrategySettings>[] = [{
958
- name: 'Ekubo xSTRK/STRK',
959
- description: <div>
960
- <p>{_description.replace('{{POOL_NAME}}', 'xSTRK/STRK')}</p>
961
- <ul style={{marginLeft: '20px', listStyle: 'circle', fontSize: '12px'}}>
962
- <li style={{marginTop: '10px'}}>During withdrawal, you may receive either or both tokens depending on market conditions and prevailing prices.</li>
963
- <li style={{marginTop: '10px'}}>Sometimes you might see a negative APY — this is usually not a big deal. It happens when xSTRK's price drops on DEXes, but things typically bounce back within a few days or a week.</li>
964
- </ul>
965
- </div>,
966
- address: ContractAddr.from('0x01f083b98674bc21effee29ef443a00c7b9a500fd92cf30341a3da12c73f2324'),
967
- type: 'Other',
968
- // must be same order as poolKey token0 and token1
969
- depositTokens: [Global.getDefaultTokens().find(t => t.symbol === 'xSTRK')!, Global.getDefaultTokens().find(t => t.symbol === 'STRK')!],
970
- protocols: [_protocol],
971
- auditUrl: AUDIT_URL,
972
- maxTVL: Web3Number.fromWei('0', 18),
973
- risk: {
1432
+ export const EkuboCLVaultStrategies: IStrategyMetadata<CLVaultStrategySettings>[] =
1433
+ [
1434
+ {
1435
+ name: "Ekubo xSTRK/STRK",
1436
+ description: (
1437
+ <div>
1438
+ <p>{_description.replace("{{POOL_NAME}}", "xSTRK/STRK")}</p>
1439
+ <ul
1440
+ style={{
1441
+ marginLeft: "20px",
1442
+ listStyle: "circle",
1443
+ fontSize: "12px",
1444
+ }}
1445
+ >
1446
+ <li style={{ marginTop: "10px" }}>
1447
+ During withdrawal, you may receive either or both tokens depending
1448
+ on market conditions and prevailing prices.
1449
+ </li>
1450
+ <li style={{ marginTop: "10px" }}>
1451
+ Sometimes you might see a negative APY — this is usually not a big
1452
+ deal. It happens when xSTRK's price drops on DEXes, but things
1453
+ typically bounce back within a few days or a week.
1454
+ </li>
1455
+ </ul>
1456
+ </div>
1457
+ ),
1458
+ address: ContractAddr.from(
1459
+ "0x01f083b98674bc21effee29ef443a00c7b9a500fd92cf30341a3da12c73f2324"
1460
+ ),
1461
+ type: "Other",
1462
+ // must be same order as poolKey token0 and token1
1463
+ depositTokens: [
1464
+ Global.getDefaultTokens().find((t) => t.symbol === "xSTRK")!,
1465
+ Global.getDefaultTokens().find((t) => t.symbol === "STRK")!,
1466
+ ],
1467
+ protocols: [_protocol],
1468
+ auditUrl: AUDIT_URL,
1469
+ maxTVL: Web3Number.fromWei("0", 18),
1470
+ risk: {
974
1471
  riskFactor: _riskFactor,
975
- netRisk: _riskFactor.reduce((acc, curr) => acc + curr.value * curr.weight, 0) / _riskFactor.reduce((acc, curr) => acc + curr.weight, 0),
976
- notARisks: getNoRiskTags(_riskFactor)
977
- },
978
- apyMethodology: 'APY based on 7-day historical performance, including fees and rewards.',
979
- additionalInfo: {
1472
+ netRisk:
1473
+ _riskFactor.reduce((acc, curr) => acc + curr.value * curr.weight, 0) /
1474
+ _riskFactor.reduce((acc, curr) => acc + curr.weight, 0),
1475
+ notARisks: getNoRiskTags(_riskFactor),
1476
+ },
1477
+ apyMethodology:
1478
+ "APY based on 7-day historical performance, including fees and rewards.",
1479
+ additionalInfo: {
980
1480
  newBounds: {
981
- lower: -1,
982
- upper: 1
1481
+ lower: -1,
1482
+ upper: 1,
983
1483
  },
984
- lstContract: ContractAddr.from('0x028d709c875c0ceac3dce7065bec5328186dc89fe254527084d1689910954b0a'),
985
- feeBps: 1000
986
- }
987
- }]
1484
+ lstContract: ContractAddr.from(
1485
+ "0x028d709c875c0ceac3dce7065bec5328186dc89fe254527084d1689910954b0a"
1486
+ ),
1487
+ feeBps: 1000,
1488
+ },
1489
+ faqs: [
1490
+ ...faqs,
1491
+ {
1492
+ question: "Why might I see a negative APY?",
1493
+ answer:
1494
+ "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.",
1495
+ },
1496
+ ]
1497
+ },
1498
+ ];