@strkfarm/sdk 2.0.0-dev.3 → 2.0.0-dev.30

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