@strkfarm/sdk 2.0.0-dev.4 → 2.0.0-dev.41

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 (78) hide show
  1. package/dist/cli.js +190 -36
  2. package/dist/cli.mjs +188 -34
  3. package/dist/index.browser.global.js +116250 -90801
  4. package/dist/index.browser.mjs +13050 -10957
  5. package/dist/index.d.ts +2232 -1933
  6. package/dist/index.js +13380 -11084
  7. package/dist/index.mjs +13280 -11007
  8. package/package.json +6 -7
  9. package/src/data/avnu.abi.json +840 -0
  10. package/src/data/ekubo-price-fethcer.abi.json +265 -0
  11. package/src/data/redeem-request-nft.abi.json +752 -0
  12. package/src/data/universal-vault.abi.json +8 -7
  13. package/src/dataTypes/_bignumber.ts +13 -4
  14. package/src/dataTypes/bignumber.browser.ts +10 -1
  15. package/src/dataTypes/bignumber.node.ts +10 -1
  16. package/src/dataTypes/index.ts +3 -2
  17. package/src/dataTypes/mynumber.ts +141 -0
  18. package/src/global.ts +93 -36
  19. package/src/index.browser.ts +2 -1
  20. package/src/interfaces/common.tsx +218 -5
  21. package/src/modules/apollo-client-config.ts +28 -0
  22. package/src/modules/avnu.ts +21 -12
  23. package/src/modules/ekubo-pricer.ts +79 -0
  24. package/src/modules/ekubo-quoter.ts +48 -30
  25. package/src/modules/erc20.ts +17 -0
  26. package/src/modules/harvests.ts +43 -29
  27. package/src/modules/index.ts +2 -1
  28. package/src/modules/pragma.ts +23 -8
  29. package/src/modules/pricer-avnu-api.ts +114 -0
  30. package/src/modules/pricer-from-api.ts +156 -15
  31. package/src/modules/pricer-lst.ts +1 -1
  32. package/src/modules/pricer.ts +94 -40
  33. package/src/modules/pricerBase.ts +2 -1
  34. package/src/node/deployer.ts +36 -1
  35. package/src/node/pricer-redis.ts +3 -1
  36. package/src/strategies/base-strategy.ts +168 -16
  37. package/src/strategies/constants.ts +8 -3
  38. package/src/strategies/ekubo-cl-vault.tsx +1047 -351
  39. package/src/strategies/factory.ts +199 -0
  40. package/src/strategies/index.ts +5 -3
  41. package/src/strategies/registry.ts +262 -0
  42. package/src/strategies/sensei.ts +353 -9
  43. package/src/strategies/svk-strategy.ts +283 -31
  44. package/src/strategies/token-boosted-xstrk-carry-strategy.tsx +1262 -0
  45. package/src/strategies/types.ts +4 -0
  46. package/src/strategies/universal-adapters/adapter-utils.ts +4 -1
  47. package/src/strategies/universal-adapters/avnu-adapter.ts +196 -272
  48. package/src/strategies/universal-adapters/baseAdapter.ts +263 -251
  49. package/src/strategies/universal-adapters/common-adapter.ts +206 -203
  50. package/src/strategies/universal-adapters/index.ts +10 -8
  51. package/src/strategies/universal-adapters/svk-troves-adapter.ts +511 -0
  52. package/src/strategies/universal-adapters/token-transfer-adapter.ts +200 -0
  53. package/src/strategies/universal-adapters/vesu-adapter.ts +120 -82
  54. package/src/strategies/universal-adapters/vesu-modify-position-adapter.ts +525 -0
  55. package/src/strategies/universal-adapters/vesu-multiply-adapter.ts +1098 -712
  56. package/src/strategies/universal-adapters/vesu-position-common.ts +258 -0
  57. package/src/strategies/universal-adapters/vesu-supply-only-adapter.ts +18 -3
  58. package/src/strategies/universal-lst-muliplier-strategy.tsx +631 -414
  59. package/src/strategies/universal-strategy.tsx +1331 -1173
  60. package/src/strategies/vesu-rebalance.tsx +252 -152
  61. package/src/strategies/yoloVault.ts +1087 -0
  62. package/src/utils/cacheClass.ts +11 -2
  63. package/src/utils/health-factor-math.ts +33 -1
  64. package/src/utils/index.ts +3 -1
  65. package/src/utils/logger.browser.ts +22 -4
  66. package/src/utils/logger.node.ts +259 -24
  67. package/src/utils/starknet-call-parser.ts +1036 -0
  68. package/src/utils/strategy-utils.ts +61 -0
  69. package/src/modules/ExtendedWrapperSDk/index.ts +0 -62
  70. package/src/modules/ExtendedWrapperSDk/types.ts +0 -311
  71. package/src/modules/ExtendedWrapperSDk/wrapper.ts +0 -395
  72. package/src/strategies/universal-adapters/extended-adapter.ts +0 -661
  73. package/src/strategies/universal-adapters/unused-balance-adapter.ts +0 -109
  74. package/src/strategies/vesu-extended-strategy/services/operationService.ts +0 -34
  75. package/src/strategies/vesu-extended-strategy/utils/config.runtime.ts +0 -77
  76. package/src/strategies/vesu-extended-strategy/utils/constants.ts +0 -49
  77. package/src/strategies/vesu-extended-strategy/utils/helper.ts +0 -372
  78. package/src/strategies/vesu-extended-strategy/vesu-extended-strategy.tsx +0 -1140
@@ -1,333 +1,472 @@
1
1
  import { ContractAddr, Web3Number } from "@/dataTypes";
2
- import { IConfig, Protocols, TokenInfo } from "@/interfaces";
3
- import { PricerBase } from "@/modules/pricerBase";
4
- import { BaseAdapter, BaseAdapterConfig, SupportedPosition, PositionInfo, PositionAPY, APYType, ManageCall, AdapterLeafType, GenerateCallFn, DepositParams, WithdrawParams, PositionAmount } from "./baseAdapter";
5
- import { SIMPLE_SANITIZER, SIMPLE_SANITIZER_V2, toBigInt, VESU_SINGLETON, VESU_V2_MODIFY_POSITION_SANITIZER } from "./adapter-utils";
2
+ import { Protocols, TokenInfo } from "@/interfaces";
3
+ import {
4
+ BaseAdapter,
5
+ BaseAdapterConfig,
6
+ SupportedPosition,
7
+ PositionInfo,
8
+ PositionAPY,
9
+ APYType,
10
+ ManageCall,
11
+ AdapterLeafType,
12
+ GenerateCallFn,
13
+ PositionAmount,
14
+ SwapPriceInfo,
15
+ } from "./baseAdapter";
16
+ import {
17
+ SIMPLE_SANITIZER,
18
+ SIMPLE_SANITIZER_V2,
19
+ SIMPLE_SANITIZER_VESU_V1_DELEGATIONS,
20
+ toBigInt,
21
+ } from "./adapter-utils";
6
22
  import { hash, uint256, Contract, CairoCustomEnum, num } from "starknet";
7
- import { VesuAdapter, VesuMultiplyCallParams, VesuModifyDelegationCallParams, getVesuSingletonAddress, VesuPools, Swap, IncreaseLeverParams, DecreaseLeverParams } from "./vesu-adapter";
8
- import { logger } from "@/utils";
9
- import { WALLET_ADDRESS } from "../vesu-extended-strategy/utils/constants";
10
- import VesuMultiplyAbi from '@/data/vesu-multiple.abi.json';
11
- import VesuSingletonAbi from '../../data/vesu-singleton.abi.json';
12
- import VesuPoolV2Abi from '@/data/vesu-pool-v2.abi.json';
13
- import { EkuboQuoter, TokenMarketData } from "@/modules";
14
- import { calculateDebtReductionAmountForWithdrawal } from "../vesu-extended-strategy/utils/helper";
23
+ import {
24
+ VesuAdapter,
25
+ getVesuSingletonAddress,
26
+ Swap,
27
+ IncreaseLeverParams,
28
+ DecreaseLeverParams,
29
+ } from "./vesu-adapter";
30
+ import { assert, logger } from "@/utils";
31
+ import VesuMultiplyAbi from "@/data/vesu-multiple.abi.json";
32
+ import { EkuboQuote, EkuboQuoter, TokenMarketData } from "@/modules";
15
33
  import { HealthFactorMath } from "@/utils/health-factor-math";
16
- import { MAX_LIQUIDATION_RATIO } from "../vesu-extended-strategy/utils/constants";
34
+ import {
35
+ VesuPositionCommonContext,
36
+ getVesuCommonAPY,
37
+ getVesuCommonMaxBorrowableAPY,
38
+ getVesuCommonMaxDeposit,
39
+ getVesuCommonMaxWithdraw,
40
+ getVesuCommonPosition,
41
+ } from "./vesu-position-common";
42
+
43
+ const MIN_REMAINING_DEBT_USD = 11;
44
+ const SCALE_128 = BigInt('1000000000000000000'); // 1e18 (matches vesu::units::SCALE_128)
45
+ const MIN_SQRT_RATIO_LIMIT = BigInt('18446748437148339061');
46
+ const MAX_SQRT_RATIO_LIMIT = BigInt('6277100250585753475930931601400621808602321654880405518632');
47
+
48
+ export interface VesuDepositParams {
49
+ amount: Web3Number;
50
+ marginSwap?: {
51
+ marginToken: TokenInfo;
52
+ };
53
+ leverSwap?: {
54
+ exactOutput?: Web3Number;
55
+ };
56
+ }
57
+
58
+ export interface VesuWithdrawParams {
59
+ amount: Web3Number;
60
+ withdrawSwap?: {
61
+ outputToken: TokenInfo;
62
+ };
63
+ }
17
64
 
18
65
  export interface VesuMultiplyAdapterConfig extends BaseAdapterConfig {
19
66
  poolId: ContractAddr;
20
67
  collateral: TokenInfo;
21
68
  debt: TokenInfo;
69
+ marginToken: TokenInfo; // in case of using ekubo to do margin swap, this can be diff token, else collateral itself.
22
70
  targetHealthFactor: number;
23
71
  minHealthFactor: number;
24
72
  quoteAmountToFetchPrice: Web3Number;
73
+ minimumVesuMovementAmount: number;
74
+ maxSlippage?: number; // e.g. 0.005 for 0.5%
25
75
  }
26
76
 
27
- export class VesuMultiplyAdapter extends BaseAdapter<DepositParams, WithdrawParams> {
77
+ export class VesuMultiplyAdapter extends BaseAdapter<
78
+ VesuDepositParams,
79
+ VesuWithdrawParams
80
+ > {
28
81
  readonly config: VesuMultiplyAdapterConfig;
29
- readonly vesuAdapter: VesuAdapter;
82
+ readonly _vesuAdapter: VesuAdapter;
30
83
  readonly tokenMarketData: TokenMarketData;
31
-
84
+ readonly minimumVesuMovementAmount: number;
85
+ lastSwapPriceInfo: SwapPriceInfo | null = null;
86
+ maxSlippage: number = 0.002; // 0.2%
32
87
  constructor(config: VesuMultiplyAdapterConfig) {
33
88
  super(config, VesuMultiplyAdapter.name, Protocols.VESU);
34
89
  this.config = config;
35
- this.vesuAdapter = new VesuAdapter({
90
+ this._vesuAdapter = new VesuAdapter({
36
91
  poolId: config.poolId,
37
92
  collateral: config.collateral,
38
93
  debt: config.debt,
39
94
  vaultAllocator: config.vaultAllocator,
40
- id: ''
95
+ id: "",
41
96
  });
42
- this.tokenMarketData = new TokenMarketData(this.config.pricer, this.config.networkConfig);
97
+ this.minimumVesuMovementAmount = config.minimumVesuMovementAmount ?? 5; //5 usdc
98
+ this.tokenMarketData = new TokenMarketData(
99
+ this.config.pricer,
100
+ this.config.networkConfig
101
+ );
102
+ this.config.maxSlippage = config.maxSlippage ?? 0.002; // 0.2%
103
+ this.maxSlippage = config.maxSlippage ?? 0.002; // 0.2%
43
104
  }
44
105
 
45
- protected async getAPY(supportedPosition: SupportedPosition): Promise<PositionAPY> {
46
- const CACHE_KEY = `apy_${this.config.poolId.address}_${supportedPosition.asset.symbol}`;
47
- const cacheData = this.getCache<PositionAPY>(CACHE_KEY);
48
- console.log(`${VesuMultiplyAdapter.name}::getAPY cacheData: ${JSON.stringify(cacheData)}`, this.vesuAdapter.config.poolId.shortString(), this.vesuAdapter.config.collateral.symbol, this.vesuAdapter.config.debt.symbol);
49
- if (cacheData) {
50
- return cacheData;
51
- }
52
- try {
53
- // Get Vesu pools to find APY for the asset
54
- const allVesuPools = await VesuAdapter.getVesuPools();
55
- const asset = supportedPosition.asset;
56
- const pool = allVesuPools.pools.find(p => this.vesuAdapter.config.poolId.eqString(num.getHexString(p.id)));
57
- if (!pool) {
58
- logger.warn(`VesuMultiplyAdapter: Pool not found for token ${asset.symbol}`);
59
- return {
60
- apy: 0,
61
- type: APYType.BASE
62
- };
63
- }
64
- // Find the asset stats for our token
65
- const assetStats = pool.assets.find((a: any) =>
66
- a.symbol.toLowerCase() === asset.symbol.toLowerCase()
67
- )?.stats;
68
-
69
- if (!assetStats) {
70
- logger.warn(`VesuMultiplyAdapter: Asset stats not found for token ${asset.symbol}`);
71
- return {
72
- apy: 0,
73
- type: APYType.BASE
74
- };
75
- }
76
- // Get appropriate APY based on position type
77
- let apy = 0;
78
- if (supportedPosition.isDebt) {
79
- // For debt positions, use borrow APY
80
- apy = Number(assetStats.borrowApr?.value || 0) / 1e18;
81
-
82
- // todo
83
- // Account for rewards on debt token
84
- } else {
85
- // For collateral positions, use supply APY
86
- const isAssetBTC = asset.symbol.toLowerCase().includes("btc");
87
- const baseAPY = Number(isAssetBTC ? assetStats.btcFiSupplyApr?.value + assetStats.supplyApy?.value : assetStats.supplyApy?.value || 0) / 1e18;
88
-
89
- // account for reward yield (like STRK rewards)
90
- const rewardAPY = Number(assetStats.defiSpringSupplyApr?.value || "0") / 1e18;
91
-
92
- // account for base yield of LST
93
- const isSupported = this.tokenMarketData.isAPYSupported(asset);
94
- apy = baseAPY + rewardAPY;
95
- if (isSupported) {
96
- const tokenAPY = await this.tokenMarketData.getAPY(asset);
97
- apy += tokenAPY;
98
- }
99
- }
100
- const result = {
101
- apy,
102
- type: supportedPosition.isDebt ? APYType.BASE : APYType.BASE
103
- };
106
+ // ─── Shared Helpers ──────────────────────────────────────────────────────────
104
107
 
105
- this.setCache(CACHE_KEY, result, 300000); // Cache for 5 minutes
106
- return result;
107
- } catch (error) {
108
- logger.error(`VesuMultiplyAdapter: Error getting APY for ${supportedPosition.asset.symbol}:`, error);
109
- // return {
110
- // apy: 0,
111
- // type: APYType.BASE
112
- // };
113
- throw error;
114
- }
108
+ private _getMultiplyContract(): {
109
+ contract: Contract;
110
+ vesuMultiply: ContractAddr;
111
+ vesuSingleton: ContractAddr;
112
+ isV2: boolean;
113
+ } {
114
+ const { addr: vesuSingleton, isV2 } = getVesuSingletonAddress(
115
+ this.config.poolId
116
+ );
117
+ const vesuMultiply = isV2
118
+ ? this._vesuAdapter.VESU_WITHDRAW_SWAP_FIXED_MULTIPLIER
119
+ : this._vesuAdapter.VESU_MULTIPLY_V1;
120
+ const contract = new Contract({
121
+ abi: VesuMultiplyAbi,
122
+ address: vesuMultiply.address,
123
+ providerOrAccount: this.config.networkConfig.provider,
124
+ });
125
+ return { contract, vesuMultiply, vesuSingleton, isV2 };
115
126
  }
116
127
 
117
- protected async getPosition(supportedPosition: SupportedPosition): Promise<PositionAmount> {
118
- const CACHE_KEY = `position_${this.config.poolId.address}_${supportedPosition.asset.symbol}`;
119
- const cacheData = this.getCache<PositionAmount>(CACHE_KEY);
120
- if (cacheData) {
121
- return cacheData;
122
- }
128
+ private _buildDelegationWrappedCalls(
129
+ preCalls: ManageCall[],
130
+ modifyLeverCalldata: bigint[],
131
+ proofReadableIds: {
132
+ delegationOn: string;
133
+ modifyLever: string;
134
+ delegationOff: string;
135
+ },
136
+ ): ManageCall[] {
137
+ const { vesuMultiply, vesuSingleton, isV2 } = this._getMultiplyContract();
123
138
 
124
- try {
125
- // Use VesuAdapter to get positions
126
- this.vesuAdapter.networkConfig = this.config.networkConfig;
127
- this.vesuAdapter.pricer = this.config.pricer;
128
-
129
- const positions = await this.vesuAdapter.getPositions(this.config.networkConfig);
130
-
131
- // Find the position for our asset
132
- let position = positions.find(p =>
133
- p.token.address.eq(supportedPosition.asset.address)
134
- );
139
+ const delegationOn: ManageCall = {
140
+ proofReadableId: proofReadableIds.delegationOn,
141
+ sanitizer: isV2 ? SIMPLE_SANITIZER_V2 : SIMPLE_SANITIZER_VESU_V1_DELEGATIONS,
142
+ call: {
143
+ contractAddress: vesuSingleton,
144
+ selector: hash.getSelectorFromName("modify_delegation"),
145
+ calldata: isV2
146
+ ? [vesuMultiply.toBigInt(), BigInt(1)]
147
+ : [this.config.poolId.toBigInt(), vesuMultiply.toBigInt(), BigInt(1)],
148
+ },
149
+ };
135
150
 
136
- if (!position) {
137
- logger.warn(`VesuMultiplyAdapter: Position not found for token ${supportedPosition.asset.symbol}`);
138
- return {
139
- amount: new Web3Number('0', supportedPosition.asset.decimals),
140
- remarks: "Position not found"
141
- };
142
- }
151
+ const modifyLever: ManageCall = {
152
+ proofReadableId: proofReadableIds.modifyLever,
153
+ sanitizer: isV2 ? SIMPLE_SANITIZER_V2 : SIMPLE_SANITIZER_VESU_V1_DELEGATIONS,
154
+ call: {
155
+ contractAddress: vesuMultiply,
156
+ selector: hash.getSelectorFromName("modify_lever"),
157
+ calldata: modifyLeverCalldata,
158
+ },
159
+ };
143
160
 
144
- if (supportedPosition.isDebt) {
145
- position.amount = position.amount.multipliedBy(-1);
146
- position.usdValue = position.usdValue * -1;
147
- }
161
+ const delegationOff: ManageCall = {
162
+ proofReadableId: proofReadableIds.delegationOff,
163
+ sanitizer: isV2 ? SIMPLE_SANITIZER_V2 : SIMPLE_SANITIZER_VESU_V1_DELEGATIONS,
164
+ call: {
165
+ contractAddress: vesuSingleton,
166
+ selector: hash.getSelectorFromName("modify_delegation"),
167
+ calldata: isV2
168
+ ? [vesuMultiply.toBigInt(), BigInt(0)]
169
+ : [this.config.poolId.toBigInt(), vesuMultiply.toBigInt(), BigInt(0)],
170
+ },
171
+ };
148
172
 
149
- this.setCache(CACHE_KEY, position, 60000); // Cache for 1 minute
150
- return position;
151
- } catch (error) {
152
- logger.error(`VesuMultiplyAdapter: Error getting position for ${supportedPosition.asset.symbol}:`, error);
153
- // return new Web3Number('0', supportedPosition.asset.decimals);
154
- throw error;
155
- }
173
+ return [...preCalls, delegationOn, modifyLever, delegationOff];
156
174
  }
157
175
 
158
- async maxBorrowableAPY(): Promise<number> {
159
- // get collateral APY
160
- const collateralAPY = await this.getAPY({ asset: this.config.collateral, isDebt: false });
161
- const apy = collateralAPY.apy * 0.8;
162
- return apy;
176
+ private _getDepositProofReadableIds() {
177
+ const collateral = this.config.collateral;
178
+ const debt = this.config.debt;
179
+ return {
180
+ approve_collateral: `amc_${this.config.poolId.shortString()}_${collateral.symbol}`,
181
+ approve_margin: `amc_${this.config.poolId.shortString()}_${this.config.marginToken.symbol}`,
182
+ delegationOn: `sd1_${this.config.poolId.shortString()}_${collateral.symbol}_${debt.symbol}`,
183
+ modifyLever: `vm_${this.config.poolId.shortString()}_${collateral.symbol}_${debt.symbol}`,
184
+ delegationOff: `sd2_${this.config.poolId.shortString()}_${collateral.symbol}_${debt.symbol}`,
185
+ };
163
186
  }
164
187
 
165
- async maxDeposit(amount?: Web3Number): Promise<PositionInfo> {
188
+ private _getWithdrawProofReadableIds() {
166
189
  const collateral = this.config.collateral;
167
190
  const debt = this.config.debt;
191
+ return {
192
+ delegationOn: `sdow_${this.config.poolId.shortString()}_${collateral.symbol}_${debt.symbol}`,
193
+ modifyLever: `vmw_${this.config.poolId.shortString()}_${collateral.symbol}_${debt.symbol}`,
194
+ delegationOff: `sdofw_${this.config.poolId.shortString()}_${collateral.symbol}_${debt.symbol}`,
195
+ };
196
+ }
168
197
 
169
- try {
170
- // Get current positions
171
- this.vesuAdapter.networkConfig = this.config.networkConfig;
172
- this.vesuAdapter.pricer = this.config.pricer;
173
-
174
- const positions = await this.vesuAdapter.getPositions(this.config.networkConfig);
175
- const collateralPosition = positions.find(p => p.token.address.eq(collateral.address));
176
- const debtPosition = positions.find(p => p.token.address.eq(debt.address));
177
-
178
- if (!collateralPosition || !debtPosition) {
179
- throw new Error('Could not find current positions');
180
- }
181
-
182
- // Calculate max borrowable amount
183
- const maxBorrowableAPY = await this.maxBorrowableAPY();
184
- const maxBorrowable = await this.vesuAdapter.getMaxBorrowableByInterestRate(
185
- this.config.networkConfig,
186
- debt,
187
- maxBorrowableAPY
188
- );
189
- logger.verbose(`VesuMultiplyAdapter: Max borrowable: ${maxBorrowable.toNumber()}`);
190
- const debtCap = await this.vesuAdapter.getDebtCap(this.config.networkConfig);
191
- logger.verbose(`VesuMultiplyAdapter: Debt cap: ${debtCap.toNumber()}`);
192
- const actualMaxBorrowable = maxBorrowable.minimum(debtCap);
193
- logger.verbose(`VesuMultiplyAdapter: Actual max borrowable: ${actualMaxBorrowable.toNumber()}`);
194
-
195
- // Calculate max collateral that can be deposited based on LTV
196
- const maxLTV = await this.vesuAdapter.getLTVConfig(this.config.networkConfig);
197
- const collateralPrice = await this.config.pricer.getPrice(collateral.symbol);
198
- if (collateralPrice.price === 0) {
199
- throw new Error('Collateral price is 0');
200
- }
201
- const debtPrice = await this.config.pricer.getPrice(debt.symbol);
202
- if (debtPrice.price === 0) {
203
- throw new Error('Debt price is 0');
204
- }
205
- const maxCollateralFromDebt = HealthFactorMath.getMinCollateralRequiredOnLooping(
206
- actualMaxBorrowable,
207
- debtPrice.price,
208
- this.config.targetHealthFactor,
209
- maxLTV,
210
- collateralPrice.price,
211
- collateral
212
- )
198
+ private _buildZeroAmountSwapsWithWeights(
199
+ quote: EkuboQuote,
200
+ token: TokenInfo,
201
+ reverseRoutes = false,
202
+ ): { swaps: Swap[]; weights: Web3Number[] } {
203
+ const swaps: Swap[] = quote.splits.map((split) => {
204
+ let sellToken = token.address;
205
+ const routeNodes = split.route.map((_route: any) => {
206
+ // switch to other for next route.
207
+ let isSellToken0 = sellToken.eqString(_route.pool_key.token0);
208
+ isSellToken0 = reverseRoutes ? !isSellToken0 : isSellToken0;
209
+ sellToken = isSellToken0 ? ContractAddr.from(_route.pool_key.token1) : ContractAddr.from(_route.pool_key.token0);
213
210
 
214
- const maxDepositAmount = amount ? amount.minimum(maxCollateralFromDebt) : maxCollateralFromDebt;
215
- const usdValue = await this.getUSDValue(collateral, maxDepositAmount);
216
- logger.verbose(`VesuMultiplyAdapter: Max deposit::USD value: ${usdValue}, amount: ${maxDepositAmount.toNumber()}`);
217
- const apys = await Promise.all([this.getAPY({ asset: collateral, isDebt: false }), this.getAPY({ asset: debt, isDebt: true })]);
218
- logger.verbose(`VesuMultiplyAdapter: Apys: ${apys[0].apy}, ${apys[1].apy}`);
219
-
220
- const borrowAmountUSD = actualMaxBorrowable.multipliedBy(debtPrice.price);
221
- logger.verbose(`VesuMultiplyAdapter: Borrow amount: ${actualMaxBorrowable.toNumber()}, borrow amount USD: ${borrowAmountUSD.toNumber()}`);
222
- const netCollateralUSD = usdValue + borrowAmountUSD.toNumber();
223
- const netAPY = (apys[0].apy * netCollateralUSD + apys[1].apy * borrowAmountUSD.toNumber()) / (usdValue);
224
- logger.verbose(`VesuMultiplyAdapter: Max deposit amount: ${maxDepositAmount.toNumber()}, netAPY: ${netAPY}`);
211
+ return {
212
+ pool_key: {
213
+ token0: ContractAddr.from(_route.pool_key.token0),
214
+ token1: ContractAddr.from(_route.pool_key.token1),
215
+ fee: _route.pool_key.fee,
216
+ tick_spacing: _route.pool_key.tick_spacing.toString(),
217
+ extension: _route.pool_key.extension,
218
+ },
219
+ sqrt_ratio_limit: isSellToken0 ? Web3Number.fromWei(MIN_SQRT_RATIO_LIMIT.toString(), 18) : Web3Number.fromWei(MAX_SQRT_RATIO_LIMIT.toString(), 18),
220
+ skip_ahead: Web3Number.fromWei(_route.skip_ahead, 0),
221
+ }
222
+ });
223
+ // incorrect logic, hence commented. Ekubo already returns routes in correct order.
224
+ // if (reverseRoutes) {
225
+ // routeNodes.reverse();
226
+ // }
225
227
  return {
226
- tokenInfo: collateral,
227
- amount: maxDepositAmount,
228
- usdValue,
229
- remarks: "Max deposit based on available debt capacity",
230
- apy: {
231
- apy: netAPY,
232
- type: APYType.BASE
228
+ route: routeNodes,
229
+ token_amount: {
230
+ token: token.address,
231
+ amount: Web3Number.fromWei(0, token.decimals),
233
232
  },
234
- protocol: this.protocol
235
233
  };
236
- } catch (error) {
237
- logger.error(`VesuMultiplyAdapter: Error calculating max deposit:`, error);
238
- throw error;
234
+ });
235
+
236
+ const totalSpecifiedBig = quote.splits.reduce(
237
+ (sum: bigint, s: any) => sum + BigInt(Math.abs(Number(s.amount_specified))),
238
+ 0n
239
+ );
240
+ const weights: Web3Number[] = [];
241
+ let allocatedWeight = 0n;
242
+ for (let i = 0; i < quote.splits.length; i++) {
243
+ const split = quote.splits[i];
244
+ let weight: bigint;
245
+ if (i === quote.splits.length - 1) {
246
+ weight = SCALE_128 - allocatedWeight;
247
+ } else if (totalSpecifiedBig > 0n) {
248
+ const splitAbs = BigInt(Math.abs(Number(split.amount_specified)));
249
+ weight = (splitAbs * SCALE_128) / totalSpecifiedBig;
250
+ } else {
251
+ weight = SCALE_128;
252
+ }
253
+ allocatedWeight += weight;
254
+ weights.push(Web3Number.fromWei(weight.toString(), 0));
239
255
  }
256
+
257
+ return { swaps, weights };
240
258
  }
241
259
 
242
- async maxWithdraw(): Promise<PositionInfo> {
243
- const collateral = this.config.collateral;
244
- const debt = this.config.debt;
260
+ private async _fetchPositionAndPrices(): Promise<{
261
+ existingCollateralInfo: any;
262
+ existingDebtInfo: any;
263
+ collateralisation: any;
264
+ collateralPrice: number;
265
+ debtPrice: number;
266
+ ekuboQuoter: EkuboQuoter;
267
+ }> {
268
+ this._vesuAdapter.networkConfig = this.config.networkConfig;
269
+ this._vesuAdapter.pricer = this.config.pricer;
245
270
 
246
- try {
247
- // Calculate how much can be withdrawn without affecting health factor too much
248
- this.vesuAdapter.networkConfig = this.config.networkConfig;
249
- this.vesuAdapter.pricer = this.config.pricer;
250
-
251
- const positions = await this.vesuAdapter.getPositions(this.config.networkConfig);
252
- const collateralPosition = positions.find(p => p.token.address.eq(collateral.address));
253
- const debtPosition = positions.find(p => p.token.address.eq(this.config.debt.address));
254
-
255
- if (!collateralPosition || !debtPosition) {
256
- throw new Error('Could not find current positions');
257
- }
271
+ const existingPositions = await this._vesuAdapter.getPositions(
272
+ this.config.networkConfig
273
+ );
274
+ const collateralisation = await this._vesuAdapter.getCollateralization(
275
+ this.config.networkConfig
276
+ );
277
+ const existingCollateralInfo = existingPositions[0];
278
+ const existingDebtInfo = existingPositions[1];
279
+
280
+ const collateralPrice =
281
+ collateralisation[0].usdValue > 0
282
+ ? collateralisation[0].usdValue /
283
+ existingCollateralInfo.amount.toNumber()
284
+ : (await this.config.pricer.getPrice(this.config.collateral.symbol))
285
+ .price;
286
+ const debtPrice =
287
+ collateralisation[1].usdValue > 0
288
+ ? collateralisation[1].usdValue / existingDebtInfo.amount.toNumber()
289
+ : (await this.config.pricer.getPrice(this.config.debt.symbol)).price;
290
+
291
+ const ekuboQuoter = new EkuboQuoter(
292
+ this.config.networkConfig,
293
+ this.config.pricer
294
+ );
258
295
 
259
- // Calculate max withdrawable (conservative approach)
260
- const collateralPrice = collateralPosition.usdValue / collateralPosition.amount.toNumber();
261
- const debtInCollateral = debtPosition.usdValue / collateralPrice;
262
- const maxWithdrawable = collateralPosition.amount.minus(debtInCollateral);
263
-
264
- const result = maxWithdrawable.greaterThan(0) ? maxWithdrawable : new Web3Number('0', collateral.decimals);
265
- const usdValue = await this.getUSDValue(collateral, result);
266
- const debtUSD = debtPosition.usdValue;
267
- logger.verbose(`VesuMultiplyAdapter: Debt USD: ${debtUSD}, collateral USD: ${usdValue}`);
268
- const apys = await Promise.all([this.getAPY({ asset: collateral, isDebt: false }), this.getAPY({ asset: debt, isDebt: true })]);
269
- logger.verbose(`VesuMultiplyAdapter: Apys: ${apys[0].apy}, ${apys[1].apy}`);
270
- const netAPY = (usdValue - debtUSD) > 0 ? (apys[0].apy * usdValue + apys[1].apy * debtUSD) / (usdValue - debtUSD) : 0;
271
- logger.verbose(`VesuMultiplyAdapter: Max withdraw amount: ${result.toNumber()}, netAPY: ${netAPY}`);
296
+ return {
297
+ existingCollateralInfo,
298
+ existingDebtInfo,
299
+ collateralisation,
300
+ collateralPrice,
301
+ debtPrice,
302
+ ekuboQuoter,
303
+ };
304
+ }
305
+
306
+ private _computeTargetDebtDelta(
307
+ addedCollateral: Web3Number,
308
+ existingCollateral: Web3Number,
309
+ existingDebt: Web3Number,
310
+ collateralPrice: number,
311
+ debtPrice: number,
312
+ ltv: number,
313
+ dexPrice: number
314
+ ): Web3Number {
315
+ // target hf = (((collateral + addedCollateral) * collateralPrice + X)) * ltv / (debt * debtPrice + X)
316
+ // => X = (((collateral + addedCollateral) * collateralPrice * ltv) - (debt * debtPrice * target hf)) / (target hf - ltv)
317
+ const numeratorPart1 = existingCollateral
318
+ .plus(addedCollateral)
319
+ .multipliedBy(collateralPrice)
320
+ .multipliedBy(ltv);
321
+ const numeratorPart2 = existingDebt
322
+ .multipliedBy(debtPrice)
323
+ .multipliedBy(this.config.targetHealthFactor);
324
+ if (dexPrice === 0) {
325
+ throw new Error("_computeTargetDebtDelta::dexPrice is 0");
326
+ }
327
+ const denominatorPart = this.config.targetHealthFactor - ltv / dexPrice;
328
+ if (denominatorPart === 0) {
329
+ throw new Error("_computeTargetDebtDelta::denominatorPart is 0");
330
+ }
331
+ const x_debt_usd = numeratorPart1
332
+ .minus(numeratorPart2)
333
+ .dividedBy(denominatorPart);
334
+
335
+ return new Web3Number(
336
+ x_debt_usd.dividedBy(debtPrice).toFixed(this.config.debt.decimals),
337
+ this.config.debt.decimals
338
+ );
339
+ }
340
+
341
+ private _getPositionCommonContext(): VesuPositionCommonContext {
342
+ return {
343
+ adapterName: VesuMultiplyAdapter.name,
344
+ protocol: this.protocol,
345
+ poolId: this.config.poolId,
346
+ collateral: this.config.collateral,
347
+ debt: this.config.debt,
348
+ networkConfig: this.config.networkConfig,
349
+ pricer: this.config.pricer,
350
+ vesuAdapter: this._vesuAdapter,
351
+ tokenMarketData: this.tokenMarketData,
352
+ targetHealthFactor: this.config.targetHealthFactor,
353
+ getCache: this.getCache.bind(this),
354
+ setCache: this.setCache.bind(this),
355
+ getUSDValue: this.getUSDValue.bind(this),
356
+ };
357
+ }
358
+
359
+ // ─── APY / Position / Max Deposit/Withdraw ─────────────────────────────────
360
+
361
+ protected async getAPY(
362
+ supportedPosition: SupportedPosition
363
+ ): Promise<PositionAPY> {
364
+ // always add vesu modify position adapter, which shall
365
+ // return correct apy
366
+ return {
367
+ apy: 0,
368
+ type: APYType.BASE,
369
+ }
370
+ }
371
+
372
+ protected async getPosition(
373
+ supportedPosition: SupportedPosition
374
+ ): Promise<PositionAmount | null> {
375
+ // always add vesu modify position adapter, which shall
376
+ // return correct position amount
377
+ return null;
378
+ }
379
+
380
+ async maxBorrowableAPY(): Promise<number> {
381
+ // always add vesu modify position adapter, which shall
382
+ // return correct max borrowable apy
383
+ return 0;
384
+ }
385
+
386
+ private _computeMax(collateralPosition: PositionInfo, debtPosition: PositionInfo): PositionInfo {
387
+ // in multiply, most of the debt also goes as collateral. hence, net addable is collateral - debt.
388
+ if (collateralPosition.usdValue <= 0 || collateralPosition.usdValue <= debtPosition.usdValue) {
272
389
  return {
273
- tokenInfo: collateral,
274
- amount: result,
275
- usdValue,
276
- remarks: "Max withdraw based on health factor",
277
- apy: {
278
- apy: netAPY,
279
- type: APYType.BASE
280
- },
281
- protocol: this.protocol
390
+ tokenInfo: this.config.collateral,
391
+ amount: new Web3Number(0, this.config.collateral.decimals),
392
+ usdValue: 0,
393
+ remarks: "Max deposit",
394
+ apy: { apy: 0, type: APYType.BASE },
395
+ protocol: this.protocol,
282
396
  };
283
- } catch (error) {
284
- logger.error(`VesuMultiplyAdapter: Error calculating max withdraw:`, error);
285
- throw error;
286
397
  }
398
+ const maxCollateralUsd = collateralPosition.usdValue - debtPosition.usdValue;
399
+ const collateralPrice = collateralPosition.usdValue / collateralPosition.amount.toNumber();
400
+ const collateralAmount = new Web3Number(maxCollateralUsd / collateralPrice, this.config.collateral.decimals);
401
+ const netAPY =
402
+ (collateralPosition.apy.apy * collateralPosition.usdValue - debtPosition.apy.apy * debtPosition.usdValue) /
403
+ (maxCollateralUsd);
404
+ return {
405
+ tokenInfo: this.config.collateral,
406
+ amount: collateralAmount,
407
+ usdValue: maxCollateralUsd,
408
+ remarks: "Max deposit",
409
+ apy: { apy: netAPY, type: APYType.BASE },
410
+ protocol: this.protocol,
411
+ };
287
412
  }
288
413
 
414
+ async maxDeposit(amount?: Web3Number): Promise<PositionInfo> {
415
+ const { collateralPosition, debtPosition: debtPosition } = await getVesuCommonMaxDeposit(this._getPositionCommonContext(), amount);
416
+ return this._computeMax(collateralPosition, debtPosition);
417
+ }
418
+
419
+ async maxWithdraw(): Promise<PositionInfo> {
420
+ const { collateralPosition, debtPosition } = await getVesuCommonMaxWithdraw(this._getPositionCommonContext());
421
+ return this._computeMax(collateralPosition, debtPosition);
422
+ }
423
+
424
+ // ─── Leaf Configuration ────────────────────────────────────────────────────
425
+
289
426
  protected _getDepositLeaf(): {
290
- target: ContractAddr,
291
- method: string,
292
- packedArguments: bigint[],
293
- sanitizer: ContractAddr,
294
- id: string
427
+ target: ContractAddr;
428
+ method: string;
429
+ packedArguments: bigint[];
430
+ sanitizer: ContractAddr;
431
+ id: string;
295
432
  }[] {
296
433
  const collateral = this.config.collateral;
297
434
  const debt = this.config.debt;
298
- const { addr: vesuSingleton, isV2 } = getVesuSingletonAddress(this.config.poolId);
299
- const vesuMultiply = isV2 ? this.vesuAdapter.VESU_MULTIPLY : this.vesuAdapter.VESU_MULTIPLY_V1;
435
+ const { vesuMultiply, vesuSingleton, isV2 } = this._getMultiplyContract();
436
+ const idApprovePrefix = "amc";
437
+ const idDelegOnPrefix = "sd1";
438
+ const idMultiplyPrefix = "vm";
439
+ const idDelegOffPrefix = "sd2";
300
440
 
301
441
  return [
302
- // Approval step for collateral
442
+ // allow approval of margin token and collateral to vesu multiply contract
443
+ // depending on strategy, respective token needs to be approved by implementer
444
+ {
445
+ target: this.config.marginToken.address,
446
+ method: "approve",
447
+ packedArguments: [vesuMultiply.toBigInt()],
448
+ sanitizer: SIMPLE_SANITIZER,
449
+ id: `${idApprovePrefix}_${this.config.poolId.shortString()}_${this.config.marginToken.symbol}`,
450
+ },
303
451
  {
304
452
  target: collateral.address,
305
- method: 'approve',
306
- packedArguments: [
307
- vesuMultiply.toBigInt(), // spender
308
- ],
453
+ method: "approve",
454
+ packedArguments: [vesuMultiply.toBigInt()],
309
455
  sanitizer: SIMPLE_SANITIZER,
310
- // amc = approve multiply collateral
311
- id: `amc_${this.config.poolId.shortString()}_${collateral.symbol}_${debt.symbol}`
456
+ id: `${idApprovePrefix}_${this.config.poolId.shortString()}_${collateral.symbol}`,
312
457
  },
313
- // Switch delegation on
314
458
  {
315
459
  target: vesuSingleton,
316
- method: 'modify_delegation',
317
- packedArguments: isV2 ? [
318
- vesuMultiply.toBigInt(), // delegatee
319
- ] : [
320
- this.config.poolId.toBigInt(),
321
- vesuMultiply.toBigInt(), // delegatee
322
- ],
323
- sanitizer: isV2 ? SIMPLE_SANITIZER_V2 : SIMPLE_SANITIZER,
324
- // sd1 = switch delegation on
325
- id: `sd1_${this.config.poolId.shortString()}_${collateral.symbol}_${debt.symbol}`
460
+ method: "modify_delegation",
461
+ packedArguments: isV2
462
+ ? [vesuMultiply.toBigInt()]
463
+ : [this.config.poolId.toBigInt(), vesuMultiply.toBigInt()],
464
+ sanitizer: isV2 ? SIMPLE_SANITIZER_V2 : SIMPLE_SANITIZER_VESU_V1_DELEGATIONS,
465
+ id: `${idDelegOnPrefix}_${this.config.poolId.shortString()}_${collateral.symbol}_${debt.symbol}`,
326
466
  },
327
- // Vesu multiply call
328
467
  {
329
468
  target: vesuMultiply,
330
- method: 'modify_lever',
469
+ method: "modify_lever",
331
470
  packedArguments: [
332
471
  this.config.poolId.toBigInt(),
333
472
  collateral.address.toBigInt(),
@@ -335,589 +474,836 @@ export class VesuMultiplyAdapter extends BaseAdapter<DepositParams, WithdrawPara
335
474
  this.config.vaultAllocator.toBigInt(),
336
475
  ],
337
476
  sanitizer: SIMPLE_SANITIZER_V2,
338
- // vm = vesu multiply
339
- id: `vm_${this.config.poolId.shortString()}_${collateral.symbol}_${debt.symbol}`
477
+ id: `${idMultiplyPrefix}_${this.config.poolId.shortString()}_${collateral.symbol}_${debt.symbol}`,
340
478
  },
341
- // Switch delegation off
342
479
  {
343
480
  target: vesuSingleton,
344
- method: 'modify_delegation',
345
- packedArguments: isV2 ? [
346
- vesuMultiply.toBigInt(), // delegatee
347
- ] : [
348
- this.config.poolId.toBigInt(),
349
- vesuMultiply.toBigInt(), // delegatee
350
- ],
351
- sanitizer: isV2 ? SIMPLE_SANITIZER_V2 : SIMPLE_SANITIZER,
352
- // sd2 = switch delegation off
353
- id: `sd2_${this.config.poolId.shortString()}_${collateral.symbol}_${debt.symbol}`
354
- }
481
+ method: "modify_delegation",
482
+ packedArguments: isV2
483
+ ? [vesuMultiply.toBigInt()]
484
+ : [this.config.poolId.toBigInt(), vesuMultiply.toBigInt()],
485
+ sanitizer: isV2 ? SIMPLE_SANITIZER_V2 : SIMPLE_SANITIZER_VESU_V1_DELEGATIONS,
486
+ id: `${idDelegOffPrefix}_${this.config.poolId.shortString()}_${collateral.symbol}_${debt.symbol}`,
487
+ },
355
488
  ];
356
489
  }
357
490
 
358
491
  protected _getWithdrawLeaf(): {
359
- target: ContractAddr,
360
- method: string,
361
- packedArguments: bigint[],
362
- sanitizer: ContractAddr,
363
- id: string
492
+ target: ContractAddr;
493
+ method: string;
494
+ packedArguments: bigint[];
495
+ sanitizer: ContractAddr;
496
+ id: string;
364
497
  }[] {
365
- const { addr: vesuSingleton, isV2 } = getVesuSingletonAddress(this.config.poolId);
366
- const vesuMultiply = isV2 ? this.vesuAdapter.VESU_MULTIPLY : this.vesuAdapter.VESU_MULTIPLY_V1;
498
+ const { vesuMultiply, vesuSingleton, isV2 } = this._getMultiplyContract();
367
499
  const collateral = this.config.collateral;
368
500
  const debt = this.config.debt;
369
501
 
370
502
  return [
371
- // Switch delegation on
372
503
  {
373
504
  target: vesuSingleton,
374
- method: 'modify_delegation',
375
- packedArguments: isV2 ? [
376
- vesuMultiply.toBigInt(), // delegatee
377
- ] : [
378
- this.config.poolId.toBigInt(),
379
- vesuMultiply.toBigInt(), // delegatee
380
- ],
381
- sanitizer: isV2 ? SIMPLE_SANITIZER_V2 : SIMPLE_SANITIZER,
382
- // sdow = switch delegation on withdraw
383
- id: `sdow_${this.config.poolId.shortString()}_${collateral.symbol}_${debt.symbol}`
505
+ method: "modify_delegation",
506
+ packedArguments: isV2
507
+ ? [vesuMultiply.toBigInt()]
508
+ : [this.config.poolId.toBigInt(), vesuMultiply.toBigInt()],
509
+ sanitizer: isV2 ? SIMPLE_SANITIZER_V2 : SIMPLE_SANITIZER_VESU_V1_DELEGATIONS,
510
+ id: `sdow_${this.config.poolId.shortString()}_${collateral.symbol}_${debt.symbol}`,
384
511
  },
385
- // Vesu multiply call
386
512
  {
387
513
  target: vesuMultiply,
388
- method: 'modify_lever',
514
+ method: "modify_lever",
389
515
  packedArguments: [
390
516
  this.config.poolId.toBigInt(),
391
517
  this.config.collateral.address.toBigInt(),
392
518
  this.config.debt.address.toBigInt(),
393
519
  this.config.vaultAllocator.toBigInt(),
394
520
  ],
395
- sanitizer: isV2 ? SIMPLE_SANITIZER_V2 : SIMPLE_SANITIZER,
396
- // vmw = vesu multiply withdraw
397
- id: `vmw_${this.config.poolId.shortString()}_${collateral.symbol}_${debt.symbol}`
521
+ sanitizer: isV2 ? SIMPLE_SANITIZER_V2 : SIMPLE_SANITIZER_VESU_V1_DELEGATIONS,
522
+ id: `vmw_${this.config.poolId.shortString()}_${collateral.symbol}_${debt.symbol}`,
398
523
  },
399
- // Switch delegation off
400
524
  {
401
525
  target: vesuSingleton,
402
- method: 'modify_delegation',
403
- packedArguments: isV2 ? [
404
- vesuMultiply.toBigInt(), // delegatee
405
- ] : [
406
- this.config.poolId.toBigInt(),
407
- vesuMultiply.toBigInt(), // delegatee
408
- ],
409
- sanitizer: isV2 ? SIMPLE_SANITIZER_V2 : SIMPLE_SANITIZER,
410
- // sdofw = switch delegation off withdraw
411
- id: `sdofw_${this.config.poolId.shortString()}_${collateral.symbol}_${debt.symbol}`
412
- }
526
+ method: "modify_delegation",
527
+ packedArguments: isV2
528
+ ? [vesuMultiply.toBigInt()]
529
+ : [this.config.poolId.toBigInt(), vesuMultiply.toBigInt()],
530
+ sanitizer: isV2 ? SIMPLE_SANITIZER_V2 : SIMPLE_SANITIZER_VESU_V1_DELEGATIONS,
531
+ id: `sdofw_${this.config.poolId.shortString()}_${collateral.symbol}_${debt.symbol}`,
532
+ },
413
533
  ];
414
534
  }
415
535
 
416
- getDepositAdapter(): AdapterLeafType<DepositParams> {
536
+ // ─── Leaf Adapters ─────────────────────────────────────────────────────────
537
+
538
+ getDepositAdapter(approveToken?: TokenInfo): AdapterLeafType<VesuDepositParams> {
539
+ throw new Error("getDepositAdapter::Not implemented");
417
540
  const leafConfigs = this._getDepositLeaf();
418
- const leaves = leafConfigs.map(config => {
541
+ const leaves = leafConfigs.map((config) => {
419
542
  const { target, method, packedArguments, sanitizer, id } = config;
420
- const leaf = this.constructSimpleLeafData({
421
- id: id,
422
- target,
423
- method,
424
- packedArguments
425
- }, sanitizer);
426
- return leaf;
543
+ return this.constructSimpleLeafData(
544
+ { id, target, method, packedArguments },
545
+ sanitizer
546
+ );
427
547
  });
428
- return { leaves, callConstructor: this.getDepositCall.bind(this) as unknown as GenerateCallFn<DepositParams> };
548
+ return {
549
+ leaves,
550
+ callConstructor: this.getDepositCall.bind(
551
+ this
552
+ ) as unknown as GenerateCallFn<VesuDepositParams>,
553
+ };
429
554
  }
430
555
 
431
- getWithdrawAdapter(): AdapterLeafType<WithdrawParams> {
556
+ getWithdrawAdapter(): AdapterLeafType<VesuWithdrawParams> {
557
+ throw new Error("getWithdrawAdapter::Dont think this fn is used anywhere");
432
558
  const leafConfigs = this._getWithdrawLeaf();
433
- const leaves = leafConfigs.map(config => {
559
+ const leaves = leafConfigs.map((config) => {
434
560
  const { target, method, packedArguments, sanitizer, id } = config;
435
- const leaf = this.constructSimpleLeafData({
436
- id: id,
437
- target,
438
- method,
439
- packedArguments
440
- }, sanitizer);
441
- return leaf;
561
+ return this.constructSimpleLeafData(
562
+ { id, target, method, packedArguments },
563
+ sanitizer
564
+ );
442
565
  });
443
- return { leaves, callConstructor: this.getWithdrawCall.bind(this) as unknown as GenerateCallFn<WithdrawParams> };
566
+ return {
567
+ leaves,
568
+ callConstructor: this.getWithdrawCall.bind(
569
+ this
570
+ ) as unknown as GenerateCallFn<VesuWithdrawParams>,
571
+ };
444
572
  }
445
573
 
446
- async getDepositCall(params: DepositParams): Promise<ManageCall[]> {
447
- const collateral = this.config.collateral;
448
- const { addr: vesuSingleton, isV2 } = getVesuSingletonAddress(this.config.poolId);
449
- const vesuMultiply = isV2 ? this.vesuAdapter.VESU_MULTIPLY : this.vesuAdapter.VESU_MULTIPLY_V1;
574
+ // ─── Public Call Builders ──────────────────────────────────────────────────
450
575
 
451
- const uint256MarginAmount = uint256.bnToUint256(params.amount.toWei());
576
+ async getDepositCall(params: VesuDepositParams): Promise<ManageCall[]> {
577
+ const { calldata, approveToken, approveAmount } = await this._getIncreaseCalldata(params);
578
+ const { vesuMultiply } = this._getMultiplyContract();
579
+ const proofReadableIds = this._getDepositProofReadableIds();
452
580
 
453
- return [
454
- // Approval call
455
- {
456
- sanitizer: SIMPLE_SANITIZER,
457
- call: {
458
- contractAddress: collateral.address,
459
- selector: hash.getSelectorFromName('approve'),
460
- calldata: [
461
- vesuMultiply.toBigInt(), // spender
462
- toBigInt(uint256MarginAmount.low.toString()), // amount low
463
- toBigInt(uint256MarginAmount.high.toString()), // amount high
464
- ]
465
- }
466
- },
467
- // Switch delegation on
468
- {
469
- sanitizer: isV2 ? SIMPLE_SANITIZER_V2 : SIMPLE_SANITIZER,
470
- call: {
471
- contractAddress: vesuSingleton,
472
- selector: hash.getSelectorFromName('modify_delegation'),
473
- calldata: isV2 ? [
474
- vesuMultiply.toBigInt(), // delegatee
475
- BigInt(1), // delegation: true
476
- ] : [
477
- this.config.poolId.toBigInt(),
478
- vesuMultiply.toBigInt(), // delegatee
479
- BigInt(1), // delegation: true
480
- ]
481
- }
482
- },
483
- // Vesu multiply call
484
- {
485
- sanitizer: SIMPLE_SANITIZER_V2,
486
- call: {
487
- contractAddress: vesuMultiply,
488
- selector: hash.getSelectorFromName('modify_lever'),
489
- calldata: await this.getMultiplyCallCalldata(params, true)
490
- }
491
- },
492
- // Switch delegation off
493
- {
494
- sanitizer: isV2 ? SIMPLE_SANITIZER_V2 : SIMPLE_SANITIZER,
495
- call: {
496
- contractAddress: vesuSingleton,
497
- selector: hash.getSelectorFromName('modify_delegation'),
498
- calldata: isV2 ? [
499
- vesuMultiply.toBigInt(), // delegatee
500
- BigInt(0), // delegation: false
501
- ] : [
502
- this.config.poolId.toBigInt(),
503
- vesuMultiply.toBigInt(), // delegatee
504
- BigInt(0), // delegation: false
505
- ]
506
- }
507
- }
508
- ];
509
- }
510
581
 
511
- async getWithdrawCall(params: WithdrawParams): Promise<ManageCall[]> {
512
- const { addr: vesuSingleton, isV2 } = getVesuSingletonAddress(this.config.poolId);
513
- const vesuMultiply = isV2 ? this.vesuAdapter.VESU_MULTIPLY : this.vesuAdapter.VESU_MULTIPLY_V1;
582
+ const uint256Amount = uint256.bnToUint256(approveAmount.toWei());
514
583
 
515
- return [
516
- // Switch delegation on
517
- {
518
- sanitizer: isV2 ? SIMPLE_SANITIZER_V2 : SIMPLE_SANITIZER,
519
- call: {
520
- contractAddress: vesuSingleton,
521
- selector: hash.getSelectorFromName('modify_delegation'),
522
- calldata: isV2 ? [
523
- vesuMultiply.toBigInt(), // delegatee
524
- BigInt(1), // delegation: true
525
- ] : [
526
- this.config.poolId.toBigInt(),
527
- vesuMultiply.toBigInt(), // delegatee
528
- BigInt(1), // delegation: true
529
- ]
530
- }
531
- },
532
- // Vesu multiply call
533
- {
534
- sanitizer: isV2 ? SIMPLE_SANITIZER_V2 : SIMPLE_SANITIZER,
535
- call: {
536
- contractAddress: vesuMultiply,
537
- selector: hash.getSelectorFromName('modify_lever'),
538
- calldata: await this.getWithdrawalCalldata(params)
539
- }
584
+ const approveCall: ManageCall = {
585
+ proofReadableId: approveToken.address.eq(this.config.collateral.address) ? proofReadableIds.approve_collateral : proofReadableIds.approve_margin,
586
+ sanitizer: SIMPLE_SANITIZER,
587
+ call: {
588
+ contractAddress: approveToken.address,
589
+ selector: hash.getSelectorFromName("approve"),
590
+ calldata: [
591
+ vesuMultiply.toBigInt(),
592
+ toBigInt(uint256Amount.low.toString()),
593
+ toBigInt(uint256Amount.high.toString()),
594
+ ],
540
595
  },
541
- // Switch delegation off
542
- {
543
- sanitizer: isV2 ? SIMPLE_SANITIZER_V2 : SIMPLE_SANITIZER,
544
- call: {
545
- contractAddress: vesuSingleton,
546
- selector: hash.getSelectorFromName('modify_delegation'),
547
- calldata: isV2 ? [
548
- vesuMultiply.toBigInt(), // delegatee
549
- BigInt(0), // delegation: false
550
- ] : [
551
- this.config.poolId.toBigInt(),
552
- vesuMultiply.toBigInt(), // delegatee
553
- BigInt(0), // delegation: false
554
- ]
555
- }
556
- }
557
- ];
596
+ };
597
+
598
+ return this._buildDelegationWrappedCalls([approveCall], calldata, {
599
+ delegationOn: proofReadableIds.delegationOn,
600
+ modifyLever: proofReadableIds.modifyLever,
601
+ delegationOff: proofReadableIds.delegationOff,
602
+ });
558
603
  }
559
604
 
560
- private async getMultiplyCallCalldata(params: DepositParams | WithdrawParams, isDeposit: boolean): Promise<bigint[]> {
561
- logger.verbose(`${VesuMultiplyAdapter.name}::getMultiplyCallCalldata params: ${JSON.stringify(params)}, isDeposit: ${isDeposit}, collateral: ${this.config.collateral.symbol}, debt: ${this.config.debt.symbol}`);
562
- const { isV2 } = getVesuSingletonAddress(this.config.poolId);
563
- const vesuMultiply = isV2 ? this.vesuAdapter.VESU_MULTIPLY : this.vesuAdapter.VESU_MULTIPLY_V1;
564
-
565
- // Create a temporary contract instance to populate the call
566
- const multiplyContract = new Contract({
567
- abi: VesuMultiplyAbi,
568
- address: vesuMultiply.address,
569
- providerOrAccount: this.config.networkConfig.provider
605
+ async getWithdrawCall(params: VesuWithdrawParams): Promise<ManageCall[]> {
606
+ const calldata = await this._getDecreaseCalldata(params);
607
+ const proofReadableIds = this._getWithdrawProofReadableIds();
608
+ return this._buildDelegationWrappedCalls([], calldata, {
609
+ delegationOn: proofReadableIds.delegationOn,
610
+ modifyLever: proofReadableIds.modifyLever,
611
+ delegationOff: proofReadableIds.delegationOff,
570
612
  });
613
+ }
571
614
 
572
- // Configure swaps based on the operation
573
- let leverSwap: Swap[] = [];
574
- let leverSwapLimitAmount = Web3Number.fromWei(0, this.config.debt.decimals);
615
+ // ─── Consolidated Calldata Builders ────────────────────────────────────────
575
616
 
576
- const existingPositions = await this.vesuAdapter.getPositions(this.config.networkConfig);
577
- const collateralisation = await this.vesuAdapter.getCollateralization(this.config.networkConfig);
578
- const existingCollateralInfo = existingPositions[0];
579
- const existingDebtInfo = existingPositions[1];
580
- const isDexPriceRequired = existingDebtInfo.token.symbol !== "USDC";
581
- logger.debug(`${VesuMultiplyAdapter.name}::getVesuMultiplyCall existingCollateralInfo: ${JSON.stringify(existingCollateralInfo)},
582
- existingDebtInfo: ${JSON.stringify(existingDebtInfo)}, collateralisation: ${JSON.stringify(collateralisation)}`);
583
-
584
- // - Prices as seen by Vesu contracts, ideal for HF math
585
- // Price 1 is ok as fallback bcz that would relatively price the
586
- // collateral and debt as equal.
587
- const collateralPrice = collateralisation[0].usdValue > 0 ?
588
- collateralisation[0].usdValue / existingCollateralInfo.amount.toNumber() :
589
- (await this.config.pricer.getPrice(this.config.collateral.symbol)).price;
590
- const debtPrice = collateralisation[1].usdValue > 0 ?
591
- collateralisation[1].usdValue / existingDebtInfo.amount.toNumber() :
592
- (await this.config.pricer.getPrice(this.config.debt.symbol)).price;
593
- logger.debug(`${VesuMultiplyAdapter.name}::getVesuMultiplyCall collateralPrice: ${collateralPrice}, debtPrice: ${debtPrice}`);
594
-
595
- const legLTV = await this.vesuAdapter.getLTVConfig(this.config.networkConfig);
596
- const ekuboQuoter = new EkuboQuoter(this.config.networkConfig, this.config.pricer);
597
- const dexPrice = isDexPriceRequired ? await ekuboQuoter.getDexPrice(this.config.collateral, this.config.debt, this.config.quoteAmountToFetchPrice) : 1;
598
- logger.verbose(`${VesuMultiplyAdapter.name}::getVesuMultiplyCall dexPrice: ${dexPrice}, ltv: ${legLTV}`);
599
-
600
- // compute optimal amount of collateral and debt post addition/removal
601
- // target hf = collateral * collateralPrice * ltv / debt * debtPrice
602
- // assuming X to be the usd amount of debt borrowed or repaied (negative).
603
- // target hf = (((collateral + legDepositAmount) * collateralPrice + X)) * ltv / (debt * debtPrice + X)
604
- // => X * target hf = (((collateral + legDepositAmount) * collateralPrice + X)) * ltv - (debt * debtPrice * target hf)
605
- // => X * (target hf - ltv)= ((collateral + legDepositAmount) * collateralPrice * ltv) - (debt * debtPrice * target hf)
606
- // => X = (((collateral + legDepositAmount) * collateralPrice * ltv) - (debt * debtPrice * target hf)) / (target hf - ltv)
607
-
608
- const addedCollateral = params.amount
609
- .multipliedBy(isDeposit ? 1 : -1)
610
- logger.verbose(`${VesuMultiplyAdapter.name}::getVesuMultiplyCall addedCollateral: ${addedCollateral}`);
611
- const numeratorPart1 = (existingCollateralInfo.amount.plus((addedCollateral))).multipliedBy(collateralPrice).multipliedBy(legLTV);
612
- logger.verbose(`${VesuMultiplyAdapter.name}::getVesuMultiplyCall numeratorPart1: ${numeratorPart1}`);
613
- const numeratorPart2 = existingDebtInfo.amount.multipliedBy(debtPrice).multipliedBy(this.config.targetHealthFactor);
614
- logger.verbose(`${VesuMultiplyAdapter.name}::getVesuMultiplyCall numeratorPart2: ${numeratorPart2}`);
615
- const denominatorPart = this.config.targetHealthFactor - (legLTV / dexPrice); // TODO Write reason for this. this dexPrice is some custom thing. this dexPrice is probably exchange rate (1 xWBTC in WBTC terms)
616
- logger.verbose(`${VesuMultiplyAdapter.name}::getVesuMultiplyCall denominatorPart: ${denominatorPart}`);
617
- const x_debt_usd = numeratorPart1.minus(numeratorPart2).dividedBy(denominatorPart);
618
- logger.verbose(`${VesuMultiplyAdapter.name}::getVesuMultiplyCall x_debt_usd: ${x_debt_usd}`);
619
- logger.debug(`${VesuMultiplyAdapter.name}::getVesuMultiplyCall numeratorPart1: ${numeratorPart1}, numeratorPart2: ${numeratorPart2}, denominatorPart: ${denominatorPart}`);
620
-
621
- // both in underlying
622
- // debtAmount in debt units
623
- let debtAmount = new Web3Number(x_debt_usd.dividedBy(debtPrice).toFixed(this.config.debt.decimals), this.config.debt.decimals);
624
- const marginAmount = addedCollateral;
617
+ private async _getIncreaseCalldata(
618
+ params: VesuDepositParams
619
+ ): Promise<{ calldata: bigint[]; approveToken: TokenInfo; approveAmount: Web3Number }> {
625
620
  const collateralToken = this.config.collateral;
626
621
  const debtToken = this.config.debt;
627
- const debtAmountInCollateralUnits = new Web3Number(debtAmount.multipliedBy(debtPrice).dividedBy(collateralPrice).multipliedBy(10 ** collateralToken.decimals).toFixed(0), collateralToken.decimals);
628
-
629
- // increase multiply lever or not
630
- const isIncrease = debtAmount.greaterThanOrEqualTo(0);
631
-
632
-
633
- // due to directional limitations in multiply contract
634
- if (isIncrease && debtAmount.lessThan(0)) {
635
- // we are increasing lever but math says reduce debt
636
- // - this is ok
637
- } else if (!isIncrease && debtAmount.greaterThan(0)) {
638
- // we are decreasing level but math says increase debt
639
- // - such actions must be done with zero margin amount
640
- // - so just set debt 0
641
- debtAmount = Web3Number.fromWei(0, this.config.debt.decimals);
622
+ const { contract: multiplyContract } = this._getMultiplyContract();
623
+ this.lastSwapPriceInfo = null;
624
+
625
+ const {
626
+ existingCollateralInfo,
627
+ existingDebtInfo,
628
+ collateralPrice,
629
+ debtPrice,
630
+ ekuboQuoter,
631
+ } = await this._fetchPositionAndPrices();
632
+
633
+ logger.verbose(
634
+ `${VesuMultiplyAdapter.name}::_getIncreaseCalldata params: ${JSON.stringify(
635
+ params
636
+ )}, collateral: ${collateralToken.symbol}, debt: ${debtToken.symbol}`
637
+ );
638
+ logger.debug(
639
+ `${VesuMultiplyAdapter.name}::_getIncreaseCalldata existingCollateralInfo: ${JSON.stringify(
640
+ existingCollateralInfo
641
+ )}, existingDebtInfo: ${JSON.stringify(existingDebtInfo)}`
642
+ );
643
+ logger.debug(
644
+ `${VesuMultiplyAdapter.name}::_getIncreaseCalldata collateralPrice: ${collateralPrice}, debtPrice: ${debtPrice}`
645
+ );
646
+
647
+ const legLTV = await this._vesuAdapter.getLTVConfig(
648
+ this.config.networkConfig
649
+ );
650
+ const isDexPriceRequired = debtToken.symbol !== "USDC";
651
+ const dexPrice = isDexPriceRequired
652
+ ? await ekuboQuoter.getDexPrice(
653
+ collateralToken,
654
+ debtToken,
655
+ this.config.quoteAmountToFetchPrice
656
+ )
657
+ : 1;
658
+ logger.verbose(
659
+ `${VesuMultiplyAdapter.name}::_getIncreaseCalldata dexPrice: ${dexPrice}, ltv: ${legLTV}`
660
+ );
661
+
662
+ let marginSwap: Swap[] = [];
663
+ let marginSwapLimitAmount = Web3Number.fromWei(0, collateralToken.decimals);
664
+ let addedCollateral: Web3Number = params.amount;
665
+ let approveAmount: Web3Number = params.amount;
666
+ let aggregatedFromAmount = 0; // e.g. USDC
667
+ let aggregatedToAmount = 0; // e.g. BTC
668
+ let aggregatedFromSymbol: string = debtToken.symbol;
669
+ const aggregatedToSymbol = collateralToken.symbol;
670
+ let executedSwapCount = 0;
671
+
672
+ if (params.marginSwap) {
673
+ const marginToken = params.marginSwap.marginToken;
674
+ const requiredAmount = params.amount;
675
+
676
+ // bcz, last swap price is configured to be common between margin swap and lever swap,
677
+ // hence need same tokens in both
678
+ assert(marginToken.address.eq(debtToken.address), 'Margin token must be the same as debt token');
679
+
680
+ const marginSwapQuote = await ekuboQuoter.getQuoteExactOutput(
681
+ marginToken.address.address,
682
+ collateralToken.address.address,
683
+ requiredAmount
684
+ );
685
+ if (marginSwapQuote.price_impact > 0.01) {
686
+ throw new Error(
687
+ `VesuMultiplyAdapter: Margin swap price impact too high (${marginSwapQuote.price_impact})`
688
+ );
689
+ }
690
+
691
+ marginSwap = ekuboQuoter.getVesuMultiplyQuote(
692
+ marginSwapQuote,
693
+ marginToken,
694
+ collateralToken
695
+ );
696
+ const marginSwapInputAmount = Web3Number
697
+ .fromWei(marginSwapQuote.total_calculated, marginToken.decimals)
698
+ .abs()
699
+ .toNumber();
700
+ const marginSwapOutputAmount = requiredAmount.abs().toNumber();
701
+ aggregatedFromAmount += marginSwapInputAmount;
702
+ aggregatedToAmount += marginSwapOutputAmount;
703
+ executedSwapCount += 1;
704
+
705
+ approveAmount = Web3Number
706
+ .fromWei(marginSwapQuote.total_calculated, marginToken.decimals)
707
+ .multipliedBy(1 + this.maxSlippage)
708
+ .abs();
642
709
  }
643
- logger.verbose(`${VesuMultiplyAdapter.name}::getVesuMultiplyCall debtAmount: ${debtAmount}, marginAmount: ${marginAmount}`);
644
- if (!debtAmount.isZero()) {
645
- // Get swap quote for leverage operation
646
- // Determine swap direction based on operation type
647
-
648
- try {
649
- const swapQuote = await ekuboQuoter.getQuote(
650
- collateralToken.address.address,
710
+
711
+ let debtAmount = this._computeTargetDebtDelta(
712
+ addedCollateral,
713
+ existingCollateralInfo.amount,
714
+ existingDebtInfo.amount,
715
+ collateralPrice,
716
+ debtPrice,
717
+ legLTV,
718
+ dexPrice
719
+ );
720
+
721
+ logger.info(
722
+ `${VesuMultiplyAdapter.name}::_getIncreaseCalldata debtAmount: ${debtAmount}, addedCollateral: ${addedCollateral}`
723
+ );
724
+ let leverSwap: Swap[] = [];
725
+ let leverSwapLimitAmount = Web3Number.fromWei(0, debtToken.decimals);
726
+
727
+ if (!debtAmount.isZero() && debtAmount.greaterThan(0)) {
728
+ try {
729
+ let swapQuote: EkuboQuote;
730
+ if (params.leverSwap?.exactOutput) {
731
+ swapQuote = await ekuboQuoter.getQuoteExactOutput(
732
+ debtToken.address.address,
733
+ collateralToken.address.address,
734
+ params.leverSwap?.exactOutput?.abs()!
735
+ );
736
+ debtAmount = Web3Number.fromWei(swapQuote.total_calculated, debtToken.decimals).abs();
737
+ }
738
+
739
+ swapQuote = await ekuboQuoter.getQuoteExactInput(
651
740
  debtToken.address.address,
652
- debtAmountInCollateralUnits.multipliedBy(-1)// negative for exact amount out
741
+ collateralToken.address.address,
742
+ debtAmount.abs()
653
743
  );
654
-
655
- // todo add better slip checks
656
- // Check price impact
657
- if (swapQuote.price_impact < 0.01) { // 1% max price impact
658
- // from and toToken param position reversed, to fetch the required quote and keep things generalised
659
- leverSwap = ekuboQuoter.getVesuMultiplyQuote(swapQuote, debtToken, collateralToken);
660
- //console.log("leverSwap", leverSwap[-1].token_amount);
661
- //console.log(JSON.stringify(leverSwap));
662
- // Calculate limit amount with slippage protection
663
- const MAX_SLIPPAGE = 0.002; // 0.2% slippage
664
- if (debtAmount.greaterThan(0)) {
665
- // For increase: minimum amount of collateral received
666
- // from debt token to collateral token
667
- //console.log("debtAmountInCollateralUnits", debtAmountInCollateralUnits.toNumber());
668
- //leverSwapLimitAmount = await ekuboQuoter.getSwapLimitAmount(debtToken, collateralToken, debtAmount, MAX_SLIPPAGE);
669
- leverSwapLimitAmount = debtAmount.multipliedBy(1 + MAX_SLIPPAGE);
670
- //console.log("anotherleverSwapLimitAmount", anotherleverSwapLimitAmount, leverSwapLimitAmount);
671
- } else if (debtAmount.lessThan(0)) {
672
- // For decrease: maximum amount of collateral used
673
- // from collateral token to debt token
674
- //leverSwapLimitAmount = await ekuboQuoter.getSwapLimitAmount(collateralToken, debtToken, debtAmountInCollateralUnits.multipliedBy(-1), MAX_SLIPPAGE);
675
- leverSwapLimitAmount = debtAmount.abs().multipliedBy(1 - MAX_SLIPPAGE);
676
- //console.log("anotherleverSwapLimitAmount", anotherleverSwapLimitAmount, leverSwapLimitAmount);
677
- } else {
678
- leverSwapLimitAmount = Web3Number.fromWei(0, this.config.debt.decimals);
679
- }
744
+ const expectedOutputAmount = debtAmount.multipliedBy(debtPrice).dividedBy(collateralPrice);
745
+ expectedOutputAmount.decimals = collateralToken.decimals;
746
+ leverSwapLimitAmount = expectedOutputAmount.multipliedBy(1 - this.maxSlippage);
747
+
748
+ if (swapQuote.price_impact < 0.01) {
749
+ const quoteOutputAmount = Web3Number.fromWei(
750
+ swapQuote.total_calculated,
751
+ collateralToken.decimals
752
+ )
753
+ .abs()
754
+ .toNumber();
755
+
756
+ const inputAmt = debtAmount.abs().toNumber();
757
+ const outputAmt = quoteOutputAmount;
758
+ aggregatedFromAmount += inputAmt;
759
+ aggregatedToAmount += outputAmt;
760
+ executedSwapCount += 1;
761
+
762
+ leverSwap = ekuboQuoter.getVesuMultiplyQuote(
763
+ swapQuote,
764
+ debtToken,
765
+ collateralToken
766
+ );
680
767
  await new Promise((resolve) => setTimeout(resolve, 10000));
681
- //console.log("leverSwapLimitAmount", leverSwapLimitAmount);
682
768
  } else {
683
- throw new Error(`VesuMultiplyAdapter: Price impact too high (${swapQuote.price_impact}), skipping swap`);
769
+ throw new Error(
770
+ `VesuMultiplyAdapter: Price impact too high (${swapQuote.price_impact}), skipping swap, debtAmount=${debtAmount.toNumber()}, collateralPrice=${collateralPrice}, debtPrice=${debtPrice}`
771
+ );
684
772
  }
685
773
  } catch (error) {
686
- throw new Error(`VesuMultiplyAdapter: Failed to get swap quote: ${error}`);
774
+ throw new Error(
775
+ `VesuMultiplyAdapter: Failed to get swap quote: ${error}`
776
+ );
687
777
  }
688
778
  }
689
-
690
- const multiplyParams = await this.getLeverParams(isIncrease, params, leverSwap, leverSwapLimitAmount);
691
- const call = multiplyContract.populate('modify_lever', {
692
- modify_lever_params: this.formatMultiplyParams(isIncrease, multiplyParams)
693
- });
694
779
 
695
- return call.calldata as bigint[];
696
- }
780
+ if (executedSwapCount > 0) {
781
+ this.lastSwapPriceInfo = {
782
+ source: "ekubo",
783
+ fromTokenSymbol: aggregatedFromSymbol ?? debtToken.symbol,
784
+ toTokenSymbol: aggregatedToSymbol,
785
+ fromAmount: aggregatedFromAmount,
786
+ toAmount: aggregatedToAmount,
787
+ effectivePrice: aggregatedToAmount !== 0 ? aggregatedFromAmount / aggregatedToAmount : 0,
788
+ };
789
+ logger.verbose(
790
+ `${VesuMultiplyAdapter.name}::_getIncreaseCalldata stored aggregated price info: ` +
791
+ `${aggregatedFromAmount} ${this.lastSwapPriceInfo.fromTokenSymbol} → ${aggregatedToAmount} ${aggregatedToSymbol}, ` +
792
+ `effectivePrice=${this.lastSwapPriceInfo.effectivePrice}, swaps=${executedSwapCount}`
793
+ );
794
+ }
697
795
 
698
- private async getLeverParams(isIncrease: boolean, params: DepositParams | WithdrawParams, leverSwap: Swap[], leverSwapLimitAmount: Web3Number): Promise<IncreaseLeverParams | DecreaseLeverParams> {
699
- const multiplyParams: IncreaseLeverParams | DecreaseLeverParams = isIncrease ? {
796
+ logger.verbose(
797
+ `${VesuMultiplyAdapter.name}::_getIncreaseCalldata leverSwapLimitAmount: ${leverSwapLimitAmount.toWei()}`
798
+ );
799
+ logger.verbose(
800
+ `${VesuMultiplyAdapter.name}::_getIncreaseCalldata approveAmount: ${approveAmount.toWei()}, marginSwapLimitAmount: ${marginSwapLimitAmount.toWei()}`
801
+ );
802
+ logger.verbose(
803
+ `${VesuMultiplyAdapter.name}::_getIncreaseCalldata marginSwap: ${JSON.stringify(marginSwap)}`
804
+ );
805
+ const multiplyParams: IncreaseLeverParams = {
700
806
  user: this.config.vaultAllocator,
701
807
  pool_id: this.config.poolId,
702
- collateral_asset: this.config.collateral.address,
703
- debt_asset: this.config.debt.address,
704
- recipient: this.config.vaultAllocator,
705
- add_margin: params.amount, // multiplied by collateral decimals in format
706
- margin_swap: [],
707
- margin_swap_limit_amount: Web3Number.fromWei(0, this.config.collateral.decimals),
808
+ collateral_asset: collateralToken.address,
809
+ debt_asset: debtToken.address,
810
+ add_margin: params.marginSwap
811
+ ? Web3Number.fromWei(0, collateralToken.decimals)
812
+ : params.amount,
813
+ margin_swap: marginSwap,
814
+ margin_swap_limit_amount: params.marginSwap ? approveAmount : marginSwapLimitAmount,
708
815
  lever_swap: leverSwap,
709
- lever_swap_limit_amount: leverSwapLimitAmount
710
- } : {
816
+ lever_swap_limit_amount: leverSwapLimitAmount,
817
+ };
818
+
819
+ const call = multiplyContract.populate("modify_lever", {
820
+ modify_lever_params: this.formatMultiplyParams(true, multiplyParams),
821
+ });
822
+ logger.debug(
823
+ `${VesuMultiplyAdapter.name}::_getIncreaseCalldata marginSwapCount=${marginSwap.length}, leverSwapCount=${leverSwap.length}`,
824
+ );
825
+
826
+ return {
827
+ calldata: call.calldata as bigint[],
828
+ approveToken: params.marginSwap?.marginToken ?? this.config.collateral,
829
+ approveAmount: approveAmount,
830
+ };
831
+ }
832
+
833
+ // private _setLastSwapPriceAsNoSwap(): void {
834
+ // this.lastSwapPriceInfo = {
835
+ // source: "no-swap",
836
+ // fromTokenSymbol: this.config.collateral.symbol,
837
+ // toTokenSymbol: this.config.debt.symbol,
838
+ // fromAmount: 0,
839
+ // toAmount: 0,
840
+ // effectivePrice: 0,
841
+ // };
842
+ // }
843
+
844
+ private async _buildDecreaseLikeCalldata(params: {
845
+ subMargin: Web3Number;
846
+ debtToRepayAbs: Web3Number;
847
+ existingCollateral: Web3Number;
848
+ closePosition: boolean;
849
+ outputToken?: TokenInfo;
850
+ collateralPrice: number;
851
+ debtPrice: number;
852
+ }): Promise<bigint[]> {
853
+ const collateralToken = this.config.collateral;
854
+ const debtToken = this.config.debt;
855
+ const { contract: multiplyContract } = this._getMultiplyContract();
856
+ this.lastSwapPriceInfo = null;
857
+ const ekuboQuoter = new EkuboQuoter(
858
+ this.config.networkConfig,
859
+ this.config.pricer
860
+ );
861
+
862
+ let leverSwap: Swap[] = [];
863
+ let leverSwapWeights: Web3Number[] = [];
864
+ let leverSwapLimitAmount = Web3Number.fromWei(0, collateralToken.decimals);
865
+ let leverCollateralUsed = Web3Number.fromWei(0, collateralToken.decimals);
866
+ let aggregatedFromAmount = 0; // collateral sold
867
+ let aggregatedToAmount = 0; // debt bought
868
+ let executedSwapCount = 0;
869
+
870
+ if (params.closePosition) {
871
+ const debtQuote = await ekuboQuoter.getQuoteExactOutput(
872
+ collateralToken.address.address,
873
+ debtToken.address.address,
874
+ params.debtToRepayAbs
875
+ );
876
+
877
+ const built = this._buildZeroAmountSwapsWithWeights(
878
+ debtQuote,
879
+ debtToken,
880
+ true
881
+ );
882
+ leverSwap = built.swaps;
883
+ leverSwapWeights = built.weights;
884
+ leverCollateralUsed = Web3Number.fromWei(
885
+ debtQuote.total_calculated,
886
+ collateralToken.decimals
887
+ ).abs();
888
+ leverSwapLimitAmount = leverCollateralUsed.multipliedBy(1 + this.maxSlippage);
889
+ aggregatedFromAmount += leverCollateralUsed.toNumber();
890
+ aggregatedToAmount += params.debtToRepayAbs.abs().toNumber();
891
+ executedSwapCount += 1;
892
+
893
+ } else {
894
+ if (params.collateralPrice === undefined || params.debtPrice === undefined) {
895
+ throw new Error(
896
+ "VesuMultiplyAdapter: Missing prices for non-close decrease calldata"
897
+ );
898
+ }
899
+ const collateralToSwap = new Web3Number(
900
+ params.debtToRepayAbs
901
+ .multipliedBy(params.debtPrice)
902
+ .dividedBy(params.collateralPrice)
903
+ .toFixed(collateralToken.decimals),
904
+ collateralToken.decimals
905
+ );
906
+ const leverSwapQuote = await ekuboQuoter.getQuoteExactInput(
907
+ collateralToken.address.address,
908
+ debtToken.address.address,
909
+ collateralToSwap
910
+ );
911
+ if (leverSwapQuote.price_impact < 0.0025) {
912
+ const inputAmt = collateralToSwap.toNumber();
913
+ const outputAmt = Web3Number.fromWei(
914
+ leverSwapQuote.total_calculated,
915
+ debtToken.decimals
916
+ )
917
+ .abs()
918
+ .toNumber();
919
+ aggregatedFromAmount += inputAmt;
920
+ aggregatedToAmount += outputAmt;
921
+ executedSwapCount += 1;
922
+
923
+ leverSwap = ekuboQuoter.getVesuMultiplyQuote(
924
+ leverSwapQuote,
925
+ collateralToken,
926
+ debtToken
927
+ );
928
+ leverSwapLimitAmount = collateralToSwap
929
+ .multipliedBy(params.collateralPrice)
930
+ .dividedBy(params.debtPrice)
931
+ .multipliedBy(1 - this.maxSlippage);
932
+ leverSwapLimitAmount.decimals = debtToken.decimals;
933
+ } else {
934
+ throw new Error(`VesuMultiplyAdapter: Lever swap price impact too high (${leverSwapQuote.price_impact})`);
935
+ }
936
+
937
+ leverCollateralUsed = collateralToSwap;
938
+ }
939
+
940
+ let withdrawSwap: Swap[] = [];
941
+ let withdrawSwapLimitAmount = Web3Number.fromWei(
942
+ 0,
943
+ params.outputToken?.decimals ?? collateralToken.decimals
944
+ );
945
+ const withdrawSwapWeights: Web3Number[] = [];
946
+
947
+ if (params.outputToken && !params.outputToken.address.eq(collateralToken.address)) {
948
+ // bcz, last swap price is configured to be common between withdraw swap and lever swap,
949
+ // hence need same tokens in both
950
+ assert(params.outputToken.address.eq(debtToken.address), 'Withdraw output token must be the same as debt token');
951
+ const residualCollateral = params.closePosition
952
+ ? params.existingCollateral.minus(leverCollateralUsed)
953
+ : params.subMargin;
954
+ const outputTokenPrice = await this.config.pricer.getPrice(params.outputToken.symbol);
955
+ if (residualCollateral.greaterThan(0)) {
956
+ await new Promise((r) => setTimeout(r, 10000));
957
+ const withdrawQuote = await ekuboQuoter.getQuoteExactInput(
958
+ collateralToken.address.address,
959
+ params.outputToken.address.address,
960
+ residualCollateral
961
+ );
962
+ if (withdrawQuote.price_impact < 0.0025) {
963
+ const built = this._buildZeroAmountSwapsWithWeights(
964
+ withdrawQuote,
965
+ collateralToken
966
+ );
967
+ withdrawSwap = built.swaps;
968
+ withdrawSwapWeights.push(...built.weights);
969
+ const withdrawOutputAmount = Web3Number.fromWei(
970
+ withdrawQuote.total_calculated,
971
+ params.outputToken.decimals
972
+ ).abs().toNumber();
973
+ aggregatedFromAmount += residualCollateral.toNumber();
974
+ aggregatedToAmount += withdrawOutputAmount;
975
+ executedSwapCount += 1;
976
+ const estimatedOutput = residualCollateral
977
+ .multipliedBy(params.collateralPrice)
978
+ .dividedBy(outputTokenPrice.price);
979
+ estimatedOutput.decimals = params.outputToken.decimals;
980
+ withdrawSwapLimitAmount = estimatedOutput
981
+ .multipliedBy(1 - this.maxSlippage);
982
+ } else {
983
+ throw new Error(`VesuMultiplyAdapter: Withdraw swap price impact too high (${withdrawQuote.price_impact})`);
984
+ }
985
+ }
986
+ }
987
+
988
+ if (executedSwapCount > 0) {
989
+ this.lastSwapPriceInfo = {
990
+ source: "ekubo",
991
+ fromTokenSymbol: collateralToken.symbol,
992
+ toTokenSymbol: debtToken.symbol,
993
+ fromAmount: aggregatedFromAmount,
994
+ toAmount: aggregatedToAmount,
995
+ effectivePrice: aggregatedFromAmount !== 0 ? aggregatedToAmount / aggregatedFromAmount : 0,
996
+ };
997
+ logger.verbose(
998
+ `${VesuMultiplyAdapter.name}::_buildDecreaseLikeCalldata stored aggregated price info: ` +
999
+ `${aggregatedFromAmount} ${collateralToken.symbol} → ${aggregatedToAmount} ${debtToken.symbol}, ` +
1000
+ `effectivePrice=${this.lastSwapPriceInfo.effectivePrice}, swaps=${executedSwapCount}`
1001
+ );
1002
+ } else {
1003
+ this.lastSwapPriceInfo = null;
1004
+ }
1005
+
1006
+ logger.debug(
1007
+ `${VesuMultiplyAdapter.name}::_buildDecreaseLikeCalldata leverSwapCount=${leverSwap.length}, withdrawSwapCount=${withdrawSwap.length}, withdrawSwapLimitAmount=${withdrawSwapLimitAmount.toNumber()}, subMargin=${params.subMargin.toNumber()}`,
1008
+ );
1009
+ const multiplyParams: DecreaseLeverParams = {
711
1010
  user: this.config.vaultAllocator,
712
1011
  pool_id: this.config.poolId,
713
- collateral_asset: this.config.collateral.address,
714
- debt_asset: this.config.debt.address,
1012
+ collateral_asset: collateralToken.address,
1013
+ debt_asset: debtToken.address,
715
1014
  recipient: this.config.vaultAllocator,
716
- sub_margin: params.amount,
1015
+ sub_margin: params.closePosition
1016
+ ? Web3Number.fromWei(0, collateralToken.decimals)
1017
+ : params.subMargin,
717
1018
  lever_swap: leverSwap,
718
1019
  lever_swap_limit_amount: leverSwapLimitAmount,
719
- lever_swap_weights: [],
720
- withdraw_swap: [],
721
- withdraw_swap_limit_amount: Web3Number.fromWei(0, this.config.collateral.decimals),
722
- withdraw_swap_weights: [],
723
- close_position: false
1020
+ lever_swap_weights: leverSwapWeights,
1021
+ withdraw_swap: withdrawSwap,
1022
+ withdraw_swap_limit_amount: withdrawSwapLimitAmount,
1023
+ withdraw_swap_weights: withdrawSwapWeights,
1024
+ close_position: params.closePosition,
724
1025
  };
725
- return multiplyParams;
726
- }
727
1026
 
728
- private async getWithdrawalCalldata(params: WithdrawParams): Promise<bigint[]> {
729
- //params.amount must be in btc here
730
- const { isV2 } = getVesuSingletonAddress(this.config.poolId);
731
- const vesuMultiply = isV2 ? this.vesuAdapter.VESU_MULTIPLY : this.vesuAdapter.VESU_MULTIPLY_V1;
732
- const multiplyContract = new Contract({
733
- abi: VesuMultiplyAbi,
734
- address: vesuMultiply.address,
735
- providerOrAccount: this.config.networkConfig.provider
1027
+ const call = multiplyContract.populate("modify_lever", {
1028
+ modify_lever_params: this.formatMultiplyParams(false, multiplyParams),
736
1029
  });
737
- let leverSwap: Swap[] = [];
738
- let leverSwapLimitAmount = Web3Number.fromWei(0, this.config.debt.decimals);
739
- const existingPositions = await this.vesuAdapter.getPositions(this.config.networkConfig);
740
- const existingCollateralInfo = existingPositions[0];
741
- const existingDebtInfo = existingPositions[1];
1030
+ return call.calldata as bigint[];
1031
+ }
1032
+
1033
+ private async _getDecreaseCalldata(
1034
+ params: VesuWithdrawParams
1035
+ ): Promise<bigint[]> {
742
1036
  const collateralToken = this.config.collateral;
743
1037
  const debtToken = this.config.debt;
744
- const collateralPrice = await this.config.pricer.getPrice(collateralToken.symbol);
1038
+
1039
+ this._vesuAdapter.networkConfig = this.config.networkConfig;
1040
+ this._vesuAdapter.pricer = this.config.pricer;
1041
+ const existingPositions = await this._vesuAdapter.getPositions(
1042
+ this.config.networkConfig
1043
+ );
1044
+ const existingCollateralInfo = existingPositions[0];
1045
+ const existingDebtInfo = existingPositions[1];
1046
+ const collateralPrice = await this.config.pricer.getPrice(
1047
+ collateralToken.symbol
1048
+ );
745
1049
  const debtPrice = await this.config.pricer.getPrice(debtToken.symbol);
746
- // the debt amount is negative as we are reducing debt to withdraw
747
- const { deltadebtAmountUnits: debtAmountToRepay } =
748
- calculateDebtReductionAmountForWithdrawal(
749
- existingDebtInfo.amount,
750
- existingCollateralInfo.amount,
751
- MAX_LIQUIDATION_RATIO,
752
- params.amount,
753
- collateralPrice.price,
754
- debtPrice.price,
755
- debtToken.decimals
1050
+
1051
+ const maxLTV = await this._vesuAdapter.getLTVConfig(
1052
+ this.config.networkConfig
756
1053
  );
757
- //console.log("debtAmountToRepay", debtAmountToRepay);
758
- if(!debtAmountToRepay) {
1054
+
1055
+ const { deltadebtAmountUnits: debtAmountToRepay } =
1056
+ HealthFactorMath.calculateDebtReductionAmountForWithdrawal(
1057
+ existingDebtInfo.amount,
1058
+ existingCollateralInfo.amount,
1059
+ maxLTV,
1060
+ this.config.targetHealthFactor,
1061
+ params.amount,
1062
+ collateralPrice.price,
1063
+ debtPrice.price,
1064
+ collateralToken,
1065
+ debtToken
1066
+ );
1067
+ if (!debtAmountToRepay) {
759
1068
  throw new Error("error calculating debt amount to repay");
760
1069
  }
761
- const ekuboQuoter = new EkuboQuoter(this.config.networkConfig, this.config.pricer);
762
- const debtInDebtUnits = new Web3Number(debtAmountToRepay, debtToken.decimals).dividedBy(debtPrice.price).multipliedBy(10 ** debtToken.decimals);
763
- const swapQuote = await ekuboQuoter.getQuote(
764
- debtToken.address.address,
765
- collateralToken.address.address,
766
- debtInDebtUnits
767
- )
768
- const MAX_SLIPPAGE = 0.002; // 0.2% slippag
769
- if(swapQuote.price_impact < 0.0025) {
770
- leverSwap = ekuboQuoter.getVesuMultiplyQuote(swapQuote, collateralToken, debtToken);
771
- } else {
772
- logger.error(`VesuMultiplyAdapter: Price impact too high (${swapQuote.price_impact}), skipping swap`);
1070
+
1071
+ const debtToRepayAbs = debtAmountToRepay.abs();
1072
+ const existingDebtAbs = existingDebtInfo.amount.abs();
1073
+
1074
+ const shouldCloseByDebt = debtToRepayAbs.greaterThanOrEqualTo(existingDebtAbs);
1075
+ const remainingDebtUSD = existingDebtAbs
1076
+ .minus(debtToRepayAbs)
1077
+ .multipliedBy(debtPrice.price)
1078
+ .toNumber();
1079
+ const shouldCloseByDust = remainingDebtUSD < MIN_REMAINING_DEBT_USD;
1080
+
1081
+ if (shouldCloseByDebt) {
1082
+ logger.info(
1083
+ `${VesuMultiplyAdapter.name}::_getDecreaseCalldata debt to repay (${debtToRepayAbs.toNumber()}) >= existing debt (${existingDebtAbs.toNumber()}), auto-closing position`
1084
+ );
1085
+ } else if (shouldCloseByDust) {
1086
+ logger.info(
1087
+ `${VesuMultiplyAdapter.name}::_getDecreaseCalldata remaining debt $${remainingDebtUSD.toFixed(2)} < $${MIN_REMAINING_DEBT_USD}, auto-closing position`
1088
+ );
773
1089
  }
774
1090
 
775
- leverSwapLimitAmount = new Web3Number(debtAmountToRepay, debtToken.decimals).abs().multipliedBy(1 + MAX_SLIPPAGE);
776
- //leverSwapLimitAmount = await ekuboQuoter.getSwapLimitAmount(debtToken, collateralToken, debtInCollateralUnits, MAX_SLIPPAGE);
777
- const multiplyParams = await this.getLeverParams(false, params, leverSwap, leverSwapLimitAmount);
778
- const call = multiplyContract.populate('modify_lever', {
779
- modify_lever_params: this.formatMultiplyParams(false, multiplyParams)
1091
+ return this._buildDecreaseLikeCalldata({
1092
+ subMargin: params.amount,
1093
+ debtToRepayAbs: shouldCloseByDebt || shouldCloseByDust ? existingDebtAbs : debtToRepayAbs,
1094
+ existingCollateral: existingCollateralInfo.amount,
1095
+ closePosition: shouldCloseByDebt || shouldCloseByDust,
1096
+ outputToken: params.withdrawSwap?.outputToken,
1097
+ collateralPrice: collateralPrice.price,
1098
+ debtPrice: debtPrice.price,
780
1099
  });
781
- return call.calldata as bigint[];
782
1100
  }
783
1101
 
784
- formatMultiplyParams(isIncrease: boolean, params: IncreaseLeverParams | DecreaseLeverParams) {
1102
+ // private async _getClosePositionCalldata(
1103
+ // outputToken?: TokenInfo
1104
+ // ): Promise<bigint[]> {
1105
+ // this._vesuAdapter.networkConfig = this.config.networkConfig;
1106
+ // this._vesuAdapter.pricer = this.config.pricer;
1107
+ // const existingPositions = await this._vesuAdapter.getPositions(
1108
+ // this.config.networkConfig
1109
+ // );
1110
+ // const existingCollateralInfo = existingPositions[0];
1111
+ // const existingDebtInfo = existingPositions[1];
1112
+
1113
+ // if (existingDebtInfo.amount.isZero()) {
1114
+ // throw new Error("VesuMultiplyAdapter: No debt to close");
1115
+ // }
1116
+
1117
+ // return this._buildDecreaseLikeCalldata({
1118
+ // subMargin: Web3Number.fromWei(0, this.config.collateral.decimals),
1119
+ // debtToRepayAbs: existingDebtInfo.amount.abs(),
1120
+ // existingCollateral: existingCollateralInfo.amount,
1121
+ // closePosition: true,
1122
+ // outputToken,
1123
+ // });
1124
+ // }
1125
+
1126
+ // ─── Param Formatting ─────────────────────────────────────────────────────
1127
+
1128
+ formatMultiplyParams(
1129
+ isIncrease: boolean,
1130
+ params: IncreaseLeverParams | DecreaseLeverParams
1131
+ ) {
785
1132
  if (isIncrease) {
786
1133
  const _params = params as IncreaseLeverParams;
787
1134
  return {
788
- action: new CairoCustomEnum({ IncreaseLever: {
1135
+ action: new CairoCustomEnum({
1136
+ IncreaseLever: {
1137
+ pool_id: _params.pool_id.toBigInt(),
1138
+ collateral_asset: _params.collateral_asset.toBigInt(),
1139
+ debt_asset: _params.debt_asset.toBigInt(),
1140
+ user: _params.user.toBigInt(),
1141
+ add_margin: BigInt(_params.add_margin.toWei()),
1142
+ margin_swap: _params.margin_swap.map((swap) => ({
1143
+ route: swap.route.map((route) => ({
1144
+ pool_key: {
1145
+ token0: route.pool_key.token0.toBigInt(),
1146
+ token1: route.pool_key.token1.toBigInt(),
1147
+ fee: route.pool_key.fee,
1148
+ tick_spacing: route.pool_key.tick_spacing,
1149
+ extension: BigInt(
1150
+ num.hexToDecimalString(route.pool_key.extension)
1151
+ ),
1152
+ },
1153
+ sqrt_ratio_limit: uint256.bnToUint256(
1154
+ route.sqrt_ratio_limit.toWei()
1155
+ ),
1156
+ skip_ahead: BigInt(100),
1157
+ })),
1158
+ token_amount: {
1159
+ token: swap.token_amount.token.toBigInt(),
1160
+ amount: swap.token_amount.amount.toI129(),
1161
+ },
1162
+ })),
1163
+ margin_swap_limit_amount: BigInt(
1164
+ _params.margin_swap_limit_amount.toWei()
1165
+ ),
1166
+ lever_swap: _params.lever_swap.map((swap) => ({
1167
+ route: swap.route.map((route) => ({
1168
+ pool_key: {
1169
+ token0: route.pool_key.token0.toBigInt(),
1170
+ token1: route.pool_key.token1.toBigInt(),
1171
+ fee: route.pool_key.fee,
1172
+ tick_spacing: route.pool_key.tick_spacing,
1173
+ extension: BigInt(
1174
+ num.hexToDecimalString(route.pool_key.extension)
1175
+ ),
1176
+ },
1177
+ sqrt_ratio_limit: uint256.bnToUint256(
1178
+ route.sqrt_ratio_limit.toWei()
1179
+ ),
1180
+ skip_ahead: BigInt(100),
1181
+ })),
1182
+ token_amount: {
1183
+ token: swap.token_amount.token.toBigInt(),
1184
+ amount: swap.token_amount.amount.toI129(),
1185
+ },
1186
+ })),
1187
+ lever_swap_limit_amount: BigInt(
1188
+ _params.lever_swap_limit_amount.toWei()
1189
+ ),
1190
+ },
1191
+ }),
1192
+ };
1193
+ }
1194
+
1195
+ const _params = params as DecreaseLeverParams;
1196
+ return {
1197
+ action: new CairoCustomEnum({
1198
+ DecreaseLever: {
789
1199
  pool_id: _params.pool_id.toBigInt(),
790
1200
  collateral_asset: _params.collateral_asset.toBigInt(),
791
1201
  debt_asset: _params.debt_asset.toBigInt(),
792
1202
  user: _params.user.toBigInt(),
793
- add_margin: BigInt(_params.add_margin.toWei()),
794
- margin_swap: _params.margin_swap.map(swap => ({
795
- route: swap.route.map(route => ({
1203
+ sub_margin: BigInt(_params.sub_margin.toWei()),
1204
+ recipient: _params.recipient.toBigInt(),
1205
+ lever_swap: _params.lever_swap.map((swap) => ({
1206
+ route: swap.route.map((route) => ({
796
1207
  pool_key: {
797
1208
  token0: route.pool_key.token0.toBigInt(),
798
1209
  token1: route.pool_key.token1.toBigInt(),
799
1210
  fee: route.pool_key.fee,
800
1211
  tick_spacing: route.pool_key.tick_spacing,
801
- extension: BigInt(num.hexToDecimalString(route.pool_key.extension)),
1212
+ extension: ContractAddr.from(
1213
+ route.pool_key.extension
1214
+ ).toBigInt(),
802
1215
  },
803
- sqrt_ratio_limit: uint256.bnToUint256(route.sqrt_ratio_limit.toWei()),
804
- skip_ahead: BigInt(100)
1216
+ sqrt_ratio_limit: uint256.bnToUint256(
1217
+ route.sqrt_ratio_limit.toWei()
1218
+ ),
1219
+ skip_ahead: BigInt(route.skip_ahead.toWei()),
805
1220
  })),
806
1221
  token_amount: {
807
1222
  token: swap.token_amount.token.toBigInt(),
808
- amount: swap.token_amount.amount.toI129()
809
- }
1223
+ amount: swap.token_amount.amount.toI129(),
1224
+ },
810
1225
  })),
811
- margin_swap_limit_amount: BigInt(_params.margin_swap_limit_amount.toWei()),
812
- lever_swap: _params.lever_swap.map(swap => ({
813
- route: swap.route.map(route => ({
1226
+ lever_swap_limit_amount: BigInt(
1227
+ _params.lever_swap_limit_amount.toWei()
1228
+ ),
1229
+ lever_swap_weights: _params.lever_swap_weights.map((weight) =>
1230
+ BigInt(weight.toWei())
1231
+ ),
1232
+ withdraw_swap: _params.withdraw_swap.map((swap) => ({
1233
+ route: swap.route.map((route) => ({
814
1234
  pool_key: {
815
1235
  token0: route.pool_key.token0.toBigInt(),
816
1236
  token1: route.pool_key.token1.toBigInt(),
817
1237
  fee: route.pool_key.fee,
818
1238
  tick_spacing: route.pool_key.tick_spacing,
819
- extension: BigInt(num.hexToDecimalString(route.pool_key.extension)),
1239
+ extension: ContractAddr.from(
1240
+ route.pool_key.extension
1241
+ ).toBigInt(),
820
1242
  },
821
- sqrt_ratio_limit: uint256.bnToUint256(route.sqrt_ratio_limit.toWei()),
822
- skip_ahead: BigInt(0)
1243
+ sqrt_ratio_limit: uint256.bnToUint256(
1244
+ route.sqrt_ratio_limit.toWei()
1245
+ ),
1246
+ skip_ahead: BigInt(route.skip_ahead.toWei()),
823
1247
  })),
824
1248
  token_amount: {
825
1249
  token: swap.token_amount.token.toBigInt(),
826
- amount: swap.token_amount.amount.toI129()
827
- }
828
- })),
829
- lever_swap_limit_amount: BigInt(_params.lever_swap_limit_amount.toWei()),
830
- } }),
831
- }
832
- }
833
-
834
- const _params = params as DecreaseLeverParams;
835
- return {
836
- action: new CairoCustomEnum({ DecreaseLever: {
837
- pool_id: _params.pool_id.toBigInt(),
838
- collateral_asset: _params.collateral_asset.toBigInt(),
839
- debt_asset: _params.debt_asset.toBigInt(),
840
- user: _params.user.toBigInt(),
841
- sub_margin: BigInt(_params.sub_margin.toWei()),
842
- recipient: _params.recipient.toBigInt(),
843
- lever_swap: _params.lever_swap.map(swap => ({
844
- route: swap.route.map(route => ({
845
- pool_key: {
846
- token0: route.pool_key.token0.toBigInt(),
847
- token1: route.pool_key.token1.toBigInt(),
848
- fee: route.pool_key.fee,
849
- tick_spacing: route.pool_key.tick_spacing,
850
- extension: ContractAddr.from(route.pool_key.extension).toBigInt(),
1250
+ amount: swap.token_amount.amount.toI129(),
851
1251
  },
852
- sqrt_ratio_limit: uint256.bnToUint256(route.sqrt_ratio_limit.toWei()),
853
- skip_ahead: BigInt(route.skip_ahead.toWei())
854
1252
  })),
855
- token_amount: {
856
- token: swap.token_amount.token.toBigInt(),
857
- amount: swap.token_amount.amount.toI129()
858
- }
859
- })),
860
- lever_swap_limit_amount: BigInt(_params.lever_swap_limit_amount.toWei()),
861
- lever_swap_weights: _params.lever_swap_weights.map(weight => BigInt(weight.toWei())),
862
- withdraw_swap: _params.withdraw_swap.map(swap => ({
863
- route: swap.route.map(route => ({
864
- pool_key: {
865
- token0: route.pool_key.token0.toBigInt(),
866
- token1: route.pool_key.token1.toBigInt(),
867
- fee: route.pool_key.fee,
868
- tick_spacing: route.pool_key.tick_spacing,
869
- extension: ContractAddr.from(route.pool_key.extension).toBigInt(),
870
- },
871
- sqrt_ratio_limit: uint256.bnToUint256(route.sqrt_ratio_limit.toWei()),
872
- skip_ahead: BigInt(route.skip_ahead.toWei())
873
- })),
874
- token_amount: {
875
- token: swap.token_amount.token.toBigInt(),
876
- amount: swap.token_amount.amount.toI129()
877
- }
878
- })),
879
- withdraw_swap_limit_amount: BigInt(_params.withdraw_swap_limit_amount.toWei()),
880
- withdraw_swap_weights: _params.withdraw_swap_weights.map(weight => BigInt(weight.toWei())),
881
- close_position: _params.close_position,
882
- }}),
883
- }
1253
+ withdraw_swap_limit_amount: BigInt(
1254
+ _params.withdraw_swap_limit_amount.toWei()
1255
+ ),
1256
+ withdraw_swap_weights: _params.withdraw_swap_weights.map((weight) =>
1257
+ BigInt(weight.toWei())
1258
+ ),
1259
+ close_position: _params.close_position,
1260
+ },
1261
+ }),
1262
+ };
884
1263
  }
885
1264
 
1265
+ // ─── Health Factor / Net APY ───────────────────────────────────────────────
1266
+
886
1267
  async getHealthFactor(): Promise<number> {
887
- const healthFactor = await this.vesuAdapter.getHealthFactor();
1268
+ const healthFactor = await this._vesuAdapter.getHealthFactor();
888
1269
  return healthFactor;
889
1270
  }
890
1271
 
891
1272
  async getNetAPY(): Promise<number> {
892
1273
  const positions = await this.getPositions();
893
- logger.verbose(`${this.name}::getNetAPY: positions: ${JSON.stringify(positions)}`);
894
- const allZero = positions.every(p => p.usdValue === 0);
1274
+ logger.verbose(
1275
+ `${this.name}::getNetAPY: positions: ${JSON.stringify(positions)}`
1276
+ );
1277
+ const allZero = positions.every((p) => p.usdValue === 0);
895
1278
 
896
- // in case of zero positions, apy will come zero/NaN
897
- // bcz of net 0 zero weights
898
- if (allZero) {
899
- // use approx dummy usd values to compute netAPY
1279
+ if (allZero && positions.length == 2) {
900
1280
  const collateralUSD = 1000;
901
- const maxLTV = await this.vesuAdapter.getLTVConfig(this.config.networkConfig);
1281
+ const maxLTV = await this._vesuAdapter.getLTVConfig(
1282
+ this.config.networkConfig
1283
+ );
902
1284
  const targetHF = this.config.targetHealthFactor;
1285
+ // though we are passing in token terms, but by simulating with price 1, usd terms == token terms
903
1286
  const maxDebt = HealthFactorMath.getMaxDebtAmountOnLooping(
904
1287
  new Web3Number(collateralUSD, this.config.collateral.decimals),
905
- 1, // assume price 1 for simplicity
1288
+ 1,
906
1289
  maxLTV,
907
1290
  targetHF,
908
- 1, // assume price 1 for simplicity
1291
+ 1,
909
1292
  this.config.debt
910
- )
1293
+ );
911
1294
 
912
- // debt is also added to collateral bcz, we assume debt is swapped to collateral
913
- const debtUSD = maxDebt.multipliedBy(1); // assume price 1 for simplicity
914
- const netAPY = (positions[0].apy.apy * (collateralUSD + debtUSD.toNumber()) + positions[1].apy.apy * debtUSD.toNumber()) / (collateralUSD);
1295
+ const debtUSD = maxDebt.multipliedBy(1);
1296
+ const netAPY =
1297
+ (positions[0].apy.apy * (collateralUSD + debtUSD.toNumber()) +
1298
+ positions[1].apy.apy * debtUSD.toNumber()) /
1299
+ collateralUSD;
915
1300
  return netAPY;
916
1301
  }
917
1302
 
918
- // Return true APY
919
1303
  const netAmount = positions.reduce((acc, curr) => acc + curr.usdValue, 0);
920
- const netAPY = positions.reduce((acc, curr) => acc + curr.apy.apy * curr.usdValue, 0) / netAmount;
1304
+ const netAPY =
1305
+ positions.reduce((acc, curr) => acc + curr.apy.apy * curr.usdValue, 0) /
1306
+ netAmount;
921
1307
  logger.verbose(`${this.name}::getNetAPY: netAPY: ${netAPY}`);
922
1308
  return netAPY;
923
1309
  }