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