@strkfarm/sdk 2.0.0-dev.27 → 2.0.0-dev.29

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