curvance 4.0.1 → 4.0.3

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 (41) hide show
  1. package/dist/abis/ProtocolReader.json +64 -0
  2. package/dist/classes/CToken.d.ts +33 -6
  3. package/dist/classes/CToken.d.ts.map +1 -1
  4. package/dist/classes/CToken.js +280 -57
  5. package/dist/classes/CToken.js.map +1 -1
  6. package/dist/classes/DexAggregators/IDexAgg.d.ts +3 -1
  7. package/dist/classes/DexAggregators/IDexAgg.d.ts.map +1 -1
  8. package/dist/classes/DexAggregators/Kuru.d.ts +3 -3
  9. package/dist/classes/DexAggregators/Kuru.d.ts.map +1 -1
  10. package/dist/classes/DexAggregators/Kuru.js +19 -9
  11. package/dist/classes/DexAggregators/Kuru.js.map +1 -1
  12. package/dist/classes/DexAggregators/KyberSwap.d.ts +3 -3
  13. package/dist/classes/DexAggregators/KyberSwap.d.ts.map +1 -1
  14. package/dist/classes/DexAggregators/KyberSwap.js +16 -10
  15. package/dist/classes/DexAggregators/KyberSwap.js.map +1 -1
  16. package/dist/classes/DexAggregators/MultiDexAgg.d.ts +3 -3
  17. package/dist/classes/DexAggregators/MultiDexAgg.d.ts.map +1 -1
  18. package/dist/classes/DexAggregators/MultiDexAgg.js +13 -13
  19. package/dist/classes/DexAggregators/MultiDexAgg.js.map +1 -1
  20. package/dist/classes/OracleManager.js.map +1 -1
  21. package/dist/classes/ProtocolReader.d.ts +10 -0
  22. package/dist/classes/ProtocolReader.d.ts.map +1 -1
  23. package/dist/classes/ProtocolReader.js +4 -0
  24. package/dist/classes/ProtocolReader.js.map +1 -1
  25. package/dist/classes/Zapper.d.ts.map +1 -1
  26. package/dist/classes/Zapper.js +37 -1
  27. package/dist/classes/Zapper.js.map +1 -1
  28. package/dist/contracts/monad-mainnet.json +1 -1
  29. package/dist/feePolicy.d.ts +156 -0
  30. package/dist/feePolicy.d.ts.map +1 -0
  31. package/dist/feePolicy.js +102 -0
  32. package/dist/feePolicy.js.map +1 -0
  33. package/dist/index.d.ts +1 -0
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +1 -0
  36. package/dist/index.js.map +1 -1
  37. package/dist/setup.d.ts +8 -1
  38. package/dist/setup.d.ts.map +1 -1
  39. package/dist/setup.js +3 -1
  40. package/dist/setup.js.map +1 -1
  41. package/package.json +1 -1
@@ -18,6 +18,87 @@ const NativeToken_1 = require("./NativeToken");
18
18
  const ERC4626_1 = require("./ERC4626");
19
19
  const FormatConverter_1 = __importDefault(require("./FormatConverter"));
20
20
  const chains_1 = require("../chains");
21
+ const EXCLUDED_ZAP_SYMBOLS = new Set([
22
+ 'eBTC', 'earnAUSD', 'vUSD', 'syzUSD', 'ezETH', 'YZM', 'wsrUSD', 'sAUSD',
23
+ ]);
24
+ /**
25
+ * Leverage operation buffers — centralized for tuning.
26
+ * Calibrated for fresh-state operation via getLeverageSnapshot under
27
+ * Curvance's permanent single-oracle architecture.
28
+ *
29
+ * Single-oracle architecture (permanent design)
30
+ * ---------------------------------------------
31
+ * Curvance uses single-adaptor oracle configs only (Redstone Core/Classic
32
+ * via BaseOracleAdaptor, which ignores the getLower flag — see line 78 of
33
+ * BaseOracleAdaptor.sol). Dual-feed mode was deprecated in favor of the
34
+ * price-guard system and orderflow MEV tech, and is not coming back.
35
+ * This means MarketManager._statusOf returns symmetric prices for
36
+ * collateral (queries with getLower=true) and debt (getLower=false), so
37
+ * there is no oracle bound asymmetry contributing to checkSlippage forced
38
+ * loss. Buffers below are sized accordingly — do not re-introduce
39
+ * (L-1)-scaled buffers to "future-proof" against dual-feed.
40
+ *
41
+ * MEV / slippage protection model
42
+ * -------------------------------
43
+ * The on-chain BasePositionManager.checkSlippage modifier is per its own
44
+ * docstring "primarily a sanity check rather than a security guarantee."
45
+ * Real MEV protection comes from SwapperLib._swapSafe, which oracle-prices
46
+ * the swap input and output and reverts if realized slippage exceeds the
47
+ * Swap.slippage parameter we pass (= the user's raw slippage in WAD).
48
+ *
49
+ * That swap-level check bounds any sandwich extraction to the user's
50
+ * tolerance regardless of how the buffers below are tuned. The buffers
51
+ * here only adjust the contract-level sanity check so it doesn't fire
52
+ * false-positives from intentional or unavoidable forced losses.
53
+ *
54
+ * Asymmetry between leverage up and deleverage
55
+ * --------------------------------------------
56
+ * Leverage UP: under single-oracle, the contract sees zero forced loss
57
+ * for a perfect swap. The only real sources of difference between
58
+ * snapshot-time prices and execution-time prices are: (a) wei-level share
59
+ * rounding, (b) Redstone update drift between the snapshot RPC and the
60
+ * tx broadcast block. Both are small constants in absolute terms, NOT
61
+ * leverage-scaled. A small flat buffer suffices.
62
+ *
63
+ * DELEVERAGE (full): forced loss comes from intentional swap overshoot
64
+ * (DELEVERAGE_OVERHEAD_BPS) which prevents dust debt by oversizing the
65
+ * collateral→debt swap. This is a real bps-level loss in absolute terms
66
+ * which becomes (L-1) × bps in equity-fraction terms — so the deleverage
67
+ * contract-slippage expansion DOES scale with leverage. Note: the contract
68
+ * returns excess debt token to the user's wallet (BasePositionManager
69
+ * onRedeem lines 482-493), so the economic loss from the overshoot is
70
+ * zero — only the contract's naive equity-loss check sees it as loss.
71
+ */
72
+ const LEVERAGE = {
73
+ /** Max leverage cap: fraction of theoretical max the user can select.
74
+ * Prevents boundary singularity at exact max leverage. Independent of
75
+ * the slippage buffers below — protects post-op position health, not
76
+ * in-op slippage. */
77
+ MAX_LEVERAGE_FACTOR: (0, decimal_js_1.default)(0.995),
78
+ /** Flat BPS buffer added to leverage-up contract slippage tolerance.
79
+ * Under single-oracle, the only forced loss comes from wei-level share
80
+ * rounding plus possible Redstone price drift between the snapshot RPC
81
+ * and the tx broadcast block (typically same-block or 1-3 blocks
82
+ * later). Both are small constants in absolute terms; the equity-
83
+ * fraction amplification at high leverage happens automatically inside
84
+ * checkSlippage's denominator and does not require leverage-scaling
85
+ * the buffer itself. Conservative starting value — reduce after
86
+ * empirically observing successful leverage-up across the leverage
87
+ * range, especially at L > 5 with low (1%) user slippage. */
88
+ LEVERAGE_UP_BUFFER_BPS: 10n,
89
+ /** BPS overhead on full deleverage swap sizing — absolute terms.
90
+ * Oversizes the collateral→debt swap so DEX impact + drift doesn't
91
+ * underdeliver and leave dust debt. The contract returns any excess
92
+ * debt token to the user, so economic loss is zero — but the contract's
93
+ * checkSlippage modifier sees the overshoot as equity loss and amplifies
94
+ * it by (L-1)x. The deleverage contract slippage expansion compensates
95
+ * for that amplification (see leverageDown). Bump when aggregator fees
96
+ * are enabled to keep dust prevention reliable. */
97
+ DELEVERAGE_OVERHEAD_BPS: 20n,
98
+ /** BPS buffer on virtualConvertToShares for leverage + collateral cap.
99
+ * Covers exchange rate drift from interest accrual since cache load. */
100
+ SHARES_BUFFER_BPS: 2n,
101
+ };
21
102
  class CToken extends Calldata_1.Calldata {
22
103
  provider;
23
104
  address;
@@ -45,16 +126,7 @@ class CToken extends Calldata_1.Calldata {
45
126
  this.isNativeVault = chain_config.native_vaults.some(vault => vault.contract.toLowerCase() == assetAddr);
46
127
  this.isVault = chain_config.vaults.some(vault => vault.contract.toLowerCase() == assetAddr);
47
128
  this.isWrappedNative = chain_config.wrapped_native.toLowerCase() == assetAddr;
48
- if ([
49
- 'csAUSD',
50
- 'cwsrUSD',
51
- 'cezETH',
52
- 'csyzUSD',
53
- 'cearnAUSD',
54
- 'cYZM',
55
- 'cvUSD',
56
- 'ceBTC'
57
- ].includes(this.symbol)) {
129
+ if (EXCLUDED_ZAP_SYMBOLS.has(this.asset.symbol)) {
58
130
  return;
59
131
  }
60
132
  if (this.isNativeVault)
@@ -88,7 +160,7 @@ class CToken extends Calldata_1.Calldata {
88
160
  // to account for share rounding and fee losses that prevent reaching the exact max.
89
161
  const theoretical = (0, decimal_js_1.default)(this.cache.maxLeverage).div(helpers_1.BPS);
90
162
  const factor = theoretical.sub(1);
91
- return (0, decimal_js_1.default)(1).add(factor.mul((0, decimal_js_1.default)(0.99)));
163
+ return (0, decimal_js_1.default)(1).add(factor.mul(LEVERAGE.MAX_LEVERAGE_FACTOR));
92
164
  }
93
165
  get canLeverage() { return this.leverageTypes.length > 0; }
94
166
  get totalAssets() { return this.cache.totalAssets; }
@@ -105,8 +177,15 @@ class CToken extends Calldata_1.Calldata {
105
177
  virtualConvertToAssets(shares) {
106
178
  return (shares * this.totalAssets) / this.totalSupply;
107
179
  }
108
- virtualConvertToShares(assets) {
109
- return (assets * this.totalSupply) / this.totalAssets;
180
+ /**
181
+ * Convert assets to shares using cached totalSupply/totalAssets.
182
+ * @param bufferBps Optional downward buffer in BPS to account for
183
+ * exchange rate drift from interest accrual since cache load.
184
+ * Matches the buffer pattern in async convertToShares().
185
+ */
186
+ virtualConvertToShares(assets, bufferBps = 0n) {
187
+ const shares = (assets * this.totalSupply) / this.totalAssets;
188
+ return bufferBps > 0n ? shares * (10000n - bufferBps) / 10000n : shares;
110
189
  }
111
190
  getLeverage() {
112
191
  if (this.getUserCollateral(true).equals(0)) {
@@ -391,11 +470,17 @@ class CToken extends Calldata_1.Calldata {
391
470
  async fetchPrice(asset = false, getLower = false, inUSD = true) {
392
471
  const priceForAddress = asset ? this.asset.address : this.address;
393
472
  const price = await this.market.oracle_manager.getPrice(priceForAddress, inUSD, getLower);
394
- if (getLower) {
395
- this.cache.sharePriceLower = price;
473
+ if (asset) {
474
+ if (getLower)
475
+ this.cache.assetPriceLower = price;
476
+ else
477
+ this.cache.assetPrice = price;
396
478
  }
397
479
  else {
398
- this.cache.sharePrice = price;
480
+ if (getLower)
481
+ this.cache.sharePriceLower = price;
482
+ else
483
+ this.cache.sharePrice = price;
399
484
  }
400
485
  return price;
401
486
  }
@@ -611,6 +696,7 @@ class CToken extends Calldata_1.Calldata {
611
696
  tokens_exclude.push(helpers_1.NATIVE_ADDRESS.toLowerCase());
612
697
  }
613
698
  }
699
+ tokens = tokens.filter(token => token.type === 'none' || !EXCLUDED_ZAP_SYMBOLS.has(token.interface.symbol ?? ''));
614
700
  if (search) {
615
701
  const lowerSearch = search.toLowerCase();
616
702
  tokens = tokens.filter(token => (token.interface.name ?? '').toLowerCase().includes(lowerSearch) ||
@@ -624,18 +710,43 @@ class CToken extends Calldata_1.Calldata {
624
710
  return this.market.reader.hypotheticalRedemptionOf(signer.address, this, shares);
625
711
  }
626
712
  /**
627
- * Compute slippage BPS for the contract's checkSlippage modifier when leveraging up.
628
- * Share rounding (vault + cToken) causes equity loss ≈ 20bps × (leverage - 1).
629
- * The user's swap slippage is preserved for DEX protection; this adds a buffer
630
- * so the on-chain sanity check doesn't reject legitimate leverage operations.
713
+ * Single-RPC snapshot of fresh position state for leverage operations.
714
+ * Calls ProtocolReader.getLeverageSnapshot which internally uses
715
+ * hypotheticalLiquidityOf for aggregate position + fresh oracle prices
716
+ * + projected debt balance. Updates the local cache so downstream
717
+ * preview computations (previewLeverageUp/Down) read fresh values.
718
+ *
719
+ * Returns the snapshot for direct use where needed (e.g. debtTokenBalance
720
+ * for full deleverage swap sizing).
721
+ */
722
+ async _getLeverageSnapshot(borrow) {
723
+ const signer = (0, helpers_1.validateProviderAsSigner)(this.provider);
724
+ const snapshot = await this.market.reader.getLeverageSnapshot(signer.address, this.address, borrow.address, 120n);
725
+ if (snapshot.oracleError) {
726
+ throw new Error(`Oracle error fetching leverage snapshot for ${this.symbol}/${borrow.symbol}`);
727
+ }
728
+ // Update cache so preview functions read fresh values
729
+ this.cache.assetPrice = snapshot.collateralAssetPrice;
730
+ this.cache.sharePrice = snapshot.sharePrice;
731
+ borrow.cache.assetPrice = snapshot.debtAssetPrice;
732
+ this.market.cache.user.debt = snapshot.debtUsd;
733
+ return snapshot;
734
+ }
735
+ /**
736
+ * Compute slippage BPS for the contract's checkSlippage modifier when
737
+ * leveraging up. Under Curvance's permanent single-oracle architecture
738
+ * with fresh state from _getLeverageSnapshot, the only forced equity
739
+ * loss comes from wei-level share rounding plus possible Redstone price
740
+ * drift between snapshot RPC and tx broadcast — both small constants
741
+ * in absolute terms. We add a small flat buffer; the contract's
742
+ * equity-fraction denominator amplifies it by (L-1)x automatically.
743
+ * The user's swap-level slippage (passed separately to _swapSafe) is
744
+ * unaffected — that's the layer that bounds MEV extraction.
631
745
  */
632
746
  _leverageUpSlippage(slippage, leverage) {
633
- const leverageFactor = leverage.sub(1);
634
- if (leverageFactor.lte(0))
747
+ if (leverage.lte(1))
635
748
  return slippage;
636
- // ~20bps per unit of leverage factor for rounding losses
637
- const buffer = BigInt(leverageFactor.mul(20).ceil().toFixed(0));
638
- return slippage + buffer;
749
+ return slippage + LEVERAGE.LEVERAGE_UP_BUFFER_BPS;
639
750
  }
640
751
  previewLeverageUp(newLeverage, borrow, depositAmount) {
641
752
  const currentLeverage = this.getLeverage() ?? (0, decimal_js_1.default)(0);
@@ -649,29 +760,35 @@ class CToken extends Calldata_1.Calldata {
649
760
  const collateralInUsd = this.convertTokensToUsd(collateralAvail, false);
650
761
  const currentDebt = this.market.userDebt;
651
762
  const notional = collateralInUsd.sub(currentDebt);
652
- // Cap effective leverage slightly below target to account for protocol
653
- // leverage fee and rounding losses. The fee reduces collateral gained
654
- // relative to debt incurred, causing equity loss ≈ fee% × (leverage-1).
655
- // Capping at 98% of the leverage factor ensures the on-chain slippage
656
- // check passes even at max leverage.
657
763
  const leverageFactor = newLeverage.sub(1);
658
764
  const borrowPrice = borrow.getPrice(true);
659
- // Raw borrow amount — what the user actually owes as debt
660
765
  const rawDebtInUsd = notional.mul(newLeverage).sub(notional);
661
- const rawBorrowAmount = rawDebtInUsd.sub(currentDebt).div(borrowPrice);
662
- // Reduced borrow amount — what we send to the contract to avoid
663
- // tripping the on-chain slippage check at max leverage
664
- const effectiveLeverage = (0, decimal_js_1.default)(1).add(leverageFactor.mul((0, decimal_js_1.default)(0.99)));
665
- const effectiveDebtInUsd = notional.mul(effectiveLeverage).sub(notional);
666
- const borrowAmount = effectiveDebtInUsd.sub(currentDebt).div(borrowPrice);
766
+ const borrowAmount = rawDebtInUsd.sub(currentDebt).div(borrowPrice);
667
767
  const newCollateralInUsd = notional.add(rawDebtInUsd);
768
+ // Fee preview: queried from the configured fee policy. Returned as
769
+ // ancillary fields so callers can display "you'll be charged $X in
770
+ // fees" without requiring the SDK's primary preview math (which
771
+ // preserves the equity-conservation invariant) to change.
772
+ const borrowAssets = FormatConverter_1.default.decimalToBigInt(borrowAmount, borrow.asset.decimals);
773
+ const feeBps = setup_1.setup_config.feePolicy.getFeeBps({
774
+ operation: 'leverage-up',
775
+ inputToken: borrow.asset.address,
776
+ outputToken: this.asset.address,
777
+ inputAmount: borrowAssets,
778
+ currentLeverage,
779
+ targetLeverage: newLeverage,
780
+ });
781
+ const feeAssets = borrowAmount.mul((0, decimal_js_1.default)(Number(feeBps))).div((0, decimal_js_1.default)(10000));
782
+ const feeUsd = feeAssets.mul(borrowPrice);
668
783
  return {
669
784
  borrowAmount,
670
- rawBorrowAmount,
671
785
  newDebt: rawDebtInUsd,
672
786
  newDebtInAssets: borrow.convertUsdToTokens(rawDebtInUsd, true),
673
787
  newCollateral: newCollateralInUsd,
674
- newCollateralInAssets: this.convertUsdToTokens(newCollateralInUsd, true)
788
+ newCollateralInAssets: this.convertUsdToTokens(newCollateralInUsd, true),
789
+ feeBps,
790
+ feeAssets,
791
+ feeUsd,
675
792
  };
676
793
  }
677
794
  previewLeverageDown(newLeverage, currentLeverage, borrow) {
@@ -690,6 +807,25 @@ class CToken extends Calldata_1.Calldata {
690
807
  const collateralAssetReductionUsd = collateralInUsd.sub(targetCollateralUsd);
691
808
  const collateralAssetReduction = FormatConverter_1.default.decimalToBigInt(collateralAssetReductionUsd.div(this.getPrice(true)), this.asset.decimals);
692
809
  const leverageDiff = (0, decimal_js_1.default)(1).sub(newLeverage.div(currentLeverage));
810
+ // Fee preview: queried from the configured fee policy. The fee is
811
+ // taken on the collateral→debt swap; size of the swap depends on
812
+ // whether this is a partial or full deleverage. We use
813
+ // collateralAssetReductionUsd as the swap notional approximation
814
+ // (exact for partial; for full deleverage the actual swap is sized
815
+ // by leverageDown using the snapshot, but the preview is close enough
816
+ // for display purposes).
817
+ const feeBps = borrow ? setup_1.setup_config.feePolicy.getFeeBps({
818
+ operation: 'leverage-down',
819
+ inputToken: this.asset.address,
820
+ outputToken: borrow.asset.address,
821
+ inputAmount: collateralAssetReduction,
822
+ currentLeverage,
823
+ targetLeverage: newLeverage,
824
+ }) : 0n;
825
+ const feeUsd = collateralAssetReductionUsd.mul((0, decimal_js_1.default)(Number(feeBps))).div((0, decimal_js_1.default)(10000));
826
+ const feeAssets = this.getPrice(true).gt(0)
827
+ ? feeUsd.div(this.getPrice(true))
828
+ : (0, decimal_js_1.default)(0);
693
829
  return {
694
830
  collateralAssetReduction,
695
831
  collateralAssetReductionUsd,
@@ -697,7 +833,10 @@ class CToken extends Calldata_1.Calldata {
697
833
  newDebt: newDebtUsd,
698
834
  newDebtInAssets: borrow ? borrow.convertUsdToTokens(newDebtUsd, true) : undefined,
699
835
  newCollateral: targetCollateralUsd,
700
- newCollateralInAssets: this.convertUsdToTokens(targetCollateralUsd, true)
836
+ newCollateralInAssets: this.convertUsdToTokens(targetCollateralUsd, true),
837
+ feeBps,
838
+ feeAssets,
839
+ feeUsd,
701
840
  };
702
841
  }
703
842
  async leverageUp(borrow, newLeverage, type, slippage_ = (0, decimal_js_1.default)(0.05), simulate = false) {
@@ -706,15 +845,26 @@ class CToken extends Calldata_1.Calldata {
706
845
  const slippage = this._leverageUpSlippage(FormatConverter_1.default.percentageToBps(slippage_), newLeverage);
707
846
  const manager = this.getPositionManager(type);
708
847
  let calldata;
848
+ await this._getLeverageSnapshot(borrow);
709
849
  const { borrowAmount } = this.previewLeverageUp(newLeverage, borrow);
710
850
  switch (type) {
711
851
  case 'simple': {
712
- const { action, quote } = await chains_1.chain_config[setup_1.setup_config.chain].dexAgg.quoteAction(manager.address, borrow.asset.address, this.asset.address, FormatConverter_1.default.decimalToBigInt(borrowAmount, borrow.asset.decimals), slippage);
852
+ const borrowAssets = FormatConverter_1.default.decimalToBigInt(borrowAmount, borrow.asset.decimals);
853
+ const feeBps = setup_1.setup_config.feePolicy.getFeeBps({
854
+ operation: 'leverage-up',
855
+ inputToken: borrow.asset.address,
856
+ outputToken: this.asset.address,
857
+ inputAmount: borrowAssets,
858
+ currentLeverage: this.getLeverage() ?? (0, decimal_js_1.default)(1),
859
+ targetLeverage: newLeverage,
860
+ });
861
+ const feeReceiver = feeBps > 0n ? setup_1.setup_config.feePolicy.feeReceiver : undefined;
862
+ const { action, quote } = await chains_1.chain_config[setup_1.setup_config.chain].dexAgg.quoteAction(manager.address, borrow.asset.address, this.asset.address, borrowAssets, slippage, feeBps, feeReceiver);
713
863
  calldata = manager.getLeverageCalldata({
714
864
  borrowableCToken: borrow.address,
715
865
  borrowAssets: FormatConverter_1.default.decimalToBigInt(borrowAmount, borrow.asset.decimals),
716
866
  cToken: this.address,
717
- expectedShares: this.virtualConvertToShares(BigInt(quote.min_out)),
867
+ expectedShares: this.virtualConvertToShares(BigInt(quote.min_out), LEVERAGE.SHARES_BUFFER_BPS),
718
868
  swapAction: action,
719
869
  auxData: "0x",
720
870
  }, FormatConverter_1.default.bpsToBpsWad(slippage));
@@ -760,24 +910,77 @@ class CToken extends Calldata_1.Calldata {
760
910
  const slippage = (0, helpers_1.toBps)(slippage_);
761
911
  const manager = this.getPositionManager(type);
762
912
  let calldata;
913
+ const snapshot = await this._getLeverageSnapshot(borrowToken);
763
914
  const { collateralAssetReduction } = this.previewLeverageDown(newLeverage, currentLeverage);
764
915
  const isFullDeleverage = newLeverage.equals(1);
765
- const repay_balance = isFullDeleverage ? await borrowToken.fetchDebtBalanceAtTimestamp(100n, false) : null;
766
916
  switch (type) {
767
917
  case 'simple': {
768
918
  let swapCollateral = collateralAssetReduction;
919
+ // Resolve fee policy once for this operation. The fee bps
920
+ // contributes to the deleverage overhead because KyberSwap
921
+ // deducts the fee from the swap input before swapping —
922
+ // effective swap input = swapCollateral × (1 - feeBps).
923
+ // We must oversize swapCollateral to compensate, otherwise
924
+ // the post-fee swap underdelivers and dust debt remains.
925
+ //
926
+ // Order-of-operations note: we pass collateralAssetReduction
927
+ // as the inputAmount estimate. For partial deleverage this
928
+ // is the actual swap size; for full deleverage the actual
929
+ // size is computed below from the snapshot and is slightly
930
+ // larger. flatFeePolicy ignores inputAmount, so this is
931
+ // exact for current callers. Future notional-tiered policies
932
+ // should be aware that for full deleverage the inputAmount
933
+ // passed here is an underestimate.
934
+ const feeBps = setup_1.setup_config.feePolicy.getFeeBps({
935
+ operation: 'leverage-down',
936
+ inputToken: this.asset.address,
937
+ outputToken: borrowToken.asset.address,
938
+ inputAmount: collateralAssetReduction,
939
+ currentLeverage: currentLeverage,
940
+ targetLeverage: newLeverage,
941
+ });
942
+ const feeReceiver = feeBps > 0n ? setup_1.setup_config.feePolicy.feeReceiver : undefined;
769
943
  if (isFullDeleverage) {
770
- const initialQuote = await config.dexAgg.quote(manager.address, this.asset.address, borrowToken.asset.address, collateralAssetReduction, slippage);
771
- if (initialQuote.out < repay_balance) {
772
- swapCollateral = collateralAssetReduction * repay_balance * 1005n / (initialQuote.out * 1000n);
944
+ // Use exact projected debt from snapshot to size the swap.
945
+ // debtTokenBalance is in debt-token native decimals, projected
946
+ // forward by bufferTime. Convert to collateral-asset terms via
947
+ // snapshot prices (lower-bound collateral, standard debt — both
948
+ // conservative, overshooting slightly). Overhead covers DEX
949
+ // routing impact + oracle drift + fee deduction.
950
+ const debtDecimals = 10n ** borrowToken.asset.decimals;
951
+ const collDecimals = 10n ** this.asset.decimals;
952
+ const debtInCollateral = (snapshot.debtTokenBalance * snapshot.debtAssetPrice * collDecimals) / (snapshot.collateralAssetPrice * debtDecimals);
953
+ // Total overhead = base overhead (DEX impact + drift) + fee bps.
954
+ // Additive approximation is accurate to sub-bp at typical
955
+ // fee+overhead magnitudes (< 100 bps combined).
956
+ const overheadBps = LEVERAGE.DELEVERAGE_OVERHEAD_BPS + feeBps;
957
+ swapCollateral = debtInCollateral * (10000n + overheadBps) / 10000n;
958
+ const maxCollateral = this.virtualConvertToAssets(this.cache.userCollateral);
959
+ if (swapCollateral > maxCollateral) {
960
+ swapCollateral = maxCollateral;
773
961
  }
774
962
  }
775
- const { action, quote } = await config.dexAgg.quoteAction(manager.address, this.asset.address, borrowToken.asset.address, swapCollateral, slippage);
776
- const minRepay = isFullDeleverage ? 1n : quote.out - (BigInt((0, decimal_js_1.default)(quote.out).mul(.05).toFixed(0)));
777
- // For full deleverage, add 50bps buffer to the contract-level slippage
778
- // check to account for oracle price variance in the oracleRoute multicall.
963
+ const { action, quote } = await config.dexAgg.quoteAction(manager.address, this.asset.address, borrowToken.asset.address, swapCollateral, slippage, feeBps, feeReceiver);
964
+ const minRepay = isFullDeleverage ? 1n : quote.min_out;
965
+ // Full deleverage oversizes the swap by (DELEVERAGE_OVERHEAD_BPS +
966
+ // feeBps) in absolute terms to prevent dust debt. The contract's
967
+ // checkSlippage modifier compares equity-before vs equity-after
968
+ // as a fraction of starting equity, so the absolute overshoot
969
+ // becomes (L-1) × overhead in equity-fraction terms. We expand
970
+ // the contract slippage tolerance by exactly that forced amount,
971
+ // leaving the user's `slippage` budget available for variable
972
+ // DEX impact + oracle drift.
973
+ //
974
+ // This does NOT loosen MEV protection — that lives at the
975
+ // _swapSafe layer (which still receives raw user slippage).
976
+ // The contract checkSlippage is sanity-only per its docstring.
977
+ // Note: the contract returns excess debt token to the user's
978
+ // wallet, so the economic loss from the overshoot is zero.
779
979
  const contractSlippage = isFullDeleverage
780
- ? slippage + 50n
980
+ ? slippage + BigInt(currentLeverage.sub(1)
981
+ .mul(Number(LEVERAGE.DELEVERAGE_OVERHEAD_BPS + feeBps))
982
+ .ceil()
983
+ .toFixed(0))
781
984
  : slippage;
782
985
  calldata = manager.getDeleverageCalldata({
783
986
  cToken: this.address,
@@ -817,15 +1020,26 @@ class CToken extends Calldata_1.Calldata {
817
1020
  const manager = this.getPositionManager(type);
818
1021
  let calldata;
819
1022
  const depositAssets = FormatConverter_1.default.decimalToBigInt(depositAmount, this.asset.decimals);
1023
+ await this._getLeverageSnapshot(borrow);
820
1024
  const { borrowAmount } = this.previewLeverageUp(multiplier, borrow, depositAssets);
821
1025
  switch (type) {
822
1026
  case 'simple': {
823
- const { action, quote } = await chains_1.chain_config[setup_1.setup_config.chain].dexAgg.quoteAction(manager.address, borrow.asset.address, this.asset.address, FormatConverter_1.default.decimalToBigInt(borrowAmount, borrow.asset.decimals), slippage);
1027
+ const borrowAssets = FormatConverter_1.default.decimalToBigInt(borrowAmount, borrow.asset.decimals);
1028
+ const feeBps = setup_1.setup_config.feePolicy.getFeeBps({
1029
+ operation: 'deposit-and-leverage',
1030
+ inputToken: borrow.asset.address,
1031
+ outputToken: this.asset.address,
1032
+ inputAmount: borrowAssets,
1033
+ currentLeverage: this.getLeverage() ?? (0, decimal_js_1.default)(1),
1034
+ targetLeverage: multiplier,
1035
+ });
1036
+ const feeReceiver = feeBps > 0n ? setup_1.setup_config.feePolicy.feeReceiver : undefined;
1037
+ const { action, quote } = await chains_1.chain_config[setup_1.setup_config.chain].dexAgg.quoteAction(manager.address, borrow.asset.address, this.asset.address, borrowAssets, slippage, feeBps, feeReceiver);
824
1038
  calldata = manager.getDepositAndLeverageCalldata(FormatConverter_1.default.decimalToBigInt(depositAmount, this.asset.decimals), {
825
1039
  borrowableCToken: borrow.address,
826
- borrowAssets: FormatConverter_1.default.decimalToBigInt(borrowAmount, borrow.asset.decimals),
1040
+ borrowAssets: borrowAssets,
827
1041
  cToken: this.address,
828
- expectedShares: this.virtualConvertToShares(BigInt(quote.min_out)),
1042
+ expectedShares: this.virtualConvertToShares(BigInt(quote.min_out), LEVERAGE.SHARES_BUFFER_BPS),
829
1043
  swapAction: action,
830
1044
  auxData: "0x",
831
1045
  }, FormatConverter_1.default.bpsToBpsWad(slippage));
@@ -850,6 +1064,7 @@ class CToken extends Calldata_1.Calldata {
850
1064
  }
851
1065
  if (simulate)
852
1066
  return this.simulateOracleRoute(calldata, { to: manager.address });
1067
+ await this._checkPositionManagerApproval(manager);
853
1068
  return this.oracleRoute(calldata, { to: manager.address });
854
1069
  }
855
1070
  catch (error) {
@@ -1013,7 +1228,7 @@ class CToken extends Calldata_1.Calldata {
1013
1228
  if (remainingCollateral == 0n)
1014
1229
  throw new Error(collateralCapError);
1015
1230
  if (remainingCollateral > 0n) {
1016
- const shares = this.virtualConvertToShares(depositAssets);
1231
+ const shares = this.virtualConvertToShares(depositAssets, LEVERAGE.SHARES_BUFFER_BPS);
1017
1232
  if (shares > remainingCollateral) {
1018
1233
  throw new Error(collateralCapError);
1019
1234
  }
@@ -1066,7 +1281,13 @@ class CToken extends Calldata_1.Calldata {
1066
1281
  }
1067
1282
  convertTokensToUsd(tokenAmount, asset = true) {
1068
1283
  const price = this.getPrice(asset, false, false);
1069
- return FormatConverter_1.default.bigIntTokensToUsd(tokenAmount, price, this.decimals);
1284
+ // Pair the price with the matching decimals: asset price ↔ asset
1285
+ // decimals, share price ↔ share decimals. Falls back to share
1286
+ // decimals if asset.decimals is somehow unset (cToken share decimals
1287
+ // always equal asset decimals on current Curvance markets, so the
1288
+ // fallback is value-equivalent).
1289
+ const decimals = asset ? (this.asset.decimals ?? this.decimals) : this.decimals;
1290
+ return FormatConverter_1.default.bigIntTokensToUsd(tokenAmount, price, decimals);
1070
1291
  }
1071
1292
  async fetchConvertTokensToUsd(tokenAmount, asset = true) {
1072
1293
  // Reload cache
@@ -1080,7 +1301,9 @@ class CToken extends Calldata_1.Calldata {
1080
1301
  }
1081
1302
  convertAssetsToUsd(tokenAmount) {
1082
1303
  const price = this.getPrice(true, false, false);
1083
- const decimals = this.decimals;
1304
+ // Asset price ↔ asset decimals (with fallback to share decimals,
1305
+ // which equal asset decimals on current Curvance markets).
1306
+ const decimals = this.asset.decimals ?? this.decimals;
1084
1307
  return FormatConverter_1.default.bigIntTokensToUsd(tokenAmount, price, decimals);
1085
1308
  }
1086
1309
  async convertSharesToUsd(tokenAmount) {