@strkfarm/sdk 1.0.55 → 1.0.57

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.
@@ -0,0 +1,695 @@
1
+ import { ContractAddr, Web3Number } from "@/dataTypes";
2
+ import { BaseStrategy, SingleActionAmount, SingleTokenInfo } from "./base-strategy";
3
+ import { PricerBase } from "@/modules/pricerBase";
4
+ import { getNoRiskTags, IConfig, IStrategyMetadata, Protocols, RiskFactor, RiskType, VaultPosition } from "@/interfaces";
5
+ import { Call, CallData, Contract, num, uint256 } from "starknet";
6
+ import { VesuRebalanceSettings } from "./vesu-rebalance";
7
+ import { assert, LeafData, logger, StandardMerkleTree } from "@/utils";
8
+ import UniversalVaultAbi from '../data/universal-vault.abi.json';
9
+ import ManagerAbi from '../data/vault-manager.abi.json';
10
+ import { ApproveCallParams, BaseAdapter, CommonAdapter, FlashloanCallParams, GenerateCallFn, LeafAdapterFn, ManageCall, VesuAdapter, VesuModifyPositionCallParams, VesuPools } from "./universal-adapters";
11
+ import { Global } from "@/global";
12
+
13
+ export interface UniversalStrategySettings {
14
+ manager: ContractAddr,
15
+ vaultAllocator: ContractAddr,
16
+ redeemRequestNFT: ContractAddr,
17
+ leafAdapters: LeafAdapterFn<any>[],
18
+ adapters: {id: string, adapter: BaseAdapter}[],
19
+ targetHealthFactor: number,
20
+ minHealthFactor: number
21
+ }
22
+
23
+
24
+ export class UniversalStrategy<
25
+ S extends UniversalStrategySettings
26
+ > extends BaseStrategy<
27
+ SingleTokenInfo,
28
+ SingleActionAmount
29
+ > {
30
+
31
+ /** Contract address of the strategy */
32
+ readonly address: ContractAddr;
33
+ /** Pricer instance for token price calculations */
34
+ readonly pricer: PricerBase;
35
+ /** Metadata containing strategy information */
36
+ readonly metadata: IStrategyMetadata<S>;
37
+ /** Contract instance for interacting with the strategy */
38
+ readonly contract: Contract;
39
+ readonly managerContract: Contract;
40
+ merkleTree: StandardMerkleTree | undefined;
41
+
42
+ constructor(
43
+ config: IConfig,
44
+ pricer: PricerBase,
45
+ metadata: IStrategyMetadata<S>
46
+ ) {
47
+ super(config);
48
+ this.pricer = pricer;
49
+
50
+ assert(
51
+ metadata.depositTokens.length === 1,
52
+ "VesuRebalance only supports 1 deposit token"
53
+ );
54
+ this.metadata = metadata;
55
+ this.address = metadata.address;
56
+
57
+ this.contract = new Contract(
58
+ UniversalVaultAbi,
59
+ this.address.address,
60
+ this.config.provider
61
+ );
62
+ this.managerContract = new Contract(
63
+ ManagerAbi,
64
+ this.metadata.additionalInfo.manager.address,
65
+ this.config.provider
66
+ );
67
+ }
68
+
69
+ getMerkleTree() {
70
+ if (this.merkleTree) return this.merkleTree;
71
+ const leaves = this.metadata.additionalInfo.leafAdapters.map((adapter, index) => {
72
+ return adapter()
73
+ });
74
+ const standardTree = StandardMerkleTree.of(leaves.map(l => l.leaf));
75
+ this.merkleTree = standardTree;
76
+ return standardTree;
77
+ }
78
+
79
+ getMerkleRoot() {
80
+ return this.getMerkleTree().root;
81
+ }
82
+
83
+ getProofs<T>(id: string): { proofs: string[], callConstructor: GenerateCallFn<T> } {
84
+ const tree = this.getMerkleTree();
85
+ let proofs: string[] = [];
86
+ for (const [i, v] of tree.entries()) {
87
+ if (v.readableId == id) {
88
+ proofs = tree.getProof(i);
89
+ }
90
+ }
91
+ if (proofs.length === 0) {
92
+ throw new Error(`Proof not found for ID: ${id}`);
93
+ }
94
+
95
+ // find leaf adapter
96
+ const leafAdapter = this.metadata.additionalInfo.leafAdapters.find(adapter => adapter().leaf.readableId === id);
97
+ if (!leafAdapter) {
98
+ throw new Error(`Leaf adapter not found for ID: ${id}`);
99
+ }
100
+ const leafInfo = leafAdapter();
101
+ return {
102
+ proofs,
103
+ callConstructor: leafInfo.callConstructor.bind(leafInfo),
104
+ };
105
+ }
106
+
107
+ getAdapter(id: string): BaseAdapter {
108
+ const adapter = this.metadata.additionalInfo.adapters.find(adapter => adapter.id === id);
109
+ if (!adapter) {
110
+ throw new Error(`Adapter not found for ID: ${id}`);
111
+ }
112
+ return adapter.adapter;
113
+ }
114
+
115
+ asset() {
116
+ return this.metadata.depositTokens[0];
117
+ }
118
+
119
+ async depositCall(amountInfo: SingleActionAmount, receiver: ContractAddr): Promise<Call[]> {
120
+ // Technically its not erc4626 abi, but we just need approve call
121
+ // so, its ok to use it
122
+ assert(
123
+ amountInfo.tokenInfo.address.eq(this.asset().address),
124
+ "Deposit token mismatch"
125
+ );
126
+ const assetContract = new Contract(
127
+ UniversalVaultAbi,
128
+ this.asset().address.address,
129
+ this.config.provider
130
+ );
131
+ const call1 = assetContract.populate("approve", [
132
+ this.address.address,
133
+ uint256.bnToUint256(amountInfo.amount.toWei())
134
+ ]);
135
+ const call2 = this.contract.populate("deposit", [
136
+ uint256.bnToUint256(amountInfo.amount.toWei()),
137
+ receiver.address
138
+ ]);
139
+ return [call1, call2];
140
+ }
141
+
142
+ /**
143
+ * Calculates the Total Value Locked (TVL) for a specific user.
144
+ * @param user - Address of the user
145
+ * @returns Object containing the amount in token units and USD value
146
+ */
147
+ async getUserTVL(user: ContractAddr) {
148
+ const shares = await this.contract.balanceOf(user.address);
149
+ const assets = await this.contract.convert_to_assets(
150
+ uint256.bnToUint256(shares)
151
+ );
152
+ const amount = Web3Number.fromWei(
153
+ assets.toString(),
154
+ this.metadata.depositTokens[0].decimals
155
+ );
156
+ let price = await this.pricer.getPrice(
157
+ this.metadata.depositTokens[0].symbol
158
+ );
159
+ const usdValue = Number(amount.toFixed(6)) * price.price;
160
+ return {
161
+ tokenInfo: this.asset(),
162
+ amount,
163
+ usdValue
164
+ };
165
+ }
166
+
167
+ /**
168
+ * Calculates the weighted average APY across all pools based on USD value.
169
+ * @returns {Promise<number>} The weighted average APY across all pools
170
+ */
171
+ async netAPY(): Promise<{ net: number, splits: { apy: number, id: string }[] }> {
172
+ const [vesuAdapter1, vesuAdapter2] = this.getVesuAdapters();
173
+ const pools = await VesuAdapter.getVesuPools();
174
+ const pool1 = pools.pools.find(p => vesuAdapter1.config.poolId.eqString(num.getHexString(p.id)));
175
+ const pool2 = pools.pools.find(p => vesuAdapter2.config.poolId.eqString(num.getHexString(p.id)));
176
+ if (!pool1 || !pool2) {
177
+ throw new Error('Pool not found');
178
+ };
179
+ const collateralAsset1 = pool1.assets.find((a: any) => a.symbol === vesuAdapter1.config.collateral.symbol)?.stats!;
180
+ const debtAsset1 = pool1.assets.find((a: any) => a.symbol === vesuAdapter1.config.debt.symbol)?.stats!;
181
+ const collateralAsset2 = pool2.assets.find((a: any) => a.symbol === vesuAdapter2.config.collateral.symbol)?.stats!;
182
+ const debtAsset2 = pool2.assets.find((a: any) => a.symbol === vesuAdapter2.config.debt.symbol)?.stats!;
183
+
184
+ // supplyApy: { value: '8057256029163289', decimals: 18 },
185
+ // defiSpringSupplyApr: { value: '46856062629264560', decimals: 18 },
186
+ // borrowApr: { value: '12167825982336000', decimals: 18 },
187
+ const collateral1APY = Number(collateralAsset1.supplyApy.value) / 1e18;
188
+ const debt1APY = Number(debtAsset1.borrowApr.value) / 1e18;
189
+ const collateral2APY = Number(collateralAsset2.supplyApy.value) / 1e18;
190
+ const debt2APY = Number(debtAsset2.borrowApr.value) / 1e18;
191
+
192
+ const positions = await this.getVaultPositions();
193
+ const weights = positions.map((p, index) => p.usdValue * (index % 2 == 0 ? 1 : -1));
194
+ const baseAPYs = [collateral1APY, debt1APY, collateral2APY, debt2APY];
195
+ assert(positions.length == baseAPYs.length, "Positions and APYs length mismatch");
196
+ const rewardAPYs = [Number(collateralAsset1.defiSpringSupplyApr.value) / 1e18, 0, Number(collateralAsset2.defiSpringSupplyApr.value) / 1e18, 0];
197
+ const baseAPY = this.computeAPY(baseAPYs, weights);
198
+ const rewardAPY = this.computeAPY(rewardAPYs, weights);
199
+ const apys = [...baseAPYs, ...rewardAPYs];
200
+ const netAPY = baseAPY + rewardAPY;
201
+ return { net: netAPY, splits: [{
202
+ apy: baseAPY, id: 'base'
203
+ }, {
204
+ apy: rewardAPY, id: 'defispring'
205
+ }] };
206
+ }
207
+
208
+ private computeAPY(apys: number[], weights: number[]) {
209
+ assert(apys.length === weights.length, "APYs and weights length mismatch");
210
+ const weightedSum = apys.reduce((acc, apy, i) => acc + apy * weights[i], 0);
211
+ const totalWeight = weights.reduce((acc, weight) => acc + weight, 0);
212
+ return weightedSum / totalWeight;
213
+ }
214
+
215
+ /**
216
+ * Calculates the total TVL of the strategy.
217
+ * @returns Object containing the total amount in token units and USD value
218
+ */
219
+ async getTVL() {
220
+ const assets = await this.contract.total_assets();
221
+ const amount = Web3Number.fromWei(
222
+ assets.toString(),
223
+ this.metadata.depositTokens[0].decimals
224
+ );
225
+ let price = await this.pricer.getPrice(
226
+ this.metadata.depositTokens[0].symbol
227
+ );
228
+ const usdValue = Number(amount.toFixed(6)) * price.price;
229
+ return {
230
+ tokenInfo: this.asset(),
231
+ amount,
232
+ usdValue
233
+ };
234
+ }
235
+
236
+ async getAUM(): Promise<{net: SingleTokenInfo, prevAum: Web3Number, splits: {id: string, aum: Web3Number}[]}> {
237
+ const currentAUM: bigint = await this.contract.call('aum', []) as bigint;
238
+ const lastReportTime = await this.contract.call('last_report_timestamp', []);
239
+
240
+ const token1Price = await this.pricer.getPrice(this.metadata.depositTokens[0].symbol);
241
+
242
+ // calculate actual aum
243
+ const [vesuAdapter1, vesuAdapter2] = this.getVesuAdapters();
244
+ const leg1AUM = await vesuAdapter1.getPositions(this.config);
245
+ const leg2AUM = await vesuAdapter2.getPositions(this.config);
246
+
247
+ const aumToken = leg1AUM[0].amount
248
+ .plus(leg2AUM[0].usdValue / token1Price.price)
249
+ .minus(leg1AUM[1].usdValue / token1Price.price)
250
+ .minus(leg2AUM[1].amount);
251
+ logger.verbose(`${this.getTag()} Actual AUM: ${aumToken}`);
252
+
253
+ // calculate estimated growth from strk rewards
254
+ const netAPY = await this.netAPY();
255
+ const defispringAPY = netAPY.splits.find(s => s.id === 'defispring')?.apy || 0;
256
+ if (!defispringAPY) throw new Error('DefiSpring APY not found');
257
+
258
+ const timeDiff = (Math.round(Date.now() / 1000) - Number(lastReportTime));
259
+ const growthRate = timeDiff * defispringAPY / (365 * 24 * 60 * 60);
260
+ const prevAum = Web3Number.fromWei(currentAUM.toString(), this.asset().decimals);
261
+ const rewardAssets = prevAum.multipliedBy(growthRate);
262
+ logger.verbose(`${this.getTag()} DefiSpring AUM time difference: ${timeDiff}`);
263
+ logger.verbose(`${this.getTag()} Current AUM: ${currentAUM}`);
264
+ logger.verbose(`${this.getTag()} Net APY: ${JSON.stringify(netAPY)}`);
265
+ logger.verbose(`${this.getTag()} rewards AUM: ${rewardAssets}`);
266
+
267
+ const newAUM = aumToken.plus(rewardAssets);
268
+ logger.verbose(`${this.getTag()} New AUM: ${newAUM}`);
269
+
270
+ const net = {
271
+ tokenInfo: this.asset(),
272
+ amount: newAUM,
273
+ usdValue: newAUM.multipliedBy(token1Price.price).toNumber()
274
+ };
275
+ const splits = [{
276
+ id: 'finalised',
277
+ aum: aumToken
278
+ }, {
279
+ id: 'defispring',
280
+ aum: rewardAssets
281
+ }];
282
+ return { net, splits, prevAum };
283
+ }
284
+
285
+ getVesuAdapters() {
286
+ const vesuAdapter1 = this.getAdapter(UNIVERSAL_ADAPTERS.VESU_LEG1) as VesuAdapter;
287
+ const vesuAdapter2 = this.getAdapter(UNIVERSAL_ADAPTERS.VESU_LEG2) as VesuAdapter;
288
+ vesuAdapter1.pricer = this.pricer;
289
+ vesuAdapter2.pricer = this.pricer;
290
+ vesuAdapter1.networkConfig = this.config;
291
+ vesuAdapter2.networkConfig = this.config;
292
+
293
+ return [vesuAdapter1, vesuAdapter2];
294
+ }
295
+
296
+ async getVaultPositions(): Promise<VaultPosition[]> {
297
+ const [vesuAdapter1, vesuAdapter2] = this.getVesuAdapters();
298
+ const leg1Positions = await vesuAdapter1.getPositions(this.config);
299
+ const leg2Positions = await vesuAdapter2.getPositions(this.config);
300
+ return [...leg1Positions, ...leg2Positions];
301
+ }
302
+
303
+ getSetManagerCall(strategist: ContractAddr, root = this.getMerkleRoot()) {
304
+ return this.managerContract.populate('set_manage_root', [strategist.address, num.getHexString(root)]);
305
+ }
306
+
307
+ getManageCall(proofIds: string[], manageCalls: ManageCall[]) {
308
+ assert(proofIds.length == manageCalls.length, 'Proof IDs and Manage Calls length mismatch');
309
+ return this.managerContract.populate('manage_vault_with_merkle_verification', {
310
+ proofs: proofIds.map(id => this.getProofs(id).proofs),
311
+ decoder_and_sanitizers: manageCalls.map(call => call.sanitizer.address),
312
+ targets: manageCalls.map(call => call.call.contractAddress.address),
313
+ selectors: manageCalls.map(call => call.call.selector),
314
+ calldatas: manageCalls.map(call => call.call.calldata), // Calldata[]
315
+ });
316
+ }
317
+
318
+ getVesuModifyPositionCalls(params: {
319
+ isLeg1: boolean,
320
+ isDeposit: boolean,
321
+ depositAmount: Web3Number,
322
+ debtAmount: Web3Number
323
+ }) {
324
+ assert(params.depositAmount.gt(0) || params.debtAmount.gt(0), 'Either deposit or debt amount must be greater than 0');
325
+ // approve token
326
+ const isToken1 = params.isLeg1 == params.isDeposit; // XOR
327
+ const STEP1_ID = isToken1 ? UNIVERSAL_MANAGE_IDS.APPROVE_TOKEN1 :UNIVERSAL_MANAGE_IDS.APPROVE_TOKEN2;
328
+ const manage4Info = this.getProofs<ApproveCallParams>(STEP1_ID);
329
+ const approveAmount = params.isDeposit ? params.depositAmount : params.debtAmount;
330
+ const manageCall4 = manage4Info.callConstructor({
331
+ amount: approveAmount
332
+ })
333
+
334
+ // deposit and borrow or repay and withdraw
335
+ const STEP2_ID = params.isLeg1 ? UNIVERSAL_MANAGE_IDS.VESU_LEG1 : UNIVERSAL_MANAGE_IDS.VESU_LEG2;
336
+ const manage5Info = this.getProofs<VesuModifyPositionCallParams>(STEP2_ID);
337
+ const manageCall5 = manage5Info.callConstructor(VesuAdapter.getDefaultModifyPositionCallParams({
338
+ collateralAmount: params.depositAmount,
339
+ isAddCollateral: params.isDeposit,
340
+ debtAmount: params.debtAmount,
341
+ isBorrow: params.isDeposit
342
+ }))
343
+
344
+ const output = [{
345
+ proofs: manage5Info.proofs,
346
+ manageCall: manageCall5,
347
+ step: STEP2_ID
348
+ }];
349
+ if (approveAmount.gt(0)) {
350
+ output.unshift({
351
+ proofs: manage4Info.proofs,
352
+ manageCall: manageCall4,
353
+ step: STEP1_ID
354
+ })
355
+ }
356
+ return output;
357
+ }
358
+
359
+ getTag() {
360
+ return `${UniversalStrategy.name}:${this.metadata.name}`;
361
+ }
362
+
363
+ async getVesuHealthFactors() {
364
+ return await Promise.all(this.getVesuAdapters().map(v => v.getHealthFactor()));
365
+ }
366
+
367
+ async computeRebalanceConditionAndReturnCalls(): Promise<Call[]> {
368
+ const vesuAdapters = this.getVesuAdapters();
369
+ const healthFactors = await this.getVesuHealthFactors();
370
+ const leg1HealthFactor = healthFactors[0];
371
+ const leg2HealthFactor = healthFactors[1];
372
+ logger.verbose(`${this.getTag()}: HealthFactorLeg1: ${leg1HealthFactor}`);
373
+ logger.verbose(`${this.getTag()}: HealthFactorLeg2: ${leg2HealthFactor}`);
374
+
375
+ const minHf = this.metadata.additionalInfo.minHealthFactor;
376
+ const isRebalanceNeeded1 = leg1HealthFactor < minHf;
377
+ const isRebalanceNeeded2 = leg2HealthFactor < minHf;
378
+ if (!isRebalanceNeeded1 && !isRebalanceNeeded2) {
379
+ return [];
380
+ }
381
+
382
+ if (isRebalanceNeeded1) {
383
+ const amount = await this.getLegRebalanceAmount(vesuAdapters[0], leg1HealthFactor, false);
384
+ const leg2HF = await this.getNewHealthFactor(vesuAdapters[1], amount, true);
385
+ assert(leg2HF > minHf, `Rebalance Leg1 failed: Leg2 HF after rebalance would be too low: ${leg2HF}`);
386
+ return [await this.getRebalanceCall({
387
+ isLeg1toLeg2: false,
388
+ amount: amount
389
+ })];
390
+ } else {
391
+ const amount = await this.getLegRebalanceAmount(vesuAdapters[1], leg2HealthFactor, true);
392
+ const leg1HF = await this.getNewHealthFactor(vesuAdapters[0], amount, false);
393
+ assert(leg1HF > minHf, `Rebalance Leg2 failed: Leg1 HF after rebalance would be too low: ${leg1HF}`);
394
+ return [await this.getRebalanceCall({
395
+ isLeg1toLeg2: true,
396
+ amount: amount
397
+ })];
398
+ }
399
+ }
400
+
401
+ private async getNewHealthFactor(vesuAdapter: VesuAdapter, newAmount: Web3Number, isWithdraw: boolean) {
402
+ const {
403
+ collateralTokenAmount,
404
+ collateralUSDAmount,
405
+ collateralPrice,
406
+ debtTokenAmount,
407
+ debtUSDAmount,
408
+ debtPrice,
409
+ ltv
410
+ } = await vesuAdapter.getAssetPrices();
411
+
412
+ if (isWithdraw) {
413
+ const newHF = ((collateralTokenAmount.toNumber() - newAmount.toNumber()) * collateralPrice * ltv) / debtUSDAmount;
414
+ logger.verbose(`getNewHealthFactor:: HF: ${newHF}, amoutn: ${newAmount.toNumber()}, isDeposit`);
415
+ return newHF;
416
+ } else { // is borrow
417
+ const newHF = (collateralUSDAmount * ltv) / ((debtTokenAmount.toNumber() + newAmount.toNumber()) * debtPrice);
418
+ logger.verbose(`getNewHealthFactor:: HF: ${newHF}, amoutn: ${newAmount.toNumber()}, isRepay`);
419
+ return newHF;
420
+ }
421
+ }
422
+
423
+ /**
424
+ *
425
+ * @param vesuAdapter
426
+ * @param currentHf
427
+ * @param isDeposit if true, attempt by adding collateral, else by repaying
428
+ * @returns
429
+ */
430
+ private async getLegRebalanceAmount(vesuAdapter: VesuAdapter, currentHf: number, isDeposit: boolean) {
431
+ const {
432
+ collateralTokenAmount,
433
+ collateralUSDAmount,
434
+ collateralPrice,
435
+ debtTokenAmount,
436
+ debtUSDAmount,
437
+ debtPrice,
438
+ ltv
439
+ } = await vesuAdapter.getAssetPrices();
440
+
441
+ // debt is zero, nothing to rebalance
442
+ if(debtTokenAmount.isZero()) {
443
+ return Web3Number.fromWei(0, 0);
444
+ }
445
+
446
+ assert(collateralPrice > 0 && debtPrice > 0, "getRebalanceAmount: Invalid price");
447
+
448
+ // avoid calculating for too close
449
+ const targetHF = this.metadata.additionalInfo.targetHealthFactor;
450
+ if (currentHf > targetHF - 0.01)
451
+ throw new Error("getLegRebalanceAmount: Current health factor is healthy");
452
+
453
+ if (isDeposit) {
454
+ // TargetHF = (collAmount + newAmount) * price * ltv / debtUSD
455
+ const newAmount = targetHF * debtUSDAmount / (collateralPrice * ltv) - collateralTokenAmount.toNumber();
456
+ logger.verbose(`${this.getTag()}:: getLegRebalanceAmount: addCollateral, currentHf: ${currentHf}, targetHF: ${targetHF}, collAmount: ${collateralTokenAmount.toString()}, collUSD: ${collateralUSDAmount}, collPrice: ${collateralPrice}, debtAmount: ${debtTokenAmount.toString()}, debtUSD: ${debtUSDAmount}, debtPrice: ${debtPrice}, ltv: ${ltv}, newAmount: ${newAmount}`);
457
+ return new Web3Number(newAmount.toFixed(8), collateralTokenAmount.decimals);
458
+ } else {
459
+ // TargetHF = collUSD * ltv / (debtAmount - newAmount) * debtPrice
460
+ const newAmount = debtTokenAmount.toNumber() - collateralUSDAmount * ltv / (targetHF * debtPrice);
461
+ logger.verbose(`${this.getTag()}:: getLegRebalanceAmount: repayDebt, currentHf: ${currentHf}, targetHF: ${targetHF}, collAmount: ${collateralTokenAmount.toString()}, collUSD: ${collateralUSDAmount}, collPrice: ${collateralPrice}, debtAmount: ${debtTokenAmount.toString()}, debtUSD: ${debtUSDAmount}, debtPrice: ${debtPrice}, ltv: ${ltv}, newAmount: ${newAmount}`);
462
+ return new Web3Number(newAmount.toFixed(8), debtTokenAmount.decimals);
463
+ }
464
+ }
465
+
466
+ async getVesuMultiplyCall(params: {
467
+ isDeposit: boolean,
468
+ leg1DepositAmount: Web3Number
469
+ }) {
470
+ const [vesuAdapter1, vesuAdapter2] = this.getVesuAdapters();
471
+ const leg1LTV = await vesuAdapter1.getLTVConfig(this.config);
472
+ const leg2LTV = await vesuAdapter2.getLTVConfig(this.config);
473
+ logger.verbose(`${this.getTag()}: LTVLeg1: ${leg1LTV}`);
474
+ logger.verbose(`${this.getTag()}: LTVLeg2: ${leg2LTV}`);
475
+
476
+ const token1Price = await this.pricer.getPrice(vesuAdapter1.config.collateral.symbol);
477
+ const token2Price = await this.pricer.getPrice(vesuAdapter2.config.collateral.symbol);
478
+ logger.verbose(`${this.getTag()}: Price${vesuAdapter1.config.collateral.symbol}: ${token1Price.price}`);
479
+ logger.verbose(`${this.getTag()}: Price${vesuAdapter2.config.collateral.symbol}: ${token2Price.price}`);
480
+
481
+ const TARGET_HF = this.metadata.additionalInfo.targetHealthFactor;
482
+
483
+ const k1 = token1Price.price * leg1LTV / token2Price.price / TARGET_HF;
484
+ const k2 = token1Price.price * TARGET_HF / token2Price.price / leg2LTV;
485
+
486
+ const borrow2Amount = new Web3Number(
487
+ params.leg1DepositAmount.multipliedBy(k1.toFixed(6)).dividedBy(k2 - k1).toFixed(6),
488
+ vesuAdapter2.config.debt.decimals
489
+ );
490
+ const borrow1Amount = new Web3Number(
491
+ borrow2Amount.multipliedBy(k2).toFixed(6),
492
+ vesuAdapter1.config.debt.decimals
493
+ );
494
+ logger.verbose(`${this.getTag()}:: leg1DepositAmount: ${params.leg1DepositAmount.toString()} ${vesuAdapter1.config.collateral.symbol}`);
495
+ logger.verbose(`${this.getTag()}:: borrow1Amount: ${borrow1Amount.toString()} ${vesuAdapter1.config.debt.symbol}`);
496
+ logger.verbose(`${this.getTag()}:: borrow2Amount: ${borrow2Amount.toString()} ${vesuAdapter2.config.debt.symbol}`);
497
+
498
+ const callSet1 = this.getVesuModifyPositionCalls({
499
+ isLeg1: true,
500
+ isDeposit: params.isDeposit,
501
+ depositAmount: params.leg1DepositAmount.plus(borrow2Amount),
502
+ debtAmount: borrow1Amount
503
+ });
504
+
505
+ const callSet2 = this.getVesuModifyPositionCalls({
506
+ isLeg1: false,
507
+ isDeposit: params.isDeposit,
508
+ depositAmount: borrow1Amount,
509
+ debtAmount: borrow2Amount
510
+ });
511
+
512
+ const allActions = [...callSet1.map(i => i.manageCall), ...callSet2.map(i => i.manageCall)];
513
+ const flashloanCalldata = CallData.compile([
514
+ [...callSet1.map(i => i.proofs), ...callSet2.map(i => i.proofs)],
515
+ allActions.map(i => i.sanitizer.address),
516
+ allActions.map(i => i.call.contractAddress.address),
517
+ allActions.map(i => i.call.selector),
518
+ allActions.map(i => i.call.calldata)
519
+ ])
520
+
521
+ // flash loan
522
+ const STEP1_ID = UNIVERSAL_MANAGE_IDS.FLASH_LOAN;
523
+ const manage1Info = this.getProofs<FlashloanCallParams>(STEP1_ID);
524
+ const manageCall1 = manage1Info.callConstructor({
525
+ amount: borrow2Amount,
526
+ data: flashloanCalldata.map(i => BigInt(i))
527
+ })
528
+ const manageCall = this.getManageCall([UNIVERSAL_MANAGE_IDS.FLASH_LOAN], [manageCall1]);
529
+ return manageCall;
530
+ }
531
+
532
+ async getRebalanceCall(params: {
533
+ isLeg1toLeg2: boolean,
534
+ amount: Web3Number
535
+ }) {
536
+ let callSet1 = this.getVesuModifyPositionCalls({
537
+ isLeg1: true,
538
+ isDeposit: params.isLeg1toLeg2,
539
+ depositAmount: Web3Number.fromWei(0, 0),
540
+ debtAmount: params.amount
541
+ });
542
+
543
+ let callSet2 = this.getVesuModifyPositionCalls({
544
+ isLeg1: false,
545
+ isDeposit: params.isLeg1toLeg2,
546
+ depositAmount: params.amount,
547
+ debtAmount: Web3Number.fromWei(0, 0)
548
+ });
549
+
550
+ if (params.isLeg1toLeg2) {
551
+ const manageCall = this.getManageCall([
552
+ ...callSet1.map(i => i.step), ...callSet2.map(i => i.step)
553
+ ], [...callSet1.map(i => i.manageCall), ...callSet2.map(i => i.manageCall)]);
554
+ return manageCall;
555
+ } else {
556
+ const manageCall = this.getManageCall([
557
+ ...callSet2.map(i => i.step), ...callSet1.map(i => i.step)
558
+ ], [...callSet2.map(i => i.manageCall), ...callSet1.map(i => i.manageCall)]);
559
+ return manageCall;
560
+ }
561
+ }
562
+ }
563
+
564
+
565
+ export enum UNIVERSAL_MANAGE_IDS {
566
+ FLASH_LOAN = 'flash_loan_init',
567
+ VESU_LEG1 = 'vesu_leg1',
568
+ VESU_LEG2 = 'vesu_leg2',
569
+ APPROVE_TOKEN1 = 'approve_token1',
570
+ APPROVE_TOKEN2 = 'approve_token2'
571
+ }
572
+
573
+ export enum UNIVERSAL_ADAPTERS {
574
+ COMMON = 'common_adapter',
575
+ VESU_LEG1 = 'vesu_leg1_adapter',
576
+ VESU_LEG2 = 'vesu_leg2_adapter'
577
+ }
578
+
579
+ function getLooperSettings(
580
+ token1Symbol: string,
581
+ token2Symbol: string,
582
+ vaultSettings: UniversalStrategySettings,
583
+ pool1: ContractAddr,
584
+ pool2: ContractAddr
585
+ ) {
586
+ const USDCToken = Global.getDefaultTokens().find(token => token.symbol === token1Symbol)!;
587
+ const ETHToken = Global.getDefaultTokens().find(token => token.symbol === token2Symbol)!;
588
+
589
+ const commonAdapter = new CommonAdapter({
590
+ manager: vaultSettings.manager,
591
+ asset: USDCToken.address,
592
+ id: UNIVERSAL_MANAGE_IDS.FLASH_LOAN
593
+ })
594
+ const vesuAdapterUSDCETH = new VesuAdapter({
595
+ poolId: pool1,
596
+ collateral: USDCToken,
597
+ debt: ETHToken,
598
+ vaultAllocator: vaultSettings.vaultAllocator,
599
+ id: UNIVERSAL_MANAGE_IDS.VESU_LEG1
600
+ })
601
+ const vesuAdapterETHUSDC = new VesuAdapter({
602
+ poolId: pool2,
603
+ collateral: ETHToken,
604
+ debt: USDCToken,
605
+ vaultAllocator: vaultSettings.vaultAllocator,
606
+ id: UNIVERSAL_MANAGE_IDS.VESU_LEG2
607
+ })
608
+ vaultSettings.adapters.push(...[{
609
+ id: UNIVERSAL_ADAPTERS.COMMON,
610
+ adapter: commonAdapter
611
+ }, {
612
+ id: UNIVERSAL_ADAPTERS.VESU_LEG1,
613
+ adapter: vesuAdapterUSDCETH
614
+ }, {
615
+ id: UNIVERSAL_ADAPTERS.VESU_LEG2,
616
+ adapter: vesuAdapterETHUSDC
617
+ }])
618
+ vaultSettings.leafAdapters.push(commonAdapter.getFlashloanAdapter.bind(commonAdapter));
619
+ vaultSettings.leafAdapters.push(vesuAdapterUSDCETH.getModifyPosition.bind(vesuAdapterUSDCETH));
620
+ vaultSettings.leafAdapters.push(vesuAdapterETHUSDC.getModifyPosition.bind(vesuAdapterETHUSDC));
621
+ vaultSettings.leafAdapters.push(commonAdapter.getApproveAdapter(USDCToken.address, vesuAdapterUSDCETH.VESU_SINGLETON, UNIVERSAL_MANAGE_IDS.APPROVE_TOKEN1).bind(commonAdapter));
622
+ vaultSettings.leafAdapters.push(commonAdapter.getApproveAdapter(ETHToken.address, vesuAdapterETHUSDC.VESU_SINGLETON, UNIVERSAL_MANAGE_IDS.APPROVE_TOKEN2).bind(commonAdapter));
623
+ return vaultSettings;
624
+ }
625
+
626
+ const _riskFactor: RiskFactor[] = [
627
+ { type: RiskType.SMART_CONTRACT_RISK, value: 0.5, weight: 25, reason: "Audited by Zellic" },
628
+ { type: RiskType.LIQUIDATION_RISK, value: 1, weight: 50, reason: "Liquidation risk is mitigated btable price feed on Starknet" }
629
+ ];
630
+
631
+ const usdcVaultSettings: UniversalStrategySettings = {
632
+ manager: ContractAddr.from('0xf41a2b1f498a7f9629db0b8519259e66e964260a23d20003f3e42bb1997a07'),
633
+ vaultAllocator: ContractAddr.from('0x228cca1005d3f2b55cbaba27cb291dacf1b9a92d1d6b1638195fbd3d0c1e3ba'),
634
+ redeemRequestNFT: ContractAddr.from('0x906d03590010868cbf7590ad47043959d7af8e782089a605d9b22567b64fda'),
635
+ leafAdapters: [],
636
+ adapters: [],
637
+ targetHealthFactor: 1.3,
638
+ minHealthFactor: 1.25
639
+ }
640
+
641
+ const wbtcVaultSettings: UniversalStrategySettings = {
642
+ manager: ContractAddr.from('0xef8a664ffcfe46a6af550766d27c28937bf1b77fb4ab54d8553e92bca5ba34'),
643
+ vaultAllocator: ContractAddr.from('0x1e01c25f0d9494570226ad28a7fa856c0640505e809c366a9fab4903320e735'),
644
+ redeemRequestNFT: ContractAddr.from('0x4fec59a12f8424281c1e65a80b5de51b4e754625c60cddfcd00d46941ec37b2'),
645
+ leafAdapters: [],
646
+ adapters: [],
647
+ targetHealthFactor: 1.3,
648
+ minHealthFactor: 1.25
649
+ }
650
+
651
+ export const UniversalStrategies: IStrategyMetadata<UniversalStrategySettings>[] =
652
+ [
653
+ {
654
+ name: "USDC Evergreen",
655
+ description: "A universal strategy for managing USDC assets",
656
+ address: ContractAddr.from('0x7e6498cf6a1bfc7e6fc89f1831865e2dacb9756def4ec4b031a9138788a3b5e'),
657
+ launchBlock: 0,
658
+ type: 'ERC4626',
659
+ depositTokens: [Global.getDefaultTokens().find(token => token.symbol === 'USDC')!],
660
+ additionalInfo: getLooperSettings('USDC', 'ETH', usdcVaultSettings, VesuPools.Genesis, VesuPools.Genesis),
661
+ risk: {
662
+ riskFactor: _riskFactor,
663
+ netRisk:
664
+ _riskFactor.reduce((acc, curr) => acc + curr.value * curr.weight, 0) /
665
+ _riskFactor.reduce((acc, curr) => acc + curr.weight, 0),
666
+ notARisks: getNoRiskTags(_riskFactor)
667
+ },
668
+ protocols: [Protocols.VESU],
669
+ maxTVL: Web3Number.fromWei(0, 6),
670
+ contractDetails: [],
671
+ faqs: [],
672
+ investmentSteps: [],
673
+ },
674
+ {
675
+ name: "WBTC Evergreen",
676
+ description: "A universal strategy for managing WBTC assets",
677
+ address: ContractAddr.from('0x5a4c1651b913aa2ea7afd9024911603152a19058624c3e425405370d62bf80c'),
678
+ launchBlock: 0,
679
+ type: 'ERC4626',
680
+ depositTokens: [Global.getDefaultTokens().find(token => token.symbol === 'WBTC')!],
681
+ additionalInfo: getLooperSettings('WBTC', 'ETH', wbtcVaultSettings, VesuPools.Genesis, VesuPools.Genesis),
682
+ risk: {
683
+ riskFactor: _riskFactor,
684
+ netRisk:
685
+ _riskFactor.reduce((acc, curr) => acc + curr.value * curr.weight, 0) /
686
+ _riskFactor.reduce((acc, curr) => acc + curr.weight, 0),
687
+ notARisks: getNoRiskTags(_riskFactor)
688
+ },
689
+ protocols: [Protocols.VESU],
690
+ maxTVL: Web3Number.fromWei(0, 8),
691
+ contractDetails: [],
692
+ faqs: [],
693
+ investmentSteps: [],
694
+ },
695
+ ]