@strkfarm/sdk 1.0.17 → 1.0.19
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 +276 -90
- package/dist/cli.mjs +270 -84
- package/dist/index.browser.global.js +27175 -18059
- package/dist/index.browser.mjs +8078 -1370
- package/dist/index.d.ts +211 -25
- package/dist/index.js +7773 -1035
- package/dist/index.mjs +8146 -1417
- package/package.json +4 -1
- package/src/data/cl-vault.abi.json +1434 -0
- package/src/data/ekubo-math.abi.json +333 -0
- package/src/data/ekubo-positions.abi.json +1594 -0
- package/src/data/erc20.abi.json +1122 -0
- package/src/data/erc4626.abi.json +1530 -0
- package/src/dataTypes/_bignumber.ts +53 -0
- package/src/dataTypes/address.ts +4 -0
- package/src/dataTypes/bignumber.browser.ts +8 -0
- package/src/dataTypes/bignumber.node.ts +22 -0
- package/src/dataTypes/bignumber.ts +1 -55
- package/src/dataTypes/index.ts +1 -1
- package/src/global.ts +54 -4
- package/src/interfaces/common.ts +64 -10
- package/src/interfaces/lending.ts +1 -1
- package/src/modules/avnu.ts +93 -0
- package/src/modules/erc20.ts +23 -0
- package/src/modules/index.ts +2 -0
- package/src/modules/pricer.ts +1 -1
- package/src/modules/zkLend.ts +2 -2
- package/src/node/headless.browser.ts +9 -0
- package/src/node/headless.node.ts +36 -0
- package/src/node/headless.ts +1 -0
- package/src/node/index.ts +2 -1
- package/src/strategies/base-strategy.ts +47 -0
- package/src/strategies/ekubo-cl-vault.ts +535 -0
- package/src/strategies/index.ts +2 -1
- package/src/strategies/vesu-rebalance.ts +123 -35
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
import { ContractAddr, Web3Number } from "@/dataTypes";
|
|
2
|
+
import { IConfig, IProtocol, IStrategyMetadata, RiskFactor, RiskType } from "@/interfaces";
|
|
3
|
+
import { PricerBase } from "@/modules/pricerBase";
|
|
4
|
+
import { assert } from "@/utils";
|
|
5
|
+
import { Call, Contract, 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';
|
|
10
|
+
import { Global, logger } from "@/global";
|
|
11
|
+
import { AvnuWrapper, ERC20, SwapInfo } from "@/modules";
|
|
12
|
+
import { BaseStrategy } from "./base-strategy";
|
|
13
|
+
import { DualActionAmount } from "./base-strategy";
|
|
14
|
+
import { DualTokenInfo } from "./base-strategy";
|
|
15
|
+
import { log } from "winston";
|
|
16
|
+
|
|
17
|
+
export interface EkuboPoolKey {
|
|
18
|
+
token0: ContractAddr,
|
|
19
|
+
token1: ContractAddr,
|
|
20
|
+
fee: string,
|
|
21
|
+
tick_spacing: string,
|
|
22
|
+
extension: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface EkuboBounds {
|
|
26
|
+
lowerTick: bigint,
|
|
27
|
+
upperTick: bigint
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Settings for the CLVaultStrategy
|
|
32
|
+
*
|
|
33
|
+
* @property newBounds - The new bounds for the strategy
|
|
34
|
+
* @property newBounds.lower - relative to the current tick
|
|
35
|
+
* @property newBounds.upper - relative to the current tick
|
|
36
|
+
*/
|
|
37
|
+
export interface CLVaultStrategySettings {
|
|
38
|
+
newBounds: {
|
|
39
|
+
lower: number,
|
|
40
|
+
upper: number
|
|
41
|
+
},
|
|
42
|
+
// to get true price
|
|
43
|
+
lstContract: ContractAddr
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class EkuboCLVault extends BaseStrategy<DualTokenInfo, DualActionAmount> {
|
|
47
|
+
/** Contract address of the strategy */
|
|
48
|
+
readonly address: ContractAddr;
|
|
49
|
+
/** Pricer instance for token price calculations */
|
|
50
|
+
readonly pricer: PricerBase;
|
|
51
|
+
/** Metadata containing strategy information */
|
|
52
|
+
readonly metadata: IStrategyMetadata<CLVaultStrategySettings>
|
|
53
|
+
/** Contract instance for interacting with the strategy */
|
|
54
|
+
readonly contract: Contract;
|
|
55
|
+
readonly BASE_WEIGHT = 10000; // 10000 bps = 100%
|
|
56
|
+
|
|
57
|
+
readonly ekuboPositionsContract: Contract;
|
|
58
|
+
readonly ekuboMathContract: Contract;
|
|
59
|
+
readonly lstContract: Contract;
|
|
60
|
+
poolKey: EkuboPoolKey | undefined;
|
|
61
|
+
readonly avnu: AvnuWrapper;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Creates a new VesuRebalance strategy instance.
|
|
65
|
+
* @param config - Configuration object containing provider and other settings
|
|
66
|
+
* @param pricer - Pricer instance for token price calculations
|
|
67
|
+
* @param metadata - Strategy metadata including deposit tokens and address
|
|
68
|
+
* @throws {Error} If more than one deposit token is specified
|
|
69
|
+
*/
|
|
70
|
+
constructor(config: IConfig, pricer: PricerBase, metadata: IStrategyMetadata<CLVaultStrategySettings>) {
|
|
71
|
+
super(config);
|
|
72
|
+
this.pricer = pricer;
|
|
73
|
+
|
|
74
|
+
assert(metadata.depositTokens.length === 2, 'EkuboCL only supports 2 deposit token');
|
|
75
|
+
this.metadata = metadata;
|
|
76
|
+
this.address = metadata.address;
|
|
77
|
+
|
|
78
|
+
this.contract = new Contract(CLVaultAbi, this.address.address, this.config.provider);
|
|
79
|
+
this.lstContract = new Contract(ERC4626Abi, this.metadata.additionalInfo.lstContract.address, this.config.provider);
|
|
80
|
+
|
|
81
|
+
// ekubo positions contract
|
|
82
|
+
const EKUBO_POSITION = '0x02e0af29598b407c8716b17f6d2795eca1b471413fa03fb145a5e33722184067'
|
|
83
|
+
this.ekuboPositionsContract = new Contract(EkuboPositionsAbi, EKUBO_POSITION, this.config.provider);
|
|
84
|
+
const EKUBO_MATH = '0x04a72e9e166f6c0e9d800af4dc40f6b6fb4404b735d3f528d9250808b2481995';
|
|
85
|
+
this.ekuboMathContract = new Contract(EkuboMathAbi, EKUBO_MATH, this.config.provider);
|
|
86
|
+
|
|
87
|
+
this.avnu = new AvnuWrapper();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
depositCall(amountInfo: DualActionAmount, receiver: ContractAddr): Call[] {
|
|
91
|
+
return []
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
withdrawCall(amountInfo: DualActionAmount, receiver: ContractAddr, owner: ContractAddr): Call[] {
|
|
95
|
+
return []
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
rebalanceCall(newBounds: EkuboBounds, swapParams: SwapInfo): Call[] {
|
|
99
|
+
return [this.contract.populate('rebalance', [
|
|
100
|
+
{
|
|
101
|
+
lower: EkuboCLVault.tickToi129(Number(newBounds.lowerTick)),
|
|
102
|
+
upper: EkuboCLVault.tickToi129(Number(newBounds.upperTick))
|
|
103
|
+
},
|
|
104
|
+
swapParams
|
|
105
|
+
])]
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
handleUnusedCall(swapParams: SwapInfo): Call[] {
|
|
109
|
+
return [this.contract.populate('handle_unused', [
|
|
110
|
+
swapParams
|
|
111
|
+
])]
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
handleFeesCall(): Call[] {
|
|
115
|
+
return [this.contract.populate('handle_fees', [])]
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async getUserTVL(user: ContractAddr): Promise<DualTokenInfo> {
|
|
119
|
+
throw new Error('Not implemented');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async getTVL(): Promise<DualTokenInfo> {
|
|
123
|
+
const result = await this.contract.call('total_liquidity', []);
|
|
124
|
+
const bounds = await this.getCurrentBounds();
|
|
125
|
+
const {amount0, amount1} = await this.getLiquidityToAmounts(Web3Number.fromWei(result.toString(), 18), bounds);
|
|
126
|
+
const poolKey = await this.getPoolKey();
|
|
127
|
+
const token0Info = await Global.getTokenInfoFromAddr(poolKey.token0);
|
|
128
|
+
const token1Info = await Global.getTokenInfoFromAddr(poolKey.token1);
|
|
129
|
+
const P0 = await this.pricer.getPrice(token0Info.symbol);
|
|
130
|
+
const P1 = await this.pricer.getPrice(token1Info.symbol);
|
|
131
|
+
const token0Usd = Number(amount0.toFixed(13)) * P0.price;
|
|
132
|
+
const token1Usd = Number(amount1.toFixed(13)) * P1.price;
|
|
133
|
+
return {
|
|
134
|
+
netUsdValue: token0Usd + token1Usd,
|
|
135
|
+
token0: {
|
|
136
|
+
tokenInfo: token0Info,
|
|
137
|
+
amount: amount0,
|
|
138
|
+
usdValue: token0Usd
|
|
139
|
+
},
|
|
140
|
+
token1: {
|
|
141
|
+
tokenInfo: token1Info,
|
|
142
|
+
amount: amount1,
|
|
143
|
+
usdValue: token1Usd
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async getUncollectedFees(): Promise<DualTokenInfo> {
|
|
149
|
+
const nftID = await this.getCurrentNFTID();
|
|
150
|
+
const poolKey = await this.getPoolKey();
|
|
151
|
+
const currentBounds = await this.getCurrentBounds();
|
|
152
|
+
const result: any = await this.ekuboPositionsContract.call('get_token_info', [
|
|
153
|
+
nftID,
|
|
154
|
+
{
|
|
155
|
+
token0: poolKey.token0.address,
|
|
156
|
+
token1: poolKey.token1.address,
|
|
157
|
+
fee: poolKey.fee,
|
|
158
|
+
tick_spacing: poolKey.tick_spacing,
|
|
159
|
+
extension: poolKey.extension
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
lower: EkuboCLVault.tickToi129(Number(currentBounds.lowerTick)),
|
|
163
|
+
upper: EkuboCLVault.tickToi129(Number(currentBounds.upperTick))
|
|
164
|
+
}
|
|
165
|
+
]);
|
|
166
|
+
const token0Info = await Global.getTokenInfoFromAddr(poolKey.token0);
|
|
167
|
+
const token1Info = await Global.getTokenInfoFromAddr(poolKey.token1);
|
|
168
|
+
const P0 = await this.pricer.getPrice(token0Info.symbol);
|
|
169
|
+
const P1 = await this.pricer.getPrice(token1Info.symbol);
|
|
170
|
+
const token0Web3 = Web3Number.fromWei(result.fees0.toString(), token0Info.decimals);
|
|
171
|
+
const token1Web3 = Web3Number.fromWei(result.fees1.toString(), token1Info.decimals);
|
|
172
|
+
const token0Usd = Number(token0Web3.toFixed(13)) * P0.price;
|
|
173
|
+
const token1Usd = Number(token1Web3.toFixed(13)) * P1.price;
|
|
174
|
+
return {
|
|
175
|
+
netUsdValue: token0Usd + token1Usd,
|
|
176
|
+
token0: {
|
|
177
|
+
tokenInfo: token0Info,
|
|
178
|
+
amount: token0Web3,
|
|
179
|
+
usdValue: token0Usd
|
|
180
|
+
},
|
|
181
|
+
token1: {
|
|
182
|
+
tokenInfo: token1Info,
|
|
183
|
+
amount: token1Web3,
|
|
184
|
+
usdValue: token1Usd
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async getCurrentNFTID(): Promise<number> {
|
|
190
|
+
const result: any = await this.contract.call('get_position_key', []);
|
|
191
|
+
return Number(result.salt.toString());
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async truePrice() {
|
|
195
|
+
const result: any = await this.lstContract.call('convert_to_assets', [uint256.bnToUint256(BigInt(1e18).toString())]);
|
|
196
|
+
const truePrice = Number(BigInt(result.toString()) * BigInt(1e9)/ BigInt(1e18)) / 1e9;
|
|
197
|
+
return truePrice;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async getCurrentPrice() {
|
|
201
|
+
const poolKey = await this.getPoolKey();
|
|
202
|
+
return this._getCurrentPrice(poolKey);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private async _getCurrentPrice(poolKey: EkuboPoolKey) {
|
|
206
|
+
const priceInfo: any = await this.ekuboPositionsContract.call('get_pool_price', [
|
|
207
|
+
{
|
|
208
|
+
token0: poolKey.token0.address,
|
|
209
|
+
token1: poolKey.token1.address,
|
|
210
|
+
fee: poolKey.fee,
|
|
211
|
+
tick_spacing: poolKey.tick_spacing,
|
|
212
|
+
extension: poolKey.extension
|
|
213
|
+
}
|
|
214
|
+
])
|
|
215
|
+
const sqrtRatio = EkuboCLVault.div2Power128(BigInt(priceInfo.sqrt_ratio.toString()));
|
|
216
|
+
const price = sqrtRatio * sqrtRatio;
|
|
217
|
+
const tick = EkuboCLVault.priceToTick(price, true, Number(poolKey.tick_spacing));
|
|
218
|
+
return {
|
|
219
|
+
price,
|
|
220
|
+
tick: tick.mag * (tick.sign == 0 ? 1 : -1)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async getCurrentBounds(): Promise<EkuboBounds> {
|
|
225
|
+
const result: any = await this.contract.call('get_position_key', []);
|
|
226
|
+
return {
|
|
227
|
+
lowerTick: EkuboCLVault.i129ToNumber(result.bounds.lower),
|
|
228
|
+
upperTick: EkuboCLVault.i129ToNumber(result.bounds.upper)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
static div2Power128(num: BigInt): number {
|
|
233
|
+
return (Number(((BigInt(num.toString()) * 1000000n) / BigInt(2 ** 128))) / 1000000)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
static priceToTick(price: number, isRoundDown: boolean, tickSpacing: number) {
|
|
237
|
+
const value = isRoundDown ? Math.floor(Math.log(price) / Math.log(1.000001)) : Math.ceil(Math.log(price) / Math.log(1.000001));
|
|
238
|
+
const tick = Math.floor(value / tickSpacing) * tickSpacing;
|
|
239
|
+
return this.tickToi129(tick);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async getPoolKey(): Promise<EkuboPoolKey> {
|
|
243
|
+
if (this.poolKey) {
|
|
244
|
+
return this.poolKey;
|
|
245
|
+
}
|
|
246
|
+
const result: any = await this.contract.call('get_settings', []);
|
|
247
|
+
const poolKey: EkuboPoolKey = {
|
|
248
|
+
token0: ContractAddr.from(result.pool_key.token0.toString()),
|
|
249
|
+
token1: ContractAddr.from(result.pool_key.token1.toString()),
|
|
250
|
+
fee: result.pool_key.fee.toString(),
|
|
251
|
+
tick_spacing: result.pool_key.tick_spacing.toString(),
|
|
252
|
+
extension: result.pool_key.extension.toString()
|
|
253
|
+
};
|
|
254
|
+
const token0Info = await Global.getTokenInfoFromAddr(poolKey.token0);
|
|
255
|
+
const token1Info = await Global.getTokenInfoFromAddr(poolKey.token1);
|
|
256
|
+
assert(token0Info.decimals == token1Info.decimals, 'Tested only for equal decimals');
|
|
257
|
+
this.poolKey = poolKey;
|
|
258
|
+
return poolKey;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async getNewBounds(): Promise<EkuboBounds> {
|
|
262
|
+
const poolKey = await this.getPoolKey();
|
|
263
|
+
const currentPrice = await this._getCurrentPrice(poolKey);
|
|
264
|
+
|
|
265
|
+
const newLower = currentPrice.tick + (Number(this.metadata.additionalInfo.newBounds.lower) * Number(poolKey.tick_spacing));
|
|
266
|
+
const newUpper = currentPrice.tick + (Number(this.metadata.additionalInfo.newBounds.upper) * Number(poolKey.tick_spacing));
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
lowerTick: BigInt(newLower),
|
|
270
|
+
upperTick: BigInt(newUpper)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Computes the expected amounts to fully utilize amount in
|
|
276
|
+
* to add liquidity to the pool
|
|
277
|
+
* @param amount0: amount of token0
|
|
278
|
+
* @param amount1: amount of token1
|
|
279
|
+
* @returns {amount0, amount1}
|
|
280
|
+
*/
|
|
281
|
+
private async _getExpectedAmountsForLiquidity(
|
|
282
|
+
amount0: Web3Number,
|
|
283
|
+
amount1: Web3Number,
|
|
284
|
+
bounds: EkuboBounds
|
|
285
|
+
) {
|
|
286
|
+
assert(amount0.greaterThan(0) || amount1.greaterThan(0), 'Amount is 0');
|
|
287
|
+
|
|
288
|
+
// token is token0 or token1
|
|
289
|
+
const poolKey = await this.getPoolKey();
|
|
290
|
+
|
|
291
|
+
// get amount ratio for 1e18 liquidity
|
|
292
|
+
const sampleLiq = 1e18;
|
|
293
|
+
const {amount0: sampleAmount0, amount1: sampleAmount1} = await this.getLiquidityToAmounts(Web3Number.fromWei(sampleLiq.toString(), 18), bounds);
|
|
294
|
+
logger.verbose(`${EkuboCLVault.name}: _getExpectedAmountsForLiquidity => sampleAmount0: ${sampleAmount0.toString()}, sampleAmount1: ${sampleAmount1.toString()}`);
|
|
295
|
+
|
|
296
|
+
assert(!sampleAmount0.eq(0) && !sampleAmount1.eq(0), 'Sample amount is 0');
|
|
297
|
+
|
|
298
|
+
// notation: P = P0 / P1
|
|
299
|
+
const price = await (await this.getCurrentPrice()).price;
|
|
300
|
+
logger.verbose(`${EkuboCLVault.name}: _getExpectedAmountsForLiquidity => price: ${price}`);
|
|
301
|
+
// Account for edge cases
|
|
302
|
+
// i.e. when liquidity is out of range
|
|
303
|
+
if (amount1.eq(0) && amount0.greaterThan(0)) {
|
|
304
|
+
if (sampleAmount1.eq(0)) {
|
|
305
|
+
return {
|
|
306
|
+
amount0: amount0,
|
|
307
|
+
amount1: Web3Number.fromWei('0', 18),
|
|
308
|
+
ratio: Infinity
|
|
309
|
+
}
|
|
310
|
+
} else if (sampleAmount0.eq(0)) {
|
|
311
|
+
// swap all to token1
|
|
312
|
+
return {
|
|
313
|
+
amount0: Web3Number.fromWei('0', 18),
|
|
314
|
+
amount1: amount0.multipliedBy(price),
|
|
315
|
+
ratio: 0
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
} else if (amount0.eq(0) && amount1.greaterThan(0)) {
|
|
319
|
+
if (sampleAmount0.eq(0)) {
|
|
320
|
+
return {
|
|
321
|
+
amount0: Web3Number.fromWei('0', 18),
|
|
322
|
+
amount1: amount1,
|
|
323
|
+
ratio: 0
|
|
324
|
+
}
|
|
325
|
+
} else if (sampleAmount1.eq(0)) {
|
|
326
|
+
// swap all to token0
|
|
327
|
+
return {
|
|
328
|
+
amount0: amount1.dividedBy(price),
|
|
329
|
+
amount1: Web3Number.fromWei('0', 18),
|
|
330
|
+
ratio: Infinity
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const ratio = (sampleAmount0.multipliedBy(1e18).dividedBy(sampleAmount1.toString())).dividedBy(1e18);
|
|
336
|
+
logger.verbose(`${EkuboCLVault.name}: ${this.metadata.name} => ratio: ${ratio.toString()}`);
|
|
337
|
+
|
|
338
|
+
return this._solveExpectedAmountsEq(amount0, amount1, ratio, price);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private _solveExpectedAmountsEq(availableAmount0: Web3Number, availableAmount1: Web3Number, ratio: Web3Number, price: number) {
|
|
342
|
+
// (amount0 + x) / (amount1 - y) = ratio
|
|
343
|
+
// x = y * Py / Px ---- (1)
|
|
344
|
+
// => (amount0 + y * Py / Px) / (amount1 - y) = ratio
|
|
345
|
+
// => amount0 + y * Py / Px = ratio * (amount1 - y)
|
|
346
|
+
// => amount0 + y * Py / Px = ratio * amount1 - ratio * y
|
|
347
|
+
// => y * (ratio + Py/Px) = ratio * amount1 - amount0
|
|
348
|
+
// => y = (ratio * amount1 - amount0) / (ratio + Py/Px) ---- (2)
|
|
349
|
+
const y = ((ratio.multipliedBy(availableAmount1)).minus(availableAmount0)).dividedBy(ratio.plus(1 / price));
|
|
350
|
+
const x = y.dividedBy(price);
|
|
351
|
+
return {
|
|
352
|
+
amount0: availableAmount0.plus(x),
|
|
353
|
+
amount1: availableAmount1.minus(y),
|
|
354
|
+
ratio: Number(ratio.toString())
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async getSwapInfoToHandleUnused(considerRebalance: boolean = true) {
|
|
359
|
+
const poolKey = await this.getPoolKey();
|
|
360
|
+
|
|
361
|
+
// fetch current unused balances of vault
|
|
362
|
+
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
|
+
const token0Info = await Global.getTokenInfoFromAddr(poolKey.token0);
|
|
368
|
+
const token1Info = await Global.getTokenInfoFromAddr(poolKey.token1);
|
|
369
|
+
const token0Price = await this.pricer.getPrice(token0Info.symbol);
|
|
370
|
+
const token1Price = await this.pricer.getPrice(token1Info.symbol);
|
|
371
|
+
const token0PriceUsd = token0Price.price * Number(token0Bal1.toFixed(13));
|
|
372
|
+
const token1PriceUsd = token1Price.price * Number(token1Bal1.toFixed(13));
|
|
373
|
+
if (token0PriceUsd > 1 && token1PriceUsd > 1) {
|
|
374
|
+
// the swap is designed to handle one token only.
|
|
375
|
+
// i.e. all balance should be in one token
|
|
376
|
+
// except small amount of dust
|
|
377
|
+
// so we need to call handle_fees first, which will atleast use
|
|
378
|
+
// most of one token
|
|
379
|
+
throw new Error('Both tokens are non-zero and above $1, call handle_fees first');
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
let token0Bal = token0Bal1;
|
|
384
|
+
let token1Bal = token1Bal1;
|
|
385
|
+
|
|
386
|
+
// if rebalancing, consider whole TVL as available
|
|
387
|
+
if (considerRebalance) {
|
|
388
|
+
const tvl = await this.getTVL();
|
|
389
|
+
token0Bal = token0Bal.plus(tvl.token0.amount.toString());
|
|
390
|
+
token1Bal = token1Bal.plus(tvl.token1.amount.toString());
|
|
391
|
+
} else {
|
|
392
|
+
logger.verbose(`${EkuboCLVault.name}: getSwapInfoToHandleUnused => considerRebalance: false`);
|
|
393
|
+
}
|
|
394
|
+
logger.verbose(`${EkuboCLVault.name}: getSwapInfoToHandleUnused => token0Bal: ${token0Bal.toString()}, token1Bal: ${token1Bal.toString()}`);
|
|
395
|
+
|
|
396
|
+
// get expected amounts for liquidity
|
|
397
|
+
const newBounds = await this.getNewBounds();
|
|
398
|
+
logger.verbose(`${EkuboCLVault.name}: getSwapInfoToHandleUnused => newBounds: ${newBounds.lowerTick}, ${newBounds.upperTick}`);
|
|
399
|
+
let expectedAmounts = await this._getExpectedAmountsForLiquidity(token0Bal, token1Bal, newBounds);
|
|
400
|
+
logger.verbose(`${EkuboCLVault.name}: getSwapInfoToHandleUnused => expectedAmounts: ${expectedAmounts.amount0.toString()}, ${expectedAmounts.amount1.toString()}`);
|
|
401
|
+
|
|
402
|
+
// get swap info
|
|
403
|
+
// fetch avnu routes to ensure expected amounts
|
|
404
|
+
let retry = 0;
|
|
405
|
+
const maxRetry = 10;
|
|
406
|
+
while (retry < maxRetry) {
|
|
407
|
+
retry++;
|
|
408
|
+
// assert one token is increased and other is decreased
|
|
409
|
+
if (expectedAmounts.amount0.lessThan(token0Bal) && expectedAmounts.amount1.lessThan(token1Bal)) {
|
|
410
|
+
throw new Error('Both tokens are decreased, something is wrong');
|
|
411
|
+
}
|
|
412
|
+
if (expectedAmounts.amount0.greaterThan(token0Bal) && expectedAmounts.amount1.greaterThan(token1Bal)) {
|
|
413
|
+
throw new Error('Both tokens are increased, something is wrong');
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const tokenToSell = expectedAmounts.amount0.lessThan(token0Bal) ? poolKey.token0 : poolKey.token1;
|
|
417
|
+
const tokenToBuy = tokenToSell == poolKey.token0 ? poolKey.token1 : poolKey.token0;
|
|
418
|
+
let amountToSell = tokenToSell == poolKey.token0 ? token0Bal.minus(expectedAmounts.amount0) : token1Bal.minus(expectedAmounts.amount1);
|
|
419
|
+
const remainingSellAmount = tokenToSell == poolKey.token0 ? expectedAmounts.amount0 : expectedAmounts.amount1;
|
|
420
|
+
const tokenToBuyInfo = await Global.getTokenInfoFromAddr(tokenToBuy);
|
|
421
|
+
const expectedRatio = expectedAmounts.ratio;
|
|
422
|
+
|
|
423
|
+
logger.verbose(`${EkuboCLVault.name}: getSwapInfoToHandleUnused => tokenToSell: ${tokenToSell.address}, tokenToBuy: ${tokenToBuy.address}, amountToSell: ${amountToSell.toWei()}`);
|
|
424
|
+
logger.verbose(`${EkuboCLVault.name}: getSwapInfoToHandleUnused => remainingSellAmount: ${remainingSellAmount.toString()}`);
|
|
425
|
+
logger.verbose(`${EkuboCLVault.name}: getSwapInfoToHandleUnused => expectedRatio: ${expectedRatio}`);
|
|
426
|
+
|
|
427
|
+
const quote = await this.avnu.getQuotes(tokenToSell.address, tokenToBuy.address, amountToSell.toWei(), this.address.address);
|
|
428
|
+
if (remainingSellAmount.eq(0)) {
|
|
429
|
+
const minAmountOut = Web3Number.fromWei(quote.buyAmount.toString(), tokenToBuyInfo.decimals).multipliedBy(0.9999);
|
|
430
|
+
return await this.avnu.getSwapInfo(quote, this.address.address, 0, this.address.address, minAmountOut.toWei());
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const amountOut = Web3Number.fromWei(quote.buyAmount.toString(), tokenToBuyInfo.decimals);
|
|
434
|
+
const swapPrice = tokenToSell == poolKey.token0 ? amountOut.dividedBy(amountToSell) : amountToSell.dividedBy(amountOut);
|
|
435
|
+
const newRatio = tokenToSell == poolKey.token0 ? remainingSellAmount.dividedBy(token1Bal.plus(amountOut)) : token0Bal.plus(amountOut).dividedBy(remainingSellAmount);
|
|
436
|
+
logger.verbose(`${EkuboCLVault.name}: getSwapInfoToHandleUnused => amountOut: ${amountOut.toString()}`);
|
|
437
|
+
logger.verbose(`${EkuboCLVault.name}: getSwapInfoToHandleUnused => swapPrice: ${swapPrice.toString()}`);
|
|
438
|
+
logger.verbose(`${EkuboCLVault.name}: getSwapInfoToHandleUnused => newRatio: ${newRatio.toString()}`);
|
|
439
|
+
if (Number(newRatio.toString()) > expectedRatio * 1.0000001 || Number(newRatio.toString()) < expectedRatio * 0.9999999) {
|
|
440
|
+
expectedAmounts = await this._solveExpectedAmountsEq(token0Bal, token1Bal, new Web3Number(Number(expectedRatio).toFixed(13), 18), Number(swapPrice.toString()));
|
|
441
|
+
logger.verbose(`${EkuboCLVault.name}: getSwapInfoToHandleUnused => expectedAmounts: ${expectedAmounts.amount0.toString()}, ${expectedAmounts.amount1.toString()}`);
|
|
442
|
+
} else {
|
|
443
|
+
const minAmountOut = Web3Number.fromWei(quote.buyAmount.toString(), tokenToBuyInfo.decimals).multipliedBy(0.9999);
|
|
444
|
+
return await this.avnu.getSwapInfo(quote, this.address.address, 0, this.address.address, minAmountOut.toWei());
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
retry++;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
throw new Error('Failed to get swap info');
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
static tickToi129(tick: number) {
|
|
454
|
+
if (tick < 0) {
|
|
455
|
+
return {
|
|
456
|
+
mag: -tick,
|
|
457
|
+
sign: 1
|
|
458
|
+
};
|
|
459
|
+
} else {
|
|
460
|
+
return {
|
|
461
|
+
mag: tick,
|
|
462
|
+
sign: 0
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
static priceToSqrtRatio(price: number) {
|
|
468
|
+
return BigInt(Math.floor(Math.sqrt(price) * 10**9)) * BigInt(2 ** 128) / BigInt(1e9);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
static i129ToNumber(i129: { mag: bigint, sign: number }) {
|
|
472
|
+
return i129.mag * (i129.sign.toString() == "false" ? 1n : -1n);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
static tickToPrice(tick: bigint) {
|
|
476
|
+
return Math.pow(1.000001, Number(tick));
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async getLiquidityToAmounts(liquidity: Web3Number, bounds: EkuboBounds) {
|
|
480
|
+
const currentPrice = await this.getCurrentPrice();
|
|
481
|
+
const lowerPrice = await EkuboCLVault.tickToPrice(bounds.lowerTick);
|
|
482
|
+
const upperPrice = await EkuboCLVault.tickToPrice(bounds.upperTick);
|
|
483
|
+
logger.verbose(`${EkuboCLVault.name}: getLiquidityToAmounts => currentPrice: ${currentPrice.price}, lowerPrice: ${lowerPrice}, upperPrice: ${upperPrice}`);
|
|
484
|
+
const result: any = await this.ekuboMathContract.call('liquidity_delta_to_amount_delta', [
|
|
485
|
+
uint256.bnToUint256(EkuboCLVault.priceToSqrtRatio(currentPrice.price).toString()),
|
|
486
|
+
{
|
|
487
|
+
mag: liquidity.toWei(),
|
|
488
|
+
sign: 0
|
|
489
|
+
},
|
|
490
|
+
uint256.bnToUint256(EkuboCLVault.priceToSqrtRatio(lowerPrice).toString()),
|
|
491
|
+
uint256.bnToUint256(EkuboCLVault.priceToSqrtRatio(upperPrice).toString())
|
|
492
|
+
] as any);
|
|
493
|
+
const poolKey = await this.getPoolKey();
|
|
494
|
+
const token0Info = await Global.getTokenInfoFromAddr(poolKey.token0);
|
|
495
|
+
const token1Info = await Global.getTokenInfoFromAddr(poolKey.token1);
|
|
496
|
+
const amount0 = Web3Number.fromWei(EkuboCLVault.i129ToNumber(result.amount0).toString(), token0Info.decimals);
|
|
497
|
+
const amount1 = Web3Number.fromWei(EkuboCLVault.i129ToNumber(result.amount1).toString(), token1Info.decimals);
|
|
498
|
+
|
|
499
|
+
return {
|
|
500
|
+
amount0, amount1
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
const _description = 'Automatically rebalances liquidity near current price to maximize yield while reducing the necessity to manually rebalance positions frequently. Fees earn and Defi spring rewards are automatically re-invested.'
|
|
507
|
+
const _protocol: IProtocol = {name: 'Ekubo', logo: 'https://app.ekubo.org/favicon.ico'}
|
|
508
|
+
// need to fine tune better
|
|
509
|
+
const _riskFactor: RiskFactor[] = [
|
|
510
|
+
{type: RiskType.SMART_CONTRACT_RISK, value: 0.5, weight: 25},
|
|
511
|
+
{type: RiskType.IMPERMANENT_LOSS, value: 1, weight: 75}
|
|
512
|
+
]
|
|
513
|
+
/**
|
|
514
|
+
* Represents the Vesu Rebalance Strategies.
|
|
515
|
+
*/
|
|
516
|
+
export const EkuboCLVaultStrategies: IStrategyMetadata<CLVaultStrategySettings>[] = [{
|
|
517
|
+
name: 'Ekubo xSTRK/STRK',
|
|
518
|
+
description: _description,
|
|
519
|
+
address: ContractAddr.from('0x01f083b98674bc21effee29ef443a00c7b9a500fd92cf30341a3da12c73f2324'),
|
|
520
|
+
type: 'Other',
|
|
521
|
+
depositTokens: [Global.getDefaultTokens().find(t => t.symbol === 'STRK')!, Global.getDefaultTokens().find(t => t.symbol === 'xSTRK')!],
|
|
522
|
+
protocols: [_protocol],
|
|
523
|
+
maxTVL: Web3Number.fromWei('0', 18),
|
|
524
|
+
risk: {
|
|
525
|
+
riskFactor: _riskFactor,
|
|
526
|
+
netRisk: _riskFactor.reduce((acc, curr) => acc + curr.value * curr.weight, 0) / _riskFactor.reduce((acc, curr) => acc + curr.weight, 0),
|
|
527
|
+
},
|
|
528
|
+
additionalInfo: {
|
|
529
|
+
newBounds: {
|
|
530
|
+
lower: -1,
|
|
531
|
+
upper: 1
|
|
532
|
+
},
|
|
533
|
+
lstContract: ContractAddr.from('0x028d709c875c0ceac3dce7065bec5328186dc89fe254527084d1689910954b0a')
|
|
534
|
+
}
|
|
535
|
+
}]
|
package/src/strategies/index.ts
CHANGED