curvance 5.0.0 → 5.1.0

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 (115) hide show
  1. package/README.md +22 -7
  2. package/dist/abis/OptimizerReader.json +43 -45
  3. package/dist/abis/ProtocolReader.json +279 -55
  4. package/dist/chains/arbitrum.js +3 -3
  5. package/dist/chains/arbitrum.js.map +1 -1
  6. package/dist/classes/Api.d.ts +2 -2
  7. package/dist/classes/Api.d.ts.map +1 -1
  8. package/dist/classes/Api.js +87 -14
  9. package/dist/classes/Api.js.map +1 -1
  10. package/dist/classes/BorrowableCToken.d.ts +9 -1
  11. package/dist/classes/BorrowableCToken.d.ts.map +1 -1
  12. package/dist/classes/BorrowableCToken.js +67 -12
  13. package/dist/classes/BorrowableCToken.js.map +1 -1
  14. package/dist/classes/CToken.d.ts +75 -18
  15. package/dist/classes/CToken.d.ts.map +1 -1
  16. package/dist/classes/CToken.js +638 -269
  17. package/dist/classes/CToken.js.map +1 -1
  18. package/dist/classes/Calldata.d.ts +2 -2
  19. package/dist/classes/Calldata.d.ts.map +1 -1
  20. package/dist/classes/Calldata.js +6 -2
  21. package/dist/classes/Calldata.js.map +1 -1
  22. package/dist/classes/DexAggregators/IDexAgg.d.ts +1 -1
  23. package/dist/classes/DexAggregators/Kuru.js +1 -1
  24. package/dist/classes/DexAggregators/Kuru.js.map +1 -1
  25. package/dist/classes/DexAggregators/KyberSwap.d.ts.map +1 -1
  26. package/dist/classes/DexAggregators/KyberSwap.js +112 -61
  27. package/dist/classes/DexAggregators/KyberSwap.js.map +1 -1
  28. package/dist/classes/DexAggregators/MultiDexAgg.d.ts +2 -2
  29. package/dist/classes/DexAggregators/MultiDexAgg.d.ts.map +1 -1
  30. package/dist/classes/DexAggregators/MultiDexAgg.js +30 -18
  31. package/dist/classes/DexAggregators/MultiDexAgg.js.map +1 -1
  32. package/dist/classes/DexAggregators/UnsupportedDexAgg.d.ts +19 -0
  33. package/dist/classes/DexAggregators/UnsupportedDexAgg.d.ts.map +1 -0
  34. package/dist/classes/DexAggregators/UnsupportedDexAgg.js +29 -0
  35. package/dist/classes/DexAggregators/UnsupportedDexAgg.js.map +1 -0
  36. package/dist/classes/DexAggregators/helpers.d.ts +30 -0
  37. package/dist/classes/DexAggregators/helpers.d.ts.map +1 -0
  38. package/dist/classes/DexAggregators/helpers.js +57 -0
  39. package/dist/classes/DexAggregators/helpers.js.map +1 -0
  40. package/dist/classes/DexAggregators/index.d.ts +2 -0
  41. package/dist/classes/DexAggregators/index.d.ts.map +1 -1
  42. package/dist/classes/DexAggregators/index.js +1 -0
  43. package/dist/classes/DexAggregators/index.js.map +1 -1
  44. package/dist/classes/ERC20.d.ts +3 -3
  45. package/dist/classes/ERC20.d.ts.map +1 -1
  46. package/dist/classes/ERC20.js +26 -10
  47. package/dist/classes/ERC20.js.map +1 -1
  48. package/dist/classes/FormatConverter.d.ts.map +1 -1
  49. package/dist/classes/FormatConverter.js +4 -1
  50. package/dist/classes/FormatConverter.js.map +1 -1
  51. package/dist/classes/Market.d.ts +40 -6
  52. package/dist/classes/Market.d.ts.map +1 -1
  53. package/dist/classes/Market.js +333 -106
  54. package/dist/classes/Market.js.map +1 -1
  55. package/dist/classes/NativeToken.d.ts +2 -2
  56. package/dist/classes/NativeToken.d.ts.map +1 -1
  57. package/dist/classes/NativeToken.js +21 -7
  58. package/dist/classes/NativeToken.js.map +1 -1
  59. package/dist/classes/OptimizerReader.d.ts +15 -7
  60. package/dist/classes/OptimizerReader.d.ts.map +1 -1
  61. package/dist/classes/OptimizerReader.js +108 -30
  62. package/dist/classes/OptimizerReader.js.map +1 -1
  63. package/dist/classes/OracleManager.d.ts.map +1 -1
  64. package/dist/classes/OracleManager.js +11 -4
  65. package/dist/classes/OracleManager.js.map +1 -1
  66. package/dist/classes/PositionManager.d.ts.map +1 -1
  67. package/dist/classes/PositionManager.js +1 -12
  68. package/dist/classes/PositionManager.js.map +1 -1
  69. package/dist/classes/ProtocolReader.d.ts +30 -9
  70. package/dist/classes/ProtocolReader.d.ts.map +1 -1
  71. package/dist/classes/ProtocolReader.js +158 -138
  72. package/dist/classes/ProtocolReader.js.map +1 -1
  73. package/dist/classes/Zapper.d.ts +5 -5
  74. package/dist/classes/Zapper.d.ts.map +1 -1
  75. package/dist/classes/Zapper.js +21 -18
  76. package/dist/classes/Zapper.js.map +1 -1
  77. package/dist/contracts/monad-mainnet.json +1 -1
  78. package/dist/feePolicy.d.ts +2 -0
  79. package/dist/feePolicy.d.ts.map +1 -1
  80. package/dist/feePolicy.js +19 -0
  81. package/dist/feePolicy.js.map +1 -1
  82. package/dist/format/borrow.d.ts.map +1 -1
  83. package/dist/format/borrow.js +7 -1
  84. package/dist/format/borrow.js.map +1 -1
  85. package/dist/format/health.d.ts +3 -2
  86. package/dist/format/health.d.ts.map +1 -1
  87. package/dist/format/health.js +10 -9
  88. package/dist/format/health.js.map +1 -1
  89. package/dist/format/leverage.js +5 -4
  90. package/dist/format/leverage.js.map +1 -1
  91. package/dist/helpers.d.ts +14 -11
  92. package/dist/helpers.d.ts.map +1 -1
  93. package/dist/helpers.js +171 -52
  94. package/dist/helpers.js.map +1 -1
  95. package/dist/integrations/merkl.d.ts +2 -1
  96. package/dist/integrations/merkl.d.ts.map +1 -1
  97. package/dist/integrations/merkl.js +199 -4
  98. package/dist/integrations/merkl.js.map +1 -1
  99. package/dist/integrations/snapshot.d.ts +28 -24
  100. package/dist/integrations/snapshot.d.ts.map +1 -1
  101. package/dist/integrations/snapshot.js +106 -42
  102. package/dist/integrations/snapshot.js.map +1 -1
  103. package/dist/retry-provider.d.ts +6 -0
  104. package/dist/retry-provider.d.ts.map +1 -1
  105. package/dist/retry-provider.js +103 -11
  106. package/dist/retry-provider.js.map +1 -1
  107. package/dist/setup.d.ts +8 -6
  108. package/dist/setup.d.ts.map +1 -1
  109. package/dist/setup.js +125 -45
  110. package/dist/setup.js.map +1 -1
  111. package/dist/validation.d.ts +1 -1
  112. package/dist/validation.d.ts.map +1 -1
  113. package/dist/validation.js +13 -7
  114. package/dist/validation.js.map +1 -1
  115. package/package.json +6 -3
@@ -19,6 +19,7 @@ const FormatConverter_1 = __importDefault(require("./FormatConverter"));
19
19
  const EXCLUDED_ZAP_SYMBOLS = new Set([
20
20
  'eBTC', 'earnAUSD', 'vUSD', 'syzUSD', 'ezETH', 'YZM', 'wsrUSD', 'sAUSD',
21
21
  ]);
22
+ const EXECUTION_DEBT_BUFFER_TIME = 100n;
22
23
  /**
23
24
  * Leverage operation buffers — centralized for tuning.
24
25
  * Calibrated for fresh-state operation via getLeverageSnapshot under
@@ -113,7 +114,7 @@ exports.LEVERAGE = {
113
114
  * for that amplification (see leverageDown). Bump when aggregator fees
114
115
  * are enabled to keep dust prevention reliable. */
115
116
  DELEVERAGE_OVERHEAD_BPS: 20n,
116
- /** BPS buffer on virtualConvertToShares for leverage + collateral cap.
117
+ /** BPS buffer on expected-share calculations for zap/leverage paths.
117
118
  * Covers exchange rate drift from interest accrual since cache load. */
118
119
  SHARES_BUFFER_BPS: 2n,
119
120
  /** Per-leverage-unit BPS buffer for `checkSlippage` on vault + native-vault
@@ -133,6 +134,24 @@ exports.LEVERAGE = {
133
134
  * and tx-time state, not between two independent feeds. */
134
135
  LEVERAGE_UP_VAULT_DRIFT_BPS: 30n,
135
136
  };
137
+ const USER_CACHE_FIELDS = [
138
+ "userAssetBalance",
139
+ "userShareBalance",
140
+ "userUnderlyingBalance",
141
+ "userCollateral",
142
+ "userDebt",
143
+ "liquidationPrice",
144
+ ];
145
+ function createUserCacheFreshness(value) {
146
+ return {
147
+ userAssetBalance: value,
148
+ userShareBalance: value,
149
+ userUnderlyingBalance: value,
150
+ userCollateral: value,
151
+ userDebt: value,
152
+ liquidationPrice: value,
153
+ };
154
+ }
136
155
  class CToken extends Calldata_1.Calldata {
137
156
  provider;
138
157
  address;
@@ -148,13 +167,14 @@ class CToken extends Calldata_1.Calldata {
148
167
  nativeApy = (0, decimal_js_1.default)(0);
149
168
  incentiveSupplyApy = (0, decimal_js_1.default)(0);
150
169
  incentiveBorrowApy = (0, decimal_js_1.default)(0);
170
+ userCacheFreshness;
151
171
  get signer() { return this.market.signer; }
152
172
  get account() { return this.market.account; }
153
173
  constructor(provider, address, cache, market) {
154
174
  super();
155
175
  this.provider = provider;
156
176
  this.address = address;
157
- this.contract = (0, helpers_1.contractSetup)(provider, address, BaseCToken_json_1.default);
177
+ this.contract = (0, helpers_1.contractSetup)(this.provider, address, BaseCToken_json_1.default);
158
178
  this.cache = cache;
159
179
  this.market = market;
160
180
  const chainSettings = this.currentChainConfig;
@@ -179,6 +199,31 @@ class CToken extends Calldata_1.Calldata {
179
199
  this.leverageTypes.push('simple');
180
200
  this.zapTypes.push('simple');
181
201
  }
202
+ getUserCacheFreshness() {
203
+ if (this.userCacheFreshness == undefined) {
204
+ this.userCacheFreshness = createUserCacheFreshness(true);
205
+ }
206
+ return this.userCacheFreshness;
207
+ }
208
+ markUserCacheFresh(fields = USER_CACHE_FIELDS) {
209
+ const freshness = this.getUserCacheFreshness();
210
+ for (const field of fields) {
211
+ freshness[field] = true;
212
+ }
213
+ }
214
+ invalidateUserCache(fields = USER_CACHE_FIELDS) {
215
+ const freshness = this.getUserCacheFreshness();
216
+ for (const field of fields) {
217
+ freshness[field] = false;
218
+ }
219
+ }
220
+ readFreshUserCache(field, accessLabel) {
221
+ if (!this.getUserCacheFreshness()[field]) {
222
+ throw new Error(`Token-level user data is stale for ${this.address} after a summary-only refresh on market ${this.market.address}. ` +
223
+ `Call market.reloadUserData(account) or Market.reloadUserMarkets(...) before ${accessLabel}.`);
224
+ }
225
+ return this.cache[field];
226
+ }
182
227
  get setup() { return this.market.setup; }
183
228
  get currentChain() { return this.setup.chain; }
184
229
  get currentChainConfig() { return (0, helpers_1.getChainConfig)(this.currentChain); }
@@ -189,6 +234,49 @@ class CToken extends Calldata_1.Calldata {
189
234
  getWriteContract() {
190
235
  return (0, helpers_1.contractSetup)(this.requireSigner(), this.address, BaseCToken_json_1.default);
191
236
  }
237
+ getZapType(zap) {
238
+ return typeof zap === "object" ? zap.type : zap;
239
+ }
240
+ isZapInstruction(zap) {
241
+ return this.getZapType(zap) !== "none";
242
+ }
243
+ async getZapInputDecimals(zap) {
244
+ const zapType = this.getZapType(zap);
245
+ switch (zapType) {
246
+ case "none":
247
+ return this.asset.decimals;
248
+ case "native-vault":
249
+ case "native-simple":
250
+ return 18n;
251
+ case "vault": {
252
+ const vaultAsset = await this.getVaultAsset(true);
253
+ return vaultAsset.decimals ?? await vaultAsset.contract.decimals();
254
+ }
255
+ case "simple":
256
+ if (typeof zap !== "object") {
257
+ return this.asset.decimals;
258
+ }
259
+ if (zap.inputToken.toLowerCase() === helpers_1.NATIVE_ADDRESS.toLowerCase()) {
260
+ return 18n;
261
+ }
262
+ const inputErc20 = new ERC20_1.ERC20(this.provider, zap.inputToken, undefined, undefined, this.signer);
263
+ return inputErc20.decimals ?? await inputErc20.contract.decimals();
264
+ }
265
+ }
266
+ async getZapAssetAmount(amount, zap) {
267
+ return FormatConverter_1.default.decimalToBigInt(amount, await this.getZapInputDecimals(zap));
268
+ }
269
+ async assertVaultLeverageBorrowAssetSupported(borrow, type) {
270
+ const expectedAsset = type === "native-vault"
271
+ ? this.currentChainConfig.wrapped_native
272
+ : await this.getVaultAsset(false);
273
+ const actualAsset = borrow.asset.address;
274
+ if (actualAsset.toLowerCase() === expectedAsset.toLowerCase()) {
275
+ return;
276
+ }
277
+ throw new Error(`${type} leverage requires borrow asset ${expectedAsset}, received ${actualAsset}. ` +
278
+ `Use simple leverage for cross-asset borrow routes.`);
279
+ }
192
280
  get adapters() { return this.cache.adapters; }
193
281
  get borrowPaused() { return this.cache.borrowPaused; }
194
282
  get collateralizationPaused() { return this.cache.collateralizationPaused; }
@@ -213,17 +301,30 @@ class CToken extends Calldata_1.Calldata {
213
301
  get totalAssets() { return this.cache.totalAssets; }
214
302
  get totalSupply() { return this.cache.totalSupply; }
215
303
  get liquidationPrice() {
216
- if (this.cache.liquidationPrice == helpers_1.UINT256_MAX)
304
+ const liquidationPrice = this.readFreshUserCache("liquidationPrice", "reading token liquidationPrice");
305
+ if (liquidationPrice == helpers_1.UINT256_MAX)
217
306
  return null;
218
- return (0, helpers_1.toDecimal)(this.cache.liquidationPrice, 18n);
307
+ return (0, helpers_1.toDecimal)(liquidationPrice, 18n);
219
308
  }
220
309
  get irmTargetRate() { return (0, decimal_js_1.default)(this.cache.irmTargetRate).div(helpers_1.WAD); }
221
310
  get irmMaxRate() { return (0, decimal_js_1.default)(this.cache.irmMaxRate).div(helpers_1.WAD); }
222
311
  get irmTargetUtilization() { return (0, decimal_js_1.default)(this.cache.irmTargetUtilization).div(helpers_1.WAD); }
223
312
  get interestFee() { return (0, decimal_js_1.default)(this.cache.interestFee).div(helpers_1.BPS); }
224
313
  virtualConvertToAssets(shares) {
314
+ if (this.totalSupply === 0n || this.totalAssets === 0n) {
315
+ return shares;
316
+ }
225
317
  return (shares * this.totalAssets) / this.totalSupply;
226
318
  }
319
+ formatAssets(assets) {
320
+ return FormatConverter_1.default.bigIntToDecimal(assets, this.asset.decimals);
321
+ }
322
+ formatShares(shares) {
323
+ return FormatConverter_1.default.bigIntToDecimal(shares, this.decimals);
324
+ }
325
+ formatSharesAsAssets(shares) {
326
+ return this.formatAssets(this.virtualConvertToAssets(shares));
327
+ }
227
328
  /**
228
329
  * Convert assets to shares using cached totalSupply/totalAssets.
229
330
  * @param bufferBps Optional downward buffer in BPS to account for
@@ -231,19 +332,35 @@ class CToken extends Calldata_1.Calldata {
231
332
  * Matches the buffer pattern in async convertToShares().
232
333
  */
233
334
  virtualConvertToShares(assets, bufferBps = 0n) {
234
- const shares = (assets * this.totalSupply) / this.totalAssets;
335
+ const shares = this.totalSupply === 0n || this.totalAssets === 0n
336
+ ? assets
337
+ : (assets * this.totalSupply) / this.totalAssets;
235
338
  return bufferBps > 0n ? shares * (10000n - bufferBps) / 10000n : shares;
236
339
  }
237
- getLeverage() {
238
- if (this.getUserCollateral(true).equals(0)) {
239
- return null;
340
+ getMarketLeverageState() {
341
+ const currentCollateralInUsd = this.market.userCollateral;
342
+ const currentDebt = this.market.userDebt;
343
+ const equity = currentCollateralInUsd.sub(currentDebt);
344
+ if (currentCollateralInUsd.lte(0) || equity.lte(0)) {
345
+ return {
346
+ currentCollateralInUsd,
347
+ currentDebt,
348
+ currentLeverage: null,
349
+ };
240
350
  }
241
- const leverage = this.getUserCollateral(true).div(this.getUserCollateral(true).sub(this.market.userDebt));
242
- return leverage.eq(1) ? null : leverage;
351
+ const currentLeverage = currentCollateralInUsd.div(equity);
352
+ return {
353
+ currentCollateralInUsd,
354
+ currentDebt,
355
+ currentLeverage: currentLeverage.eq(1) ? null : currentLeverage,
356
+ };
357
+ }
358
+ getLeverage() {
359
+ return this.getMarketLeverageState().currentLeverage;
243
360
  }
244
361
  getRemainingCollateral(formatted = true) {
245
362
  const diff = this.cache.collateralCap - this.cache.collateral;
246
- return formatted ? this.convertTokensToUsd(diff) : diff;
363
+ return formatted ? this.convertSharesToUsdSync(diff) : diff;
247
364
  }
248
365
  getRemainingDebt(formatted = true) {
249
366
  const diff = this.cache.debtCap - this.cache.debt;
@@ -283,36 +400,52 @@ class CToken extends Calldata_1.Calldata {
283
400
  return inBPS ? (0, decimal_js_1.default)(this.cache.closeFactorMax).div(helpers_1.BPS) : this.cache.closeFactorMax;
284
401
  }
285
402
  getUserShareBalance(inUSD) {
286
- return inUSD ? this.convertTokensToUsd(this.cache.userShareBalance, false) : FormatConverter_1.default.bigIntToDecimal(this.cache.userShareBalance, this.decimals);
403
+ const userShareBalance = this.readFreshUserCache("userShareBalance", "reading token user share balance");
404
+ return inUSD ? this.convertTokensToUsd(userShareBalance, false) : this.formatShares(userShareBalance);
405
+ }
406
+ getUserShareBalanceAssets() {
407
+ const userShareBalance = this.readFreshUserCache("userShareBalance", "reading token user share balance as assets");
408
+ return this.formatSharesAsAssets(userShareBalance);
287
409
  }
288
410
  getUserAssetBalance(inUSD) {
289
- return inUSD ? this.convertTokensToUsd(this.cache.userAssetBalance) : FormatConverter_1.default.bigIntToDecimal(this.cache.userAssetBalance, this.asset.decimals);
411
+ const userAssetBalance = this.readFreshUserCache("userAssetBalance", "reading token user asset balance");
412
+ return inUSD ? this.convertTokensToUsd(userAssetBalance) : this.formatAssets(userAssetBalance);
290
413
  }
291
414
  getUserUnderlyingBalance(inUSD) {
292
- return inUSD ? this.convertTokensToUsd(this.cache.userUnderlyingBalance) : FormatConverter_1.default.bigIntToDecimal(this.cache.userUnderlyingBalance, this.decimals);
415
+ const userUnderlyingBalance = this.readFreshUserCache("userUnderlyingBalance", "reading token user underlying balance");
416
+ return inUSD ? this.convertTokensToUsd(userUnderlyingBalance) : this.formatAssets(userUnderlyingBalance);
293
417
  }
294
418
  getCollateralCap(inUSD) {
295
- return inUSD ? this.convertTokensToUsd(this.cache.collateralCap) : this.cache.collateralCap;
419
+ return inUSD ? this.convertSharesToUsdSync(this.cache.collateralCap) : this.cache.collateralCap;
296
420
  }
297
421
  getDebtCap(inUSD) {
298
422
  return inUSD ? this.convertTokensToUsd(this.cache.debtCap) : this.cache.debtCap;
299
423
  }
300
424
  getCollateral(inUSD) {
301
- return inUSD ? this.convertTokensToUsd(this.cache.collateral) : this.cache.collateral;
425
+ return inUSD ? this.convertSharesToUsdSync(this.cache.collateral) : this.cache.collateral;
302
426
  }
303
427
  getDebt(inUSD) {
304
428
  return inUSD ? this.convertTokensToUsd(this.cache.debt) : this.cache.debt;
305
429
  }
306
430
  getUserCollateral(inUSD) {
307
- return inUSD ? this.convertTokensToUsd(this.cache.userCollateral, false) : FormatConverter_1.default.bigIntToDecimal(this.cache.userCollateral, this.decimals);
431
+ const userCollateral = this.getUserCollateralShares();
432
+ return inUSD ? this.convertTokensToUsd(userCollateral, false) : this.formatShares(userCollateral);
433
+ }
434
+ getUserCollateralShares() {
435
+ return this.readFreshUserCache("userCollateral", "reading token user collateral shares");
436
+ }
437
+ getUserCollateralAssets() {
438
+ return this.formatSharesAsAssets(this.getUserCollateralShares());
308
439
  }
309
440
  async fetchUserCollateral(formatted = false) {
310
441
  const collateral = await this.contract.collateralPosted(this.getAccountOrThrow());
311
442
  this.cache.userCollateral = collateral;
443
+ this.markUserCacheFresh(["userCollateral"]);
312
444
  return formatted ? (0, helpers_1.toDecimal)(collateral, this.decimals) : collateral;
313
445
  }
314
446
  getUserDebt(inUSD) {
315
- return inUSD ? this.convertTokensToUsd(this.cache.userDebt) : FormatConverter_1.default.bigIntToDecimal(this.cache.userDebt, this.asset.decimals);
447
+ const userDebt = this.readFreshUserCache("userDebt", "reading token user debt");
448
+ return inUSD ? this.convertTokensToUsd(userDebt) : FormatConverter_1.default.bigIntToDecimal(userDebt, this.asset.decimals);
316
449
  }
317
450
  earnChange(amount, rateType) {
318
451
  const rate = this.getApy(false);
@@ -336,6 +469,16 @@ class CToken extends Calldata_1.Calldata {
336
469
  async getVaultAsset(asErc20) {
337
470
  return asErc20 ? await this.getUnderlyingVault().fetchAsset(true) : await this.getUnderlyingVault().fetchAsset(false);
338
471
  }
472
+ async getExpectedVaultShares(assets) {
473
+ const vault = this.getUnderlyingVault();
474
+ const vaultSharesRaw = await vault.previewDeposit(assets);
475
+ // Vault/native-vault flows mint vault shares first, then convert those
476
+ // into Curvance shares. Buffer the inner preview so exchange-rate drift
477
+ // between quote time and inclusion cannot trip the outer expectedShares
478
+ // check on otherwise-valid deposits/leverage/zaps.
479
+ const vaultShares = vaultSharesRaw * (10000n - exports.LEVERAGE.SHARES_BUFFER_BPS) / 10000n;
480
+ return this.convertToShares(vaultShares);
481
+ }
339
482
  getAsset(asErc20) {
340
483
  return asErc20
341
484
  ? new ERC20_1.ERC20(this.provider, this.cache.asset.address, this.cache.asset, this.setup.contracts.OracleManager, this.signer)
@@ -376,11 +519,11 @@ class CToken extends Calldata_1.Calldata {
376
519
  }
377
520
  getTotalCollateral(inUSD = true) {
378
521
  const totalCollateral = this.cache.collateral;
379
- return inUSD ? this.convertTokensToUsd(totalCollateral) : totalCollateral;
522
+ return inUSD ? this.convertSharesToUsdSync(totalCollateral) : totalCollateral;
380
523
  }
381
524
  async fetchTotalCollateral(inUSD = true) {
382
525
  const totalCollateral = await this.contract.marketCollateralPosted();
383
- return inUSD ? this.fetchConvertTokensToUsd(totalCollateral) : totalCollateral;
526
+ return inUSD ? this.fetchConvertSharesToUsd(totalCollateral) : totalCollateral;
384
527
  }
385
528
  getPositionManager(type) {
386
529
  const signer = this.requireSigner();
@@ -399,29 +542,24 @@ class CToken extends Calldata_1.Calldata {
399
542
  return new Zapper_1.Zapper(zap_contract, signer, type, this.setup);
400
543
  }
401
544
  async isZapAssetApproved(instructions, amount) {
402
- if (instructions == 'none' || typeof instructions != 'object') {
545
+ if (instructions == 'none') {
403
546
  return true;
404
547
  }
405
- if (instructions.inputToken.toLowerCase() === helpers_1.NATIVE_ADDRESS.toLowerCase()) {
548
+ const approvalTarget = await this.resolveZapApprovalTarget(instructions);
549
+ if (approvalTarget == null) {
406
550
  return true;
407
551
  }
408
- const signer = this.requireSigner();
409
- const asset = new ERC20_1.ERC20(this.provider, instructions.inputToken, undefined, undefined, signer);
410
- const plugin = this.getPluginAddress(instructions.type, 'zapper');
411
- const allowance = await asset.allowance(signer.address, plugin);
412
- return allowance >= amount;
552
+ return this.hasTokenApproval(approvalTarget, amount);
413
553
  }
414
554
  async approveZapAsset(instructions, amount) {
415
- if (instructions == 'none' || typeof instructions != 'object') {
555
+ if (instructions == 'none') {
416
556
  throw new Error("Plugin does not have an associated contract");
417
557
  }
418
- if (instructions.inputToken.toLowerCase() === helpers_1.NATIVE_ADDRESS.toLowerCase()) {
558
+ const approvalTarget = await this.resolveZapApprovalTarget(instructions);
559
+ if (approvalTarget == null) {
419
560
  return;
420
561
  }
421
- const signer = this.requireSigner();
422
- const asset = new ERC20_1.ERC20(this.provider, instructions.inputToken, undefined, undefined, signer);
423
- const plugin = this.getPluginAddress(instructions.type, 'zapper');
424
- return asset.approve(plugin, amount);
562
+ return approvalTarget.token.approve(approvalTarget.spender, amount);
425
563
  }
426
564
  async isPluginApproved(plugin, type) {
427
565
  if (plugin == 'none') {
@@ -561,8 +699,22 @@ class CToken extends Calldata_1.Calldata {
561
699
  receiver ??= signer.address;
562
700
  owner ??= signer.address;
563
701
  const shares = this.convertTokenInputToShares(amount);
564
- const calldata = this.getCallData("redeemCollateral", [shares, receiver, owner]);
565
- return this.oracleRoute(calldata);
702
+ const signerAddress = signer.address;
703
+ let method = "redeemCollateral";
704
+ if (owner.toLowerCase() !== signerAddress.toLowerCase()) {
705
+ const isDelegated = await this.contract.isDelegate(owner, signerAddress);
706
+ if (isDelegated) {
707
+ method = "redeemCollateralFor";
708
+ }
709
+ else {
710
+ const allowance = await this.contract.allowance(owner, signerAddress);
711
+ if (allowance < shares) {
712
+ throw new Error(`Please approve ${this.symbol} shares for ${signerAddress} or delegate ${signerAddress} before redeeming collateral for ${owner}.`);
713
+ }
714
+ }
715
+ }
716
+ const calldata = this.getCallData(method, [shares, receiver, owner]);
717
+ return this.oracleRoute(calldata, {}, owner);
566
718
  }
567
719
  async postCollateral(amount) {
568
720
  const signer = this.requireSigner();
@@ -571,6 +723,9 @@ class CToken extends Calldata_1.Calldata {
571
723
  const collateral = await this.fetchUserCollateral();
572
724
  const available_shares = balance - collateral;
573
725
  const max_shares = available_shares < shares ? available_shares : shares;
726
+ if (max_shares <= 0n) {
727
+ throw new Error("No cToken shares available to post as collateral.");
728
+ }
574
729
  const calldata = this.getCallData("postCollateral", [max_shares]);
575
730
  const tx = await this.oracleRoute(calldata);
576
731
  // Reload collateral state after execution
@@ -607,56 +762,56 @@ class CToken extends Calldata_1.Calldata {
607
762
  }
608
763
  return asset.balanceOf(signer.address, false);
609
764
  }
610
- // TODO: Hack to remove
611
765
  async ensureUnderlyingAmount(amount, zap) {
612
766
  const balance = await this.getZapBalance(zap);
613
- const isZapping = typeof zap === 'object' && zap.type !== 'none';
614
- // Use the zap input token's decimals when zapping, otherwise the deposit token's decimals
615
- let decimals = this.asset.decimals;
616
- if (isZapping && zap.inputToken) {
617
- if (zap.inputToken.toLowerCase() === helpers_1.NATIVE_ADDRESS.toLowerCase()) {
618
- decimals = 18n;
619
- }
620
- else {
621
- const inputErc20 = new ERC20_1.ERC20(this.provider, zap.inputToken, undefined, undefined, this.signer);
622
- decimals = inputErc20.decimals ?? await inputErc20.contract.decimals();
623
- }
624
- }
767
+ const decimals = await this.getZapInputDecimals(zap);
625
768
  const assets = FormatConverter_1.default.decimalToBigInt(amount, decimals);
626
769
  if (assets > balance) {
627
- console.warn('[WARNING] Detected higher deposit amount then underlying balance, changing to the underlying balance. Diff: ', {
628
- balance: balance,
629
- formatted: FormatConverter_1.default.bigIntToDecimal(balance, decimals),
630
- attempt: {
631
- raw: assets,
632
- formatted: amount
633
- },
634
- });
635
- return FormatConverter_1.default.bigIntToDecimal(balance, decimals);
770
+ const formattedBalance = FormatConverter_1.default.bigIntToDecimal(balance, decimals);
771
+ throw new Error(`Insufficient balance: requested ${amount.toString()}, available ${formattedBalance.toString()}.`);
636
772
  }
637
773
  return amount;
638
774
  }
639
- async removeCollateral(amount, removeAll = false) {
640
- const current_shares = await this.fetchUserCollateral();
641
- let max_shares;
642
- if (removeAll) {
643
- max_shares = current_shares;
775
+ getExecutionDebtBufferTime() {
776
+ return this.market.userDebt.greaterThan(0) ? EXECUTION_DEBT_BUFFER_TIME : 0n;
777
+ }
778
+ async resolveCollateralRemovalShares(amount) {
779
+ const max_removable_shares = await this.maxRemovableCollateral(true, this.getExecutionDebtBufferTime());
780
+ const requested_shares = this.convertTokenInputToShares(amount);
781
+ let shares = max_removable_shares < requested_shares ? max_removable_shares : requested_shares;
782
+ // If within 0.1% of the safe removable collateral, remove it all to avoid dust.
783
+ const threshold = max_removable_shares / 1000n || 10n;
784
+ if (max_removable_shares - shares <= threshold) {
785
+ shares = max_removable_shares;
644
786
  }
645
- else {
646
- const shares = this.convertTokenInputToShares(amount);
647
- max_shares = current_shares < shares ? current_shares : shares;
648
- // If within 0.1% of full collateral, remove everything to avoid dust
649
- const threshold = current_shares / 1000n || 10n;
650
- if (current_shares - max_shares <= threshold) {
651
- max_shares = current_shares;
652
- }
787
+ return shares;
788
+ }
789
+ async executeCollateralRemoval(shares) {
790
+ if (shares === 0n) {
791
+ throw new Error("No removable collateral available.");
653
792
  }
654
- const calldata = this.getCallData("removeCollateral", [max_shares]);
793
+ const calldata = this.getCallData("removeCollateral", [shares]);
655
794
  const tx = await this.oracleRoute(calldata);
656
795
  // Reload collateral state after execution
657
796
  await this.fetchUserCollateral();
658
797
  return tx;
659
798
  }
799
+ async maxRemovableCollateral(in_shares = false, bufferTime = 0n) {
800
+ if (in_shares) {
801
+ const breakdown = await this.maxRedemption(true, bufferTime, true);
802
+ return breakdown.max_collateral;
803
+ }
804
+ const breakdown = await this.maxRedemption(false, bufferTime, true);
805
+ return breakdown.max_collateral;
806
+ }
807
+ async removeCollateralExact(amount) {
808
+ const shares = await this.resolveCollateralRemovalShares(amount);
809
+ return this.executeCollateralRemoval(shares);
810
+ }
811
+ async removeMaxCollateral() {
812
+ const shares = await this.maxRemovableCollateral(true, this.getExecutionDebtBufferTime());
813
+ return this.executeCollateralRemoval(shares);
814
+ }
660
815
  convertTokenInputToShares(amount) {
661
816
  return this.virtualConvertToShares(FormatConverter_1.default.decimalToBigInt(amount, this.asset.decimals));
662
817
  }
@@ -677,7 +832,7 @@ class CToken extends Calldata_1.Calldata {
677
832
  async convertToAssets(shares) {
678
833
  return this.contract.convertToAssets(shares);
679
834
  }
680
- async convertToShares(assets, bufferBps = 2n) {
835
+ async convertToShares(assets, bufferBps = exports.LEVERAGE.SHARES_BUFFER_BPS) {
681
836
  const shares = await this.contract.convertToShares(assets);
682
837
  return bufferBps > 0n ? shares * (10000n - bufferBps) / 10000n : shares;
683
838
  }
@@ -711,7 +866,7 @@ class CToken extends Calldata_1.Calldata {
711
866
  interface: new NativeToken_1.NativeToken(this.currentChain, this.provider, this.setup.contracts.OracleManager, this.signer, this.account),
712
867
  type: 'native-vault'
713
868
  });
714
- tokens_exclude.push(helpers_1.EMPTY_ADDRESS, helpers_1.NATIVE_ADDRESS);
869
+ tokens_exclude.push(helpers_1.EMPTY_ADDRESS.toLowerCase(), helpers_1.NATIVE_ADDRESS.toLowerCase());
715
870
  }
716
871
  if (this.zapTypes.includes('native-simple')) {
717
872
  tokens.push({
@@ -719,7 +874,7 @@ class CToken extends Calldata_1.Calldata {
719
874
  type: 'native-simple'
720
875
  });
721
876
  if (!this.zapTypes.includes('native-vault')) {
722
- tokens_exclude.push(helpers_1.EMPTY_ADDRESS, helpers_1.NATIVE_ADDRESS);
877
+ tokens_exclude.push(helpers_1.EMPTY_ADDRESS.toLowerCase(), helpers_1.NATIVE_ADDRESS.toLowerCase());
723
878
  }
724
879
  }
725
880
  if (this.zapTypes.includes('vault')) {
@@ -730,7 +885,7 @@ class CToken extends Calldata_1.Calldata {
730
885
  });
731
886
  tokens_exclude.push(vault_asset.address.toLocaleLowerCase());
732
887
  }
733
- if (this.zapTypes.includes('simple')) {
888
+ if (this.zapTypes.includes('simple') && this.currentChainConfig.dexAgg.router !== helpers_1.EMPTY_ADDRESS) {
734
889
  let dexAggSearch = await this.currentChainConfig.dexAgg.getAvailableTokens(this.provider, search, this.account);
735
890
  tokens = tokens.concat(dexAggSearch.filter(token => !tokens_exclude.includes(token.interface.address.toLocaleLowerCase())));
736
891
  // Add native MON as a zap option for any token with a simple zapper
@@ -774,6 +929,7 @@ class CToken extends Calldata_1.Calldata {
774
929
  this.cache.assetPrice = snapshot.collateralAssetPrice;
775
930
  this.cache.sharePrice = snapshot.sharePrice;
776
931
  borrow.cache.assetPrice = snapshot.debtAssetPrice;
932
+ this.market.cache.user.collateral = snapshot.collateralUsd;
777
933
  this.market.cache.user.debt = snapshot.debtUsd;
778
934
  return snapshot;
779
935
  }
@@ -804,41 +960,116 @@ class CToken extends Calldata_1.Calldata {
804
960
  return slippage;
805
961
  return slippage + exports.LEVERAGE.LEVERAGE_UP_BUFFER_BPS;
806
962
  }
807
- previewLeverageUp(newLeverage, borrow, depositAmount) {
808
- const currentLeverage = this.getLeverage() ?? (0, decimal_js_1.default)(0);
809
- if (newLeverage.lte(currentLeverage)) {
963
+ assertSimpleLeverageSwapAssetsDiffer(borrow) {
964
+ if (borrow.asset.address.toLowerCase() === this.asset.address.toLowerCase()) {
965
+ throw new Error("Simple leverage requires distinct collateral and borrow assets.");
966
+ }
967
+ }
968
+ async assertLeverageBorrowCapacity(borrow, borrowAssets) {
969
+ if (borrowAssets === 0n) {
970
+ return;
971
+ }
972
+ const [assetsHeld, outstandingDebt] = await Promise.all([
973
+ borrow.fetchLiquidity(),
974
+ borrow.marketOutstandingDebt(),
975
+ ]);
976
+ const remainingDebtCap = borrow.cache.debtCap > outstandingDebt
977
+ ? borrow.cache.debtCap - outstandingDebt
978
+ : 0n;
979
+ const capacity = assetsHeld < remainingDebtCap ? assetsHeld : remainingDebtCap;
980
+ if (borrowAssets > capacity) {
981
+ throw new Error("Selected borrow token does not have enough remaining debt capacity or liquidity for this leverage operation.");
982
+ }
983
+ }
984
+ assertSelectedBorrowDebtCanDeleverage(borrow, selectedDebtAssets, requiredDebtReductionUsd) {
985
+ if (requiredDebtReductionUsd.lte(0)) {
986
+ return;
987
+ }
988
+ const price = borrow.getPrice(true);
989
+ if (!price.isFinite() || price.lte(0)) {
990
+ throw new Error("Selected borrow token has an invalid price for deleverage sizing.");
991
+ }
992
+ const requiredDebtAssets = FormatConverter_1.default.decimalToBigInt(requiredDebtReductionUsd.div(price), borrow.asset.decimals);
993
+ if (requiredDebtAssets > selectedDebtAssets) {
994
+ throw new Error("Selected borrow token debt is too small for the requested deleverage target.");
995
+ }
996
+ }
997
+ computePostDepositNaturalLeverage(currentCollateralInUsd, currentDebtInUsd, depositInUsd) {
998
+ if (currentDebtInUsd.lte(0))
999
+ return (0, decimal_js_1.default)(1);
1000
+ const collateralAfterDeposit = currentCollateralInUsd.add(depositInUsd);
1001
+ const equityAfterDeposit = collateralAfterDeposit.sub(currentDebtInUsd);
1002
+ if (equityAfterDeposit.lte(0))
1003
+ return (0, decimal_js_1.default)(1);
1004
+ return collateralAfterDeposit.div(equityAfterDeposit);
1005
+ }
1006
+ resolveLeverageUpPreview({ operation, targetLeverage, borrow, depositAssets = 0n, positionManagerType, }) {
1007
+ const leverageState = this.getMarketLeverageState();
1008
+ const currentLeverage = leverageState.currentLeverage ?? (0, decimal_js_1.default)(1);
1009
+ const currentCollateralInUsd = leverageState.currentCollateralInUsd;
1010
+ const depositInAssets = FormatConverter_1.default.bigIntToDecimal(depositAssets, this.asset.decimals);
1011
+ const depositInUsd = depositAssets > 0n
1012
+ ? this.convertTokensToUsd(depositAssets, true)
1013
+ : (0, decimal_js_1.default)(0);
1014
+ const currentDebt = leverageState.currentDebt;
1015
+ const effectiveCurrentLeverage = depositAssets > 0n
1016
+ ? this.computePostDepositNaturalLeverage(currentCollateralInUsd, currentDebt, depositInUsd)
1017
+ : currentLeverage;
1018
+ const cappedTargetLeverage = targetLeverage.gt(this.maxLeverage)
1019
+ ? this.maxLeverage
1020
+ : targetLeverage;
1021
+ const resolvedTargetLeverage = operation === 'deposit-and-leverage'
1022
+ ? decimal_js_1.default.max(cappedTargetLeverage, effectiveCurrentLeverage)
1023
+ : cappedTargetLeverage;
1024
+ if (operation === 'leverage-up' && resolvedTargetLeverage.lte(effectiveCurrentLeverage)) {
810
1025
  throw new Error("New leverage must be more than current leverage");
811
1026
  }
812
- if (newLeverage.gt(this.maxLeverage)) {
813
- newLeverage = this.maxLeverage;
1027
+ const collateralAfterDepositInUsd = currentCollateralInUsd.add(depositInUsd);
1028
+ const notional = collateralAfterDepositInUsd.sub(currentDebt);
1029
+ if (notional.lte(0)) {
1030
+ throw new Error("Position has no positive equity to leverage.");
814
1031
  }
815
- const collateralAvail = this.cache.userCollateral + (depositAmount ? depositAmount : BigInt(0));
816
- const collateralInUsd = this.convertTokensToUsd(collateralAvail, false);
817
- const currentDebt = this.market.userDebt;
818
- const notional = collateralInUsd.sub(currentDebt);
819
- const leverageFactor = newLeverage.sub(1);
820
1032
  const borrowPrice = borrow.getPrice(true);
821
- const rawDebtInUsd = notional.mul(newLeverage).sub(notional);
822
- const borrowAmount = rawDebtInUsd.sub(currentDebt).div(borrowPrice);
823
- // Fee preview: queried from the configured fee policy.
824
- const borrowAssets = FormatConverter_1.default.decimalToBigInt(borrowAmount, borrow.asset.decimals);
825
- const feeBps = this.setup.feePolicy.getFeeBps({
826
- operation: 'leverage-up',
827
- inputToken: borrow.asset.address,
828
- outputToken: this.asset.address,
829
- inputAmount: borrowAssets,
830
- currentLeverage,
831
- targetLeverage: newLeverage,
832
- });
1033
+ const rawDebtInUsd = notional.mul(resolvedTargetLeverage).sub(notional);
1034
+ const debtIncrease = decimal_js_1.default.max(rawDebtInUsd.sub(currentDebt), (0, decimal_js_1.default)(0));
1035
+ const borrowAmount = borrowPrice.gt(0)
1036
+ ? debtIncrease.div(borrowPrice)
1037
+ : (0, decimal_js_1.default)(0);
1038
+ const borrowAssets = debtIncrease.gt(0)
1039
+ ? FormatConverter_1.default.decimalToBigInt(borrowAmount, borrow.asset.decimals)
1040
+ : 0n;
1041
+ const feePolicyCurrentLeverage = operation === 'deposit-and-leverage'
1042
+ ? effectiveCurrentLeverage
1043
+ : currentLeverage;
1044
+ const hasSwapFee = positionManagerType !== 'vault' && positionManagerType !== 'native-vault';
1045
+ const feeBps = borrowAssets > 0n && hasSwapFee
1046
+ ? this.setup.feePolicy.getFeeBps({
1047
+ operation,
1048
+ inputToken: borrow.asset.address,
1049
+ outputToken: this.asset.address,
1050
+ inputAmount: borrowAssets,
1051
+ currentLeverage: feePolicyCurrentLeverage,
1052
+ targetLeverage: resolvedTargetLeverage,
1053
+ })
1054
+ : 0n;
833
1055
  const feeAssets = borrowAmount.mul((0, decimal_js_1.default)(Number(feeBps))).div((0, decimal_js_1.default)(10000));
834
1056
  const feeUsd = feeAssets.mul(borrowPrice);
835
- // Subtract fee from displayed collateral — the fee reduces swap
836
- // output, so the user receives less collateral than rawDebtInUsd.
837
- const newCollateralInUsd = notional.add(rawDebtInUsd).sub(feeUsd);
1057
+ const collateralIncreaseFromBorrow = decimal_js_1.default.max(debtIncrease.sub(feeUsd), (0, decimal_js_1.default)(0));
1058
+ const collateralIncrease = depositInUsd.add(collateralIncreaseFromBorrow);
1059
+ const collateralIncreaseInAssets = depositInAssets.add(this.convertUsdToTokens(collateralIncreaseFromBorrow, true));
1060
+ const newCollateralInUsd = currentCollateralInUsd.add(collateralIncrease);
838
1061
  return {
1062
+ currentLeverage,
1063
+ effectiveCurrentLeverage,
1064
+ targetLeverage: resolvedTargetLeverage,
839
1065
  borrowAmount,
1066
+ borrowAssets,
1067
+ debtIncrease,
1068
+ debtIncreaseInAssets: borrowAmount,
840
1069
  newDebt: rawDebtInUsd,
841
1070
  newDebtInAssets: borrow.convertUsdToTokens(rawDebtInUsd, true),
1071
+ collateralIncrease,
1072
+ collateralIncreaseInAssets,
842
1073
  newCollateral: newCollateralInUsd,
843
1074
  newCollateralInAssets: this.convertUsdToTokens(newCollateralInUsd, true),
844
1075
  feeBps,
@@ -846,6 +1077,26 @@ class CToken extends Calldata_1.Calldata {
846
1077
  feeUsd,
847
1078
  };
848
1079
  }
1080
+ previewDepositAndLeverage(newLeverage, borrow, depositAmount, positionManagerType) {
1081
+ return this.resolveLeverageUpPreview({
1082
+ operation: 'deposit-and-leverage',
1083
+ targetLeverage: newLeverage,
1084
+ borrow,
1085
+ depositAssets: depositAmount,
1086
+ positionManagerType,
1087
+ });
1088
+ }
1089
+ previewLeverageUp(newLeverage, borrow, depositAmount, positionManagerType) {
1090
+ if ((depositAmount ?? 0n) > 0n) {
1091
+ return this.previewDepositAndLeverage(newLeverage, borrow, depositAmount, positionManagerType);
1092
+ }
1093
+ return this.resolveLeverageUpPreview({
1094
+ operation: 'leverage-up',
1095
+ targetLeverage: newLeverage,
1096
+ borrow,
1097
+ positionManagerType,
1098
+ });
1099
+ }
849
1100
  previewLeverageDown(newLeverage, currentLeverage, borrow) {
850
1101
  if (newLeverage.gte(currentLeverage)) {
851
1102
  throw new Error("New leverage must be less than current leverage");
@@ -853,10 +1104,13 @@ class CToken extends Calldata_1.Calldata {
853
1104
  if (newLeverage.lt((0, decimal_js_1.default)(1))) {
854
1105
  throw new Error("New leverage must be at least 1");
855
1106
  }
856
- const collateralAvail = this.cache.userCollateral;
857
- const collateralInUsd = this.convertTokensToUsd(collateralAvail, false);
858
- const currentDebt = this.market.userDebt;
1107
+ const leverageState = this.getMarketLeverageState();
1108
+ const collateralInUsd = leverageState.currentCollateralInUsd;
1109
+ const currentDebt = leverageState.currentDebt;
859
1110
  const equity = collateralInUsd.sub(currentDebt);
1111
+ if (equity.lte(0)) {
1112
+ throw new Error("Position has no positive equity to deleverage.");
1113
+ }
860
1114
  const targetCollateralUsd = equity.mul(newLeverage);
861
1115
  const newDebtUsd = targetCollateralUsd.sub(equity);
862
1116
  const collateralAssetReductionUsd = collateralInUsd.sub(targetCollateralUsd);
@@ -897,22 +1151,30 @@ class CToken extends Calldata_1.Calldata {
897
1151
  async leverageUp(borrow, newLeverage, type, slippage_ = (0, decimal_js_1.default)(0.05), simulate = false) {
898
1152
  try {
899
1153
  this.requireSigner();
900
- const slippage = this._leverageUpSlippage(FormatConverter_1.default.percentageToBps(slippage_), newLeverage);
901
1154
  const manager = this.getPositionManager(type);
1155
+ if (type === 'vault' || type === 'native-vault') {
1156
+ await this.assertVaultLeverageBorrowAssetSupported(borrow, type);
1157
+ }
1158
+ else if (type === 'simple') {
1159
+ this.assertSimpleLeverageSwapAssetsDiffer(borrow);
1160
+ }
902
1161
  let calldata;
903
1162
  await this._getLeverageSnapshot(borrow);
904
- const { borrowAmount } = this.previewLeverageUp(newLeverage, borrow);
1163
+ const preview = this.previewLeverageUp(newLeverage, borrow, undefined, type);
1164
+ const slippage = this._leverageUpSlippage(FormatConverter_1.default.percentageToBps(slippage_), preview.targetLeverage);
1165
+ const { borrowAmount, borrowAssets, feeBps, targetLeverage } = preview;
1166
+ if (borrowAssets === 0n) {
1167
+ if (simulate) {
1168
+ return {
1169
+ success: false,
1170
+ error: "Target leverage must exceed the current leverage enough to borrow more.",
1171
+ };
1172
+ }
1173
+ throw new Error("Target leverage must exceed the current leverage enough to borrow more.");
1174
+ }
1175
+ await this.assertLeverageBorrowCapacity(borrow, borrowAssets);
905
1176
  switch (type) {
906
1177
  case 'simple': {
907
- const borrowAssets = FormatConverter_1.default.decimalToBigInt(borrowAmount, borrow.asset.decimals);
908
- const feeBps = this.setup.feePolicy.getFeeBps({
909
- operation: 'leverage-up',
910
- inputToken: borrow.asset.address,
911
- outputToken: this.asset.address,
912
- inputAmount: borrowAssets,
913
- currentLeverage: this.getLeverage() ?? (0, decimal_js_1.default)(1),
914
- targetLeverage: newLeverage,
915
- });
916
1178
  const feeReceiver = feeBps > 0n ? this.setup.feePolicy.feeReceiver : undefined;
917
1179
  const { action, quote } = await this.currentChainConfig.dexAgg.quoteAction(manager.address, borrow.asset.address, this.asset.address, borrowAssets, slippage, feeBps, feeReceiver);
918
1180
  // Fee-aware slippage expansion now lives inside KyberSwap.quoteAction
@@ -923,10 +1185,10 @@ class CToken extends Calldata_1.Calldata {
923
1185
  // as equity loss amplified by (L-1) — same pattern as
924
1186
  // deleverage. Expand the contract-level tolerance to absorb it.
925
1187
  // See `amplifyContractSlippage` in helpers.ts for rationale.
926
- const contractSlippage = (0, helpers_1.amplifyContractSlippage)(slippage, newLeverage.sub(1), feeBps);
1188
+ const contractSlippage = (0, helpers_1.amplifyContractSlippage)(slippage, targetLeverage.sub(1), feeBps);
927
1189
  calldata = manager.getLeverageCalldata({
928
1190
  borrowableCToken: borrow.address,
929
- borrowAssets: FormatConverter_1.default.decimalToBigInt(borrowAmount, borrow.asset.decimals),
1191
+ borrowAssets: borrowAssets,
930
1192
  cToken: this.address,
931
1193
  expectedShares: this.virtualConvertToShares(BigInt(quote.min_out), exports.LEVERAGE.SHARES_BUFFER_BPS),
932
1194
  swapAction: action,
@@ -941,10 +1203,10 @@ class CToken extends Calldata_1.Calldata {
941
1203
  // collateral drift between the vault's fundamental mint
942
1204
  // rate at tx time and the stored oracle price that
943
1205
  // `checkSlippage` reads. See LEVERAGE_UP_VAULT_DRIFT_BPS.
944
- const contractSlippage = (0, helpers_1.amplifyContractSlippage)(slippage, newLeverage.sub(1), exports.LEVERAGE.LEVERAGE_UP_VAULT_DRIFT_BPS);
1206
+ const contractSlippage = (0, helpers_1.amplifyContractSlippage)(slippage, targetLeverage.sub(1), exports.LEVERAGE.LEVERAGE_UP_VAULT_DRIFT_BPS);
945
1207
  calldata = manager.getLeverageCalldata({
946
1208
  borrowableCToken: borrow.address,
947
- borrowAssets: FormatConverter_1.default.decimalToBigInt(borrowAmount, borrow.asset.decimals),
1209
+ borrowAssets: borrowAssets,
948
1210
  cToken: this.address,
949
1211
  expectedShares: await PositionManager_1.PositionManager.getVaultExpectedShares(this, borrow, borrowAmount),
950
1212
  swapAction: PositionManager_1.PositionManager.emptySwapAction(),
@@ -959,7 +1221,6 @@ class CToken extends Calldata_1.Calldata {
959
1221
  }
960
1222
  if (simulate)
961
1223
  return this.simulateOracleRoute(calldata, { to: manager.address });
962
- await this._checkPositionManagerApproval(manager);
963
1224
  return this.oracleRoute(calldata, { to: manager.address });
964
1225
  }
965
1226
  catch (error) {
@@ -979,10 +1240,15 @@ class CToken extends Calldata_1.Calldata {
979
1240
  const config = this.currentChainConfig;
980
1241
  const slippage = (0, helpers_1.toBps)(slippage_);
981
1242
  const manager = this.getPositionManager(type);
1243
+ if (type === 'simple') {
1244
+ this.assertSimpleLeverageSwapAssetsDiffer(borrowToken);
1245
+ }
982
1246
  let calldata;
983
1247
  const snapshot = await this._getLeverageSnapshot(borrowToken);
984
- const { collateralAssetReduction } = this.previewLeverageDown(newLeverage, currentLeverage);
1248
+ const preview = this.previewLeverageDown(newLeverage, currentLeverage);
1249
+ const { collateralAssetReduction } = preview;
985
1250
  const isFullDeleverage = newLeverage.equals(1);
1251
+ const maxTokenCollateral = this.virtualConvertToAssets(this.readFreshUserCache("userCollateral", "executing leverage down"));
986
1252
  switch (type) {
987
1253
  case 'simple': {
988
1254
  let swapCollateral = collateralAssetReduction;
@@ -1025,18 +1291,31 @@ class CToken extends Calldata_1.Calldata {
1025
1291
  // fee+overhead magnitudes (< 100 bps combined).
1026
1292
  const overheadBps = exports.LEVERAGE.DELEVERAGE_OVERHEAD_BPS + feeBps;
1027
1293
  swapCollateral = debtInCollateral * (10000n + overheadBps) / 10000n;
1028
- const maxCollateral = this.virtualConvertToAssets(this.cache.userCollateral);
1029
- if (swapCollateral > maxCollateral) {
1030
- swapCollateral = maxCollateral;
1294
+ if (swapCollateral > maxTokenCollateral) {
1295
+ const error = "Selected collateral token does not have enough posted collateral to fully deleverage.";
1296
+ if (simulate) {
1297
+ return { success: false, error };
1298
+ }
1299
+ throw new Error(error);
1300
+ }
1301
+ }
1302
+ else {
1303
+ this.assertSelectedBorrowDebtCanDeleverage(borrowToken, snapshot.debtTokenBalance, decimal_js_1.default.max(this.market.userDebt.sub(preview.newDebt), (0, decimal_js_1.default)(0)));
1304
+ if (feeBps > 0n) {
1305
+ // Partial deleverage: inflate swap size to compensate
1306
+ // for fee deduction on input. KyberSwap deducts feeBps
1307
+ // from input before swapping, so without compensation
1308
+ // the swap underdelivers and actual leverage is slightly
1309
+ // higher than target.
1310
+ swapCollateral = swapCollateral * 10000n / (10000n - feeBps);
1031
1311
  }
1032
1312
  }
1033
- else if (feeBps > 0n) {
1034
- // Partial deleverage: inflate swap size to compensate
1035
- // for fee deduction on input. KyberSwap deducts feeBps
1036
- // from input before swapping, so without compensation
1037
- // the swap underdelivers and actual leverage is slightly
1038
- // higher than target.
1039
- swapCollateral = swapCollateral * 10000n / (10000n - feeBps);
1313
+ if (!isFullDeleverage && swapCollateral > maxTokenCollateral) {
1314
+ const error = "Selected collateral token does not have enough posted collateral to reach the requested leverage target.";
1315
+ if (simulate) {
1316
+ return { success: false, error };
1317
+ }
1318
+ throw new Error(error);
1040
1319
  }
1041
1320
  const { action, quote } = await config.dexAgg.quoteAction(manager.address, this.asset.address, borrowToken.asset.address, swapCollateral, slippage, feeBps, feeReceiver);
1042
1321
  // Fee-aware slippage expansion for `_swapSafe` is handled by
@@ -1078,7 +1357,6 @@ class CToken extends Calldata_1.Calldata {
1078
1357
  }
1079
1358
  if (simulate)
1080
1359
  return this.simulateOracleRoute(calldata, { to: manager.address });
1081
- await this._checkPositionManagerApproval(manager);
1082
1360
  return this.oracleRoute(calldata, { to: manager.address });
1083
1361
  }
1084
1362
  catch (error) {
@@ -1095,30 +1373,39 @@ class CToken extends Calldata_1.Calldata {
1095
1373
  throw new Error("Multiplier must be greater than 1");
1096
1374
  }
1097
1375
  depositAmount = await this.ensureUnderlyingAmount(depositAmount, 'none');
1098
- const slippage = this._leverageUpSlippage((0, helpers_1.toBps)(slippage_), multiplier);
1099
1376
  const manager = this.getPositionManager(type);
1377
+ if (type === 'vault' || type === 'native-vault') {
1378
+ await this.assertVaultLeverageBorrowAssetSupported(borrow, type);
1379
+ }
1380
+ else if (type === 'simple') {
1381
+ this.assertSimpleLeverageSwapAssetsDiffer(borrow);
1382
+ }
1100
1383
  let calldata;
1101
1384
  const depositAssets = FormatConverter_1.default.decimalToBigInt(depositAmount, this.asset.decimals);
1385
+ await this._checkTokenApproval(this.getPositionManagerDepositApprovalTarget(manager), depositAssets);
1102
1386
  await this._getLeverageSnapshot(borrow);
1103
- const { borrowAmount } = this.previewLeverageUp(multiplier, borrow, depositAssets);
1387
+ const preview = this.previewDepositAndLeverage(multiplier, borrow, depositAssets, type);
1388
+ if (preview.borrowAssets === 0n) {
1389
+ if (simulate) {
1390
+ return {
1391
+ success: false,
1392
+ error: "Target leverage must exceed the post-deposit leverage to borrow more.",
1393
+ };
1394
+ }
1395
+ throw new Error("Target leverage must exceed the post-deposit leverage to borrow more.");
1396
+ }
1397
+ const slippage = this._leverageUpSlippage((0, helpers_1.toBps)(slippage_), preview.targetLeverage);
1398
+ const { borrowAmount, borrowAssets, feeBps, targetLeverage } = preview;
1399
+ await this.assertLeverageBorrowCapacity(borrow, borrowAssets);
1104
1400
  switch (type) {
1105
1401
  case 'simple': {
1106
- const borrowAssets = FormatConverter_1.default.decimalToBigInt(borrowAmount, borrow.asset.decimals);
1107
- const feeBps = this.setup.feePolicy.getFeeBps({
1108
- operation: 'deposit-and-leverage',
1109
- inputToken: borrow.asset.address,
1110
- outputToken: this.asset.address,
1111
- inputAmount: borrowAssets,
1112
- currentLeverage: this.getLeverage() ?? (0, decimal_js_1.default)(1),
1113
- targetLeverage: multiplier,
1114
- });
1115
1402
  const feeReceiver = feeBps > 0n ? this.setup.feePolicy.feeReceiver : undefined;
1116
1403
  const { action, quote } = await this.currentChainConfig.dexAgg.quoteAction(manager.address, borrow.asset.address, this.asset.address, borrowAssets, slippage, feeBps, feeReceiver);
1117
1404
  // Fee-aware slippage expansion for `_swapSafe` is handled by
1118
1405
  // KyberSwap.quoteAction. See KyberSwap.ts for rationale.
1119
1406
  // Fee amplification: same pattern as leverageUp. See
1120
1407
  // `amplifyContractSlippage` in helpers.ts.
1121
- const contractSlippage = (0, helpers_1.amplifyContractSlippage)(slippage, multiplier.sub(1), feeBps);
1408
+ const contractSlippage = (0, helpers_1.amplifyContractSlippage)(slippage, targetLeverage.sub(1), feeBps);
1122
1409
  calldata = manager.getDepositAndLeverageCalldata(FormatConverter_1.default.decimalToBigInt(depositAmount, this.asset.decimals), {
1123
1410
  borrowableCToken: borrow.address,
1124
1411
  borrowAssets: borrowAssets,
@@ -1136,10 +1423,10 @@ class CToken extends Calldata_1.Calldata {
1136
1423
  // drift. Uses `multiplier.sub(1)` per the per-call-site
1137
1424
  // asymmetry documented in helpers.ts (depositAndLeverage
1138
1425
  // leverageDelta = multiplier - 1).
1139
- const contractSlippage = (0, helpers_1.amplifyContractSlippage)(slippage, multiplier.sub(1), exports.LEVERAGE.LEVERAGE_UP_VAULT_DRIFT_BPS);
1426
+ const contractSlippage = (0, helpers_1.amplifyContractSlippage)(slippage, targetLeverage.sub(1), exports.LEVERAGE.LEVERAGE_UP_VAULT_DRIFT_BPS);
1140
1427
  calldata = manager.getDepositAndLeverageCalldata(FormatConverter_1.default.decimalToBigInt(depositAmount, this.asset.decimals), {
1141
1428
  borrowableCToken: borrow.address,
1142
- borrowAssets: FormatConverter_1.default.decimalToBigInt(borrowAmount, borrow.asset.decimals),
1429
+ borrowAssets: borrowAssets,
1143
1430
  cToken: this.address,
1144
1431
  expectedShares: await PositionManager_1.PositionManager.getVaultExpectedShares(this, borrow, borrowAmount),
1145
1432
  swapAction: PositionManager_1.PositionManager.emptySwapAction(),
@@ -1154,7 +1441,6 @@ class CToken extends Calldata_1.Calldata {
1154
1441
  }
1155
1442
  if (simulate)
1156
1443
  return this.simulateOracleRoute(calldata, { to: manager.address });
1157
- await this._checkPositionManagerApproval(manager);
1158
1444
  return this.oracleRoute(calldata, { to: manager.address });
1159
1445
  }
1160
1446
  catch (error) {
@@ -1168,19 +1454,10 @@ class CToken extends Calldata_1.Calldata {
1168
1454
  amount = await this.ensureUnderlyingAmount(amount, zap);
1169
1455
  const signer = this.requireSigner();
1170
1456
  receiver ??= signer.address;
1171
- const isZapping = typeof zap === 'object' && zap.type !== 'none';
1172
1457
  const depositAssets = FormatConverter_1.default.decimalToBigInt(amount, this.asset.decimals);
1173
- let zapAssets = depositAssets;
1174
- if (isZapping && zap.inputToken) {
1175
- const isNative = zap.inputToken.toLowerCase() === helpers_1.NATIVE_ADDRESS.toLowerCase();
1176
- const zapDecimals = isNative ? 18n : (() => {
1177
- const inputErc20 = new ERC20_1.ERC20(this.provider, zap.inputToken, undefined, undefined, this.signer);
1178
- return inputErc20.decimals ?? inputErc20.contract.decimals();
1179
- })();
1180
- zapAssets = FormatConverter_1.default.decimalToBigInt(amount, await zapDecimals);
1181
- }
1458
+ const zapAssets = await this.getZapAssetAmount(amount, zap);
1182
1459
  const default_calldata = this.getCallData("deposit", [depositAssets, receiver]);
1183
- const { calldata, calldata_overrides } = await this.zap(zapAssets, zap, false, default_calldata);
1460
+ const { calldata, calldata_overrides } = await this.zap(zapAssets, zap, false, default_calldata, receiver);
1184
1461
  return this.simulateOracleRoute(calldata, calldata_overrides);
1185
1462
  }
1186
1463
  catch (error) {
@@ -1192,33 +1469,27 @@ class CToken extends Calldata_1.Calldata {
1192
1469
  amount = await this.ensureUnderlyingAmount(amount, zap);
1193
1470
  const signer = this.requireSigner();
1194
1471
  receiver ??= signer.address;
1195
- const isZapping = typeof zap === 'object' && zap.type !== 'none';
1196
1472
  const depositAssets = FormatConverter_1.default.decimalToBigInt(amount, this.asset.decimals);
1197
- let zapAssets = depositAssets;
1198
- if (isZapping && zap.inputToken) {
1199
- const isNative = zap.inputToken.toLowerCase() === helpers_1.NATIVE_ADDRESS.toLowerCase();
1200
- const zapDecimals = isNative ? 18n : (() => {
1201
- const inputErc20 = new ERC20_1.ERC20(this.provider, zap.inputToken, undefined, undefined, this.signer);
1202
- return inputErc20.decimals ?? inputErc20.contract.decimals();
1203
- })();
1204
- zapAssets = FormatConverter_1.default.decimalToBigInt(amount, await zapDecimals);
1205
- }
1206
- const default_calldata = this.getCallData("depositAsCollateral", [depositAssets, receiver]);
1207
- const { calldata, calldata_overrides } = await this.zap(zapAssets, zap, true, default_calldata);
1473
+ const zapAssets = await this.getZapAssetAmount(amount, zap);
1474
+ const collateralMethod = receiver.toLowerCase() === signer.address.toLowerCase()
1475
+ ? "depositAsCollateral"
1476
+ : "depositAsCollateralFor";
1477
+ const default_calldata = this.getCallData(collateralMethod, [depositAssets, receiver]);
1478
+ const { calldata, calldata_overrides } = await this.zap(zapAssets, zap, true, default_calldata, receiver);
1208
1479
  return this.simulateOracleRoute(calldata, calldata_overrides);
1209
1480
  }
1210
1481
  catch (error) {
1211
1482
  return { success: false, error: error?.reason || error?.message || String(error) };
1212
1483
  }
1213
1484
  }
1214
- async zap(assets, zap, collateralize = false, default_calldata) {
1485
+ async zap(assets, zap, collateralize = false, default_calldata, receiver = this.requireSigner().address) {
1215
1486
  let calldata;
1216
1487
  let calldata_overrides = {};
1217
1488
  let slippage = 0n;
1218
1489
  let inputToken = null;
1219
1490
  let type_of_zap;
1220
1491
  if (typeof zap == 'object') {
1221
- slippage = BigInt(zap.slippage.mul(helpers_1.BPS).toString());
1492
+ slippage = FormatConverter_1.default.percentageToBps(zap.slippage);
1222
1493
  inputToken = zap.inputToken;
1223
1494
  type_of_zap = zap.type;
1224
1495
  }
@@ -1236,20 +1507,20 @@ class CToken extends Calldata_1.Calldata {
1236
1507
  case 'simple':
1237
1508
  if (inputToken == null)
1238
1509
  throw new Error("Input token must be provided for simple zap");
1239
- calldata = await zapper.getSimpleZapCalldata(this, inputToken, this.asset.address, assets, collateralize, slippage);
1510
+ calldata = await zapper.getSimpleZapCalldata(this, inputToken, this.asset.address, assets, collateralize, slippage, receiver);
1240
1511
  const isNativeSimpleZap = inputToken.toLowerCase() === helpers_1.NATIVE_ADDRESS.toLowerCase();
1241
1512
  calldata_overrides = isNativeSimpleZap ? { value: assets, to: zapper.address } : { to: zapper.address };
1242
1513
  break;
1243
1514
  case 'vault':
1244
- calldata = await zapper.getVaultZapCalldata(this, assets, collateralize);
1515
+ calldata = await zapper.getVaultZapCalldata(this, assets, collateralize, false, receiver);
1245
1516
  calldata_overrides = { to: zapper.address };
1246
1517
  break;
1247
1518
  case 'native-vault':
1248
- calldata = await zapper.getNativeZapCalldata(this, assets, collateralize);
1519
+ calldata = await zapper.getNativeZapCalldata(this, assets, collateralize, false, receiver);
1249
1520
  calldata_overrides = { value: assets, to: zapper.address };
1250
1521
  break;
1251
1522
  case 'native-simple':
1252
- calldata = await zapper.getNativeZapCalldata(this, assets, collateralize, true);
1523
+ calldata = await zapper.getNativeZapCalldata(this, assets, collateralize, true, receiver);
1253
1524
  calldata_overrides = { value: assets, to: zapper.address };
1254
1525
  break;
1255
1526
  default:
@@ -1261,84 +1532,50 @@ class CToken extends Calldata_1.Calldata {
1261
1532
  amount = await this.ensureUnderlyingAmount(amount, zap);
1262
1533
  const signer = this.requireSigner();
1263
1534
  receiver ??= signer.address;
1264
- // When zapping, the swap amount uses input token decimals, but the
1265
- // default deposit calldata uses the deposit token decimals.
1266
- const isZapping = typeof zap === 'object' && zap.type !== 'none';
1267
1535
  const depositAssets = FormatConverter_1.default.decimalToBigInt(amount, this.asset.decimals);
1268
- let zapAssets = depositAssets;
1269
- if (isZapping && zap.inputToken) {
1270
- if (zap.inputToken.toLowerCase() === helpers_1.NATIVE_ADDRESS.toLowerCase()) {
1271
- zapAssets = FormatConverter_1.default.decimalToBigInt(amount, 18n);
1272
- }
1273
- else {
1274
- const inputErc20 = new ERC20_1.ERC20(this.provider, zap.inputToken, undefined, undefined, this.signer);
1275
- const zapDecimals = inputErc20.decimals ?? await inputErc20.contract.decimals();
1276
- zapAssets = FormatConverter_1.default.decimalToBigInt(amount, zapDecimals);
1277
- }
1278
- }
1279
- const zapType = typeof zap == 'object' ? zap.type : zap;
1280
- const isNative = zapType == 'native-simple' || zapType == 'native-vault' || zapType == 'none'
1281
- || (typeof zap == 'object' && zap.inputToken.toLowerCase() === helpers_1.NATIVE_ADDRESS.toLowerCase());
1536
+ const zapAssets = await this.getZapAssetAmount(amount, zap);
1537
+ await this._checkDepositApprovals(zap, depositAssets, zapAssets);
1282
1538
  const default_calldata = this.getCallData("deposit", [depositAssets, receiver]);
1283
- const { calldata, calldata_overrides } = await this.zap(zapAssets, zap, false, default_calldata);
1284
- if (isNative) {
1285
- await this._checkAssetApproval(depositAssets);
1286
- }
1287
- else {
1288
- const zapper = this.getZapper(zapType);
1289
- if (!zapper) {
1290
- throw new Error(`No zapper contract found for type '${zapType}' on ${this.symbol}`);
1291
- }
1292
- await this._checkDepositApprovals(zapper, zapAssets);
1293
- }
1294
- return this.oracleRoute(calldata, calldata_overrides);
1539
+ const { calldata, calldata_overrides } = await this.zap(zapAssets, zap, false, default_calldata, receiver);
1540
+ return this.oracleRoute(calldata, calldata_overrides, receiver);
1295
1541
  }
1296
1542
  async depositAsCollateral(amount, zap = 'none', receiver = null) {
1297
1543
  amount = await this.ensureUnderlyingAmount(amount, zap);
1298
1544
  const signer = this.requireSigner();
1299
1545
  receiver ??= signer.address;
1300
- // When zapping, the swap amount uses input token decimals, but collateral
1301
- // cap checks and the default deposit calldata use the deposit token decimals.
1302
- const isZapping = typeof zap === 'object' && zap.type !== 'none';
1303
1546
  const depositAssets = FormatConverter_1.default.decimalToBigInt(amount, this.asset.decimals);
1304
- let zapAssets = depositAssets;
1305
- if (isZapping && zap.inputToken) {
1306
- if (zap.inputToken.toLowerCase() === helpers_1.NATIVE_ADDRESS.toLowerCase()) {
1307
- zapAssets = FormatConverter_1.default.decimalToBigInt(amount, 18n);
1308
- }
1309
- else {
1310
- const inputErc20 = new ERC20_1.ERC20(this.provider, zap.inputToken, undefined, undefined, this.signer);
1311
- const zapDecimals = inputErc20.decimals ?? await inputErc20.contract.decimals();
1312
- zapAssets = FormatConverter_1.default.decimalToBigInt(amount, zapDecimals);
1313
- }
1314
- }
1315
- if (!isZapping) {
1547
+ const zapAssets = await this.getZapAssetAmount(amount, zap);
1548
+ if (!this.isZapInstruction(zap)) {
1316
1549
  const collateralCapError = "There is not enough collateral left in this tokens collateral cap for this deposit.";
1317
1550
  const remainingCollateral = this.getRemainingCollateral(false);
1318
- if (remainingCollateral == 0n)
1551
+ if (remainingCollateral <= 0n)
1319
1552
  throw new Error(collateralCapError);
1320
1553
  if (remainingCollateral > 0n) {
1321
- const shares = this.virtualConvertToShares(depositAssets, exports.LEVERAGE.SHARES_BUFFER_BPS);
1554
+ const shares = this.virtualConvertToShares(depositAssets);
1322
1555
  if (shares > remainingCollateral) {
1323
1556
  throw new Error(collateralCapError);
1324
1557
  }
1325
1558
  }
1326
1559
  }
1327
- const default_calldata = this.getCallData("depositAsCollateral", [depositAssets, receiver]);
1328
- const { calldata, calldata_overrides, zapper } = await this.zap(zapAssets, zap, true, default_calldata);
1329
- await this._checkDepositApprovals(zapper, zapAssets);
1330
- return this.oracleRoute(calldata, calldata_overrides);
1560
+ await this._checkDepositApprovals(zap, depositAssets, zapAssets, true, receiver);
1561
+ const collateralMethod = receiver.toLowerCase() === signer.address.toLowerCase()
1562
+ ? "depositAsCollateral"
1563
+ : "depositAsCollateralFor";
1564
+ const default_calldata = this.getCallData(collateralMethod, [depositAssets, receiver]);
1565
+ const { calldata, calldata_overrides } = await this.zap(zapAssets, zap, true, default_calldata, receiver);
1566
+ return this.oracleRoute(calldata, calldata_overrides, receiver);
1331
1567
  }
1332
1568
  async redeem(amount) {
1333
1569
  const signer = this.requireSigner();
1334
1570
  const receiver = signer.address;
1335
1571
  const owner = signer.address;
1336
- const buffer = this.market.userDebt.greaterThan(0) ? 100n : 0n;
1572
+ const buffer = this.getExecutionDebtBufferTime();
1337
1573
  const balance_avail = await this.balanceOf(signer.address);
1338
1574
  const max_shares = await this.maxRedemption(true, buffer);
1339
1575
  const converted_shares = this.convertTokenInputToShares(amount);
1340
- let shares = max_shares < converted_shares ? max_shares : converted_shares;
1341
- if (balance_avail - shares <= 10n) {
1576
+ const maxExecutableShares = max_shares < balance_avail ? max_shares : balance_avail;
1577
+ let shares = maxExecutableShares < converted_shares ? maxExecutableShares : converted_shares;
1578
+ if (maxExecutableShares === balance_avail && balance_avail - shares <= 10n) {
1342
1579
  shares = balance_avail;
1343
1580
  }
1344
1581
  const calldata = this.getCallData("redeem", [shares, receiver, owner]);
@@ -1361,6 +1598,7 @@ class CToken extends Calldata_1.Calldata {
1361
1598
  const snapshot = await this.contract.getSnapshot(account);
1362
1599
  return {
1363
1600
  asset: snapshot.asset,
1601
+ underlying: snapshot.underlying,
1364
1602
  decimals: BigInt(snapshot.decimals),
1365
1603
  isCollateral: snapshot.isCollateral,
1366
1604
  collateralPosted: BigInt(snapshot.collateralPosted),
@@ -1394,19 +1632,49 @@ class CToken extends Calldata_1.Calldata {
1394
1632
  const decimals = this.asset.decimals ?? this.decimals;
1395
1633
  return FormatConverter_1.default.bigIntTokensToUsd(tokenAmount, price, decimals);
1396
1634
  }
1397
- async convertSharesToUsd(tokenAmount) {
1398
- tokenAmount = this.virtualConvertToShares(tokenAmount);
1635
+ convertSharesToUsdSync(tokenAmount) {
1399
1636
  const price = this.getPrice(false, false, false);
1400
1637
  const decimals = this.decimals;
1401
1638
  return FormatConverter_1.default.bigIntTokensToUsd(tokenAmount, price, decimals);
1402
1639
  }
1403
- buildMultiCallAction(calldata) {
1640
+ async fetchConvertSharesToUsd(tokenAmount) {
1641
+ await this.fetchPrice(false);
1642
+ await this.fetchDecimals();
1643
+ return this.convertSharesToUsdSync(tokenAmount);
1644
+ }
1645
+ async convertSharesToUsd(tokenAmount) {
1646
+ return this.convertSharesToUsdSync(tokenAmount);
1647
+ }
1648
+ buildMultiCallAction(calldata, target = this.address) {
1404
1649
  return {
1405
- target: this.address,
1650
+ target,
1406
1651
  isPriceUpdate: false,
1407
1652
  data: calldata
1408
1653
  };
1409
1654
  }
1655
+ hasNonZeroNativeValueOverride(override) {
1656
+ const value = override.value;
1657
+ if (value == null) {
1658
+ return false;
1659
+ }
1660
+ if (typeof value === "bigint") {
1661
+ return value > 0n;
1662
+ }
1663
+ if (typeof value === "number") {
1664
+ return value > 0;
1665
+ }
1666
+ if (typeof value === "string") {
1667
+ return BigInt(value) > 0n;
1668
+ }
1669
+ return true;
1670
+ }
1671
+ assertOracleMulticallSupportsValue(priceUpdates, override) {
1672
+ if (priceUpdates.length === 0 || !this.hasNonZeroNativeValueOverride(override)) {
1673
+ return;
1674
+ }
1675
+ throw new Error("Native gas-token zaps cannot be combined with oracle price-update multicalls. " +
1676
+ "Use the wrapped-native zap path or retry when no oracle update is required.");
1677
+ }
1410
1678
  async _checkPositionManagerApproval(manager) {
1411
1679
  const isApproved = await this.isPluginApproved(manager.type, 'positionManager');
1412
1680
  if (!isApproved) {
@@ -1414,71 +1682,172 @@ class CToken extends Calldata_1.Calldata {
1414
1682
  }
1415
1683
  }
1416
1684
  async _checkZapperApproval(zapper) {
1417
- if (!this.setup.approval_protection) {
1418
- return;
1685
+ const plugin_allowed = await this.isPluginApproved(zapper.type, 'zapper');
1686
+ if (!plugin_allowed) {
1687
+ throw new Error(`Please approve the ${zapper.type} Zapper to be able to move ${this.symbol} on your behalf.`);
1419
1688
  }
1420
- if (this.setup.approval_protection && zapper) {
1421
- const plugin_allowed = await this.isPluginApproved(zapper.type, 'zapper');
1422
- if (!plugin_allowed) {
1423
- throw new Error(`Please approve the ${zapper.type} Zapper to be able to move ${this.symbol} on your behalf.`);
1424
- }
1689
+ }
1690
+ async _checkDelegateApproval(owner, delegate, delegateLabel) {
1691
+ const pluginAllowed = await this.contract.isDelegate(owner, delegate);
1692
+ if (!pluginAllowed) {
1693
+ throw new Error(`Please approve ${delegateLabel} as a delegate for ${this.symbol} on behalf of ${owner}.`);
1425
1694
  }
1426
1695
  }
1427
- async _checkErc20Approval(erc20_address, amount, spender) {
1428
- const signer = this.requireSigner();
1429
- const erc20 = new ERC20_1.ERC20(this.provider, erc20_address, undefined, undefined, signer);
1430
- const allowance = await erc20.allowance(signer.address, spender);
1431
- if (allowance < amount) {
1432
- const symbol = await erc20.fetchSymbol();
1433
- throw new Error(`Please approve ${symbol} for ${spender}: ${amount}`);
1696
+ getDepositAssetApprovalTarget() {
1697
+ const asset = this.getAsset(true);
1698
+ return {
1699
+ token: asset,
1700
+ spender: this.address,
1701
+ spenderLabel: this.symbol,
1702
+ };
1703
+ }
1704
+ getPositionManagerDepositApprovalTarget(manager) {
1705
+ return {
1706
+ token: this.getAsset(true),
1707
+ spender: manager.address,
1708
+ spenderLabel: `${manager.type} PositionManager`,
1709
+ };
1710
+ }
1711
+ async resolveZapApprovalTarget(instructions) {
1712
+ const zapType = typeof instructions == 'object' ? instructions.type : instructions;
1713
+ if (zapType == 'none') {
1714
+ return null;
1715
+ }
1716
+ const spender = this.getPluginAddress(zapType, 'zapper');
1717
+ if (spender == null) {
1718
+ throw new Error("Plugin does not have an associated contract");
1719
+ }
1720
+ switch (zapType) {
1721
+ case 'native-vault':
1722
+ case 'native-simple':
1723
+ return null;
1724
+ case 'vault':
1725
+ return {
1726
+ token: await this.getVaultAsset(true),
1727
+ spender,
1728
+ spenderLabel: `${zapType} Zapper`,
1729
+ };
1730
+ case 'simple':
1731
+ if (typeof instructions != 'object') {
1732
+ throw new Error("Input token must be provided for simple zap approval");
1733
+ }
1734
+ if (instructions.inputToken.toLowerCase() === helpers_1.NATIVE_ADDRESS.toLowerCase()) {
1735
+ return null;
1736
+ }
1737
+ return {
1738
+ token: new ERC20_1.ERC20(this.provider, instructions.inputToken, undefined, undefined, this.signer),
1739
+ spender,
1740
+ spenderLabel: `${zapType} Zapper`,
1741
+ };
1434
1742
  }
1435
1743
  }
1436
- async _checkAssetApproval(assets) {
1437
- if (!this.setup.approval_protection) {
1744
+ async hasTokenApproval(target, amount) {
1745
+ const owner = this.getAccountOrThrow();
1746
+ const allowance = await target.token.allowance(owner, target.spender);
1747
+ return allowance >= amount;
1748
+ }
1749
+ async _checkTokenApproval(target, amount) {
1750
+ const allowance = await this.hasTokenApproval(target, amount);
1751
+ if (allowance) {
1438
1752
  return;
1439
1753
  }
1440
- const asset = this.getAsset(true);
1441
- const owner = this.getAccountOrThrow();
1442
- const allowance = await asset.allowance(owner, this.address);
1443
- if (allowance < assets) {
1444
- throw new Error(`Please approve the ${asset.symbol} token for ${this.symbol}`);
1754
+ let tokenLabel = target.token.symbol ?? target.token.address;
1755
+ if (target.token.symbol == undefined) {
1756
+ try {
1757
+ tokenLabel = await target.token.fetchSymbol();
1758
+ }
1759
+ catch {
1760
+ tokenLabel = target.token.address;
1761
+ }
1445
1762
  }
1763
+ throw new Error(`Please approve the ${tokenLabel} token for ${target.spenderLabel}`);
1446
1764
  }
1447
- async _checkDepositApprovals(zapper, assets) {
1448
- if (!this.setup.approval_protection) {
1449
- return;
1765
+ async _checkDepositApprovals(zap, depositAssets, zapAssets, collateralize = false, receiver = null) {
1766
+ const zapType = typeof zap == 'object' ? zap.type : zap;
1767
+ const signer = this.requireSigner();
1768
+ if (zapType != 'none') {
1769
+ const zapper = this.getZapper(zapType);
1770
+ if (!zapper) {
1771
+ throw new Error(`No zapper contract found for type '${zapType}' on ${this.symbol}`);
1772
+ }
1773
+ if (collateralize) {
1774
+ const receiverAddress = receiver ?? signer.address;
1775
+ if (receiverAddress.toLowerCase() === signer.address.toLowerCase()) {
1776
+ await this._checkZapperApproval(zapper);
1777
+ }
1778
+ else {
1779
+ await this._checkDelegateApproval(receiverAddress, signer.address, "the connected signer");
1780
+ await this._checkDelegateApproval(receiverAddress, zapper.address, `${zapper.type} Zapper`);
1781
+ }
1782
+ }
1450
1783
  }
1451
- if (zapper) {
1452
- await this._checkZapperApproval(zapper);
1784
+ else if (collateralize && receiver && receiver.toLowerCase() !== signer.address.toLowerCase()) {
1785
+ await this._checkDelegateApproval(receiver, signer.address, "the connected signer");
1453
1786
  }
1454
- await this._checkAssetApproval(assets);
1787
+ const approvalTarget = zapType == 'none'
1788
+ ? this.getDepositAssetApprovalTarget()
1789
+ : await this.resolveZapApprovalTarget(zap);
1790
+ if (approvalTarget == null) {
1791
+ return;
1792
+ }
1793
+ const approvalAmount = zapType == 'none' ? depositAssets : zapAssets;
1794
+ await this._checkTokenApproval(approvalTarget, approvalAmount);
1455
1795
  }
1456
- async oracleRoute(calldata, override = {}) {
1796
+ async oracleRoute(calldata, override = {}, reloadAccount = null) {
1457
1797
  const signer = this.requireSigner();
1458
1798
  const price_updates = await this.getPriceUpdates();
1799
+ this.assertOracleMulticallSupportsValue(price_updates, override);
1459
1800
  if (price_updates.length > 0) {
1460
- const token_action = this.buildMultiCallAction(calldata);
1801
+ const actionTarget = (override.to ?? this.address);
1802
+ const token_action = this.buildMultiCallAction(calldata, actionTarget);
1461
1803
  calldata = this.getCallData("multicall", [[...price_updates, token_action]]);
1462
1804
  }
1463
1805
  const tx = await this.executeCallData(calldata, override);
1464
- await this.market.reloadUserData(signer.address);
1806
+ if (typeof tx.wait === "function") {
1807
+ await tx.wait();
1808
+ }
1809
+ const refreshAccount = reloadAccount ?? signer.address;
1810
+ await this.market.reloadUserData(refreshAccount, {
1811
+ allowSignerMismatch: refreshAccount.toLowerCase() !== signer.address.toLowerCase(),
1812
+ });
1465
1813
  return tx;
1466
1814
  }
1467
1815
  async simulateOracleRoute(calldata, override = {}) {
1468
1816
  const price_updates = await this.getPriceUpdates();
1817
+ this.assertOracleMulticallSupportsValue(price_updates, override);
1469
1818
  if (price_updates.length > 0) {
1470
- const token_action = this.buildMultiCallAction(calldata);
1819
+ const actionTarget = (override.to ?? this.address);
1820
+ const token_action = this.buildMultiCallAction(calldata, actionTarget);
1471
1821
  calldata = this.getCallData("multicall", [[...price_updates, token_action]]);
1472
1822
  }
1473
1823
  return this.simulateCallData(calldata, override);
1474
1824
  }
1475
- async getPriceUpdates() {
1476
- let price_updates = [];
1477
- if (this.adapters.includes(ProtocolReader_1.AdaptorTypes.REDSTONE_CORE)) {
1478
- const redstone = await Redstone_1.Redstone.buildMultiCallAction(this);
1479
- price_updates.push(redstone);
1825
+ getRedstonePriceUpdateTokens() {
1826
+ const candidates = (this.market?.tokens?.length ? this.market.tokens : [this]);
1827
+ const seenAssets = new Set();
1828
+ const tokens = [];
1829
+ for (const token of candidates) {
1830
+ if (!token.adapters?.includes(ProtocolReader_1.AdaptorTypes.REDSTONE_CORE)) {
1831
+ continue;
1832
+ }
1833
+ let assetAddress;
1834
+ try {
1835
+ assetAddress = token.getAsset(false);
1836
+ }
1837
+ catch {
1838
+ assetAddress = token.cache?.asset?.address ?? token.address;
1839
+ }
1840
+ const key = assetAddress.toLowerCase();
1841
+ if (seenAssets.has(key)) {
1842
+ continue;
1843
+ }
1844
+ seenAssets.add(key);
1845
+ tokens.push(token);
1480
1846
  }
1481
- return price_updates;
1847
+ return tokens;
1848
+ }
1849
+ async getPriceUpdates() {
1850
+ return Promise.all(this.getRedstonePriceUpdateTokens().map((token) => Redstone_1.Redstone.buildMultiCallAction(token)));
1482
1851
  }
1483
1852
  }
1484
1853
  exports.CToken = CToken;