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