@strkfarm/sdk 2.0.0-dev.9 → 2.0.0-staging.2

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.
Files changed (64) hide show
  1. package/dist/index.browser.global.js +111371 -93151
  2. package/dist/index.browser.mjs +27815 -32690
  3. package/dist/index.d.ts +1095 -2011
  4. package/dist/index.js +27425 -32309
  5. package/dist/index.mjs +27590 -32452
  6. package/package.json +6 -5
  7. package/src/data/ekubo-price-fethcer.abi.json +265 -0
  8. package/src/data/universal-vault.abi.json +20 -135
  9. package/src/dataTypes/address.ts +0 -7
  10. package/src/dataTypes/index.ts +3 -2
  11. package/src/dataTypes/mynumber.ts +141 -0
  12. package/src/global.ts +296 -288
  13. package/src/index.browser.ts +6 -5
  14. package/src/interfaces/common.tsx +324 -184
  15. package/src/modules/apollo-client-config.ts +28 -0
  16. package/src/modules/avnu.ts +4 -17
  17. package/src/modules/ekubo-pricer.ts +79 -0
  18. package/src/modules/ekubo-quoter.ts +11 -88
  19. package/src/modules/erc20.ts +21 -67
  20. package/src/modules/harvests.ts +26 -15
  21. package/src/modules/index.ts +11 -13
  22. package/src/modules/lst-apr.ts +0 -36
  23. package/src/modules/pragma.ts +23 -8
  24. package/src/modules/pricer-from-api.ts +150 -14
  25. package/src/modules/pricer.ts +2 -1
  26. package/src/modules/pricerBase.ts +2 -1
  27. package/src/node/deployer.ts +36 -1
  28. package/src/node/pricer-redis.ts +2 -1
  29. package/src/strategies/autoCompounderStrk.ts +1 -1
  30. package/src/strategies/base-strategy.ts +5 -22
  31. package/src/strategies/ekubo-cl-vault.tsx +2904 -2175
  32. package/src/strategies/factory.ts +165 -0
  33. package/src/strategies/index.ts +10 -11
  34. package/src/strategies/registry.ts +268 -0
  35. package/src/strategies/sensei.ts +416 -292
  36. package/src/strategies/universal-adapters/adapter-utils.ts +1 -5
  37. package/src/strategies/universal-adapters/baseAdapter.ts +153 -181
  38. package/src/strategies/universal-adapters/common-adapter.ts +77 -98
  39. package/src/strategies/universal-adapters/index.ts +1 -5
  40. package/src/strategies/universal-adapters/vesu-adapter.ts +218 -220
  41. package/src/strategies/universal-adapters/vesu-supply-only-adapter.ts +51 -58
  42. package/src/strategies/universal-lst-muliplier-strategy.tsx +1952 -992
  43. package/src/strategies/universal-strategy.tsx +1713 -1150
  44. package/src/strategies/vesu-rebalance.tsx +1189 -986
  45. package/src/utils/health-factor-math.ts +5 -11
  46. package/src/utils/index.ts +8 -9
  47. package/src/utils/strategy-utils.ts +57 -0
  48. package/src/data/extended-deposit.abi.json +0 -3613
  49. package/src/modules/ExtendedWrapperSDk/index.ts +0 -62
  50. package/src/modules/ExtendedWrapperSDk/types.ts +0 -311
  51. package/src/modules/ExtendedWrapperSDk/wrapper.ts +0 -395
  52. package/src/modules/midas.ts +0 -159
  53. package/src/modules/token-market-data.ts +0 -202
  54. package/src/strategies/svk-strategy.ts +0 -247
  55. package/src/strategies/universal-adapters/adapter-optimizer.ts +0 -65
  56. package/src/strategies/universal-adapters/avnu-adapter.ts +0 -413
  57. package/src/strategies/universal-adapters/extended-adapter.ts +0 -972
  58. package/src/strategies/universal-adapters/unused-balance-adapter.ts +0 -109
  59. package/src/strategies/universal-adapters/vesu-multiply-adapter.ts +0 -1306
  60. package/src/strategies/vesu-extended-strategy/services/operationService.ts +0 -34
  61. package/src/strategies/vesu-extended-strategy/utils/config.runtime.ts +0 -77
  62. package/src/strategies/vesu-extended-strategy/utils/constants.ts +0 -49
  63. package/src/strategies/vesu-extended-strategy/utils/helper.ts +0 -370
  64. package/src/strategies/vesu-extended-strategy/vesu-extended-strategy.tsx +0 -1379
@@ -1,18 +1,33 @@
1
1
  import { ContractAddr, Web3Number } from "@/dataTypes";
2
2
  import {
3
- FAQ,
4
- FlowChartColors,
5
- getNoRiskTags,
6
- highlightTextWithLinks,
7
- IConfig,
8
- IInvestmentFlow,
9
- IProtocol,
10
- IStrategyMetadata,
11
- RiskFactor,
12
- RiskType
3
+ FAQ,
4
+ FlowChartColors,
5
+ getNoRiskTags,
6
+ highlightTextWithLinks,
7
+ IConfig,
8
+ IInvestmentFlow,
9
+ IProtocol,
10
+ IStrategyMetadata,
11
+ RiskFactor,
12
+ RiskType,
13
+ StrategyCategory,
14
+ StrategyTag,
15
+ StrategyLiveStatus,
16
+ StrategySettings,
17
+ AuditStatus,
18
+ SourceCodeType,
19
+ AccessControlType,
20
+ InstantWithdrawalVault
13
21
  } from "@/interfaces";
14
22
  import { AvnuWrapper, Pricer, SwapInfo } from "@/modules";
15
- import { Account, CairoCustomEnum, Contract, num, uint256 } from "starknet";
23
+ import {
24
+ Account,
25
+ CairoCustomEnum,
26
+ Contract,
27
+ num,
28
+ uint256,
29
+ BlockIdentifier
30
+ } from "starknet";
16
31
  import VesuRebalanceAbi from "@/data/vesu-rebalance.abi.json";
17
32
  import { Global } from "@/global";
18
33
  import { assert } from "@/utils";
@@ -20,48 +35,47 @@ import { logger } from "@/utils/logger";
20
35
  import axios from "axios";
21
36
  import { PricerBase } from "@/modules/pricerBase";
22
37
  import {
23
- APYInfo,
24
- BaseStrategy,
25
- SingleActionAmount,
26
- SingleTokenInfo
38
+ BaseStrategy,
39
+ SingleActionAmount,
40
+ SingleTokenInfo
27
41
  } from "./base-strategy";
28
42
  import { getAPIUsingHeadlessBrowser } from "@/node/headless";
29
- import { VESU_REWARDS_ENDPOINT, VesuHarvests } from "@/modules/harvests";
43
+ import { HarvestInfo, VesuHarvests } from "@/modules/harvests";
30
44
  import VesuPoolIDs from "@/data/vesu_pools.json";
31
45
  import { COMMON_CONTRACTS, ENDPOINTS } from "./constants";
32
46
 
33
47
  interface PoolProps {
34
- pool_id: ContractAddr;
35
- max_weight: number;
36
- v_token: ContractAddr;
48
+ pool_id: ContractAddr;
49
+ max_weight: number;
50
+ v_token: ContractAddr;
37
51
  }
38
52
 
39
53
  interface Change {
40
- pool_id: ContractAddr;
41
- changeAmt: Web3Number;
42
- finalAmt: Web3Number;
43
- isDeposit: boolean;
54
+ pool_id: ContractAddr;
55
+ changeAmt: Web3Number;
56
+ finalAmt: Web3Number;
57
+ isDeposit: boolean;
44
58
  }
45
59
 
46
60
  export interface VesuRebalanceSettings {
47
- feeBps: number;
61
+ feeBps: number;
48
62
  }
49
63
 
50
64
  interface PoolInfoFull {
51
- pool_id: ContractAddr;
52
- pool_name: string | undefined;
53
- max_weight: number;
54
- current_weight: number;
55
- v_token: ContractAddr;
56
- amount: Web3Number;
57
- usdValue: Web3Number;
58
- APY: {
59
- baseApy: number;
60
- defiSpringApy: number;
61
- netApy: number;
62
- };
63
- currentUtilization: number;
64
- maxUtilization: number;
65
+ pool_id: ContractAddr;
66
+ pool_name: string | undefined;
67
+ max_weight: number;
68
+ current_weight: number;
69
+ v_token: ContractAddr;
70
+ amount: Web3Number;
71
+ usdValue: Web3Number;
72
+ APY: {
73
+ baseApy: number;
74
+ defiSpringApy: number;
75
+ netApy: number;
76
+ };
77
+ currentUtilization: number;
78
+ maxUtilization: number;
65
79
  }
66
80
  /**
67
81
  * Represents a VesuRebalance strategy.
@@ -69,336 +83,510 @@ interface PoolInfoFull {
69
83
  * managing deposits and withdrawals while optimizing yield through STRK rewards.
70
84
  */
71
85
  export class VesuRebalance extends BaseStrategy<
72
- SingleTokenInfo,
73
- SingleActionAmount
86
+ SingleTokenInfo,
87
+ SingleActionAmount
74
88
  > {
75
- /** Contract address of the strategy */
76
- readonly address: ContractAddr;
77
- /** Pricer instance for token price calculations */
78
- readonly pricer: PricerBase;
79
- /** Metadata containing strategy information */
80
- readonly metadata: IStrategyMetadata<VesuRebalanceSettings>;
81
- /** Contract instance for interacting with the strategy */
82
- readonly contract: Contract;
83
- readonly BASE_WEIGHT = 10000; // 10000 bps = 100%
84
-
85
- /**
86
- * Creates a new VesuRebalance strategy instance.
87
- * @param config - Configuration object containing provider and other settings
88
- * @param pricer - Pricer instance for token price calculations
89
- * @param metadata - Strategy metadata including deposit tokens and address
90
- * @throws {Error} If more than one deposit token is specified
91
- */
92
- constructor(
93
- config: IConfig,
94
- pricer: PricerBase,
95
- metadata: IStrategyMetadata<VesuRebalanceSettings>
96
- ) {
97
- super(config);
98
- this.pricer = pricer;
99
-
100
- assert(
101
- metadata.depositTokens.length === 1,
102
- "VesuRebalance only supports 1 deposit token"
103
- );
104
- this.metadata = metadata;
105
- this.address = metadata.address;
106
-
107
- this.contract = new Contract({
108
- abi: VesuRebalanceAbi,
109
- address: this.address.address,
110
- providerOrAccount: this.config.provider
111
- });
112
- }
113
-
114
- /**
115
- * Creates a deposit call to the strategy contract.
116
- * @param assets - Amount of assets to deposit
117
- * @param receiver - Address that will receive the strategy tokens
118
- * @returns Populated contract call for deposit
119
- */
120
- async depositCall(amountInfo: SingleActionAmount, receiver: ContractAddr) {
121
- // Technically its not erc4626 abi, but we just need approve call
122
- // so, its ok to use it
123
- assert(
124
- amountInfo.tokenInfo.address.eq(this.asset().address),
125
- "Deposit token mismatch"
126
- );
127
- const assetContract = new Contract({
128
- abi: VesuRebalanceAbi,
129
- address: this.asset().address.address,
130
- providerOrAccount: this.config.provider
131
- });
132
- const call1 = assetContract.populate("approve", [
133
- this.address.address,
134
- uint256.bnToUint256(amountInfo.amount.toWei())
135
- ]);
136
- const call2 = this.contract.populate("deposit", [
137
- uint256.bnToUint256(amountInfo.amount.toWei()),
138
- receiver.address
139
- ]);
140
- return [call1, call2];
141
- }
142
-
143
- /**
144
- * Creates a withdrawal call to the strategy contract.
145
- * @param assets - Amount of assets to withdraw
146
- * @param receiver - Address that will receive the withdrawn assets
147
- * @param owner - Address that owns the strategy tokens
148
- * @returns Populated contract call for withdrawal
149
- */
150
- async withdrawCall(
151
- amountInfo: SingleActionAmount,
152
- receiver: ContractAddr,
153
- owner: ContractAddr
154
- ) {
155
- return [
156
- this.contract.populate("withdraw", [
157
- uint256.bnToUint256(amountInfo.amount.toWei()),
158
- receiver.address,
159
- owner.address
160
- ])
161
- ];
162
- }
163
-
164
- /**
165
- * Returns the underlying asset token of the strategy.
166
- * @returns The deposit token supported by this strategy
167
- */
168
- asset() {
169
- return this.metadata.depositTokens[0];
170
- }
171
-
172
- /**
173
- * Returns the number of decimals used by the strategy token.
174
- * @returns Number of decimals (same as the underlying token)
175
- */
176
- decimals() {
177
- return this.metadata.depositTokens[0].decimals; // same as underlying token
178
- }
179
-
180
- /**
181
- * Calculates the Total Value Locked (TVL) for a specific user.
182
- * @param user - Address of the user
183
- * @returns Object containing the amount in token units and USD value
184
- */
185
- async getUserTVL(user: ContractAddr) {
186
- const shares = await this.contract.balance_of(user.address);
187
- const assets = await this.contract.convert_to_assets(
188
- uint256.bnToUint256(shares)
189
- );
190
- const amount = Web3Number.fromWei(
191
- assets.toString(),
192
- this.metadata.depositTokens[0].decimals
193
- );
194
- let price = await this.pricer.getPrice(
195
- this.metadata.depositTokens[0].symbol
196
- );
197
- const usdValue = Number(amount.toFixed(6)) * price.price;
198
- return {
199
- tokenInfo: this.asset(),
200
- amount,
201
- usdValue
202
- };
203
- }
204
-
205
- /**
206
- * Calculates the total TVL of the strategy.
207
- * @returns Object containing the total amount in token units and USD value
208
- */
209
- async getTVL() {
210
- const assets = await this.contract.total_assets();
211
- const amount = Web3Number.fromWei(
212
- assets.toString(),
213
- this.metadata.depositTokens[0].decimals
214
- );
215
- let price = await this.pricer.getPrice(
216
- this.metadata.depositTokens[0].symbol
217
- );
218
- const usdValue = Number(amount.toFixed(6)) * price.price;
219
- return {
220
- tokenInfo: this.asset(),
221
- amount,
222
- usdValue
223
- };
224
- }
225
-
226
- static async getAllPossibleVerifiedPools(asset: ContractAddr) {
227
- const data = await getAPIUsingHeadlessBrowser(`${ENDPOINTS.VESU_BASE_STAGING}/pools`);
228
- const verifiedPools = data.data.filter((d: any) => d.isVerified);
229
- const pools = verifiedPools
230
- .map((p: any) => {
231
- const hasMyAsset = p.assets.find((a: any) => asset.eqString(a.address));
232
- if (hasMyAsset) {
233
- return {
234
- pool_id: ContractAddr.from(p.id),
235
- max_weight: 10000,
236
- v_token: ContractAddr.from(hasMyAsset.vToken.address),
237
- name: p.name
238
- };
239
- }
240
- return null;
241
- })
242
- .filter((p: PoolProps | null) => p !== null);
243
- return pools;
244
- }
245
-
246
- async getPoolInfo(
247
- p: PoolProps,
248
- pools: any[],
249
- vesuPositions: any[],
250
- totalAssets: Web3Number,
251
- isErrorPositionsAPI: boolean,
252
- isErrorPoolsAPI: boolean
253
- ) {
254
- const vesuPosition = vesuPositions.find(
255
- (d: any) =>
256
- ContractAddr.from(d.id).eq(p.pool_id)
257
- );
258
- const _pool = pools.find((d: any) => {
259
- logger.verbose(
260
- `pool check: ${
261
- ContractAddr.from(d.id).eq(p.pool_id)
262
- }, id: ${d.id}, pool_id: ${num.getDecimalString(
263
- p.pool_id.address.toString()
264
- )}`
265
- );
266
- return ContractAddr.from(d.id).eq(p.pool_id);
267
- });
268
- logger.verbose(`pool: ${JSON.stringify(_pool)}`);
269
- logger.verbose(typeof _pool);
270
- logger.verbose(`name: ${_pool?.name}`);
271
- const name = _pool?.name;
272
- logger.verbose(
273
- `name2: ${name}, ${!name ? true : false}, ${name?.length}, ${typeof name}`
274
- );
275
- const assetInfo = _pool?.assets.find((d: any) =>
276
- this.asset().address.eqString(d.address)
277
- );
278
- if (!name) {
279
- logger.verbose(`Pool not found`);
280
- throw new Error(`Pool name ${p.pool_id.address.toString()} not found`);
89
+ /** Contract address of the strategy */
90
+ readonly address: ContractAddr;
91
+ /** Pricer instance for token price calculations */
92
+ readonly pricer: PricerBase;
93
+ /** Metadata containing strategy information */
94
+ readonly metadata: IStrategyMetadata<VesuRebalanceSettings>;
95
+ /** Contract instance for interacting with the strategy */
96
+ readonly contract: Contract;
97
+ readonly BASE_WEIGHT = 10000; // 10000 bps = 100%
98
+
99
+ /**
100
+ * Creates a new VesuRebalance strategy instance.
101
+ * @param config - Configuration object containing provider and other settings
102
+ * @param pricer - Pricer instance for token price calculations
103
+ * @param metadata - Strategy metadata including deposit tokens and address
104
+ * @throws {Error} If more than one deposit token is specified
105
+ */
106
+ constructor(
107
+ config: IConfig,
108
+ pricer: PricerBase,
109
+ metadata: IStrategyMetadata<VesuRebalanceSettings>
110
+ ) {
111
+ super(config);
112
+ this.pricer = pricer;
113
+
114
+ assert(
115
+ metadata.depositTokens.length === 1,
116
+ "VesuRebalance only supports 1 deposit token"
117
+ );
118
+ this.metadata = metadata;
119
+ this.address = metadata.address;
120
+
121
+ this.contract = new Contract({
122
+ abi: VesuRebalanceAbi,
123
+ address: this.address.address,
124
+ providerOrAccount: this.config.provider
125
+ });
281
126
  }
282
- if (!assetInfo) {
283
- throw new Error(
284
- `Asset ${this.asset().address.toString()} not found in pool ${p.pool_id.address.toString()}`
285
- );
127
+
128
+ /**
129
+ * Creates a deposit call to the strategy contract.
130
+ * @param assets - Amount of assets to deposit
131
+ * @param receiver - Address that will receive the strategy tokens
132
+ * @returns Populated contract call for deposit
133
+ */
134
+ async depositCall(amountInfo: SingleActionAmount, receiver: ContractAddr) {
135
+ // Technically its not erc4626 abi, but we just need approve call
136
+ // so, its ok to use it
137
+ assert(
138
+ amountInfo.tokenInfo.address.eq(this.asset().address),
139
+ "Deposit token mismatch"
140
+ );
141
+ const assetContract = new Contract({
142
+ abi: VesuRebalanceAbi,
143
+ address: this.asset().address.address,
144
+ providerOrAccount: this.config.provider
145
+ });
146
+ const call1 = assetContract.populate("approve", [
147
+ this.address.address,
148
+ uint256.bnToUint256(amountInfo.amount.toWei())
149
+ ]);
150
+ const call2 = this.contract.populate("deposit", [
151
+ uint256.bnToUint256(amountInfo.amount.toWei()),
152
+ receiver.address
153
+ ]);
154
+ return [call1, call2];
286
155
  }
287
- let vTokenContract = new Contract({
288
- abi: VesuRebalanceAbi,
289
- address: p.v_token.address,
290
- providerOrAccount: this.config.provider
291
- });
292
- const bal = await vTokenContract.balance_of(this.address.address);
293
- const assets = await vTokenContract.convert_to_assets(
294
- uint256.bnToUint256(bal.toString())
295
- );
296
- logger.verbose(`Collateral: ${JSON.stringify(vesuPosition?.collateral)}`);
297
- logger.verbose(`supplyApy: ${JSON.stringify(assetInfo?.stats.supplyApy)}`);
298
- logger.verbose(
299
- `defiSpringSupplyApr: ${JSON.stringify(
300
- assetInfo?.stats.defiSpringSupplyApr
301
- )}`
302
- );
303
- logger.verbose(
304
- `currentUtilization: ${JSON.stringify(
305
- assetInfo?.stats.currentUtilization
306
- )}`
307
- );
308
- logger.verbose(
309
- `maxUtilization: ${JSON.stringify(assetInfo?.config.maxUtilization)}`
310
- );
311
- const item = {
312
- pool_id: p.pool_id,
313
- pool_name: _pool?.name,
314
- max_weight: p.max_weight,
315
- current_weight:
316
- isErrorPositionsAPI || !vesuPosition
317
- ? 0
318
- : Number(
319
- Web3Number.fromWei(vesuPosition.collateral.value, this.decimals())
320
- .dividedBy(totalAssets.toString())
321
- .toFixed(6)
322
- ),
323
- v_token: p.v_token,
324
- amount: Web3Number.fromWei(assets.toString(), this.decimals()),
325
- usdValue:
326
- isErrorPositionsAPI || !vesuPosition
327
- ? Web3Number.fromWei("0", this.decimals())
328
- : Web3Number.fromWei(
329
- vesuPosition.collateral.usdPrice.value,
330
- vesuPosition.collateral.usdPrice.decimals
331
- ),
332
- APY:
333
- isErrorPoolsAPI || !assetInfo
334
- ? {
335
- baseApy: 0,
336
- defiSpringApy: 0,
337
- netApy: 0
156
+
157
+ /**
158
+ * Creates a withdrawal call to the strategy contract.
159
+ * @param assets - Amount of assets to withdraw
160
+ * @param receiver - Address that will receive the withdrawn assets
161
+ * @param owner - Address that owns the strategy tokens
162
+ * @returns Populated contract call for withdrawal
163
+ */
164
+ async withdrawCall(
165
+ amountInfo: SingleActionAmount,
166
+ receiver: ContractAddr,
167
+ owner: ContractAddr
168
+ ) {
169
+ return [
170
+ this.contract.populate("withdraw", [
171
+ uint256.bnToUint256(amountInfo.amount.toWei()),
172
+ receiver.address,
173
+ owner.address
174
+ ])
175
+ ];
176
+ }
177
+
178
+ /**
179
+ * Returns the underlying asset token of the strategy.
180
+ * @returns The deposit token supported by this strategy
181
+ */
182
+ asset() {
183
+ return this.metadata.depositTokens[0];
184
+ }
185
+
186
+ /**
187
+ * Returns the number of decimals used by the strategy token.
188
+ * @returns Number of decimals (same as the underlying token)
189
+ */
190
+ decimals() {
191
+ return this.metadata.depositTokens[0].decimals; // same as underlying token
192
+ }
193
+
194
+ /**
195
+ * Calculates the Total Value Locked (TVL) for a specific user.
196
+ * @param user - Address of the user
197
+ * @param blockIdentifier - Block identifier for the query
198
+ * @returns Object containing the amount in token units and USD value
199
+ */
200
+ async getUserTVL(
201
+ user: ContractAddr,
202
+ blockIdentifier: BlockIdentifier = "latest"
203
+ ) {
204
+ const shares: any = await this.contract.call(
205
+ "balanceOf",
206
+ [user.address],
207
+ { blockIdentifier }
208
+ );
209
+ const assets: any = await this.contract.call(
210
+ "convert_to_assets",
211
+ [uint256.bnToUint256(shares)],
212
+ { blockIdentifier }
213
+ );
214
+ const amount = Web3Number.fromWei(
215
+ assets.toString(),
216
+ this.metadata.depositTokens[0].decimals
217
+ );
218
+
219
+ // Convert blockIdentifier to block number for pricer if it's a number
220
+ const blockNumber =
221
+ typeof blockIdentifier === "number" ||
222
+ typeof blockIdentifier === "bigint"
223
+ ? Number(blockIdentifier)
224
+ : undefined;
225
+
226
+ let price = await this.pricer.getPrice(
227
+ this.metadata.depositTokens[0].symbol,
228
+ blockNumber
229
+ );
230
+ const usdValue = Number(amount.toFixed(6)) * price.price;
231
+ return {
232
+ tokenInfo: this.asset(),
233
+ amount,
234
+ usdValue
235
+ };
236
+ }
237
+
238
+ /**
239
+ * Calculates user realized APY based on trueSharesBasedAPY method.
240
+ * Returns the APY as a number.
241
+ */
242
+ async getUserRealizedAPY(
243
+ blockIdentifier: BlockIdentifier = "latest",
244
+ sinceBlocks = 600000
245
+ ): Promise<number> {
246
+ logger.verbose(
247
+ `${VesuRebalance.name}: getUserRealizedAPY => starting with blockIdentifier=${blockIdentifier}, sinceBlocks=${sinceBlocks}`
248
+ );
249
+
250
+ // Determine current block number and timestamp
251
+ let blockNow =
252
+ typeof blockIdentifier === "number" ||
253
+ typeof blockIdentifier === "bigint"
254
+ ? Number(blockIdentifier)
255
+ : (await this.config.provider.getBlockLatestAccepted())
256
+ .block_number;
257
+ const blockNowTime =
258
+ typeof blockIdentifier === "number" ||
259
+ typeof blockIdentifier === "bigint"
260
+ ? (await this.config.provider.getBlockWithTxs(blockIdentifier))
261
+ .timestamp
262
+ : new Date().getTime() / 1000;
263
+
264
+ // Look back window, but never before launch block
265
+ const blockBefore = Math.max(
266
+ blockNow - sinceBlocks,
267
+ this.metadata.launchBlock
268
+ );
269
+
270
+ // TVL amounts (in underlying token units) and supply at current reference block
271
+ const assetsNowRaw: bigint = (await this.contract.call(
272
+ "total_assets",
273
+ [],
274
+ {
275
+ blockIdentifier
338
276
  }
339
- : {
340
- baseApy: Number(
341
- Web3Number.fromWei(
342
- assetInfo.stats.supplyApy.value,
343
- assetInfo.stats.supplyApy.decimals
344
- ).toFixed(6)
345
- ),
346
- defiSpringApy: assetInfo.stats.defiSpringSupplyApr
347
- ? Number(
348
- Web3Number.fromWei(
349
- assetInfo.stats.defiSpringSupplyApr.value,
350
- assetInfo.stats.defiSpringSupplyApr.decimals
351
- ).toFixed(6)
352
- )
353
- : 0,
354
- netApy: 0
355
- },
356
- currentUtilization:
357
- isErrorPoolsAPI || !assetInfo
358
- ? 0
359
- : Number(
360
- Web3Number.fromWei(
361
- assetInfo.stats.currentUtilization.value,
362
- assetInfo.stats.currentUtilization.decimals
363
- ).toFixed(6)
364
- ),
365
- maxUtilization:
366
- isErrorPoolsAPI || !assetInfo
367
- ? 0
368
- : Number(
369
- Web3Number.fromWei(
370
- assetInfo.config.maxUtilization.value,
371
- assetInfo.config.maxUtilization.decimals
372
- ).toFixed(6)
373
- )
374
- };
375
- item.APY.netApy = item.APY.baseApy + item.APY.defiSpringApy;
376
- return item;
377
- }
378
-
379
- /**
380
- * Retrieves the list of allowed pools and their detailed information from multiple sources:
381
- * 1. Contract's allowed pools
382
- * 2. Vesu positions API for current positions
383
- * 3. Vesu pools API for APY and utilization data
384
- *
385
- * @returns {Promise<{
386
- * data: Array<PoolInfoFull>,
387
- * isErrorPositionsAPI: boolean
388
- * }>} Object containing:
389
- * - data: Array of pool information including IDs, weights, amounts, APYs and utilization
390
- * - isErrorPositionsAPI: Boolean indicating if there was an error fetching position data
391
- */
392
- async getPools() {
393
- const allowedPools: PoolProps[] = (
394
- await this.contract.get_allowed_pools()
395
- ).map((p: any) => ({
396
- pool_id: ContractAddr.from(p.pool_id),
397
- max_weight: Number(p.max_weight) / this.BASE_WEIGHT,
398
- v_token: ContractAddr.from(p.v_token)
399
- }));
400
-
401
- /*
277
+ )) as bigint;
278
+ const amountNow = Web3Number.fromWei(
279
+ assetsNowRaw.toString(),
280
+ this.metadata.depositTokens[0].decimals
281
+ );
282
+
283
+ const supplyNowRaw: bigint = (await this.contract.call(
284
+ "total_supply",
285
+ [],
286
+ {
287
+ blockIdentifier
288
+ }
289
+ )) as bigint;
290
+ const supplyNow = Web3Number.fromWei(
291
+ supplyNowRaw.toString(),
292
+ this.metadata.depositTokens[0].decimals
293
+ );
294
+
295
+ // Historical TVL and supply
296
+ const assetsBeforeRaw: bigint = (await this.contract.call(
297
+ "total_assets",
298
+ [],
299
+ { blockIdentifier: blockBefore }
300
+ )) as bigint;
301
+ const amountBefore = Web3Number.fromWei(
302
+ assetsBeforeRaw.toString(),
303
+ this.metadata.depositTokens[0].decimals
304
+ );
305
+
306
+ const supplyBeforeRaw: bigint = (await this.contract.call(
307
+ "total_supply",
308
+ [],
309
+ { blockIdentifier: blockBefore }
310
+ )) as bigint;
311
+ const supplyBefore = Web3Number.fromWei(
312
+ supplyBeforeRaw.toString(),
313
+ this.metadata.depositTokens[0].decimals
314
+ );
315
+
316
+ const blockBeforeInfo = await this.config.provider.getBlockWithTxs(
317
+ blockBefore
318
+ );
319
+
320
+ const assetsPerShareNow = amountNow
321
+ .multipliedBy(1e18)
322
+ .dividedBy(supplyNow.toString());
323
+
324
+ const assetsPerShareBf = amountBefore
325
+ .multipliedBy(1e18)
326
+ .dividedBy(supplyBefore.toString());
327
+
328
+ const timeDiffSeconds = blockNowTime - blockBeforeInfo.timestamp;
329
+
330
+ logger.verbose(
331
+ `${VesuRebalance.name}:${
332
+ this.metadata.name
333
+ } [getUserRealizedAPY] assetsNow: ${amountNow.toString()}`
334
+ );
335
+ logger.verbose(
336
+ `${VesuRebalance.name}:${
337
+ this.metadata.name
338
+ } [getUserRealizedAPY] assetsBefore: ${amountBefore.toString()}`
339
+ );
340
+ logger.verbose(
341
+ `${VesuRebalance.name}:${
342
+ this.metadata.name
343
+ } [getUserRealizedAPY] assetsPerShareNow: ${assetsPerShareNow.toString()}`
344
+ );
345
+ logger.verbose(
346
+ `${VesuRebalance.name}:${
347
+ this.metadata.name
348
+ } [getUserRealizedAPY] assetsPerShareBf: ${assetsPerShareBf.toString()}`
349
+ );
350
+ logger.verbose(
351
+ `${VesuRebalance.name}:${
352
+ this.metadata.name
353
+ } [getUserRealizedAPY] Supply before: ${supplyBefore.toString()}`
354
+ );
355
+ logger.verbose(
356
+ `${VesuRebalance.name}:${
357
+ this.metadata.name
358
+ } [getUserRealizedAPY] Supply now: ${supplyNow.toString()}`
359
+ );
360
+ logger.verbose(
361
+ `${VesuRebalance.name}:${this.metadata.name} [getUserRealizedAPY] Time diff in seconds: ${timeDiffSeconds}`
362
+ );
363
+
364
+ const apyForGivenBlocks =
365
+ Number(
366
+ assetsPerShareNow
367
+ .minus(assetsPerShareBf)
368
+ .multipliedBy(10000)
369
+ .dividedBy(assetsPerShareBf)
370
+ ) / 10000;
371
+
372
+ return (apyForGivenBlocks * (365 * 24 * 3600)) / timeDiffSeconds;
373
+ }
374
+
375
+ /**
376
+ * Calculates the total TVL of the strategy.
377
+ * @returns Object containing the total amount in token units and USD value
378
+ */
379
+ async getTVL(): Promise<SingleTokenInfo> {
380
+ const assets = await this.contract.total_assets();
381
+ const amount = Web3Number.fromWei(
382
+ assets.toString(),
383
+ this.metadata.depositTokens[0].decimals
384
+ );
385
+ let price = await this.pricer.getPrice(
386
+ this.metadata.depositTokens[0].symbol
387
+ );
388
+ const usdValue = Number(amount.toFixed(6)) * price.price;
389
+ return {
390
+ tokenInfo: this.asset(),
391
+ amount,
392
+ usdValue
393
+ };
394
+ }
395
+
396
+ static async getAllPossibleVerifiedPools(asset: ContractAddr) {
397
+ const data = await getAPIUsingHeadlessBrowser(
398
+ `${ENDPOINTS.VESU_BASE_STAGING}/pools`
399
+ );
400
+ const verifiedPools = data.data.filter((d: any) => d.isVerified);
401
+ const pools = verifiedPools
402
+ .map((p: any) => {
403
+ const hasMyAsset = p.assets.find((a: any) =>
404
+ asset.eqString(a.address)
405
+ );
406
+ if (hasMyAsset) {
407
+ return {
408
+ pool_id: ContractAddr.from(p.id),
409
+ max_weight: 10000,
410
+ v_token: ContractAddr.from(hasMyAsset.vToken.address),
411
+ name: p.name
412
+ };
413
+ }
414
+ return null;
415
+ })
416
+ .filter((p: PoolProps | null) => p !== null);
417
+ return pools;
418
+ }
419
+
420
+ async getPoolInfo(
421
+ p: PoolProps,
422
+ pools: any[],
423
+ vesuPositions: any[],
424
+ totalAssets: Web3Number,
425
+ isErrorPositionsAPI: boolean,
426
+ isErrorPoolsAPI: boolean
427
+ ) {
428
+ const vesuPosition = vesuPositions.find((d: any) =>
429
+ ContractAddr.from(d.id).eq(p.pool_id)
430
+ );
431
+ const _pool = pools.find((d: any) => {
432
+ logger.verbose(
433
+ `pool check: ${ContractAddr.from(d.id).eq(p.pool_id)}, id: ${
434
+ d.id
435
+ }, pool_id: ${num.getDecimalString(
436
+ p.pool_id.address.toString()
437
+ )}`
438
+ );
439
+ return ContractAddr.from(d.id).eq(p.pool_id);
440
+ });
441
+ logger.verbose(`pool: ${JSON.stringify(_pool)}`);
442
+ logger.verbose(typeof _pool);
443
+ logger.verbose(`name: ${_pool?.name}`);
444
+ const name = _pool?.name;
445
+ logger.verbose(
446
+ `name2: ${name}, ${!name ? true : false}, ${
447
+ name?.length
448
+ }, ${typeof name}`
449
+ );
450
+ const assetInfo = _pool?.assets.find((d: any) =>
451
+ this.asset().address.eqString(d.address)
452
+ );
453
+ if (!name) {
454
+ logger.verbose(`Pool not found`);
455
+ throw new Error(
456
+ `Pool name ${p.pool_id.address.toString()} not found`
457
+ );
458
+ }
459
+ if (!assetInfo) {
460
+ throw new Error(
461
+ `Asset ${this.asset().address.toString()} not found in pool ${p.pool_id.address.toString()}`
462
+ );
463
+ }
464
+ let vTokenContract = new Contract({
465
+ abi: VesuRebalanceAbi,
466
+ address: p.v_token.address,
467
+ providerOrAccount: this.config.provider
468
+ });
469
+ const bal = await vTokenContract.balanceOf(this.address.address);
470
+ const assets = await vTokenContract.convert_to_assets(
471
+ uint256.bnToUint256(bal.toString())
472
+ );
473
+ logger.verbose(
474
+ `Collateral: ${JSON.stringify(vesuPosition?.collateral)}`
475
+ );
476
+ logger.verbose(
477
+ `supplyApy: ${JSON.stringify(assetInfo?.stats.supplyApy)}`
478
+ );
479
+ logger.verbose(
480
+ `defiSpringSupplyApr: ${JSON.stringify(
481
+ assetInfo?.stats.defiSpringSupplyApr
482
+ )}`
483
+ );
484
+ logger.verbose(
485
+ `currentUtilization: ${JSON.stringify(
486
+ assetInfo?.stats.currentUtilization
487
+ )}`
488
+ );
489
+ logger.verbose(
490
+ `maxUtilization: ${JSON.stringify(
491
+ assetInfo?.config.maxUtilization
492
+ )}`
493
+ );
494
+ const item = {
495
+ pool_id: p.pool_id,
496
+ pool_name: _pool?.name,
497
+ max_weight: p.max_weight,
498
+ current_weight:
499
+ isErrorPositionsAPI || !vesuPosition
500
+ ? 0
501
+ : Number(
502
+ Web3Number.fromWei(
503
+ vesuPosition.collateral.value,
504
+ this.decimals()
505
+ )
506
+ .dividedBy(totalAssets.toString())
507
+ .toFixed(6)
508
+ ),
509
+ v_token: p.v_token,
510
+ amount: Web3Number.fromWei(assets.toString(), this.decimals()),
511
+ usdValue:
512
+ isErrorPositionsAPI || !vesuPosition
513
+ ? Web3Number.fromWei("0", this.decimals())
514
+ : Web3Number.fromWei(
515
+ vesuPosition.collateral.usdPrice.value,
516
+ vesuPosition.collateral.usdPrice.decimals
517
+ ),
518
+ APY:
519
+ isErrorPoolsAPI || !assetInfo
520
+ ? {
521
+ baseApy: 0,
522
+ defiSpringApy: 0,
523
+ netApy: 0
524
+ }
525
+ : {
526
+ baseApy: Number(
527
+ Web3Number.fromWei(
528
+ assetInfo.stats.supplyApy.value,
529
+ assetInfo.stats.supplyApy.decimals
530
+ ).toFixed(6)
531
+ ),
532
+ defiSpringApy: assetInfo.stats.defiSpringSupplyApr
533
+ ? Number(
534
+ Web3Number.fromWei(
535
+ assetInfo.stats.defiSpringSupplyApr
536
+ .value,
537
+ assetInfo.stats.defiSpringSupplyApr
538
+ .decimals
539
+ ).toFixed(6)
540
+ )
541
+ : 0,
542
+ netApy: 0
543
+ },
544
+ currentUtilization:
545
+ isErrorPoolsAPI || !assetInfo
546
+ ? 0
547
+ : Number(
548
+ Web3Number.fromWei(
549
+ assetInfo.stats.currentUtilization.value,
550
+ assetInfo.stats.currentUtilization.decimals
551
+ ).toFixed(6)
552
+ ),
553
+ maxUtilization:
554
+ isErrorPoolsAPI || !assetInfo
555
+ ? 0
556
+ : Number(
557
+ Web3Number.fromWei(
558
+ assetInfo.config.maxUtilization.value,
559
+ assetInfo.config.maxUtilization.decimals
560
+ ).toFixed(6)
561
+ )
562
+ };
563
+ item.APY.netApy = item.APY.baseApy + item.APY.defiSpringApy;
564
+ return item;
565
+ }
566
+
567
+ /**
568
+ * Retrieves the list of allowed pools and their detailed information from multiple sources:
569
+ * 1. Contract's allowed pools
570
+ * 2. Vesu positions API for current positions
571
+ * 3. Vesu pools API for APY and utilization data
572
+ *
573
+ * @returns {Promise<{
574
+ * data: Array<PoolInfoFull>,
575
+ * isErrorPositionsAPI: boolean
576
+ * }>} Object containing:
577
+ * - data: Array of pool information including IDs, weights, amounts, APYs and utilization
578
+ * - isErrorPositionsAPI: Boolean indicating if there was an error fetching position data
579
+ */
580
+ async getPools() {
581
+ const allowedPools: PoolProps[] = (
582
+ await this.contract.get_allowed_pools()
583
+ ).map((p: any) => ({
584
+ pool_id: ContractAddr.from(p.pool_id),
585
+ max_weight: Number(p.max_weight) / this.BASE_WEIGHT,
586
+ v_token: ContractAddr.from(p.v_token)
587
+ }));
588
+
589
+ /*
402
590
  Vesu Positions API Response Schema (/positions?walletAddress=):
403
591
  {
404
592
  "data": [{
@@ -445,674 +633,689 @@ export class VesuRebalance extends BaseStrategy<
445
633
  }]
446
634
  }
447
635
  */
448
- let isErrorPositionsAPI = false;
449
- let vesuPositions: any[] = [];
450
- try {
451
- const data = await getAPIUsingHeadlessBrowser(
452
- `https://staging.api.vesu.xyz/positions?walletAddress=${this.address.address}`
453
- );
454
- vesuPositions = data.data;
455
- } catch (e) {
456
- console.error(
457
- `${VesuRebalance.name}: Error fetching positions for ${this.address.address}`,
458
- e
459
- );
460
- isErrorPositionsAPI = true;
461
- }
636
+ let isErrorPositionsAPI = false;
637
+ let vesuPositions: any[] = [];
638
+ try {
639
+ const data = await getAPIUsingHeadlessBrowser(
640
+ `https://staging.api.vesu.xyz/positions?walletAddress=${this.address.address}`
641
+ );
642
+ vesuPositions = data.data;
643
+ } catch (e) {
644
+ console.error(
645
+ `${VesuRebalance.name}: Error fetching positions for ${this.address.address}`,
646
+ e
647
+ );
648
+ isErrorPositionsAPI = true;
649
+ }
462
650
 
463
- let { pools, isErrorPoolsAPI } = await this.getVesuPools();
651
+ let { pools, isErrorPoolsAPI } = await this.getVesuPools();
464
652
 
465
- const totalAssets = (await this.getTVL()).amount;
653
+ const totalAssets = (await this.getTVL()).amount;
466
654
 
467
- const info = allowedPools.map((p) =>
468
- this.getPoolInfo(
469
- p,
470
- pools,
471
- vesuPositions,
472
- totalAssets,
473
- isErrorPositionsAPI,
474
- isErrorPoolsAPI
475
- )
476
- );
477
- const data = await Promise.all(info);
478
- return {
479
- data,
480
- isErrorPositionsAPI,
481
- isErrorPoolsAPI,
482
- isError: isErrorPositionsAPI || isErrorPoolsAPI
483
- };
484
- }
485
-
486
- async getVesuPools(
487
- retry = 0
488
- ): Promise<{ pools: any[]; isErrorPoolsAPI: boolean }> {
489
- let isErrorPoolsAPI = false;
490
- let pools: any[] = [];
491
- try {
492
- const data = await getAPIUsingHeadlessBrowser(
493
- `${ENDPOINTS.VESU_BASE_STAGING}/pools`
494
- );
495
- pools = data.data;
496
-
497
- // Vesu API is unstable sometimes, some Pools may be missing sometimes
498
- for (const pool of VesuPoolIDs.data) {
499
- const found = pools.find((d: any) => ContractAddr.from(d.id).eqString(pool.id));
500
- if (!found) {
501
- logger.verbose(`VesuRebalance: pools: ${JSON.stringify(pools)}`);
502
- logger.verbose(
503
- `VesuRebalance: Pool ${pool.id} not found in Vesu API, using hardcoded data`
504
- );
505
- throw new Error(`pool not found [sanity check]: ${pool.id}`);
655
+ const info = allowedPools.map((p) =>
656
+ this.getPoolInfo(
657
+ p,
658
+ pools,
659
+ vesuPositions,
660
+ totalAssets,
661
+ isErrorPositionsAPI,
662
+ isErrorPoolsAPI
663
+ )
664
+ );
665
+ const data = await Promise.all(info);
666
+ return {
667
+ data,
668
+ isErrorPositionsAPI,
669
+ isErrorPoolsAPI,
670
+ isError: isErrorPositionsAPI || isErrorPoolsAPI
671
+ };
672
+ }
673
+
674
+ async getVesuPools(
675
+ retry = 0
676
+ ): Promise<{ pools: any[]; isErrorPoolsAPI: boolean }> {
677
+ let isErrorPoolsAPI = false;
678
+ let pools: any[] = [];
679
+ try {
680
+ const data = await getAPIUsingHeadlessBrowser(
681
+ `${ENDPOINTS.VESU_BASE_STAGING}/pools`
682
+ );
683
+ pools = data.data;
684
+
685
+ // Vesu API is unstable sometimes, some Pools may be missing sometimes
686
+ for (const pool of VesuPoolIDs.data) {
687
+ const found = pools.find((d: any) =>
688
+ ContractAddr.from(d.id).eqString(pool.id)
689
+ );
690
+ if (!found) {
691
+ logger.verbose(
692
+ `VesuRebalance: pools: ${JSON.stringify(pools)}`
693
+ );
694
+ logger.verbose(
695
+ `VesuRebalance: Pool ${pool.id} not found in Vesu API, using hardcoded data`
696
+ );
697
+ throw new Error(
698
+ `pool not found [sanity check]: ${pool.id}`
699
+ );
700
+ }
701
+ }
702
+ } catch (e) {
703
+ logger.error(
704
+ `${VesuRebalance.name}: Error fetching pools for ${this.address.address}, retry ${retry}`,
705
+ e
706
+ );
707
+ isErrorPoolsAPI = true;
708
+ if (retry < 10) {
709
+ await new Promise((resolve) =>
710
+ setTimeout(resolve, 5000 * (retry + 1))
711
+ );
712
+ return await this.getVesuPools(retry + 1);
713
+ }
506
714
  }
507
- }
508
- } catch (e) {
509
- logger.error(
510
- `${VesuRebalance.name}: Error fetching pools for ${this.address.address}, retry ${retry}`,
511
- e
512
- );
513
- isErrorPoolsAPI = true;
514
- if (retry < 10) {
515
- await new Promise((resolve) => setTimeout(resolve, 5000 * (retry + 1)));
516
- return await this.getVesuPools(retry + 1);
517
- }
715
+
716
+ return { pools, isErrorPoolsAPI };
518
717
  }
519
718
 
520
- return { pools, isErrorPoolsAPI };
521
- }
719
+ /**
720
+ * Calculates the weighted average APY across all pools based on USD value.
721
+ * @returns {Promise<number>} The weighted average APY across all pools
722
+ */
723
+ async netAPY(): Promise<number> {
724
+ const { data: pools } = await this.getPools();
725
+ return this.netAPYGivenPools(pools);
726
+ }
522
727
 
523
- /**
524
- * Calculates the weighted average APY across all pools based on USD value.
525
- * @returns {Promise<number>} The weighted average APY across all pools
526
- */
527
- async netAPY(): Promise<APYInfo > {
528
- const { data: pools } = await this.getPools();
529
- return {
530
- net: await this.netAPYGivenPools(pools),
531
- splits: []
532
- };
533
- }
534
-
535
- /**
536
- * Calculates the weighted average APY across all pools based on USD value.
537
- * @returns {Promise<number>} The weighted average APY across all pools
538
- */
539
- async netAPYGivenPools(pools: PoolInfoFull[]): Promise<number> {
540
- const weightedApyNumerator = pools.reduce((acc: number, curr) => {
541
- const weight = curr.current_weight;
542
- return acc + curr.APY.netApy * Number(curr.amount.toString());
543
- }, 0);
544
- const totalAssets = (await this.getTVL()).amount;
545
- const weightedApy = weightedApyNumerator / Number(totalAssets.toString());
546
- return weightedApy * (1 - this.metadata.additionalInfo.feeBps / 10000);
547
- }
548
-
549
- /**
550
- * Calculates optimal position changes to maximize APY while respecting max weights.
551
- * The algorithm:
552
- * 1. Sorts pools by APY (highest first)
553
- * 2. Calculates target amounts based on max weights
554
- * 3. For each pool that needs more funds:
555
- * - Takes funds from lowest APY pools that are over their target
556
- * 4. Validates that total assets remain constant
557
- *
558
- * @returns {Promise<{
559
- * changes: Change[],
560
- * finalPools: PoolInfoFull[],
561
- * isAnyPoolOverMaxWeight: boolean
562
- * }>} Object containing:
563
- * - changes: Array of position changes
564
- * - finalPools: Array of pool information after rebalance
565
- * @throws Error if rebalance is not possible while maintaining constraints
566
- */
567
- async getRebalancedPositions(_pools?: PoolInfoFull[]) {
568
- logger.verbose(`VesuRebalance: getRebalancedPositions`);
569
- if (!_pools) {
570
- const { data: _pools2 } = await this.getPools();
571
- _pools = _pools2;
728
+ /**
729
+ * Calculates the weighted average APY across all pools based on USD value.
730
+ * @returns {Promise<number>} The weighted average APY across all pools
731
+ */
732
+ async netAPYGivenPools(pools: PoolInfoFull[]): Promise<number> {
733
+ const weightedApyNumerator = pools.reduce((acc: number, curr) => {
734
+ const weight = curr.current_weight;
735
+ return acc + curr.APY.netApy * Number(curr.amount.toString());
736
+ }, 0);
737
+ const totalAssets = (await this.getTVL()).amount;
738
+ const weightedApy =
739
+ weightedApyNumerator / Number(totalAssets.toString());
740
+ return weightedApy * (1 - this.metadata.additionalInfo.feeBps / 10000);
572
741
  }
573
- const feeDeductions = await this.getFee(_pools);
574
- logger.verbose(
575
- `VesuRebalance: feeDeductions: ${JSON.stringify(feeDeductions)}`
576
- );
577
742
 
578
- // remove fee from pools
579
- const pools = _pools.map((p) => {
580
- const fee =
581
- feeDeductions.find((f) => p.v_token.eq(f.vToken))?.fee ||
582
- Web3Number.fromWei("0", this.decimals());
583
- logger.verbose(
584
- `FeeAdjustment: ${
585
- p.pool_id
586
- } => ${fee.toString()}, amt: ${p.amount.toString()}`
587
- );
588
- return {
589
- ...p,
590
- amount: p.amount.minus(fee)
591
- };
592
- });
593
- let totalAssets = (await this.getTVL()).amount;
594
- if (totalAssets.eq(0))
595
- return {
596
- changes: [],
597
- finalPools: []
598
- };
599
- // deduct fee from total assets
600
- feeDeductions.forEach((f) => {
601
- totalAssets = totalAssets.minus(f.fee);
602
- });
603
-
604
- // assert sum of pools.amount <= totalAssets
605
- const sumPools = pools.reduce(
606
- (acc, curr) => acc.plus(curr.amount.toString()),
607
- Web3Number.fromWei("0", this.decimals())
608
- );
609
- logger.verbose(`Sum of pools: ${sumPools.toString()}`);
610
- logger.verbose(`Total assets: ${totalAssets.toString()}`);
611
- assert(
612
- sumPools.lte(totalAssets.multipliedBy(1.00001).toString()),
613
- "Sum of pools.amount must be less than or equal to totalAssets"
614
- );
743
+ /**
744
+ * Calculates optimal position changes to maximize APY while respecting max weights.
745
+ * The algorithm:
746
+ * 1. Sorts pools by APY (highest first)
747
+ * 2. Calculates target amounts based on max weights
748
+ * 3. For each pool that needs more funds:
749
+ * - Takes funds from lowest APY pools that are over their target
750
+ * 4. Validates that total assets remain constant
751
+ *
752
+ * @returns {Promise<{
753
+ * changes: Change[],
754
+ * finalPools: PoolInfoFull[],
755
+ * isAnyPoolOverMaxWeight: boolean
756
+ * }>} Object containing:
757
+ * - changes: Array of position changes
758
+ * - finalPools: Array of pool information after rebalance
759
+ * @throws Error if rebalance is not possible while maintaining constraints
760
+ */
761
+ async getRebalancedPositions(_pools?: PoolInfoFull[]) {
762
+ logger.verbose(`VesuRebalance: getRebalancedPositions`);
763
+ if (!_pools) {
764
+ const { data: _pools2 } = await this.getPools();
765
+ _pools = _pools2;
766
+ }
767
+ const feeDeductions = await this.getFee(_pools);
768
+ logger.verbose(
769
+ `VesuRebalance: feeDeductions: ${JSON.stringify(feeDeductions)}`
770
+ );
771
+
772
+ // remove fee from pools
773
+ const pools = _pools.map((p) => {
774
+ const fee =
775
+ feeDeductions.find((f) => p.v_token.eq(f.vToken))?.fee ||
776
+ Web3Number.fromWei("0", this.decimals());
777
+ logger.verbose(
778
+ `FeeAdjustment: ${
779
+ p.pool_id
780
+ } => ${fee.toString()}, amt: ${p.amount.toString()}`
781
+ );
782
+ return {
783
+ ...p,
784
+ amount: p.amount.minus(fee)
785
+ };
786
+ });
787
+ let totalAssets = (await this.getTVL()).amount;
788
+ if (totalAssets.eq(0))
789
+ return {
790
+ changes: [],
791
+ finalPools: []
792
+ };
793
+ // deduct fee from total assets
794
+ feeDeductions.forEach((f) => {
795
+ totalAssets = totalAssets.minus(f.fee);
796
+ });
797
+
798
+ // assert sum of pools.amount <= totalAssets
799
+ const sumPools = pools.reduce(
800
+ (acc, curr) => acc.plus(curr.amount.toString()),
801
+ Web3Number.fromWei("0", this.decimals())
802
+ );
803
+ logger.verbose(`Sum of pools: ${sumPools.toString()}`);
804
+ logger.verbose(`Total assets: ${totalAssets.toString()}`);
805
+ assert(
806
+ sumPools.lte(totalAssets.multipliedBy(1.00001).toString()),
807
+ "Sum of pools.amount must be less than or equal to totalAssets"
808
+ );
809
+
810
+ // Sort pools by APY and calculate target amounts
811
+ const sortedPools = [...pools].sort(
812
+ (a, b) => b.APY.netApy - a.APY.netApy
813
+ );
814
+ const targetAmounts: Record<string, Web3Number> = {};
815
+ let remainingAssets = totalAssets;
816
+ logger.verbose(`Remaining assets: ${remainingAssets.toString()}`);
817
+
818
+ // First pass: Allocate to high APY pools up to their max weight
819
+ let isAnyPoolOverMaxWeight = false;
820
+ for (const pool of sortedPools) {
821
+ const maxAmount = totalAssets.multipliedBy(pool.max_weight * 0.98); // some tolerance
822
+ const targetAmount = remainingAssets.gte(maxAmount)
823
+ ? maxAmount
824
+ : remainingAssets;
825
+ logger.verbose(`Target amount: ${targetAmount.toString()}`);
826
+ logger.verbose(`Remaining assets: ${remainingAssets.toString()}`);
827
+ logger.verbose(`Max amount: ${maxAmount.toString()}`);
828
+ logger.verbose(`pool.max_weight: ${pool.max_weight}`);
829
+ targetAmounts[pool.pool_id.address.toString()] = targetAmount;
830
+ remainingAssets = remainingAssets.minus(targetAmount.toString());
831
+ if (pool.current_weight > pool.max_weight) {
832
+ isAnyPoolOverMaxWeight = true;
833
+ }
834
+ }
835
+
836
+ assert(remainingAssets.lt(0.00001), "Remaining assets must be 0");
837
+
838
+ // Calculate required changes
839
+ const changes: Change[] = sortedPools.map((pool) => {
840
+ const target =
841
+ targetAmounts[pool.pool_id.address.toString()] ||
842
+ Web3Number.fromWei("0", this.decimals());
843
+ const change = Web3Number.fromWei(
844
+ target.minus(pool.amount.toString()).toWei(),
845
+ this.decimals()
846
+ );
847
+ return {
848
+ pool_id: pool.pool_id,
849
+ changeAmt: change,
850
+ finalAmt: target,
851
+ isDeposit: change.gt(0)
852
+ };
853
+ });
854
+
855
+ logger.verbose(`Changes: ${JSON.stringify(changes)}`);
856
+ // Validate changes
857
+ const sumChanges = changes.reduce(
858
+ (sum, c) => sum.plus(c.changeAmt.toString()),
859
+ Web3Number.fromWei("0", this.decimals())
860
+ );
861
+ const sumFinal = changes.reduce(
862
+ (sum, c) => sum.plus(c.finalAmt.toString()),
863
+ Web3Number.fromWei("0", this.decimals())
864
+ );
865
+ const hasChanges = changes.some((c) => !c.changeAmt.eq(0));
615
866
 
616
- // Sort pools by APY and calculate target amounts
617
- const sortedPools = [...pools].sort((a, b) => b.APY.netApy - a.APY.netApy);
618
- const targetAmounts: Record<string, Web3Number> = {};
619
- let remainingAssets = totalAssets;
620
- logger.verbose(`Remaining assets: ${remainingAssets.toString()}`);
621
-
622
- // First pass: Allocate to high APY pools up to their max weight
623
- let isAnyPoolOverMaxWeight = false;
624
- for (const pool of sortedPools) {
625
- const maxAmount = totalAssets.multipliedBy(pool.max_weight * 0.98); // some tolerance
626
- const targetAmount = remainingAssets.gte(maxAmount)
627
- ? maxAmount
628
- : remainingAssets;
629
- logger.verbose(`Target amount: ${targetAmount.toString()}`);
630
- logger.verbose(`Remaining assets: ${remainingAssets.toString()}`);
631
- logger.verbose(`Max amount: ${maxAmount.toString()}`);
632
- logger.verbose(`pool.max_weight: ${pool.max_weight}`);
633
- targetAmounts[pool.pool_id.address.toString()] = targetAmount;
634
- remainingAssets = remainingAssets.minus(targetAmount.toString());
635
- if (pool.current_weight > pool.max_weight) {
636
- isAnyPoolOverMaxWeight = true;
637
- }
867
+ logger.verbose(`Sum of changes: ${sumChanges.toString()}`);
868
+ if (!sumChanges.eq(0)) throw new Error("Sum of changes must be zero");
869
+ logger.verbose(`Sum of final: ${sumFinal.toString()}`);
870
+ logger.verbose(`Total assets: ${totalAssets.toString()}`);
871
+ if (!sumFinal.eq(totalAssets.toString()))
872
+ throw new Error("Sum of final amounts must equal total assets");
873
+ if (!hasChanges) throw new Error("No changes required");
874
+
875
+ const finalPools: PoolInfoFull[] = pools.map((p) => {
876
+ const target =
877
+ targetAmounts[p.pool_id.address.toString()] ||
878
+ Web3Number.fromWei("0", this.decimals());
879
+ return {
880
+ ...p,
881
+ amount: target,
882
+ usdValue: Web3Number.fromWei("0", this.decimals())
883
+ };
884
+ });
885
+ return {
886
+ changes,
887
+ finalPools,
888
+ isAnyPoolOverMaxWeight
889
+ };
638
890
  }
639
891
 
640
- assert(remainingAssets.lt(0.00001), "Remaining assets must be 0");
641
-
642
- // Calculate required changes
643
- const changes: Change[] = sortedPools.map((pool) => {
644
- const target =
645
- targetAmounts[pool.pool_id.address.toString()] ||
646
- Web3Number.fromWei("0", this.decimals());
647
- const change = Web3Number.fromWei(
648
- target.minus(pool.amount.toString()).toWei(),
649
- this.decimals()
650
- );
651
- return {
652
- pool_id: pool.pool_id,
653
- changeAmt: change,
654
- finalAmt: target,
655
- isDeposit: change.gt(0)
656
- };
657
- });
658
-
659
- logger.verbose(`Changes: ${JSON.stringify(changes)}`);
660
- // Validate changes
661
- const sumChanges = changes.reduce(
662
- (sum, c) => sum.plus(c.changeAmt.toString()),
663
- Web3Number.fromWei("0", this.decimals())
664
- );
665
- const sumFinal = changes.reduce(
666
- (sum, c) => sum.plus(c.finalAmt.toString()),
667
- Web3Number.fromWei("0", this.decimals())
668
- );
669
- const hasChanges = changes.some((c) => !c.changeAmt.eq(0));
670
-
671
- logger.verbose(`Sum of changes: ${sumChanges.toString()}`);
672
- if (!sumChanges.eq(0)) throw new Error("Sum of changes must be zero");
673
- logger.verbose(`Sum of final: ${sumFinal.toString()}`);
674
- logger.verbose(`Total assets: ${totalAssets.toString()}`);
675
- if (!sumFinal.eq(totalAssets.toString()))
676
- throw new Error("Sum of final amounts must equal total assets");
677
- if (!hasChanges) throw new Error("No changes required");
678
-
679
- const finalPools: PoolInfoFull[] = pools.map((p) => {
680
- const target =
681
- targetAmounts[p.pool_id.address.toString()] ||
682
- Web3Number.fromWei("0", this.decimals());
683
- return {
684
- ...p,
685
- amount: target,
686
- usdValue: Web3Number.fromWei("0", this.decimals())
687
- };
688
- });
689
- return {
690
- changes,
691
- finalPools,
692
- isAnyPoolOverMaxWeight
693
- };
694
- }
695
-
696
- /**
697
- * Creates a rebalance Call object for the strategy contract
698
- * @param pools - Array of pool information including IDs, weights, amounts, APYs and utilization
699
- * @returns Populated contract call for rebalance
700
- */
701
- async getRebalanceCall(
702
- pools: Awaited<ReturnType<typeof this.getRebalancedPositions>>["changes"],
703
- isOverWeightAdjustment: boolean // here, yield increase doesnt matter
704
- ) {
705
- const actions: any[] = [];
706
- // sort to put withdrawals first
707
- pools.sort((a, b) => (b.isDeposit ? -1 : 1));
708
- pools.forEach((p) => {
709
- if (p.changeAmt.eq(0)) return null;
710
- actions.push({
711
- pool_id: p.pool_id.address,
712
- feature: new CairoCustomEnum(
713
- p.isDeposit ? { DEPOSIT: {} } : { WITHDRAW: {} }
714
- ),
715
- token: this.asset().address.address,
716
- amount: uint256.bnToUint256(
717
- p.changeAmt.multipliedBy(p.isDeposit ? 1 : -1).toWei()
718
- )
719
- });
720
- });
721
- if (actions.length === 0) return null;
722
- if (isOverWeightAdjustment) {
723
- return this.contract.populate("rebalance_weights", [actions]);
892
+ /**
893
+ * Creates a rebalance Call object for the strategy contract
894
+ * @param pools - Array of pool information including IDs, weights, amounts, APYs and utilization
895
+ * @returns Populated contract call for rebalance
896
+ */
897
+ async getRebalanceCall(
898
+ pools: Awaited<
899
+ ReturnType<typeof this.getRebalancedPositions>
900
+ >["changes"],
901
+ isOverWeightAdjustment: boolean // here, yield increase doesnt matter
902
+ ) {
903
+ const actions: any[] = [];
904
+ // sort to put withdrawals first
905
+ pools.sort((a, b) => (b.isDeposit ? -1 : 1));
906
+ pools.forEach((p) => {
907
+ if (p.changeAmt.eq(0)) return null;
908
+ actions.push({
909
+ pool_id: p.pool_id.address,
910
+ feature: new CairoCustomEnum(
911
+ p.isDeposit ? { DEPOSIT: {} } : { WITHDRAW: {} }
912
+ ),
913
+ token: this.asset().address.address,
914
+ amount: uint256.bnToUint256(
915
+ p.changeAmt.multipliedBy(p.isDeposit ? 1 : -1).toWei()
916
+ )
917
+ });
918
+ });
919
+ if (actions.length === 0) return null;
920
+ if (isOverWeightAdjustment) {
921
+ return this.contract.populate("rebalance_weights", [actions]);
922
+ }
923
+ return this.contract.populate("rebalance", [actions]);
724
924
  }
725
- return this.contract.populate("rebalance", [actions]);
726
- }
727
925
 
728
- async getInvestmentFlows(pools: PoolInfoFull[]) {
729
- const netYield = await this.netAPYGivenPools(pools);
926
+ async getInvestmentFlows(pools: PoolInfoFull[]) {
927
+ const netYield = await this.netAPYGivenPools(pools);
730
928
 
731
- const baseFlow: IInvestmentFlow = {
732
- title: "Your Deposit",
733
- subItems: [
734
- { key: `Net yield`, value: `${(netYield * 100).toFixed(2)}%` },
735
- {
736
- key: `Performance Fee`,
737
- value: `${(this.metadata.additionalInfo.feeBps / 100).toFixed(2)}%`
738
- }
739
- ],
740
- linkedFlows: [],
741
- style: { backgroundColor: FlowChartColors.Purple.valueOf() }
742
- };
929
+ const baseFlow: IInvestmentFlow = {
930
+ title: "Your Deposit",
931
+ subItems: [
932
+ { key: `Net yield`, value: `${(netYield * 100).toFixed(2)}%` },
933
+ {
934
+ key: `Performance Fee`,
935
+ value: `${(
936
+ this.metadata.additionalInfo.feeBps / 100
937
+ ).toFixed(2)}%`
938
+ }
939
+ ],
940
+ linkedFlows: [],
941
+ style: { backgroundColor: FlowChartColors.Purple.valueOf() }
942
+ };
743
943
 
744
- let _pools = [...pools];
745
- _pools = _pools.sort(
746
- (a, b) => Number(b.amount.toString()) - Number(a.amount.toString())
747
- );
748
- _pools.forEach((p) => {
749
- const flow: IInvestmentFlow = {
750
- title: `Pool name: ${p.pool_name}`,
751
- subItems: [
752
- { key: `APY`, value: `${(p.APY.netApy * 100).toFixed(2)}%` },
753
- {
754
- key: "Weight",
755
- value: `${(p.current_weight * 100).toFixed(2)} / ${(
756
- p.max_weight * 100
757
- ).toFixed(2)}%`
758
- }
759
- ],
760
- linkedFlows: [],
761
- style: p.amount.greaterThan(0)
762
- ? { backgroundColor: FlowChartColors.Blue.valueOf() }
763
- : { color: "gray" }
764
- };
765
- baseFlow.linkedFlows.push(flow);
766
- });
767
- return [baseFlow];
768
- }
769
-
770
- async harvest(acc: Account, endpoint = VESU_REWARDS_ENDPOINT) {
771
- const vesuHarvest = new VesuHarvests(this.config);
772
- const harvests = await vesuHarvest.getUnHarvestedRewards(this.address, endpoint);
773
- const harvest = harvests[0];
774
- const avnu = new AvnuWrapper();
775
- let swapInfo: SwapInfo = {
776
- token_from_address: harvest.token.address,
777
- token_from_amount: uint256.bnToUint256(harvest.actualReward.toWei()),
778
- token_to_address: this.asset().address.address,
779
- token_to_amount: uint256.bnToUint256(0),
780
- token_to_min_amount: uint256.bnToUint256(0),
781
- beneficiary: this.address.address,
782
- integrator_fee_amount_bps: 0,
783
- integrator_fee_recipient: this.address.address,
784
- routes: []
785
- };
786
- if (!this.asset().address.eqString(harvest.token.address)) {
787
- const quote = await avnu.getQuotes(
788
- harvest.token.address,
789
- this.asset().address.address,
790
- harvest.actualReward.toWei(),
791
- this.address.address
792
- );
793
- swapInfo = await avnu.getSwapInfo(
794
- quote,
795
- this.address.address,
796
- 0,
797
- this.address.address
798
- );
944
+ let _pools = [...pools];
945
+ _pools = _pools.sort(
946
+ (a, b) => Number(b.amount.toString()) - Number(a.amount.toString())
947
+ );
948
+ _pools.forEach((p) => {
949
+ const flow: IInvestmentFlow = {
950
+ title: `Pool name: ${p.pool_name}`,
951
+ subItems: [
952
+ {
953
+ key: `APY`,
954
+ value: `${(p.APY.netApy * 100).toFixed(2)}%`
955
+ },
956
+ {
957
+ key: "Weight",
958
+ value: `${(p.current_weight * 100).toFixed(2)} / ${(
959
+ p.max_weight * 100
960
+ ).toFixed(2)}%`
961
+ }
962
+ ],
963
+ linkedFlows: [],
964
+ style: p.amount.greaterThan(0)
965
+ ? { backgroundColor: FlowChartColors.Blue.valueOf() }
966
+ : { color: "gray" }
967
+ };
968
+ baseFlow.linkedFlows.push(flow);
969
+ });
970
+ return [baseFlow];
799
971
  }
800
972
 
801
- return [
802
- this.contract.populate("harvest", [
803
- harvest.rewardsContract.address,
804
- {
805
- id: harvest.claim.id,
806
- amount: harvest.claim.amount.toWei(),
807
- claimee: harvest.claim.claimee.address
808
- },
809
- harvest.proof,
810
- swapInfo
811
- ])
812
- ];
813
- }
814
-
815
- /**
816
- * Calculates the fees deducted in different vTokens based on the current and previous state.
817
- * @param previousTotalSupply - The total supply of the strategy token before the transaction
818
- * @returns {Promise<Array<{ vToken: ContractAddr, fee: Web3Number }>>} Array of fees deducted in different vTokens
819
- */
820
- async getFee(
821
- allowedPools: Array<PoolInfoFull>
822
- ): Promise<Array<{ vToken: ContractAddr; fee: Web3Number }>> {
823
- const assets = Web3Number.fromWei(
824
- (await this.contract.total_assets()).toString(),
825
- this.asset().decimals
826
- );
827
- const totalSupply = Web3Number.fromWei(
828
- (await this.contract.total_supply()).toString(),
829
- this.asset().decimals
830
- );
831
- const prevIndex = Web3Number.fromWei(
832
- (await this.contract.get_previous_index()).toString(),
833
- 18
834
- );
835
- const currIndex = new Web3Number(1, 18)
836
- .multipliedBy(assets)
837
- .dividedBy(totalSupply);
838
-
839
- logger.verbose(`Previous index: ${prevIndex.toString()}`);
840
- logger.verbose(`Assets: ${assets.toString()}`);
841
- logger.verbose(`Total supply: ${totalSupply.toString()}`);
842
- logger.verbose(`Current index: ${currIndex.toNumber()}`);
843
-
844
- if (currIndex.lt(prevIndex)) {
845
- logger.verbose(
846
- `getFee::Current index is less than previous index, no fees to be deducted`
847
- );
848
- return [];
973
+ async getPendingRewards(): Promise<HarvestInfo[]> {
974
+ const vesuHarvests = new VesuHarvests(this.config);
975
+ return await vesuHarvests.getUnHarvestedRewards(this.address);
849
976
  }
850
977
 
851
- const indexDiff = currIndex.minus(prevIndex);
852
- logger.verbose(`Index diff: ${indexDiff.toString()}`);
853
- const numerator = totalSupply
854
- .multipliedBy(indexDiff)
855
- .multipliedBy(this.metadata.additionalInfo.feeBps);
856
- const denominator = 10000;
857
- let fee = numerator.dividedBy(denominator);
858
- logger.verbose(`Fee: ${fee.toString()}`);
859
-
860
- if (fee.lte(0)) {
861
- return [];
978
+ async harvest(acc: Account) {
979
+ const pendingRewards = await this.getPendingRewards();
980
+ if (pendingRewards.length == 0) {
981
+ throw new Error(`No pending rewards found`);
982
+ }
983
+ const harvest = pendingRewards[0];
984
+ const avnu = new AvnuWrapper();
985
+ let swapInfo: SwapInfo = {
986
+ token_from_address: harvest.token.address,
987
+ token_from_amount: uint256.bnToUint256(
988
+ harvest.actualReward.toWei()
989
+ ),
990
+ token_to_address: this.asset().address.address,
991
+ token_to_amount: uint256.bnToUint256(0),
992
+ token_to_min_amount: uint256.bnToUint256(0),
993
+ beneficiary: this.address.address,
994
+ integrator_fee_amount_bps: 0,
995
+ integrator_fee_recipient: this.address.address,
996
+ routes: []
997
+ };
998
+ if (!this.asset().address.eqString(harvest.token.address)) {
999
+ const quote = await avnu.getQuotes(
1000
+ harvest.token.address,
1001
+ this.asset().address.address,
1002
+ harvest.actualReward.toWei(),
1003
+ this.address.address
1004
+ );
1005
+ swapInfo = await avnu.getSwapInfo(
1006
+ quote,
1007
+ this.address.address,
1008
+ 0,
1009
+ this.address.address
1010
+ );
1011
+ }
1012
+
1013
+ return [
1014
+ this.contract.populate("harvest", [
1015
+ harvest.rewardsContract.address,
1016
+ {
1017
+ id: harvest.claim.id,
1018
+ amount: harvest.claim.amount.toWei(),
1019
+ claimee: harvest.claim.claimee.address
1020
+ },
1021
+ harvest.proof,
1022
+ swapInfo
1023
+ ])
1024
+ ];
862
1025
  }
863
1026
 
864
- const fees: Array<{ vToken: ContractAddr; fee: Web3Number }> = [];
865
- let remainingFee = fee.plus(
866
- Web3Number.fromWei("100", this.asset().decimals)
867
- );
1027
+ /**
1028
+ * Calculates the fees deducted in different vTokens based on the current and previous state.
1029
+ * @param previousTotalSupply - The total supply of the strategy token before the transaction
1030
+ * @returns {Promise<Array<{ vToken: ContractAddr, fee: Web3Number }>>} Array of fees deducted in different vTokens
1031
+ */
1032
+ async getFee(
1033
+ allowedPools: Array<PoolInfoFull>
1034
+ ): Promise<Array<{ vToken: ContractAddr; fee: Web3Number }>> {
1035
+ const assets = Web3Number.fromWei(
1036
+ (await this.contract.total_assets()).toString(),
1037
+ this.asset().decimals
1038
+ );
1039
+ const totalSupply = Web3Number.fromWei(
1040
+ (await this.contract.total_supply()).toString(),
1041
+ this.asset().decimals
1042
+ );
1043
+ const prevIndex = Web3Number.fromWei(
1044
+ (await this.contract.get_previous_index()).toString(),
1045
+ 18
1046
+ );
1047
+ const currIndex = new Web3Number(1, 18)
1048
+ .multipliedBy(assets)
1049
+ .dividedBy(totalSupply);
1050
+
1051
+ logger.verbose(`Previous index: ${prevIndex.toString()}`);
1052
+ logger.verbose(`Assets: ${assets.toString()}`);
1053
+ logger.verbose(`Total supply: ${totalSupply.toString()}`);
1054
+ logger.verbose(`Current index: ${currIndex.toNumber()}`);
1055
+
1056
+ if (currIndex.lt(prevIndex)) {
1057
+ logger.verbose(
1058
+ `getFee::Current index is less than previous index, no fees to be deducted`
1059
+ );
1060
+ return [];
1061
+ }
1062
+
1063
+ const indexDiff = currIndex.minus(prevIndex);
1064
+ logger.verbose(`Index diff: ${indexDiff.toString()}`);
1065
+ const numerator = totalSupply
1066
+ .multipliedBy(indexDiff)
1067
+ .multipliedBy(this.metadata.additionalInfo.feeBps);
1068
+ const denominator = 10000;
1069
+ let fee = numerator.dividedBy(denominator);
1070
+ logger.verbose(`Fee: ${fee.toString()}`);
1071
+
1072
+ if (fee.lte(0)) {
1073
+ return [];
1074
+ }
868
1075
 
869
- for (const pool of allowedPools) {
870
- const vToken = pool.v_token;
871
- const balance = pool.amount;
872
-
873
- if (remainingFee.lte(balance)) {
874
- fees.push({ vToken, fee: remainingFee });
875
- break;
876
- } else {
877
- fees.push({ vToken, fee: Web3Number.fromWei(balance.toString(), 18) });
878
- remainingFee = remainingFee.minus(
879
- Web3Number.fromWei(balance.toString(), 18)
1076
+ const fees: Array<{ vToken: ContractAddr; fee: Web3Number }> = [];
1077
+ let remainingFee = fee.plus(
1078
+ Web3Number.fromWei("100", this.asset().decimals)
880
1079
  );
881
- }
882
- }
883
1080
 
884
- logger.verbose(`Fees: ${JSON.stringify(fees)}`);
1081
+ for (const pool of allowedPools) {
1082
+ const vToken = pool.v_token;
1083
+ const balance = pool.amount;
1084
+
1085
+ if (remainingFee.lte(balance)) {
1086
+ fees.push({ vToken, fee: remainingFee });
1087
+ break;
1088
+ } else {
1089
+ fees.push({
1090
+ vToken,
1091
+ fee: Web3Number.fromWei(balance.toString(), 18)
1092
+ });
1093
+ remainingFee = remainingFee.minus(
1094
+ Web3Number.fromWei(balance.toString(), 18)
1095
+ );
1096
+ }
1097
+ }
885
1098
 
886
- return fees;
887
- }
1099
+ logger.verbose(`Fees: ${JSON.stringify(fees)}`);
1100
+
1101
+ return fees;
1102
+ }
888
1103
  }
889
1104
 
890
- const _description = "Automatically diversify {{TOKEN}} holdings into different Vesu pools while reducing risk and maximizing yield. Defi spring STRK Rewards are auto-compounded as well.";
1105
+ const _description =
1106
+ "Automatically diversify {{TOKEN}} holdings into different Vesu pools while reducing risk and maximizing yield. Defi spring STRK Rewards are auto-compounded as well.";
891
1107
 
892
1108
  const _protocol: IProtocol = {
893
- name: "Vesu",
894
- logo: "https://static-assets-8zct.onrender.com/integrations/vesu/logo.png"
1109
+ name: "Vesu",
1110
+ logo: "https://static-assets-8zct.onrender.com/integrations/vesu/logo.png"
895
1111
  };
896
1112
  // need to fine tune better
897
1113
  const _riskFactor: RiskFactor[] = [
898
- { type: RiskType.SMART_CONTRACT_RISK, value: 0.5, weight: 25, reason: "Audited by CSC" },
899
- { type: RiskType.COUNTERPARTY_RISK, value: 1, weight: 50, reason: "Reasonable max LTV ratios and Curated by well-known risk managers like Re7" },
900
- { type: RiskType.ORACLE_RISK, value: 0.5, weight: 25, reason: "Uses Pragma price feeds, Most reputable price feed on Starknet" }
1114
+ {
1115
+ type: RiskType.SMART_CONTRACT_RISK,
1116
+ value: 0.5,
1117
+ weight: 25,
1118
+ reason: "Audited by CSC"
1119
+ },
1120
+ {
1121
+ type: RiskType.COUNTERPARTY_RISK,
1122
+ value: 1,
1123
+ weight: 50,
1124
+ reason: "Reasonable max LTV ratios and Curated by well-known risk managers like Re7"
1125
+ },
1126
+ {
1127
+ type: RiskType.ORACLE_RISK,
1128
+ value: 0.5,
1129
+ weight: 25,
1130
+ reason: "Uses Pragma price feeds, Most reputable price feed on Starknet"
1131
+ }
901
1132
  ];
902
1133
  const AUDIT_URL =
903
- "https://assets.troves.fi/strkfarm/audit_report_vesu_and_ekubo_strats.pdf";
1134
+ "https://assets.troves.fi/strkfarm/audit_report_vesu_and_ekubo_strats.pdf";
904
1135
 
905
- const faqs: FAQ[] = [
906
- {
907
- question: "What is the Vesu Rebalancing Strategy?",
908
- answer:
909
- "The Vesu Rebalancing Strategy is an automated investment strategy that diversifies your holdings across multiple Vesu pools. It optimizes yield by rebalancing assets based on pool performance while adhering to risk constraints."
910
- },
911
- {
912
- question: "Will I earn Vesu points?",
913
- answer: "Yes, of course! You will earn Vesu points for your deposits."
914
- },
915
- {
916
- question: "How does the strategy optimize yield?",
917
- answer:
918
- "The strategy calculates the weighted average APY across all pools and reallocates assets to maximize returns. It prioritizes high-performing pools while ensuring compliance with maximum weight constraints."
919
- },
920
- {
921
- question: "What are the risks associated with this strategy?",
922
- answer:
923
- "The strategy involves usual DeFi risks such as smart contract vulnerabilities, counterparty risks, and oracle inaccuracies. However, we try our best to reduce these risks through audits and careful pool selection."
924
- },
925
- {
926
- question: "How are fees calculated and deducted?",
927
- answer:
928
- "Fees are calculated based on the performance of the strategy and deducted proportionally from the total assets. We charge a 10% performance fee and is already accounted in the APY shown."
929
- },
930
- {
931
- question: "What happens if a pool exceeds its maximum weight?",
932
- answer:
933
- "If a pool exceeds its maximum weight, the strategy rebalances by withdrawing excess funds and reallocating them to other pools with available capacity."
934
- },
935
- {
936
- question: "Can I withdraw my assets at any time?",
937
- answer:
938
- "Yes, you can withdraw your assets at any time. In rare circumstances, if debt utilisation is high for certain pools on Vesu, it may not be possible to withdraw until markets restore balance."
939
- },
940
- {
941
- question: "What happens to my Defi Spring STRK rewards?",
942
- answer:
943
- "STRK rewards are automatically harvested and reinvested into the strategy every week to maximize compounding returns."
944
- },
945
- {
946
- question: "Is the strategy audited?",
947
- answer:
948
- <div>Yes, the strategy has been audited. You can review the audit report in our docs <a href="https://docs.troves.fi/p/strategies/vesu-fusion-rebalancing-vaults#technical-details" style={{textDecoration: 'underline', marginLeft: '5px'}}>Here</a>.</div>
949
- }
950
- ];
1136
+ // Helper to create common risk object
1137
+ const getVesuRebalanceRisk = () => ({
1138
+ riskFactor: _riskFactor,
1139
+ netRisk:
1140
+ _riskFactor.reduce((acc, curr) => acc + curr.value * curr.weight, 0) /
1141
+ _riskFactor.reduce((acc, curr) => acc + curr.weight, 0),
1142
+ notARisks: getNoRiskTags(_riskFactor)
1143
+ });
951
1144
 
952
- /**
953
- * Represents the Vesu Rebalance Strategies.
954
- */
955
- export const VesuRebalanceStrategies: IStrategyMetadata<VesuRebalanceSettings>[] =
956
- [
957
- {
958
- name: "Vesu Fusion STRK",
959
- description: _description,
960
- address: ContractAddr.from(
961
- "0x7fb5bcb8525954a60fde4e8fb8220477696ce7117ef264775a1770e23571929"
962
- ),
963
- launchBlock: 0,
964
- type: "ERC4626",
965
- depositTokens: [
966
- Global.getDefaultTokens().find((t) => t.symbol === "STRK")!
967
- ],
968
- protocols: [_protocol],
969
- auditUrl: AUDIT_URL,
970
- maxTVL: Web3Number.fromWei("0", 18),
971
- risk: {
972
- riskFactor: _riskFactor,
973
- netRisk:
974
- _riskFactor.reduce((acc, curr) => acc + curr.value * curr.weight, 0) /
975
- _riskFactor.reduce((acc, curr) => acc + curr.weight, 0),
976
- notARisks: getNoRiskTags(_riskFactor)
977
- },
978
- additionalInfo: {
1145
+ // Helper to create Vesu Rebalance strategy settings
1146
+ const createVesuRebalanceSettings = (tokenSymbol: string): StrategySettings => {
1147
+ const depositToken = Global.getDefaultTokens().find(
1148
+ (t) => t.symbol === tokenSymbol
1149
+ )!;
1150
+ return {
1151
+ maxTVL: Web3Number.fromWei("0", depositToken.decimals),
1152
+ isPaused: false,
1153
+ liveStatus: StrategyLiveStatus.ACTIVE,
1154
+ isAudited: true,
1155
+ isInstantWithdrawal: true,
1156
+ quoteToken: depositToken,
1157
+ alerts: []
1158
+ };
1159
+ };
1160
+
1161
+ // Helper to create a Vesu Rebalance strategy
1162
+ const createVesuRebalanceStrategy = (
1163
+ name: string,
1164
+ tokenSymbol: string,
1165
+ address: string
1166
+ ): IStrategyMetadata<VesuRebalanceSettings> => ({
1167
+ id: `vesu_fusion_${tokenSymbol.toLowerCase()}`,
1168
+ name,
1169
+ description: _description.replace("{{TOKEN}}", tokenSymbol),
1170
+ address: ContractAddr.from(address),
1171
+ launchBlock: 0,
1172
+ type: "ERC4626" as const,
1173
+ depositTokens: [
1174
+ Global.getDefaultTokens().find((t) => t.symbol === tokenSymbol)!
1175
+ ],
1176
+ protocols: [_protocol],
1177
+ auditUrl: AUDIT_URL,
1178
+ settings: createVesuRebalanceSettings(tokenSymbol),
1179
+ risk: getVesuRebalanceRisk(),
1180
+ additionalInfo: {
979
1181
  feeBps: 1000
980
- },
981
- faqs,
982
- contractDetails: [],
983
- investmentSteps: []
984
1182
  },
1183
+ faqs,
1184
+ contractDetails: [],
1185
+ investmentSteps: []
1186
+ });
1187
+
1188
+ // Shared security & redemption metadata for all Vesu strategies
1189
+ const VESU_SECURITY = {
1190
+ auditStatus: AuditStatus.AUDITED,
1191
+ sourceCode: {
1192
+ type: SourceCodeType.OPEN_SOURCE,
1193
+ contractLink: "https://github.com/trovesfi/troves-contracts"
1194
+ },
1195
+ accessControl: {
1196
+ type: AccessControlType.STANDARD_ACCOUNT,
1197
+ addresses: [ContractAddr.from("0x0")],
1198
+ timeLock: "2 Days"
1199
+ }
1200
+ };
1201
+
1202
+ const VESU_REDEMPTION_INFO = {
1203
+ instantWithdrawalVault: InstantWithdrawalVault.YES
1204
+ };
1205
+
1206
+ const faqs: FAQ[] = [
985
1207
  {
986
- name: "Vesu Fusion ETH",
987
- description: _description.replace("{{TOKEN}}", "ETH"),
988
- address: ContractAddr.from(
989
- "0x5eaf5ee75231cecf79921ff8ded4b5ffe96be718bcb3daf206690ad1a9ad0ca"
990
- ),
991
- launchBlock: 0,
992
- type: "ERC4626",
993
- auditUrl: AUDIT_URL,
994
- depositTokens: [
995
- Global.getDefaultTokens().find((t) => t.symbol === "ETH")!
996
- ],
997
- protocols: [_protocol],
998
- maxTVL: Web3Number.fromWei("0", 18),
999
- risk: {
1000
- riskFactor: _riskFactor,
1001
- netRisk:
1002
- _riskFactor.reduce((acc, curr) => acc + curr.value * curr.weight, 0) /
1003
- _riskFactor.reduce((acc, curr) => acc + curr.weight, 0),
1004
- notARisks: getNoRiskTags(_riskFactor)
1005
- },
1006
- additionalInfo: {
1007
- feeBps: 1000
1008
- },
1009
- faqs,
1010
- contractDetails: [],
1011
- investmentSteps: []
1208
+ question: "What is the Vesu Rebalancing Strategy?",
1209
+ answer: "The Vesu Rebalancing Strategy is an automated investment strategy that diversifies your holdings across multiple Vesu pools. It optimizes yield by rebalancing assets based on pool performance while adhering to risk constraints."
1012
1210
  },
1013
1211
  {
1014
- name: "Vesu Fusion USDC",
1015
- description: _description.replace("{{TOKEN}}", "USDC"),
1016
- address: ContractAddr.from(
1017
- "0xa858c97e9454f407d1bd7c57472fc8d8d8449a777c822b41d18e387816f29c"
1018
- ),
1019
- launchBlock: 0,
1020
- type: "ERC4626",
1021
- auditUrl: AUDIT_URL,
1022
- depositTokens: [
1023
- Global.getDefaultTokens().find((t) => t.symbol === "USDC")!
1024
- ],
1025
- protocols: [_protocol],
1026
- maxTVL: Web3Number.fromWei("0", 6),
1027
- risk: {
1028
- riskFactor: _riskFactor,
1029
- netRisk:
1030
- _riskFactor.reduce((acc, curr) => acc + curr.value * curr.weight, 0) /
1031
- _riskFactor.reduce((acc, curr) => acc + curr.weight, 0),
1032
- notARisks: getNoRiskTags(_riskFactor)
1033
- },
1034
- additionalInfo: {
1035
- feeBps: 1000
1036
- },
1037
- faqs,
1038
- contractDetails: [],
1039
- investmentSteps: []
1212
+ question: "Will I earn Vesu points?",
1213
+ answer: "Yes, of course! You will earn Vesu points for your deposits."
1040
1214
  },
1041
1215
  {
1042
- name: "Vesu Fusion USDT",
1043
- description: _description.replace("{{TOKEN}}", "USDT"),
1044
- address: ContractAddr.from(
1045
- "0x115e94e722cfc4c77a2f15c4aefb0928c1c0029e5a57570df24c650cb7cec2c"
1046
- ),
1047
- launchBlock: 0,
1048
- type: "ERC4626",
1049
- depositTokens: [
1050
- Global.getDefaultTokens().find((t) => t.symbol === "USDT")!
1051
- ],
1052
- auditUrl: AUDIT_URL,
1053
- protocols: [_protocol],
1054
- maxTVL: Web3Number.fromWei("0", 6),
1055
- risk: {
1056
- riskFactor: _riskFactor,
1057
- netRisk:
1058
- _riskFactor.reduce((acc, curr) => acc + curr.value * curr.weight, 0) /
1059
- _riskFactor.reduce((acc, curr) => acc + curr.weight, 0),
1060
- notARisks: getNoRiskTags(_riskFactor)
1061
- },
1062
- additionalInfo: {
1063
- feeBps: 1000
1064
- },
1065
- faqs,
1066
- contractDetails: [],
1067
- investmentSteps: []
1068
- // }, {
1069
- // name: 'Vesu Fusion WBTC',
1070
- // description: _description.replace('{{TOKEN}}', 'WBTC'),
1071
- // address: ContractAddr.from('0x778007f8136a5b827325d21613803e796bda4d676fbe1e34aeab0b2a2ec027f'),
1072
- // type: 'ERC4626',
1073
- // depositTokens: [Global.getDefaultTokens().find(t => t.symbol === 'WBTC')!],
1074
- // auditUrl: AUDIT_URL,
1075
- // protocols: [_protocol],
1076
- // maxTVL: Web3Number.fromWei('0', 8),
1077
- // risk: {
1078
- // riskFactor: _riskFactor,
1079
- // netRisk: _riskFactor.reduce((acc, curr) => acc + curr.value * curr.weight, 0) / _riskFactor.reduce((acc, curr) => acc + curr.weight, 0),
1080
- // },
1081
- // additionalInfo: {
1082
- // feeBps: 1000,
1083
- // },
1084
-
1216
+ question: "How does the strategy optimize yield?",
1217
+ answer: "The strategy calculates the weighted average APY across all pools and reallocates assets to maximize returns. It prioritizes high-performing pools while ensuring compliance with maximum weight constraints."
1218
+ },
1219
+ {
1220
+ question: "What are the risks associated with this strategy?",
1221
+ answer: "The strategy involves usual DeFi risks such as smart contract vulnerabilities, counterparty risks, and oracle inaccuracies. However, we try our best to reduce these risks through audits and careful pool selection."
1222
+ },
1223
+ {
1224
+ question: "How are fees calculated and deducted?",
1225
+ answer: "Fees are calculated based on the performance of the strategy and deducted proportionally from the total assets. We charge a 10% performance fee and is already accounted in the APY shown."
1226
+ },
1227
+ {
1228
+ question: "What happens if a pool exceeds its maximum weight?",
1229
+ answer: "If a pool exceeds its maximum weight, the strategy rebalances by withdrawing excess funds and reallocating them to other pools with available capacity."
1230
+ },
1231
+ {
1232
+ question: "Can I withdraw my assets at any time?",
1233
+ answer: "Yes, you can withdraw your assets at any time. In rare circumstances, if debt utilisation is high for certain pools on Vesu, it may not be possible to withdraw until markets restore balance."
1234
+ },
1235
+ {
1236
+ question: "What happens to my Defi Spring STRK rewards?",
1237
+ answer: "STRK rewards are automatically harvested and reinvested into the strategy every week to maximize compounding returns."
1238
+ },
1239
+ {
1240
+ question: "Is the strategy audited?",
1241
+ answer: (
1242
+ <div>
1243
+ Yes, the strategy has been audited. You can review the audit
1244
+ report in our docs{" "}
1245
+ <a
1246
+ href="https://docs.troves.fi/p/strategies/vesu-fusion-rebalancing-vaults#technical-details"
1247
+ style={{ textDecoration: "underline", marginLeft: "5px" }}
1248
+ >
1249
+ Here
1250
+ </a>
1251
+ .
1252
+ </div>
1253
+ )
1085
1254
  }
1086
1255
  ];
1087
1256
 
1257
+ /**
1258
+ * Represents the Vesu Rebalance Strategies.
1259
+ */
1260
+ export const VesuRebalanceStrategies: IStrategyMetadata<VesuRebalanceSettings>[] =
1261
+ [
1262
+ createVesuRebalanceStrategy(
1263
+ "Vesu Fusion STRK",
1264
+ "STRK",
1265
+ "0x7fb5bcb8525954a60fde4e8fb8220477696ce7117ef264775a1770e23571929"
1266
+ ),
1267
+ createVesuRebalanceStrategy(
1268
+ "Vesu Fusion ETH",
1269
+ "ETH",
1270
+ "0x5eaf5ee75231cecf79921ff8ded4b5ffe96be718bcb3daf206690ad1a9ad0ca"
1271
+ ),
1272
+ createVesuRebalanceStrategy(
1273
+ "Vesu Fusion USDC",
1274
+ "USDC",
1275
+ "0xa858c97e9454f407d1bd7c57472fc8d8d8449a777c822b41d18e387816f29c"
1276
+ ),
1277
+ createVesuRebalanceStrategy(
1278
+ "Vesu Fusion USDT",
1279
+ "USDT",
1280
+ "0x115e94e722cfc4c77a2f15c4aefb0928c1c0029e5a57570df24c650cb7cec2c"
1281
+ )
1282
+ ];
1283
+
1088
1284
  // auto assign contract details to each strategy
1089
1285
  VesuRebalanceStrategies.forEach((s) => {
1090
- // set contract details
1091
- s.contractDetails = [{
1092
- address: s.address,
1093
- name: "Vault",
1094
- sourceCodeUrl: "https://github.com/strkfarm/strkfarm-contracts/tree/main/src/strategies/vesu_rebalance"
1095
- },
1096
- ...COMMON_CONTRACTS];
1097
- // set docs link
1098
- s.docs = "https://docs.troves.fi/p/strategies/vesu-fusion-rebalancing-vaults"
1099
-
1100
- // set description
1101
- s.description = highlightTextWithLinks(
1102
- _description.replace("{{TOKEN}}", s.depositTokens[0].symbol),
1103
- [{
1104
- highlight: "Vesu pools",
1105
- link: "https://vesu.xyz/pools",
1106
- }, {
1107
- highlight: "Defi spring STRK Rewards",
1108
- link: "https://defispring.starknet.io/"
1109
- }]
1110
- );
1111
-
1112
- // add investment steps
1113
- s.investmentSteps = [
1114
- "Split the amount and Supply to configured Vesu pools",
1115
- "Monitor and Rebalance funds across multiple Vesu pools to maximize yield",
1116
- "Harvest and supply Defi Spring STRK rewards every week (Auto-compound)",
1117
- ]
1118
- });
1286
+ // set contract details
1287
+ s.contractDetails = [
1288
+ {
1289
+ address: s.address,
1290
+ name: "Vault",
1291
+ sourceCodeUrl:
1292
+ "https://github.com/strkfarm/strkfarm-contracts/tree/main/src/strategies/vesu_rebalance"
1293
+ },
1294
+ ...COMMON_CONTRACTS
1295
+ ];
1296
+ // set docs link
1297
+ s.docs =
1298
+ "https://docs.troves.fi/p/strategies/vesu-fusion-rebalancing-vaults";
1299
+
1300
+ // set description
1301
+ s.description = highlightTextWithLinks(
1302
+ _description.replace("{{TOKEN}}", s.depositTokens[0].symbol),
1303
+ [
1304
+ {
1305
+ highlight: "Vesu pools",
1306
+ link: "https://vesu.xyz/pools"
1307
+ },
1308
+ {
1309
+ highlight: "Defi spring STRK Rewards",
1310
+ link: "https://defispring.starknet.io/"
1311
+ }
1312
+ ]
1313
+ );
1314
+
1315
+ // add investment steps
1316
+ s.investmentSteps = [
1317
+ "Split the amount and Supply to configured Vesu pools",
1318
+ "Monitor and Rebalance funds across multiple Vesu pools to maximize yield",
1319
+ "Harvest and supply Defi Spring STRK rewards every week (Auto-compound)"
1320
+ ];
1321
+ });