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

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 (70) hide show
  1. package/dist/cli.js +190 -36
  2. package/dist/cli.mjs +188 -34
  3. package/dist/index.browser.global.js +79130 -49357
  4. package/dist/index.browser.mjs +18039 -11434
  5. package/dist/index.d.ts +2869 -898
  6. package/dist/index.js +19036 -12210
  7. package/dist/index.mjs +18942 -12161
  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/index.ts +3 -2
  13. package/src/dataTypes/mynumber.ts +141 -0
  14. package/src/global.ts +76 -41
  15. package/src/index.browser.ts +2 -1
  16. package/src/interfaces/common.tsx +167 -2
  17. package/src/modules/ExtendedWrapperSDk/types.ts +26 -4
  18. package/src/modules/ExtendedWrapperSDk/wrapper.ts +110 -67
  19. package/src/modules/apollo-client-config.ts +28 -0
  20. package/src/modules/avnu.ts +4 -4
  21. package/src/modules/ekubo-pricer.ts +79 -0
  22. package/src/modules/ekubo-quoter.ts +46 -30
  23. package/src/modules/erc20.ts +17 -0
  24. package/src/modules/harvests.ts +43 -29
  25. package/src/modules/pragma.ts +23 -8
  26. package/src/modules/pricer-from-api.ts +156 -15
  27. package/src/modules/pricer-lst.ts +1 -1
  28. package/src/modules/pricer.ts +40 -4
  29. package/src/modules/pricerBase.ts +2 -1
  30. package/src/node/deployer.ts +36 -1
  31. package/src/node/pricer-redis.ts +2 -1
  32. package/src/strategies/base-strategy.ts +78 -10
  33. package/src/strategies/ekubo-cl-vault.tsx +906 -347
  34. package/src/strategies/factory.ts +159 -0
  35. package/src/strategies/index.ts +6 -1
  36. package/src/strategies/registry.ts +239 -0
  37. package/src/strategies/sensei.ts +335 -7
  38. package/src/strategies/svk-strategy.ts +97 -27
  39. package/src/strategies/types.ts +4 -0
  40. package/src/strategies/universal-adapters/adapter-utils.ts +2 -1
  41. package/src/strategies/universal-adapters/avnu-adapter.ts +177 -268
  42. package/src/strategies/universal-adapters/baseAdapter.ts +263 -251
  43. package/src/strategies/universal-adapters/common-adapter.ts +206 -203
  44. package/src/strategies/universal-adapters/extended-adapter.ts +155 -336
  45. package/src/strategies/universal-adapters/index.ts +9 -8
  46. package/src/strategies/universal-adapters/token-transfer-adapter.ts +200 -0
  47. package/src/strategies/universal-adapters/usdc<>usdce-adapter.ts +200 -0
  48. package/src/strategies/universal-adapters/vesu-adapter.ts +110 -75
  49. package/src/strategies/universal-adapters/vesu-modify-position-adapter.ts +476 -0
  50. package/src/strategies/universal-adapters/vesu-multiply-adapter.ts +762 -844
  51. package/src/strategies/universal-adapters/vesu-position-common.ts +251 -0
  52. package/src/strategies/universal-adapters/vesu-supply-only-adapter.ts +18 -3
  53. package/src/strategies/universal-lst-muliplier-strategy.tsx +396 -204
  54. package/src/strategies/universal-strategy.tsx +1426 -1178
  55. package/src/strategies/vesu-extended-strategy/services/executionService.ts +2251 -0
  56. package/src/strategies/vesu-extended-strategy/services/extended-vesu-state-manager.ts +2941 -0
  57. package/src/strategies/vesu-extended-strategy/services/operationService.ts +12 -1
  58. package/src/strategies/vesu-extended-strategy/types/transaction-metadata.ts +52 -0
  59. package/src/strategies/vesu-extended-strategy/utils/config.runtime.ts +1 -0
  60. package/src/strategies/vesu-extended-strategy/utils/constants.ts +2 -0
  61. package/src/strategies/vesu-extended-strategy/utils/helper.ts +158 -124
  62. package/src/strategies/vesu-extended-strategy/vesu-extended-strategy.tsx +377 -1788
  63. package/src/strategies/vesu-rebalance.tsx +255 -152
  64. package/src/utils/health-factor-math.ts +4 -1
  65. package/src/utils/index.ts +2 -1
  66. package/src/utils/logger.browser.ts +22 -4
  67. package/src/utils/logger.node.ts +259 -24
  68. package/src/utils/starknet-call-parser.ts +1036 -0
  69. package/src/utils/strategy-utils.ts +61 -0
  70. 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
29
  import { logger } from "@/utils";
37
- import { WALLET_ADDRESS } from "../vesu-extended-strategy/utils/constants";
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
+ }
209
187
 
210
- if (supportedPosition.isDebt) {
211
- position.amount = position.amount.multipliedBy(-1);
212
- position.usdValue = position.usdValue * -1;
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
+ }
197
+
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,256 @@ 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
+
590
+ const {
591
+ existingCollateralInfo,
592
+ existingDebtInfo,
593
+ collateralPrice,
594
+ debtPrice,
595
+ ekuboQuoter,
596
+ } = await this._fetchPositionAndPrices();
597
+
598
+ logger.verbose(
599
+ `${VesuMultiplyAdapter.name}::_getIncreaseCalldata params: ${JSON.stringify(
600
+ params
601
+ )}, collateral: ${collateralToken.symbol}, debt: ${debtToken.symbol}`
787
602
  );
788
- const collateralisation = await this.vesuAdapter.getCollateralization(
789
- this.config.networkConfig
603
+ logger.debug(
604
+ `${VesuMultiplyAdapter.name}::_getIncreaseCalldata existingCollateralInfo: ${JSON.stringify(
605
+ existingCollateralInfo
606
+ )}, existingDebtInfo: ${JSON.stringify(existingDebtInfo)}`
790
607
  );
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
608
  logger.debug(
817
- `${VesuMultiplyAdapter.name}::getVesuMultiplyCall collateralPrice: ${collateralPrice}, debtPrice: ${debtPrice}`
609
+ `${VesuMultiplyAdapter.name}::_getIncreaseCalldata collateralPrice: ${collateralPrice}, debtPrice: ${debtPrice}`
818
610
  );
819
611
 
820
- const legLTV = await this.vesuAdapter.getLTVConfig(
612
+ const legLTV = await this._vesuAdapter.getLTVConfig(
821
613
  this.config.networkConfig
822
614
  );
823
- const ekuboQuoter = new EkuboQuoter(
824
- this.config.networkConfig,
825
- this.config.pricer
826
- );
615
+ const isDexPriceRequired = debtToken.symbol !== "USDC";
827
616
  const dexPrice = isDexPriceRequired
828
617
  ? await ekuboQuoter.getDexPrice(
829
- this.config.collateral,
830
- this.config.debt,
831
- this.config.quoteAmountToFetchPrice
832
- )
618
+ collateralToken,
619
+ debtToken,
620
+ this.config.quoteAmountToFetchPrice
621
+ )
833
622
  : 1;
834
623
  logger.verbose(
835
- `${VesuMultiplyAdapter.name}::getVesuMultiplyCall dexPrice: ${dexPrice}, ltv: ${legLTV}`
624
+ `${VesuMultiplyAdapter.name}::_getIncreaseCalldata dexPrice: ${dexPrice}, ltv: ${legLTV}`
836
625
  );
837
626
 
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)
627
+ let marginSwap: Swap[] = [];
628
+ let marginSwapLimitAmount = Web3Number.fromWei(0, collateralToken.decimals);
629
+ let addedCollateral: Web3Number = params.amount;
630
+ let approveAmount: Web3Number = params.amount;
845
631
 
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
- );
632
+ if (params.marginSwap) {
633
+ const marginToken = params.marginSwap.marginToken;
634
+ const requiredAmount = params.amount;
876
635
 
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
636
+ const marginSwapQuote = await ekuboQuoter.getQuoteExactOutput(
637
+ marginToken.address.address,
638
+ collateralToken.address.address,
639
+ requiredAmount
640
+ );
641
+ if (marginSwapQuote.price_impact > 0.01) {
642
+ throw new Error(
643
+ `VesuMultiplyAdapter: Margin swap price impact too high (${marginSwapQuote.price_impact})`
644
+ );
645
+ }
646
+
647
+ marginSwap = ekuboQuoter.getVesuMultiplyQuote(
648
+ marginSwapQuote,
649
+ marginToken,
650
+ collateralToken
651
+ );
652
+
653
+ approveAmount = Web3Number
654
+ .fromWei(marginSwapQuote.total_calculated, marginToken.decimals)
655
+ .multipliedBy(1 + this.maxSlippage)
656
+ .abs();
657
+ }
658
+
659
+ let debtAmount = this._computeTargetDebtDelta(
660
+ addedCollateral,
661
+ existingCollateralInfo.amount,
662
+ existingDebtInfo.amount,
663
+ collateralPrice,
664
+ debtPrice,
665
+ legLTV,
666
+ dexPrice
882
667
  );
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
668
+
669
+ logger.verbose(
670
+ `${VesuMultiplyAdapter.name}::_getIncreaseCalldata debtAmount: ${debtAmount}, addedCollateral: ${addedCollateral}`
893
671
  );
672
+ let leverSwap: Swap[] = [];
673
+ let leverSwapLimitAmount = Web3Number.fromWei(0, debtToken.decimals);
894
674
 
895
- // increase multiply lever or not
896
675
  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
676
+ if (!isIncrease && debtAmount.greaterThan(0)) {
906
677
  debtAmount = Web3Number.fromWei(0, this.config.debt.decimals);
907
678
  }
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
679
 
680
+ if (!debtAmount.isZero() && debtAmount.greaterThan(0)) {
915
681
  try {
916
- const swapQuote = await ekuboQuoter.getQuote(
917
- collateralToken.address.address,
682
+ let swapQuote: EkuboQuote;
683
+ const debtAmountInCollateralUnits = new Web3Number(
684
+ debtAmount
685
+ .multipliedBy(debtPrice)
686
+ .dividedBy(collateralPrice)
687
+ .toFixed(6),
688
+ collateralToken.decimals
689
+ );
690
+ if (params.leverSwap?.exactOutput) {
691
+ swapQuote = await ekuboQuoter.getQuoteExactOutput(
692
+ debtToken.address.address,
693
+ collateralToken.address.address,
694
+ params.leverSwap?.exactOutput?.abs()!
695
+ );
696
+ debtAmount = Web3Number.fromWei(swapQuote.total_calculated, debtToken.decimals).abs();
697
+ }
698
+
699
+ swapQuote = await ekuboQuoter.getQuoteExactInput(
918
700
  debtToken.address.address,
919
- debtAmountInCollateralUnits.multipliedBy(-1) // negative for exact amount out
701
+ collateralToken.address.address,
702
+ debtAmount.abs()
920
703
  );
704
+ const expectedOutputAmount = debtAmount.multipliedBy(debtPrice).dividedBy(collateralPrice);
705
+ expectedOutputAmount.decimals = collateralToken.decimals;
706
+ leverSwapLimitAmount = expectedOutputAmount.multipliedBy(1 - this.maxSlippage);
921
707
 
922
- // todo add better slip checks
923
- // Check price impact
924
708
  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
- }
709
+ const quoteOutputAmount = Web3Number.fromWei(
710
+ swapQuote.total_calculated,
711
+ collateralToken.decimals
712
+ )
713
+ .abs()
714
+ .toNumber();
715
+
716
+ const inputAmt = debtAmount.abs().toNumber();
717
+ const outputAmt = quoteOutputAmount;
718
+ this.lastSwapPriceInfo = {
719
+ source: "ekubo",
720
+ fromTokenSymbol: debtToken.symbol,
721
+ toTokenSymbol: collateralToken.symbol,
722
+ fromAmount: inputAmt,
723
+ toAmount: outputAmt,
724
+ effectivePrice: outputAmt !== 0 ? inputAmt / outputAmt : 0,
725
+ };
726
+ logger.verbose(
727
+ `${VesuMultiplyAdapter.name}::_getIncreaseCalldata stored price info: ` +
728
+ `${inputAmt} ${debtToken.symbol} ${outputAmt} ${collateralToken.symbol}, ` +
729
+ `effectivePrice=${this.lastSwapPriceInfo.effectivePrice}`
730
+ );
731
+
732
+ leverSwap = ekuboQuoter.getVesuMultiplyQuote(
733
+ swapQuote,
734
+ debtToken,
735
+ collateralToken
736
+ );
964
737
  await new Promise((resolve) => setTimeout(resolve, 10000));
965
- //console.log("leverSwapLimitAmount", leverSwapLimitAmount);
966
738
  } else {
967
739
  throw new Error(
968
740
  `VesuMultiplyAdapter: Price impact too high (${swapQuote.price_impact}), skipping swap`
@@ -975,92 +747,225 @@ export class VesuMultiplyAdapter extends BaseAdapter<
975
747
  }
976
748
  }
977
749
 
978
- const multiplyParams = await this.getLeverParams(
979
- isIncrease,
980
- params,
981
- leverSwap,
982
- leverSwapLimitAmount
750
+ logger.verbose(
751
+ `${VesuMultiplyAdapter.name}::_getIncreaseCalldata leverSwapLimitAmount: ${leverSwapLimitAmount.toWei()}`
752
+ );
753
+ logger.verbose(
754
+ `${VesuMultiplyAdapter.name}::_getIncreaseCalldata approveAmount: ${approveAmount.toWei()}, marginSwapLimitAmount: ${marginSwapLimitAmount.toWei()}`
983
755
  );
756
+ logger.verbose(
757
+ `${VesuMultiplyAdapter.name}::_getIncreaseCalldata marginSwap: ${JSON.stringify(marginSwap)}`
758
+ );
759
+ const multiplyParams: IncreaseLeverParams = {
760
+ user: this.config.vaultAllocator,
761
+ pool_id: this.config.poolId,
762
+ collateral_asset: collateralToken.address,
763
+ debt_asset: debtToken.address,
764
+ add_margin: params.marginSwap
765
+ ? Web3Number.fromWei(0, collateralToken.decimals)
766
+ : params.amount,
767
+ margin_swap: marginSwap,
768
+ margin_swap_limit_amount: params.marginSwap ? approveAmount : marginSwapLimitAmount,
769
+ lever_swap: leverSwap,
770
+ lever_swap_limit_amount: leverSwapLimitAmount,
771
+ };
772
+
984
773
  const call = multiplyContract.populate("modify_lever", {
985
- modify_lever_params: this.formatMultiplyParams(
986
- isIncrease,
987
- multiplyParams
988
- ),
774
+ modify_lever_params: this.formatMultiplyParams(true, multiplyParams),
989
775
  });
776
+ logger.debug(
777
+ `${VesuMultiplyAdapter.name}::_getIncreaseCalldata marginSwapCount=${marginSwap.length}, leverSwapCount=${leverSwap.length}`,
778
+ );
990
779
 
991
- return call.calldata as bigint[];
780
+ return {
781
+ calldata: call.calldata as bigint[],
782
+ approveToken: params.marginSwap?.marginToken ?? this.config.collateral,
783
+ approveAmount: approveAmount,
784
+ };
992
785
  }
993
786
 
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,
1015
- }
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,
787
+ private async _buildDecreaseLikeCalldata(params: {
788
+ subMargin: Web3Number;
789
+ debtToRepayAbs: Web3Number;
790
+ existingCollateral: Web3Number;
791
+ closePosition: boolean;
792
+ outputToken?: TokenInfo;
793
+ collateralPrice: number;
794
+ debtPrice: number;
795
+ }): Promise<bigint[]> {
796
+ const collateralToken = this.config.collateral;
797
+ const debtToken = this.config.debt;
798
+ const { contract: multiplyContract } = this._getMultiplyContract();
799
+ const ekuboQuoter = new EkuboQuoter(
800
+ this.config.networkConfig,
801
+ this.config.pricer
802
+ );
803
+
804
+ let leverSwap: Swap[] = [];
805
+ let leverSwapWeights: Web3Number[] = [];
806
+ let leverSwapLimitAmount = Web3Number.fromWei(0, collateralToken.decimals);
807
+ let leverCollateralUsed = Web3Number.fromWei(0, collateralToken.decimals);
808
+
809
+ if (params.closePosition) {
810
+ const debtQuote = await ekuboQuoter.getQuoteExactOutput(
811
+ collateralToken.address.address,
812
+ debtToken.address.address,
813
+ params.debtToRepayAbs
814
+ );
815
+
816
+ const built = this._buildZeroAmountSwapsWithWeights(
817
+ debtQuote,
818
+ debtToken,
819
+ true
820
+ );
821
+ leverSwap = built.swaps;
822
+ leverSwapWeights = built.weights;
823
+ leverCollateralUsed = Web3Number.fromWei(
824
+ debtQuote.total_calculated,
825
+ collateralToken.decimals
826
+ ).abs();
827
+ leverSwapLimitAmount = leverCollateralUsed.multipliedBy(1 + this.maxSlippage);
828
+ } else {
829
+ if (params.collateralPrice === undefined || params.debtPrice === undefined) {
830
+ throw new Error(
831
+ "VesuMultiplyAdapter: Missing prices for non-close decrease calldata"
832
+ );
833
+ }
834
+ const collateralToSwap = new Web3Number(
835
+ params.debtToRepayAbs
836
+ .multipliedBy(params.debtPrice)
837
+ .dividedBy(params.collateralPrice)
838
+ .toFixed(collateralToken.decimals),
839
+ collateralToken.decimals
840
+ );
841
+ const leverSwapQuote = await ekuboQuoter.getQuoteExactInput(
842
+ collateralToken.address.address,
843
+ debtToken.address.address,
844
+ collateralToSwap
845
+ );
846
+ if (leverSwapQuote.price_impact < 0.0025) {
847
+ const inputAmt = collateralToSwap.toNumber();
848
+ const outputAmt = Web3Number.fromWei(
849
+ leverSwapQuote.total_calculated,
850
+ debtToken.decimals
851
+ )
852
+ .abs()
853
+ .toNumber();
854
+ this.lastSwapPriceInfo = {
855
+ source: "ekubo",
856
+ fromTokenSymbol: collateralToken.symbol,
857
+ toTokenSymbol: debtToken.symbol,
858
+ fromAmount: inputAmt,
859
+ toAmount: outputAmt,
860
+ effectivePrice: outputAmt !== 0 ? outputAmt / inputAmt : 0,
1033
861
  };
1034
- return multiplyParams;
862
+ logger.verbose(
863
+ `${VesuMultiplyAdapter.name}::_buildDecreaseLikeCalldata stored price info: ` +
864
+ `${inputAmt} ${collateralToken.symbol} → ${outputAmt} ${debtToken.symbol}, ` +
865
+ `effectivePrice=${this.lastSwapPriceInfo.effectivePrice}`
866
+ );
867
+
868
+ leverSwap = ekuboQuoter.getVesuMultiplyQuote(
869
+ leverSwapQuote,
870
+ collateralToken,
871
+ debtToken
872
+ );
873
+ leverSwapLimitAmount = collateralToSwap
874
+ .multipliedBy(params.collateralPrice)
875
+ .dividedBy(params.debtPrice)
876
+ .multipliedBy(1 - this.maxSlippage);
877
+ leverSwapLimitAmount.decimals = debtToken.decimals;
878
+ } else {
879
+ throw new Error(`VesuMultiplyAdapter: Lever swap price impact too high (${leverSwapQuote.price_impact})`);
880
+ }
881
+
882
+ leverCollateralUsed = Web3Number.fromWei(
883
+ leverSwapQuote.total_calculated,
884
+ collateralToken.decimals
885
+ ).abs();
886
+ }
887
+
888
+ let withdrawSwap: Swap[] = [];
889
+ let withdrawSwapLimitAmount = Web3Number.fromWei(
890
+ 0,
891
+ params.outputToken?.decimals ?? collateralToken.decimals
892
+ );
893
+ const withdrawSwapWeights: Web3Number[] = [];
894
+
895
+ if (params.outputToken && !params.outputToken.address.eq(collateralToken.address)) {
896
+ const residualCollateral = params.closePosition
897
+ ? params.existingCollateral.minus(leverCollateralUsed)
898
+ : params.subMargin;
899
+ const outputTokenPrice = await this.config.pricer.getPrice(params.outputToken.symbol);
900
+ if (residualCollateral.greaterThan(0)) {
901
+ await new Promise((r) => setTimeout(r, 10000));
902
+ const withdrawQuote = await ekuboQuoter.getQuoteExactInput(
903
+ collateralToken.address.address,
904
+ params.outputToken.address.address,
905
+ residualCollateral
906
+ );
907
+ if (withdrawQuote.price_impact < 0.0025) {
908
+ const built = this._buildZeroAmountSwapsWithWeights(
909
+ withdrawQuote,
910
+ collateralToken
911
+ );
912
+ withdrawSwap = built.swaps;
913
+ withdrawSwapWeights.push(...built.weights);
914
+ const estimatedOutput = residualCollateral
915
+ .multipliedBy(params.collateralPrice)
916
+ .dividedBy(outputTokenPrice.price);
917
+ estimatedOutput.decimals = params.outputToken.decimals;
918
+ withdrawSwapLimitAmount = estimatedOutput
919
+ .multipliedBy(1 - this.maxSlippage);
920
+ }
921
+ }
922
+ }
923
+
924
+ logger.debug(
925
+ `${VesuMultiplyAdapter.name}::_buildDecreaseLikeCalldata leverSwapCount=${leverSwap.length}, withdrawSwapCount=${withdrawSwap.length}, withdrawSwapLimitAmount=${withdrawSwapLimitAmount.toNumber()}, subMargin=${params.subMargin.toNumber()}`,
926
+ );
927
+ const multiplyParams: DecreaseLeverParams = {
928
+ user: this.config.vaultAllocator,
929
+ pool_id: this.config.poolId,
930
+ collateral_asset: collateralToken.address,
931
+ debt_asset: debtToken.address,
932
+ recipient: this.config.vaultAllocator,
933
+ sub_margin: params.closePosition
934
+ ? Web3Number.fromWei(0, collateralToken.decimals)
935
+ : params.subMargin,
936
+ lever_swap: leverSwap,
937
+ lever_swap_limit_amount: leverSwapLimitAmount,
938
+ lever_swap_weights: leverSwapWeights,
939
+ withdraw_swap: withdrawSwap,
940
+ withdraw_swap_limit_amount: withdrawSwapLimitAmount,
941
+ withdraw_swap_weights: withdrawSwapWeights,
942
+ close_position: params.closePosition,
943
+ };
944
+
945
+ const call = multiplyContract.populate("modify_lever", {
946
+ modify_lever_params: this.formatMultiplyParams(false, multiplyParams),
947
+ });
948
+ return call.calldata as bigint[];
1035
949
  }
1036
950
 
1037
- private async getWithdrawalCalldata(
1038
- params: WithdrawParams
951
+ private async _getDecreaseCalldata(
952
+ params: VesuWithdrawParams
1039
953
  ): 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(
954
+ const collateralToken = this.config.collateral;
955
+ const debtToken = this.config.debt;
956
+
957
+ this._vesuAdapter.networkConfig = this.config.networkConfig;
958
+ this._vesuAdapter.pricer = this.config.pricer;
959
+ const existingPositions = await this._vesuAdapter.getPositions(
1053
960
  this.config.networkConfig
1054
961
  );
1055
962
  const existingCollateralInfo = existingPositions[0];
1056
963
  const existingDebtInfo = existingPositions[1];
1057
- const collateralToken = this.config.collateral;
1058
- const debtToken = this.config.debt;
1059
964
  const collateralPrice = await this.config.pricer.getPrice(
1060
965
  collateralToken.symbol
1061
966
  );
1062
967
  const debtPrice = await this.config.pricer.getPrice(debtToken.symbol);
1063
- // the debt amount is negative as we are reducing debt to withdraw
968
+
1064
969
  const { deltadebtAmountUnits: debtAmountToRepay } =
1065
970
  calculateDebtReductionAmountForWithdrawal(
1066
971
  existingDebtInfo.amount,
@@ -1071,54 +976,70 @@ export class VesuMultiplyAdapter extends BaseAdapter<
1071
976
  debtPrice.price,
1072
977
  debtToken.decimals
1073
978
  );
1074
- //console.log("debtAmountToRepay", debtAmountToRepay);
1075
979
  if (!debtAmountToRepay) {
1076
980
  throw new Error("error calculating debt amount to repay");
1077
981
  }
1078
- const ekuboQuoter = new EkuboQuoter(
1079
- this.config.networkConfig,
1080
- this.config.pricer
1081
- );
1082
- const debtInDebtUnits = new Web3Number(
982
+
983
+ const debtToRepayAbs = new Web3Number(
1083
984
  debtAmountToRepay,
1084
985
  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
986
+ ).abs();
987
+ const existingDebtAbs = existingDebtInfo.amount.abs();
988
+
989
+ const shouldCloseByDebt = debtToRepayAbs.greaterThanOrEqualTo(existingDebtAbs);
990
+ const remainingDebtUSD = existingDebtAbs
991
+ .minus(debtToRepayAbs)
992
+ .multipliedBy(debtPrice.price)
993
+ .toNumber();
994
+ const shouldCloseByDust = remainingDebtUSD < MIN_REMAINING_DEBT_USD;
995
+
996
+ if (shouldCloseByDebt) {
997
+ logger.info(
998
+ `${VesuMultiplyAdapter.name}::_getDecreaseCalldata debt to repay (${debtToRepayAbs.toNumber()}) >= existing debt (${existingDebtAbs.toNumber()}), auto-closing position`
1099
999
  );
1100
- } else {
1101
- logger.error(
1102
- `VesuMultiplyAdapter: Price impact too high (${swapQuote.price_impact}), skipping swap`
1000
+ } else if (shouldCloseByDust) {
1001
+ logger.info(
1002
+ `${VesuMultiplyAdapter.name}::_getDecreaseCalldata remaining debt $${remainingDebtUSD.toFixed(2)} < $${MIN_REMAINING_DEBT_USD}, auto-closing position`
1103
1003
  );
1104
1004
  }
1105
1005
 
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),
1006
+ return this._buildDecreaseLikeCalldata({
1007
+ subMargin: params.amount,
1008
+ debtToRepayAbs: shouldCloseByDebt || shouldCloseByDust ? existingDebtAbs : debtToRepayAbs,
1009
+ existingCollateral: existingCollateralInfo.amount,
1010
+ closePosition: shouldCloseByDebt || shouldCloseByDust,
1011
+ outputToken: params.withdrawSwap?.outputToken,
1012
+ collateralPrice: collateralPrice.price,
1013
+ debtPrice: debtPrice.price,
1118
1014
  });
1119
- return call.calldata as bigint[];
1120
1015
  }
1121
1016
 
1017
+ // private async _getClosePositionCalldata(
1018
+ // outputToken?: TokenInfo
1019
+ // ): Promise<bigint[]> {
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
+
1028
+ // if (existingDebtInfo.amount.isZero()) {
1029
+ // throw new Error("VesuMultiplyAdapter: No debt to close");
1030
+ // }
1031
+
1032
+ // return this._buildDecreaseLikeCalldata({
1033
+ // subMargin: Web3Number.fromWei(0, this.config.collateral.decimals),
1034
+ // debtToRepayAbs: existingDebtInfo.amount.abs(),
1035
+ // existingCollateral: existingCollateralInfo.amount,
1036
+ // closePosition: true,
1037
+ // outputToken,
1038
+ // });
1039
+ // }
1040
+
1041
+ // ─── Param Formatting ─────────────────────────────────────────────────────
1042
+
1122
1043
  formatMultiplyParams(
1123
1044
  isIncrease: boolean,
1124
1045
  params: IncreaseLeverParams | DecreaseLeverParams
@@ -1256,8 +1177,10 @@ export class VesuMultiplyAdapter extends BaseAdapter<
1256
1177
  };
1257
1178
  }
1258
1179
 
1180
+ // ─── Health Factor / Net APY ───────────────────────────────────────────────
1181
+
1259
1182
  async getHealthFactor(): Promise<number> {
1260
- const healthFactor = await this.vesuAdapter.getHealthFactor();
1183
+ const healthFactor = await this._vesuAdapter.getHealthFactor();
1261
1184
  return healthFactor;
1262
1185
  }
1263
1186
 
@@ -1268,26 +1191,22 @@ export class VesuMultiplyAdapter extends BaseAdapter<
1268
1191
  );
1269
1192
  const allZero = positions.every((p) => p.usdValue === 0);
1270
1193
 
1271
- // in case of zero positions, apy will come zero/NaN
1272
- // bcz of net 0 zero weights
1273
1194
  if (allZero) {
1274
- // use approx dummy usd values to compute netAPY
1275
1195
  const collateralUSD = 1000;
1276
- const maxLTV = await this.vesuAdapter.getLTVConfig(
1196
+ const maxLTV = await this._vesuAdapter.getLTVConfig(
1277
1197
  this.config.networkConfig
1278
1198
  );
1279
1199
  const targetHF = this.config.targetHealthFactor;
1280
1200
  const maxDebt = HealthFactorMath.getMaxDebtAmountOnLooping(
1281
1201
  new Web3Number(collateralUSD, this.config.collateral.decimals),
1282
- 1, // assume price 1 for simplicity
1202
+ 1,
1283
1203
  maxLTV,
1284
1204
  targetHF,
1285
- 1, // assume price 1 for simplicity
1205
+ 1,
1286
1206
  this.config.debt
1287
1207
  );
1288
1208
 
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
1209
+ const debtUSD = maxDebt.multipliedBy(1);
1291
1210
  const netAPY =
1292
1211
  (positions[0].apy.apy * (collateralUSD + debtUSD.toNumber()) +
1293
1212
  positions[1].apy.apy * debtUSD.toNumber()) /
@@ -1295,7 +1214,6 @@ export class VesuMultiplyAdapter extends BaseAdapter<
1295
1214
  return netAPY;
1296
1215
  }
1297
1216
 
1298
- // Return true APY
1299
1217
  const netAmount = positions.reduce((acc, curr) => acc + curr.usdValue, 0);
1300
1218
  const netAPY =
1301
1219
  positions.reduce((acc, curr) => acc + curr.apy.apy * curr.usdValue, 0) /