curvance 4.1.2 → 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 +65 -10
  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 +210 -21
  15. package/dist/classes/CToken.d.ts.map +1 -1
  16. package/dist/classes/CToken.js +692 -284
  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 +46 -8
  52. package/dist/classes/Market.d.ts.map +1 -1
  53. package/dist/classes/Market.js +342 -111
  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 -3
  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
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.CToken = void 0;
6
+ exports.CToken = exports.LEVERAGE = void 0;
7
7
  const helpers_1 = require("../helpers");
8
8
  const ProtocolReader_1 = require("./ProtocolReader");
9
9
  const ERC20_1 = require("./ERC20");
@@ -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
@@ -69,7 +70,7 @@ const EXCLUDED_ZAP_SYMBOLS = new Set([
69
70
  * onRedeem lines 482-493), so the economic loss from the overshoot is
70
71
  * zero — only the contract's naive equity-loss check sees it as loss.
71
72
  */
72
- const LEVERAGE = {
73
+ exports.LEVERAGE = {
73
74
  /** Max leverage cap: fraction of theoretical max the user can select.
74
75
  * Prevents boundary singularity at exact max leverage — the contract's
75
76
  * post-op `canBorrow` check re-evaluates LTV against fresh on-chain
@@ -113,10 +114,44 @@ const 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,
120
+ /** Per-leverage-unit BPS buffer for `checkSlippage` on vault + native-vault
121
+ * leverage-up paths. Absorbs the drift between the collateral vault's
122
+ * fundamental mint rate at tx time and the stored oracle price that
123
+ * `marketManager.statusOf` uses inside `checkSlippage`. The vault-token
124
+ * oracle publishes discretely; the vault's exchange rate accrues
125
+ * continuously — so new shares are minted at `r_current` but valued at
126
+ * `r_oracle`, leaving a (L-1)-amplified equity-fraction gap that the
127
+ * simple path doesn't see in practice (vault-token markets default to
128
+ * the vault/native-vault PM and `leverageDown` drift goes the other
129
+ * direction as a gain). Empirically calibrated against the ~3% user
130
+ * slippage failure threshold on shMON/WMON native-vault leverage-up;
131
+ * refine via fork testing if drift distribution turns out wider. The
132
+ * constant is NOT "feed divergence" — shMON oracle IS derived from
133
+ * p_MON × r_shMON off-chain; the gap is between publish-time snapshot
134
+ * and tx-time state, not between two independent feeds. */
135
+ LEVERAGE_UP_VAULT_DRIFT_BPS: 30n,
119
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
+ }
120
155
  class CToken extends Calldata_1.Calldata {
121
156
  provider;
122
157
  address;
@@ -132,13 +167,14 @@ class CToken extends Calldata_1.Calldata {
132
167
  nativeApy = (0, decimal_js_1.default)(0);
133
168
  incentiveSupplyApy = (0, decimal_js_1.default)(0);
134
169
  incentiveBorrowApy = (0, decimal_js_1.default)(0);
170
+ userCacheFreshness;
135
171
  get signer() { return this.market.signer; }
136
172
  get account() { return this.market.account; }
137
173
  constructor(provider, address, cache, market) {
138
174
  super();
139
175
  this.provider = provider;
140
176
  this.address = address;
141
- 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);
142
178
  this.cache = cache;
143
179
  this.market = market;
144
180
  const chainSettings = this.currentChainConfig;
@@ -163,6 +199,31 @@ class CToken extends Calldata_1.Calldata {
163
199
  this.leverageTypes.push('simple');
164
200
  this.zapTypes.push('simple');
165
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
+ }
166
227
  get setup() { return this.market.setup; }
167
228
  get currentChain() { return this.setup.chain; }
168
229
  get currentChainConfig() { return (0, helpers_1.getChainConfig)(this.currentChain); }
@@ -173,6 +234,49 @@ class CToken extends Calldata_1.Calldata {
173
234
  getWriteContract() {
174
235
  return (0, helpers_1.contractSetup)(this.requireSigner(), this.address, BaseCToken_json_1.default);
175
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
+ }
176
280
  get adapters() { return this.cache.adapters; }
177
281
  get borrowPaused() { return this.cache.borrowPaused; }
178
282
  get collateralizationPaused() { return this.cache.collateralizationPaused; }
@@ -191,23 +295,36 @@ class CToken extends Calldata_1.Calldata {
191
295
  // for the loss channels this buffer absorbs and the tuning history.
192
296
  const theoretical = (0, decimal_js_1.default)(this.cache.maxLeverage).div(helpers_1.BPS);
193
297
  const factor = theoretical.sub(1);
194
- return (0, decimal_js_1.default)(1).add(factor.mul(LEVERAGE.MAX_LEVERAGE_FACTOR));
298
+ return (0, decimal_js_1.default)(1).add(factor.mul(exports.LEVERAGE.MAX_LEVERAGE_FACTOR));
195
299
  }
196
300
  get canLeverage() { return this.leverageTypes.length > 0; }
197
301
  get totalAssets() { return this.cache.totalAssets; }
198
302
  get totalSupply() { return this.cache.totalSupply; }
199
303
  get liquidationPrice() {
200
- if (this.cache.liquidationPrice == helpers_1.UINT256_MAX)
304
+ const liquidationPrice = this.readFreshUserCache("liquidationPrice", "reading token liquidationPrice");
305
+ if (liquidationPrice == helpers_1.UINT256_MAX)
201
306
  return null;
202
- return (0, helpers_1.toDecimal)(this.cache.liquidationPrice, 18n);
307
+ return (0, helpers_1.toDecimal)(liquidationPrice, 18n);
203
308
  }
204
309
  get irmTargetRate() { return (0, decimal_js_1.default)(this.cache.irmTargetRate).div(helpers_1.WAD); }
205
310
  get irmMaxRate() { return (0, decimal_js_1.default)(this.cache.irmMaxRate).div(helpers_1.WAD); }
206
311
  get irmTargetUtilization() { return (0, decimal_js_1.default)(this.cache.irmTargetUtilization).div(helpers_1.WAD); }
207
312
  get interestFee() { return (0, decimal_js_1.default)(this.cache.interestFee).div(helpers_1.BPS); }
208
313
  virtualConvertToAssets(shares) {
314
+ if (this.totalSupply === 0n || this.totalAssets === 0n) {
315
+ return shares;
316
+ }
209
317
  return (shares * this.totalAssets) / this.totalSupply;
210
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
+ }
211
328
  /**
212
329
  * Convert assets to shares using cached totalSupply/totalAssets.
213
330
  * @param bufferBps Optional downward buffer in BPS to account for
@@ -215,19 +332,35 @@ class CToken extends Calldata_1.Calldata {
215
332
  * Matches the buffer pattern in async convertToShares().
216
333
  */
217
334
  virtualConvertToShares(assets, bufferBps = 0n) {
218
- const shares = (assets * this.totalSupply) / this.totalAssets;
335
+ const shares = this.totalSupply === 0n || this.totalAssets === 0n
336
+ ? assets
337
+ : (assets * this.totalSupply) / this.totalAssets;
219
338
  return bufferBps > 0n ? shares * (10000n - bufferBps) / 10000n : shares;
220
339
  }
221
- getLeverage() {
222
- if (this.getUserCollateral(true).equals(0)) {
223
- 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
+ };
224
350
  }
225
- const leverage = this.getUserCollateral(true).div(this.getUserCollateral(true).sub(this.market.userDebt));
226
- 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;
227
360
  }
228
361
  getRemainingCollateral(formatted = true) {
229
362
  const diff = this.cache.collateralCap - this.cache.collateral;
230
- return formatted ? this.convertTokensToUsd(diff) : diff;
363
+ return formatted ? this.convertSharesToUsdSync(diff) : diff;
231
364
  }
232
365
  getRemainingDebt(formatted = true) {
233
366
  const diff = this.cache.debtCap - this.cache.debt;
@@ -267,36 +400,52 @@ class CToken extends Calldata_1.Calldata {
267
400
  return inBPS ? (0, decimal_js_1.default)(this.cache.closeFactorMax).div(helpers_1.BPS) : this.cache.closeFactorMax;
268
401
  }
269
402
  getUserShareBalance(inUSD) {
270
- 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);
271
409
  }
272
410
  getUserAssetBalance(inUSD) {
273
- 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);
274
413
  }
275
414
  getUserUnderlyingBalance(inUSD) {
276
- 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);
277
417
  }
278
418
  getCollateralCap(inUSD) {
279
- return inUSD ? this.convertTokensToUsd(this.cache.collateralCap) : this.cache.collateralCap;
419
+ return inUSD ? this.convertSharesToUsdSync(this.cache.collateralCap) : this.cache.collateralCap;
280
420
  }
281
421
  getDebtCap(inUSD) {
282
422
  return inUSD ? this.convertTokensToUsd(this.cache.debtCap) : this.cache.debtCap;
283
423
  }
284
424
  getCollateral(inUSD) {
285
- return inUSD ? this.convertTokensToUsd(this.cache.collateral) : this.cache.collateral;
425
+ return inUSD ? this.convertSharesToUsdSync(this.cache.collateral) : this.cache.collateral;
286
426
  }
287
427
  getDebt(inUSD) {
288
428
  return inUSD ? this.convertTokensToUsd(this.cache.debt) : this.cache.debt;
289
429
  }
290
430
  getUserCollateral(inUSD) {
291
- 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());
292
439
  }
293
440
  async fetchUserCollateral(formatted = false) {
294
441
  const collateral = await this.contract.collateralPosted(this.getAccountOrThrow());
295
442
  this.cache.userCollateral = collateral;
443
+ this.markUserCacheFresh(["userCollateral"]);
296
444
  return formatted ? (0, helpers_1.toDecimal)(collateral, this.decimals) : collateral;
297
445
  }
298
446
  getUserDebt(inUSD) {
299
- 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);
300
449
  }
301
450
  earnChange(amount, rateType) {
302
451
  const rate = this.getApy(false);
@@ -320,6 +469,16 @@ class CToken extends Calldata_1.Calldata {
320
469
  async getVaultAsset(asErc20) {
321
470
  return asErc20 ? await this.getUnderlyingVault().fetchAsset(true) : await this.getUnderlyingVault().fetchAsset(false);
322
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
+ }
323
482
  getAsset(asErc20) {
324
483
  return asErc20
325
484
  ? new ERC20_1.ERC20(this.provider, this.cache.asset.address, this.cache.asset, this.setup.contracts.OracleManager, this.signer)
@@ -349,22 +508,22 @@ class CToken extends Calldata_1.Calldata {
349
508
  // TODO: add underlying yield rate
350
509
  return asPercentage ? (0, decimal_js_1.default)(this.cache.supplyRate).div(helpers_1.WAD).mul(helpers_1.SECONDS_PER_YEAR) : this.cache.supplyRate;
351
510
  }
352
- getTvl(inUSD = true) {
353
- const tvl = this.cache.totalSupply;
354
- return inUSD ? this.convertTokensToUsd(tvl) : tvl;
511
+ getDeposits(inUSD = true) {
512
+ const deposits = this.cache.totalAssets;
513
+ return inUSD ? this.convertTokensToUsd(deposits) : deposits;
355
514
  }
356
- async fetchTvl(inUSD = true) {
357
- const tvl = await this.fetchTotalSupply();
358
- this.cache.totalSupply = tvl;
359
- return inUSD ? this.getTvl(true) : this.getTvl(false);
515
+ async fetchDeposits(inUSD = true) {
516
+ const deposits = await this.fetchTotalAssets();
517
+ this.cache.totalAssets = deposits;
518
+ return inUSD ? this.getDeposits(true) : this.getDeposits(false);
360
519
  }
361
520
  getTotalCollateral(inUSD = true) {
362
521
  const totalCollateral = this.cache.collateral;
363
- return inUSD ? this.convertTokensToUsd(totalCollateral) : totalCollateral;
522
+ return inUSD ? this.convertSharesToUsdSync(totalCollateral) : totalCollateral;
364
523
  }
365
524
  async fetchTotalCollateral(inUSD = true) {
366
525
  const totalCollateral = await this.contract.marketCollateralPosted();
367
- return inUSD ? this.fetchConvertTokensToUsd(totalCollateral) : totalCollateral;
526
+ return inUSD ? this.fetchConvertSharesToUsd(totalCollateral) : totalCollateral;
368
527
  }
369
528
  getPositionManager(type) {
370
529
  const signer = this.requireSigner();
@@ -383,29 +542,24 @@ class CToken extends Calldata_1.Calldata {
383
542
  return new Zapper_1.Zapper(zap_contract, signer, type, this.setup);
384
543
  }
385
544
  async isZapAssetApproved(instructions, amount) {
386
- if (instructions == 'none' || typeof instructions != 'object') {
545
+ if (instructions == 'none') {
387
546
  return true;
388
547
  }
389
- if (instructions.inputToken.toLowerCase() === helpers_1.NATIVE_ADDRESS.toLowerCase()) {
548
+ const approvalTarget = await this.resolveZapApprovalTarget(instructions);
549
+ if (approvalTarget == null) {
390
550
  return true;
391
551
  }
392
- const signer = this.requireSigner();
393
- const asset = new ERC20_1.ERC20(this.provider, instructions.inputToken, undefined, undefined, signer);
394
- const plugin = this.getPluginAddress(instructions.type, 'zapper');
395
- const allowance = await asset.allowance(signer.address, plugin);
396
- return allowance >= amount;
552
+ return this.hasTokenApproval(approvalTarget, amount);
397
553
  }
398
554
  async approveZapAsset(instructions, amount) {
399
- if (instructions == 'none' || typeof instructions != 'object') {
555
+ if (instructions == 'none') {
400
556
  throw new Error("Plugin does not have an associated contract");
401
557
  }
402
- if (instructions.inputToken.toLowerCase() === helpers_1.NATIVE_ADDRESS.toLowerCase()) {
558
+ const approvalTarget = await this.resolveZapApprovalTarget(instructions);
559
+ if (approvalTarget == null) {
403
560
  return;
404
561
  }
405
- const signer = this.requireSigner();
406
- const asset = new ERC20_1.ERC20(this.provider, instructions.inputToken, undefined, undefined, signer);
407
- const plugin = this.getPluginAddress(instructions.type, 'zapper');
408
- return asset.approve(plugin, amount);
562
+ return approvalTarget.token.approve(approvalTarget.spender, amount);
409
563
  }
410
564
  async isPluginApproved(plugin, type) {
411
565
  if (plugin == 'none') {
@@ -545,8 +699,22 @@ class CToken extends Calldata_1.Calldata {
545
699
  receiver ??= signer.address;
546
700
  owner ??= signer.address;
547
701
  const shares = this.convertTokenInputToShares(amount);
548
- const calldata = this.getCallData("redeemCollateral", [shares, receiver, owner]);
549
- 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);
550
718
  }
551
719
  async postCollateral(amount) {
552
720
  const signer = this.requireSigner();
@@ -555,6 +723,9 @@ class CToken extends Calldata_1.Calldata {
555
723
  const collateral = await this.fetchUserCollateral();
556
724
  const available_shares = balance - collateral;
557
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
+ }
558
729
  const calldata = this.getCallData("postCollateral", [max_shares]);
559
730
  const tx = await this.oracleRoute(calldata);
560
731
  // Reload collateral state after execution
@@ -591,56 +762,56 @@ class CToken extends Calldata_1.Calldata {
591
762
  }
592
763
  return asset.balanceOf(signer.address, false);
593
764
  }
594
- // TODO: Hack to remove
595
765
  async ensureUnderlyingAmount(amount, zap) {
596
766
  const balance = await this.getZapBalance(zap);
597
- const isZapping = typeof zap === 'object' && zap.type !== 'none';
598
- // Use the zap input token's decimals when zapping, otherwise the deposit token's decimals
599
- let decimals = this.asset.decimals;
600
- if (isZapping && zap.inputToken) {
601
- if (zap.inputToken.toLowerCase() === helpers_1.NATIVE_ADDRESS.toLowerCase()) {
602
- decimals = 18n;
603
- }
604
- else {
605
- const inputErc20 = new ERC20_1.ERC20(this.provider, zap.inputToken, undefined, undefined, this.signer);
606
- decimals = inputErc20.decimals ?? await inputErc20.contract.decimals();
607
- }
608
- }
767
+ const decimals = await this.getZapInputDecimals(zap);
609
768
  const assets = FormatConverter_1.default.decimalToBigInt(amount, decimals);
610
769
  if (assets > balance) {
611
- console.warn('[WARNING] Detected higher deposit amount then underlying balance, changing to the underlying balance. Diff: ', {
612
- balance: balance,
613
- formatted: FormatConverter_1.default.bigIntToDecimal(balance, decimals),
614
- attempt: {
615
- raw: assets,
616
- formatted: amount
617
- },
618
- });
619
- 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()}.`);
620
772
  }
621
773
  return amount;
622
774
  }
623
- async removeCollateral(amount, removeAll = false) {
624
- const current_shares = await this.fetchUserCollateral();
625
- let max_shares;
626
- if (removeAll) {
627
- 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;
628
786
  }
629
- else {
630
- const shares = this.convertTokenInputToShares(amount);
631
- max_shares = current_shares < shares ? current_shares : shares;
632
- // If within 0.1% of full collateral, remove everything to avoid dust
633
- const threshold = current_shares / 1000n || 10n;
634
- if (current_shares - max_shares <= threshold) {
635
- max_shares = current_shares;
636
- }
787
+ return shares;
788
+ }
789
+ async executeCollateralRemoval(shares) {
790
+ if (shares === 0n) {
791
+ throw new Error("No removable collateral available.");
637
792
  }
638
- const calldata = this.getCallData("removeCollateral", [max_shares]);
793
+ const calldata = this.getCallData("removeCollateral", [shares]);
639
794
  const tx = await this.oracleRoute(calldata);
640
795
  // Reload collateral state after execution
641
796
  await this.fetchUserCollateral();
642
797
  return tx;
643
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
+ }
644
815
  convertTokenInputToShares(amount) {
645
816
  return this.virtualConvertToShares(FormatConverter_1.default.decimalToBigInt(amount, this.asset.decimals));
646
817
  }
@@ -661,7 +832,7 @@ class CToken extends Calldata_1.Calldata {
661
832
  async convertToAssets(shares) {
662
833
  return this.contract.convertToAssets(shares);
663
834
  }
664
- async convertToShares(assets, bufferBps = 2n) {
835
+ async convertToShares(assets, bufferBps = exports.LEVERAGE.SHARES_BUFFER_BPS) {
665
836
  const shares = await this.contract.convertToShares(assets);
666
837
  return bufferBps > 0n ? shares * (10000n - bufferBps) / 10000n : shares;
667
838
  }
@@ -695,7 +866,7 @@ class CToken extends Calldata_1.Calldata {
695
866
  interface: new NativeToken_1.NativeToken(this.currentChain, this.provider, this.setup.contracts.OracleManager, this.signer, this.account),
696
867
  type: 'native-vault'
697
868
  });
698
- 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());
699
870
  }
700
871
  if (this.zapTypes.includes('native-simple')) {
701
872
  tokens.push({
@@ -703,7 +874,7 @@ class CToken extends Calldata_1.Calldata {
703
874
  type: 'native-simple'
704
875
  });
705
876
  if (!this.zapTypes.includes('native-vault')) {
706
- 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());
707
878
  }
708
879
  }
709
880
  if (this.zapTypes.includes('vault')) {
@@ -714,7 +885,7 @@ class CToken extends Calldata_1.Calldata {
714
885
  });
715
886
  tokens_exclude.push(vault_asset.address.toLocaleLowerCase());
716
887
  }
717
- if (this.zapTypes.includes('simple')) {
888
+ if (this.zapTypes.includes('simple') && this.currentChainConfig.dexAgg.router !== helpers_1.EMPTY_ADDRESS) {
718
889
  let dexAggSearch = await this.currentChainConfig.dexAgg.getAvailableTokens(this.provider, search, this.account);
719
890
  tokens = tokens.concat(dexAggSearch.filter(token => !tokens_exclude.includes(token.interface.address.toLocaleLowerCase())));
720
891
  // Add native MON as a zap option for any token with a simple zapper
@@ -758,6 +929,7 @@ class CToken extends Calldata_1.Calldata {
758
929
  this.cache.assetPrice = snapshot.collateralAssetPrice;
759
930
  this.cache.sharePrice = snapshot.sharePrice;
760
931
  borrow.cache.assetPrice = snapshot.debtAssetPrice;
932
+ this.market.cache.user.collateral = snapshot.collateralUsd;
761
933
  this.market.cache.user.debt = snapshot.debtUsd;
762
934
  return snapshot;
763
935
  }
@@ -771,47 +943,133 @@ class CToken extends Calldata_1.Calldata {
771
943
  * equity-fraction denominator amplifies it by (L-1)x automatically.
772
944
  * The user's swap-level slippage (passed separately to _swapSafe) is
773
945
  * unaffected — that's the layer that bounds MEV extraction.
946
+ *
947
+ * Applied uniformly to simple AND vault/native-vault leverage-up paths.
948
+ * Simple path uses the buffer for share-rounding + Redstone drift as
949
+ * described above. Vault paths inherit the flat 10 bps through the
950
+ * shared `slippage` variable before the per-branch
951
+ * `amplifyContractSlippage(..., LEVERAGE_UP_VAULT_DRIFT_BPS)` expansion;
952
+ * the flat addition is not amplified (base term stays flat) and covers
953
+ * the same residual class (share-rounding, oracle drift) on vault paths
954
+ * too. Removing the buffer for vault would save a trivial amount of
955
+ * user slippage budget at the cost of a false-negative risk on the
956
+ * residuals — we keep it for symmetry.
774
957
  */
775
958
  _leverageUpSlippage(slippage, leverage) {
776
959
  if (leverage.lte(1))
777
960
  return slippage;
778
- return slippage + LEVERAGE.LEVERAGE_UP_BUFFER_BPS;
961
+ return slippage + exports.LEVERAGE.LEVERAGE_UP_BUFFER_BPS;
962
+ }
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
+ }
779
983
  }
780
- previewLeverageUp(newLeverage, borrow, depositAmount) {
781
- const currentLeverage = this.getLeverage() ?? (0, decimal_js_1.default)(0);
782
- if (newLeverage.lte(currentLeverage)) {
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)) {
783
1025
  throw new Error("New leverage must be more than current leverage");
784
1026
  }
785
- if (newLeverage.gt(this.maxLeverage)) {
786
- 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.");
787
1031
  }
788
- const collateralAvail = this.cache.userCollateral + (depositAmount ? depositAmount : BigInt(0));
789
- const collateralInUsd = this.convertTokensToUsd(collateralAvail, false);
790
- const currentDebt = this.market.userDebt;
791
- const notional = collateralInUsd.sub(currentDebt);
792
- const leverageFactor = newLeverage.sub(1);
793
1032
  const borrowPrice = borrow.getPrice(true);
794
- const rawDebtInUsd = notional.mul(newLeverage).sub(notional);
795
- const borrowAmount = rawDebtInUsd.sub(currentDebt).div(borrowPrice);
796
- // Fee preview: queried from the configured fee policy.
797
- const borrowAssets = FormatConverter_1.default.decimalToBigInt(borrowAmount, borrow.asset.decimals);
798
- const feeBps = this.setup.feePolicy.getFeeBps({
799
- operation: 'leverage-up',
800
- inputToken: borrow.asset.address,
801
- outputToken: this.asset.address,
802
- inputAmount: borrowAssets,
803
- currentLeverage,
804
- targetLeverage: newLeverage,
805
- });
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;
806
1055
  const feeAssets = borrowAmount.mul((0, decimal_js_1.default)(Number(feeBps))).div((0, decimal_js_1.default)(10000));
807
1056
  const feeUsd = feeAssets.mul(borrowPrice);
808
- // Subtract fee from displayed collateral — the fee reduces swap
809
- // output, so the user receives less collateral than rawDebtInUsd.
810
- 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);
811
1061
  return {
1062
+ currentLeverage,
1063
+ effectiveCurrentLeverage,
1064
+ targetLeverage: resolvedTargetLeverage,
812
1065
  borrowAmount,
1066
+ borrowAssets,
1067
+ debtIncrease,
1068
+ debtIncreaseInAssets: borrowAmount,
813
1069
  newDebt: rawDebtInUsd,
814
1070
  newDebtInAssets: borrow.convertUsdToTokens(rawDebtInUsd, true),
1071
+ collateralIncrease,
1072
+ collateralIncreaseInAssets,
815
1073
  newCollateral: newCollateralInUsd,
816
1074
  newCollateralInAssets: this.convertUsdToTokens(newCollateralInUsd, true),
817
1075
  feeBps,
@@ -819,6 +1077,26 @@ class CToken extends Calldata_1.Calldata {
819
1077
  feeUsd,
820
1078
  };
821
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
+ }
822
1100
  previewLeverageDown(newLeverage, currentLeverage, borrow) {
823
1101
  if (newLeverage.gte(currentLeverage)) {
824
1102
  throw new Error("New leverage must be less than current leverage");
@@ -826,10 +1104,13 @@ class CToken extends Calldata_1.Calldata {
826
1104
  if (newLeverage.lt((0, decimal_js_1.default)(1))) {
827
1105
  throw new Error("New leverage must be at least 1");
828
1106
  }
829
- const collateralAvail = this.cache.userCollateral;
830
- const collateralInUsd = this.convertTokensToUsd(collateralAvail, false);
831
- const currentDebt = this.market.userDebt;
1107
+ const leverageState = this.getMarketLeverageState();
1108
+ const collateralInUsd = leverageState.currentCollateralInUsd;
1109
+ const currentDebt = leverageState.currentDebt;
832
1110
  const equity = collateralInUsd.sub(currentDebt);
1111
+ if (equity.lte(0)) {
1112
+ throw new Error("Position has no positive equity to deleverage.");
1113
+ }
833
1114
  const targetCollateralUsd = equity.mul(newLeverage);
834
1115
  const newDebtUsd = targetCollateralUsd.sub(equity);
835
1116
  const collateralAssetReductionUsd = collateralInUsd.sub(targetCollateralUsd);
@@ -870,22 +1151,30 @@ class CToken extends Calldata_1.Calldata {
870
1151
  async leverageUp(borrow, newLeverage, type, slippage_ = (0, decimal_js_1.default)(0.05), simulate = false) {
871
1152
  try {
872
1153
  this.requireSigner();
873
- const slippage = this._leverageUpSlippage(FormatConverter_1.default.percentageToBps(slippage_), newLeverage);
874
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
+ }
875
1161
  let calldata;
876
1162
  await this._getLeverageSnapshot(borrow);
877
- 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);
878
1176
  switch (type) {
879
1177
  case 'simple': {
880
- const borrowAssets = FormatConverter_1.default.decimalToBigInt(borrowAmount, borrow.asset.decimals);
881
- const feeBps = this.setup.feePolicy.getFeeBps({
882
- operation: 'leverage-up',
883
- inputToken: borrow.asset.address,
884
- outputToken: this.asset.address,
885
- inputAmount: borrowAssets,
886
- currentLeverage: this.getLeverage() ?? (0, decimal_js_1.default)(1),
887
- targetLeverage: newLeverage,
888
- });
889
1178
  const feeReceiver = feeBps > 0n ? this.setup.feePolicy.feeReceiver : undefined;
890
1179
  const { action, quote } = await this.currentChainConfig.dexAgg.quoteAction(manager.address, borrow.asset.address, this.asset.address, borrowAssets, slippage, feeBps, feeReceiver);
891
1180
  // Fee-aware slippage expansion now lives inside KyberSwap.quoteAction
@@ -896,12 +1185,12 @@ class CToken extends Calldata_1.Calldata {
896
1185
  // as equity loss amplified by (L-1) — same pattern as
897
1186
  // deleverage. Expand the contract-level tolerance to absorb it.
898
1187
  // See `amplifyContractSlippage` in helpers.ts for rationale.
899
- const contractSlippage = (0, helpers_1.amplifyContractSlippage)(slippage, newLeverage.sub(1), feeBps);
1188
+ const contractSlippage = (0, helpers_1.amplifyContractSlippage)(slippage, targetLeverage.sub(1), feeBps);
900
1189
  calldata = manager.getLeverageCalldata({
901
1190
  borrowableCToken: borrow.address,
902
- borrowAssets: FormatConverter_1.default.decimalToBigInt(borrowAmount, borrow.asset.decimals),
1191
+ borrowAssets: borrowAssets,
903
1192
  cToken: this.address,
904
- expectedShares: this.virtualConvertToShares(BigInt(quote.min_out), LEVERAGE.SHARES_BUFFER_BPS),
1193
+ expectedShares: this.virtualConvertToShares(BigInt(quote.min_out), exports.LEVERAGE.SHARES_BUFFER_BPS),
905
1194
  swapAction: action,
906
1195
  auxData: "0x",
907
1196
  }, FormatConverter_1.default.bpsToBpsWad(contractSlippage));
@@ -909,14 +1198,20 @@ class CToken extends Calldata_1.Calldata {
909
1198
  }
910
1199
  case 'native-vault':
911
1200
  case 'vault': {
1201
+ // No DEX leg, so no fee-driven forced loss to absorb.
1202
+ // The `(L-1)×K` expansion here covers the vault-token
1203
+ // collateral drift between the vault's fundamental mint
1204
+ // rate at tx time and the stored oracle price that
1205
+ // `checkSlippage` reads. See LEVERAGE_UP_VAULT_DRIFT_BPS.
1206
+ const contractSlippage = (0, helpers_1.amplifyContractSlippage)(slippage, targetLeverage.sub(1), exports.LEVERAGE.LEVERAGE_UP_VAULT_DRIFT_BPS);
912
1207
  calldata = manager.getLeverageCalldata({
913
1208
  borrowableCToken: borrow.address,
914
- borrowAssets: FormatConverter_1.default.decimalToBigInt(borrowAmount, borrow.asset.decimals),
1209
+ borrowAssets: borrowAssets,
915
1210
  cToken: this.address,
916
1211
  expectedShares: await PositionManager_1.PositionManager.getVaultExpectedShares(this, borrow, borrowAmount),
917
1212
  swapAction: PositionManager_1.PositionManager.emptySwapAction(),
918
1213
  auxData: "0x",
919
- }, FormatConverter_1.default.bpsToBpsWad(slippage));
1214
+ }, FormatConverter_1.default.bpsToBpsWad(contractSlippage));
920
1215
  break;
921
1216
  }
922
1217
  default:
@@ -926,7 +1221,6 @@ class CToken extends Calldata_1.Calldata {
926
1221
  }
927
1222
  if (simulate)
928
1223
  return this.simulateOracleRoute(calldata, { to: manager.address });
929
- await this._checkPositionManagerApproval(manager);
930
1224
  return this.oracleRoute(calldata, { to: manager.address });
931
1225
  }
932
1226
  catch (error) {
@@ -946,10 +1240,15 @@ class CToken extends Calldata_1.Calldata {
946
1240
  const config = this.currentChainConfig;
947
1241
  const slippage = (0, helpers_1.toBps)(slippage_);
948
1242
  const manager = this.getPositionManager(type);
1243
+ if (type === 'simple') {
1244
+ this.assertSimpleLeverageSwapAssetsDiffer(borrowToken);
1245
+ }
949
1246
  let calldata;
950
1247
  const snapshot = await this._getLeverageSnapshot(borrowToken);
951
- const { collateralAssetReduction } = this.previewLeverageDown(newLeverage, currentLeverage);
1248
+ const preview = this.previewLeverageDown(newLeverage, currentLeverage);
1249
+ const { collateralAssetReduction } = preview;
952
1250
  const isFullDeleverage = newLeverage.equals(1);
1251
+ const maxTokenCollateral = this.virtualConvertToAssets(this.readFreshUserCache("userCollateral", "executing leverage down"));
953
1252
  switch (type) {
954
1253
  case 'simple': {
955
1254
  let swapCollateral = collateralAssetReduction;
@@ -990,20 +1289,33 @@ class CToken extends Calldata_1.Calldata {
990
1289
  // Total overhead = base overhead (DEX impact + drift) + fee bps.
991
1290
  // Additive approximation is accurate to sub-bp at typical
992
1291
  // fee+overhead magnitudes (< 100 bps combined).
993
- const overheadBps = LEVERAGE.DELEVERAGE_OVERHEAD_BPS + feeBps;
1292
+ const overheadBps = exports.LEVERAGE.DELEVERAGE_OVERHEAD_BPS + feeBps;
994
1293
  swapCollateral = debtInCollateral * (10000n + overheadBps) / 10000n;
995
- const maxCollateral = this.virtualConvertToAssets(this.cache.userCollateral);
996
- if (swapCollateral > maxCollateral) {
997
- 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);
998
1300
  }
999
1301
  }
1000
- else if (feeBps > 0n) {
1001
- // Partial deleverage: inflate swap size to compensate
1002
- // for fee deduction on input. KyberSwap deducts feeBps
1003
- // from input before swapping, so without compensation
1004
- // the swap underdelivers and actual leverage is slightly
1005
- // higher than target.
1006
- swapCollateral = swapCollateral * 10000n / (10000n - feeBps);
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);
1311
+ }
1312
+ }
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);
1007
1319
  }
1008
1320
  const { action, quote } = await config.dexAgg.quoteAction(manager.address, this.asset.address, borrowToken.asset.address, swapCollateral, slippage, feeBps, feeReceiver);
1009
1321
  // Fee-aware slippage expansion for `_swapSafe` is handled by
@@ -1025,7 +1337,7 @@ class CToken extends Calldata_1.Calldata {
1025
1337
  ? currentLeverage.sub(1)
1026
1338
  : currentLeverage.sub(newLeverage);
1027
1339
  const forcedBps = isFullDeleverage
1028
- ? LEVERAGE.DELEVERAGE_OVERHEAD_BPS + feeBps
1340
+ ? exports.LEVERAGE.DELEVERAGE_OVERHEAD_BPS + feeBps
1029
1341
  : feeBps;
1030
1342
  const contractSlippage = (0, helpers_1.amplifyContractSlippage)(slippage, leverageDelta, forcedBps);
1031
1343
  calldata = manager.getDeleverageCalldata({
@@ -1045,7 +1357,6 @@ class CToken extends Calldata_1.Calldata {
1045
1357
  }
1046
1358
  if (simulate)
1047
1359
  return this.simulateOracleRoute(calldata, { to: manager.address });
1048
- await this._checkPositionManagerApproval(manager);
1049
1360
  return this.oracleRoute(calldata, { to: manager.address });
1050
1361
  }
1051
1362
  catch (error) {
@@ -1062,35 +1373,44 @@ class CToken extends Calldata_1.Calldata {
1062
1373
  throw new Error("Multiplier must be greater than 1");
1063
1374
  }
1064
1375
  depositAmount = await this.ensureUnderlyingAmount(depositAmount, 'none');
1065
- const slippage = this._leverageUpSlippage((0, helpers_1.toBps)(slippage_), multiplier);
1066
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
+ }
1067
1383
  let calldata;
1068
1384
  const depositAssets = FormatConverter_1.default.decimalToBigInt(depositAmount, this.asset.decimals);
1385
+ await this._checkTokenApproval(this.getPositionManagerDepositApprovalTarget(manager), depositAssets);
1069
1386
  await this._getLeverageSnapshot(borrow);
1070
- 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);
1071
1400
  switch (type) {
1072
1401
  case 'simple': {
1073
- const borrowAssets = FormatConverter_1.default.decimalToBigInt(borrowAmount, borrow.asset.decimals);
1074
- const feeBps = this.setup.feePolicy.getFeeBps({
1075
- operation: 'deposit-and-leverage',
1076
- inputToken: borrow.asset.address,
1077
- outputToken: this.asset.address,
1078
- inputAmount: borrowAssets,
1079
- currentLeverage: this.getLeverage() ?? (0, decimal_js_1.default)(1),
1080
- targetLeverage: multiplier,
1081
- });
1082
1402
  const feeReceiver = feeBps > 0n ? this.setup.feePolicy.feeReceiver : undefined;
1083
1403
  const { action, quote } = await this.currentChainConfig.dexAgg.quoteAction(manager.address, borrow.asset.address, this.asset.address, borrowAssets, slippage, feeBps, feeReceiver);
1084
1404
  // Fee-aware slippage expansion for `_swapSafe` is handled by
1085
1405
  // KyberSwap.quoteAction. See KyberSwap.ts for rationale.
1086
1406
  // Fee amplification: same pattern as leverageUp. See
1087
1407
  // `amplifyContractSlippage` in helpers.ts.
1088
- const contractSlippage = (0, helpers_1.amplifyContractSlippage)(slippage, multiplier.sub(1), feeBps);
1408
+ const contractSlippage = (0, helpers_1.amplifyContractSlippage)(slippage, targetLeverage.sub(1), feeBps);
1089
1409
  calldata = manager.getDepositAndLeverageCalldata(FormatConverter_1.default.decimalToBigInt(depositAmount, this.asset.decimals), {
1090
1410
  borrowableCToken: borrow.address,
1091
1411
  borrowAssets: borrowAssets,
1092
1412
  cToken: this.address,
1093
- expectedShares: this.virtualConvertToShares(BigInt(quote.min_out), LEVERAGE.SHARES_BUFFER_BPS),
1413
+ expectedShares: this.virtualConvertToShares(BigInt(quote.min_out), exports.LEVERAGE.SHARES_BUFFER_BPS),
1094
1414
  swapAction: action,
1095
1415
  auxData: "0x",
1096
1416
  }, FormatConverter_1.default.bpsToBpsWad(contractSlippage));
@@ -1098,14 +1418,20 @@ class CToken extends Calldata_1.Calldata {
1098
1418
  }
1099
1419
  case 'native-vault':
1100
1420
  case 'vault': {
1421
+ // Mirrors the leverageUp vault branch: absorb (L-1) ×
1422
+ // LEVERAGE_UP_VAULT_DRIFT_BPS for vault-token collateral
1423
+ // drift. Uses `multiplier.sub(1)` per the per-call-site
1424
+ // asymmetry documented in helpers.ts (depositAndLeverage
1425
+ // leverageDelta = multiplier - 1).
1426
+ const contractSlippage = (0, helpers_1.amplifyContractSlippage)(slippage, targetLeverage.sub(1), exports.LEVERAGE.LEVERAGE_UP_VAULT_DRIFT_BPS);
1101
1427
  calldata = manager.getDepositAndLeverageCalldata(FormatConverter_1.default.decimalToBigInt(depositAmount, this.asset.decimals), {
1102
1428
  borrowableCToken: borrow.address,
1103
- borrowAssets: FormatConverter_1.default.decimalToBigInt(borrowAmount, borrow.asset.decimals),
1429
+ borrowAssets: borrowAssets,
1104
1430
  cToken: this.address,
1105
1431
  expectedShares: await PositionManager_1.PositionManager.getVaultExpectedShares(this, borrow, borrowAmount),
1106
1432
  swapAction: PositionManager_1.PositionManager.emptySwapAction(),
1107
1433
  auxData: "0x",
1108
- }, FormatConverter_1.default.bpsToBpsWad(slippage));
1434
+ }, FormatConverter_1.default.bpsToBpsWad(contractSlippage));
1109
1435
  break;
1110
1436
  }
1111
1437
  default:
@@ -1115,7 +1441,6 @@ class CToken extends Calldata_1.Calldata {
1115
1441
  }
1116
1442
  if (simulate)
1117
1443
  return this.simulateOracleRoute(calldata, { to: manager.address });
1118
- await this._checkPositionManagerApproval(manager);
1119
1444
  return this.oracleRoute(calldata, { to: manager.address });
1120
1445
  }
1121
1446
  catch (error) {
@@ -1129,19 +1454,10 @@ class CToken extends Calldata_1.Calldata {
1129
1454
  amount = await this.ensureUnderlyingAmount(amount, zap);
1130
1455
  const signer = this.requireSigner();
1131
1456
  receiver ??= signer.address;
1132
- const isZapping = typeof zap === 'object' && zap.type !== 'none';
1133
1457
  const depositAssets = FormatConverter_1.default.decimalToBigInt(amount, this.asset.decimals);
1134
- let zapAssets = depositAssets;
1135
- if (isZapping && zap.inputToken) {
1136
- const isNative = zap.inputToken.toLowerCase() === helpers_1.NATIVE_ADDRESS.toLowerCase();
1137
- const zapDecimals = isNative ? 18n : (() => {
1138
- const inputErc20 = new ERC20_1.ERC20(this.provider, zap.inputToken, undefined, undefined, this.signer);
1139
- return inputErc20.decimals ?? inputErc20.contract.decimals();
1140
- })();
1141
- zapAssets = FormatConverter_1.default.decimalToBigInt(amount, await zapDecimals);
1142
- }
1458
+ const zapAssets = await this.getZapAssetAmount(amount, zap);
1143
1459
  const default_calldata = this.getCallData("deposit", [depositAssets, receiver]);
1144
- 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);
1145
1461
  return this.simulateOracleRoute(calldata, calldata_overrides);
1146
1462
  }
1147
1463
  catch (error) {
@@ -1153,33 +1469,27 @@ class CToken extends Calldata_1.Calldata {
1153
1469
  amount = await this.ensureUnderlyingAmount(amount, zap);
1154
1470
  const signer = this.requireSigner();
1155
1471
  receiver ??= signer.address;
1156
- const isZapping = typeof zap === 'object' && zap.type !== 'none';
1157
1472
  const depositAssets = FormatConverter_1.default.decimalToBigInt(amount, this.asset.decimals);
1158
- let zapAssets = depositAssets;
1159
- if (isZapping && zap.inputToken) {
1160
- const isNative = zap.inputToken.toLowerCase() === helpers_1.NATIVE_ADDRESS.toLowerCase();
1161
- const zapDecimals = isNative ? 18n : (() => {
1162
- const inputErc20 = new ERC20_1.ERC20(this.provider, zap.inputToken, undefined, undefined, this.signer);
1163
- return inputErc20.decimals ?? inputErc20.contract.decimals();
1164
- })();
1165
- zapAssets = FormatConverter_1.default.decimalToBigInt(amount, await zapDecimals);
1166
- }
1167
- const default_calldata = this.getCallData("depositAsCollateral", [depositAssets, receiver]);
1168
- 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);
1169
1479
  return this.simulateOracleRoute(calldata, calldata_overrides);
1170
1480
  }
1171
1481
  catch (error) {
1172
1482
  return { success: false, error: error?.reason || error?.message || String(error) };
1173
1483
  }
1174
1484
  }
1175
- async zap(assets, zap, collateralize = false, default_calldata) {
1485
+ async zap(assets, zap, collateralize = false, default_calldata, receiver = this.requireSigner().address) {
1176
1486
  let calldata;
1177
1487
  let calldata_overrides = {};
1178
1488
  let slippage = 0n;
1179
1489
  let inputToken = null;
1180
1490
  let type_of_zap;
1181
1491
  if (typeof zap == 'object') {
1182
- slippage = BigInt(zap.slippage.mul(helpers_1.BPS).toString());
1492
+ slippage = FormatConverter_1.default.percentageToBps(zap.slippage);
1183
1493
  inputToken = zap.inputToken;
1184
1494
  type_of_zap = zap.type;
1185
1495
  }
@@ -1197,20 +1507,20 @@ class CToken extends Calldata_1.Calldata {
1197
1507
  case 'simple':
1198
1508
  if (inputToken == null)
1199
1509
  throw new Error("Input token must be provided for simple zap");
1200
- 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);
1201
1511
  const isNativeSimpleZap = inputToken.toLowerCase() === helpers_1.NATIVE_ADDRESS.toLowerCase();
1202
1512
  calldata_overrides = isNativeSimpleZap ? { value: assets, to: zapper.address } : { to: zapper.address };
1203
1513
  break;
1204
1514
  case 'vault':
1205
- calldata = await zapper.getVaultZapCalldata(this, assets, collateralize);
1515
+ calldata = await zapper.getVaultZapCalldata(this, assets, collateralize, false, receiver);
1206
1516
  calldata_overrides = { to: zapper.address };
1207
1517
  break;
1208
1518
  case 'native-vault':
1209
- calldata = await zapper.getNativeZapCalldata(this, assets, collateralize);
1519
+ calldata = await zapper.getNativeZapCalldata(this, assets, collateralize, false, receiver);
1210
1520
  calldata_overrides = { value: assets, to: zapper.address };
1211
1521
  break;
1212
1522
  case 'native-simple':
1213
- calldata = await zapper.getNativeZapCalldata(this, assets, collateralize, true);
1523
+ calldata = await zapper.getNativeZapCalldata(this, assets, collateralize, true, receiver);
1214
1524
  calldata_overrides = { value: assets, to: zapper.address };
1215
1525
  break;
1216
1526
  default:
@@ -1222,84 +1532,50 @@ class CToken extends Calldata_1.Calldata {
1222
1532
  amount = await this.ensureUnderlyingAmount(amount, zap);
1223
1533
  const signer = this.requireSigner();
1224
1534
  receiver ??= signer.address;
1225
- // When zapping, the swap amount uses input token decimals, but the
1226
- // default deposit calldata uses the deposit token decimals.
1227
- const isZapping = typeof zap === 'object' && zap.type !== 'none';
1228
1535
  const depositAssets = FormatConverter_1.default.decimalToBigInt(amount, this.asset.decimals);
1229
- let zapAssets = depositAssets;
1230
- if (isZapping && zap.inputToken) {
1231
- if (zap.inputToken.toLowerCase() === helpers_1.NATIVE_ADDRESS.toLowerCase()) {
1232
- zapAssets = FormatConverter_1.default.decimalToBigInt(amount, 18n);
1233
- }
1234
- else {
1235
- const inputErc20 = new ERC20_1.ERC20(this.provider, zap.inputToken, undefined, undefined, this.signer);
1236
- const zapDecimals = inputErc20.decimals ?? await inputErc20.contract.decimals();
1237
- zapAssets = FormatConverter_1.default.decimalToBigInt(amount, zapDecimals);
1238
- }
1239
- }
1240
- const zapType = typeof zap == 'object' ? zap.type : zap;
1241
- const isNative = zapType == 'native-simple' || zapType == 'native-vault' || zapType == 'none'
1242
- || (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);
1243
1538
  const default_calldata = this.getCallData("deposit", [depositAssets, receiver]);
1244
- const { calldata, calldata_overrides } = await this.zap(zapAssets, zap, false, default_calldata);
1245
- if (isNative) {
1246
- await this._checkAssetApproval(depositAssets);
1247
- }
1248
- else {
1249
- const zapper = this.getZapper(zapType);
1250
- if (!zapper) {
1251
- throw new Error(`No zapper contract found for type '${zapType}' on ${this.symbol}`);
1252
- }
1253
- await this._checkDepositApprovals(zapper, zapAssets);
1254
- }
1255
- 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);
1256
1541
  }
1257
1542
  async depositAsCollateral(amount, zap = 'none', receiver = null) {
1258
1543
  amount = await this.ensureUnderlyingAmount(amount, zap);
1259
1544
  const signer = this.requireSigner();
1260
1545
  receiver ??= signer.address;
1261
- // When zapping, the swap amount uses input token decimals, but collateral
1262
- // cap checks and the default deposit calldata use the deposit token decimals.
1263
- const isZapping = typeof zap === 'object' && zap.type !== 'none';
1264
1546
  const depositAssets = FormatConverter_1.default.decimalToBigInt(amount, this.asset.decimals);
1265
- let zapAssets = depositAssets;
1266
- if (isZapping && zap.inputToken) {
1267
- if (zap.inputToken.toLowerCase() === helpers_1.NATIVE_ADDRESS.toLowerCase()) {
1268
- zapAssets = FormatConverter_1.default.decimalToBigInt(amount, 18n);
1269
- }
1270
- else {
1271
- const inputErc20 = new ERC20_1.ERC20(this.provider, zap.inputToken, undefined, undefined, this.signer);
1272
- const zapDecimals = inputErc20.decimals ?? await inputErc20.contract.decimals();
1273
- zapAssets = FormatConverter_1.default.decimalToBigInt(amount, zapDecimals);
1274
- }
1275
- }
1276
- if (!isZapping) {
1547
+ const zapAssets = await this.getZapAssetAmount(amount, zap);
1548
+ if (!this.isZapInstruction(zap)) {
1277
1549
  const collateralCapError = "There is not enough collateral left in this tokens collateral cap for this deposit.";
1278
1550
  const remainingCollateral = this.getRemainingCollateral(false);
1279
- if (remainingCollateral == 0n)
1551
+ if (remainingCollateral <= 0n)
1280
1552
  throw new Error(collateralCapError);
1281
1553
  if (remainingCollateral > 0n) {
1282
- const shares = this.virtualConvertToShares(depositAssets, LEVERAGE.SHARES_BUFFER_BPS);
1554
+ const shares = this.virtualConvertToShares(depositAssets);
1283
1555
  if (shares > remainingCollateral) {
1284
1556
  throw new Error(collateralCapError);
1285
1557
  }
1286
1558
  }
1287
1559
  }
1288
- const default_calldata = this.getCallData("depositAsCollateral", [depositAssets, receiver]);
1289
- const { calldata, calldata_overrides, zapper } = await this.zap(zapAssets, zap, true, default_calldata);
1290
- await this._checkDepositApprovals(zapper, zapAssets);
1291
- 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);
1292
1567
  }
1293
1568
  async redeem(amount) {
1294
1569
  const signer = this.requireSigner();
1295
1570
  const receiver = signer.address;
1296
1571
  const owner = signer.address;
1297
- const buffer = this.market.userDebt.greaterThan(0) ? 100n : 0n;
1572
+ const buffer = this.getExecutionDebtBufferTime();
1298
1573
  const balance_avail = await this.balanceOf(signer.address);
1299
1574
  const max_shares = await this.maxRedemption(true, buffer);
1300
1575
  const converted_shares = this.convertTokenInputToShares(amount);
1301
- let shares = max_shares < converted_shares ? max_shares : converted_shares;
1302
- 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) {
1303
1579
  shares = balance_avail;
1304
1580
  }
1305
1581
  const calldata = this.getCallData("redeem", [shares, receiver, owner]);
@@ -1322,6 +1598,7 @@ class CToken extends Calldata_1.Calldata {
1322
1598
  const snapshot = await this.contract.getSnapshot(account);
1323
1599
  return {
1324
1600
  asset: snapshot.asset,
1601
+ underlying: snapshot.underlying,
1325
1602
  decimals: BigInt(snapshot.decimals),
1326
1603
  isCollateral: snapshot.isCollateral,
1327
1604
  collateralPosted: BigInt(snapshot.collateralPosted),
@@ -1355,19 +1632,49 @@ class CToken extends Calldata_1.Calldata {
1355
1632
  const decimals = this.asset.decimals ?? this.decimals;
1356
1633
  return FormatConverter_1.default.bigIntTokensToUsd(tokenAmount, price, decimals);
1357
1634
  }
1358
- async convertSharesToUsd(tokenAmount) {
1359
- tokenAmount = this.virtualConvertToShares(tokenAmount);
1635
+ convertSharesToUsdSync(tokenAmount) {
1360
1636
  const price = this.getPrice(false, false, false);
1361
1637
  const decimals = this.decimals;
1362
1638
  return FormatConverter_1.default.bigIntTokensToUsd(tokenAmount, price, decimals);
1363
1639
  }
1364
- 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) {
1365
1649
  return {
1366
- target: this.address,
1650
+ target,
1367
1651
  isPriceUpdate: false,
1368
1652
  data: calldata
1369
1653
  };
1370
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
+ }
1371
1678
  async _checkPositionManagerApproval(manager) {
1372
1679
  const isApproved = await this.isPluginApproved(manager.type, 'positionManager');
1373
1680
  if (!isApproved) {
@@ -1375,71 +1682,172 @@ class CToken extends Calldata_1.Calldata {
1375
1682
  }
1376
1683
  }
1377
1684
  async _checkZapperApproval(zapper) {
1378
- if (!this.setup.approval_protection) {
1379
- 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.`);
1380
1688
  }
1381
- if (this.setup.approval_protection && zapper) {
1382
- const plugin_allowed = await this.isPluginApproved(zapper.type, 'zapper');
1383
- if (!plugin_allowed) {
1384
- throw new Error(`Please approve the ${zapper.type} Zapper to be able to move ${this.symbol} on your behalf.`);
1385
- }
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}.`);
1386
1694
  }
1387
1695
  }
1388
- async _checkErc20Approval(erc20_address, amount, spender) {
1389
- const signer = this.requireSigner();
1390
- const erc20 = new ERC20_1.ERC20(this.provider, erc20_address, undefined, undefined, signer);
1391
- const allowance = await erc20.allowance(signer.address, spender);
1392
- if (allowance < amount) {
1393
- const symbol = await erc20.fetchSymbol();
1394
- 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
+ };
1395
1742
  }
1396
1743
  }
1397
- async _checkAssetApproval(assets) {
1398
- 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) {
1399
1752
  return;
1400
1753
  }
1401
- const asset = this.getAsset(true);
1402
- const owner = this.getAccountOrThrow();
1403
- const allowance = await asset.allowance(owner, this.address);
1404
- if (allowance < assets) {
1405
- 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
+ }
1406
1762
  }
1763
+ throw new Error(`Please approve the ${tokenLabel} token for ${target.spenderLabel}`);
1407
1764
  }
1408
- async _checkDepositApprovals(zapper, assets) {
1409
- if (!this.setup.approval_protection) {
1410
- 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
+ }
1411
1783
  }
1412
- if (zapper) {
1413
- await this._checkZapperApproval(zapper);
1784
+ else if (collateralize && receiver && receiver.toLowerCase() !== signer.address.toLowerCase()) {
1785
+ await this._checkDelegateApproval(receiver, signer.address, "the connected signer");
1414
1786
  }
1415
- 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);
1416
1795
  }
1417
- async oracleRoute(calldata, override = {}) {
1796
+ async oracleRoute(calldata, override = {}, reloadAccount = null) {
1418
1797
  const signer = this.requireSigner();
1419
1798
  const price_updates = await this.getPriceUpdates();
1799
+ this.assertOracleMulticallSupportsValue(price_updates, override);
1420
1800
  if (price_updates.length > 0) {
1421
- const token_action = this.buildMultiCallAction(calldata);
1801
+ const actionTarget = (override.to ?? this.address);
1802
+ const token_action = this.buildMultiCallAction(calldata, actionTarget);
1422
1803
  calldata = this.getCallData("multicall", [[...price_updates, token_action]]);
1423
1804
  }
1424
1805
  const tx = await this.executeCallData(calldata, override);
1425
- 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
+ });
1426
1813
  return tx;
1427
1814
  }
1428
1815
  async simulateOracleRoute(calldata, override = {}) {
1429
1816
  const price_updates = await this.getPriceUpdates();
1817
+ this.assertOracleMulticallSupportsValue(price_updates, override);
1430
1818
  if (price_updates.length > 0) {
1431
- const token_action = this.buildMultiCallAction(calldata);
1819
+ const actionTarget = (override.to ?? this.address);
1820
+ const token_action = this.buildMultiCallAction(calldata, actionTarget);
1432
1821
  calldata = this.getCallData("multicall", [[...price_updates, token_action]]);
1433
1822
  }
1434
1823
  return this.simulateCallData(calldata, override);
1435
1824
  }
1436
- async getPriceUpdates() {
1437
- let price_updates = [];
1438
- if (this.adapters.includes(ProtocolReader_1.AdaptorTypes.REDSTONE_CORE)) {
1439
- const redstone = await Redstone_1.Redstone.buildMultiCallAction(this);
1440
- 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);
1441
1846
  }
1442
- return price_updates;
1847
+ return tokens;
1848
+ }
1849
+ async getPriceUpdates() {
1850
+ return Promise.all(this.getRedstonePriceUpdateTokens().map((token) => Redstone_1.Redstone.buildMultiCallAction(token)));
1443
1851
  }
1444
1852
  }
1445
1853
  exports.CToken = CToken;