curvance 4.0.2 → 4.0.4

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 (61) hide show
  1. package/dist/abis/ProtocolReader.json +64 -0
  2. package/dist/chains/monad-mainnet.json +1 -26
  3. package/dist/classes/CToken.d.ts +33 -6
  4. package/dist/classes/CToken.d.ts.map +1 -1
  5. package/dist/classes/CToken.js +325 -51
  6. package/dist/classes/CToken.js.map +1 -1
  7. package/dist/classes/DexAggregators/IDexAgg.d.ts +3 -1
  8. package/dist/classes/DexAggregators/IDexAgg.d.ts.map +1 -1
  9. package/dist/classes/DexAggregators/Kuru.d.ts +3 -3
  10. package/dist/classes/DexAggregators/Kuru.d.ts.map +1 -1
  11. package/dist/classes/DexAggregators/Kuru.js +19 -9
  12. package/dist/classes/DexAggregators/Kuru.js.map +1 -1
  13. package/dist/classes/DexAggregators/KyberSwap.d.ts +3 -3
  14. package/dist/classes/DexAggregators/KyberSwap.d.ts.map +1 -1
  15. package/dist/classes/DexAggregators/KyberSwap.js +97 -14
  16. package/dist/classes/DexAggregators/KyberSwap.js.map +1 -1
  17. package/dist/classes/DexAggregators/MultiDexAgg.d.ts +3 -3
  18. package/dist/classes/DexAggregators/MultiDexAgg.d.ts.map +1 -1
  19. package/dist/classes/DexAggregators/MultiDexAgg.js +13 -13
  20. package/dist/classes/DexAggregators/MultiDexAgg.js.map +1 -1
  21. package/dist/classes/OracleManager.js.map +1 -1
  22. package/dist/classes/ProtocolReader.d.ts +10 -0
  23. package/dist/classes/ProtocolReader.d.ts.map +1 -1
  24. package/dist/classes/ProtocolReader.js +4 -0
  25. package/dist/classes/ProtocolReader.js.map +1 -1
  26. package/dist/classes/Zapper.d.ts.map +1 -1
  27. package/dist/classes/Zapper.js +39 -2
  28. package/dist/classes/Zapper.js.map +1 -1
  29. package/dist/contracts/monad-mainnet.json +1 -1
  30. package/dist/feePolicy.d.ts +182 -0
  31. package/dist/feePolicy.d.ts.map +1 -0
  32. package/dist/feePolicy.js +110 -0
  33. package/dist/feePolicy.js.map +1 -0
  34. package/dist/index.d.ts +1 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +1 -0
  37. package/dist/index.js.map +1 -1
  38. package/dist/retry-provider.js +3 -3
  39. package/dist/retry-provider.js.map +1 -1
  40. package/dist/setup.d.ts +8 -1
  41. package/dist/setup.d.ts.map +1 -1
  42. package/dist/setup.js +3 -1
  43. package/dist/setup.js.map +1 -1
  44. package/package.json +1 -1
  45. package/dist/chains/arb-sepolia.json +0 -44
  46. package/dist/classes/DexAggregators/KuruMainnet.d.ts +0 -1
  47. package/dist/classes/DexAggregators/KuruMainnet.d.ts.map +0 -1
  48. package/dist/classes/DexAggregators/KuruMainnet.js +0 -228
  49. package/dist/classes/DexAggregators/KuruMainnet.js.map +0 -1
  50. package/dist/classes/Kuru.d.ts +0 -59
  51. package/dist/classes/Kuru.d.ts.map +0 -1
  52. package/dist/classes/Kuru.js +0 -167
  53. package/dist/classes/Kuru.js.map +0 -1
  54. package/dist/classes/KuruMainnet.d.ts +0 -59
  55. package/dist/classes/KuruMainnet.d.ts.map +0 -1
  56. package/dist/classes/KuruMainnet.js +0 -167
  57. package/dist/classes/KuruMainnet.js.map +0 -1
  58. package/dist/snapshot.d.ts +0 -53
  59. package/dist/snapshot.d.ts.map +0 -1
  60. package/dist/snapshot.js +0 -103
  61. package/dist/snapshot.js.map +0 -1
@@ -21,6 +21,84 @@ const chains_1 = require("../chains");
21
21
  const EXCLUDED_ZAP_SYMBOLS = new Set([
22
22
  'eBTC', 'earnAUSD', 'vUSD', 'syzUSD', 'ezETH', 'YZM', 'wsrUSD', 'sAUSD',
23
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.
48
+ *
49
+ * Because _swapSafe measures value loss against the FULL input (pre-fee),
50
+ * the deterministic KyberSwap fee would consume feeBps of the user's MEV
51
+ * tolerance if not compensated. Each call site expands action.slippage by
52
+ * feeBps after quoting so the fee is absorbed and the user's chosen
53
+ * tolerance is preserved for actual MEV/routing variance.
54
+ *
55
+ * Asymmetry between leverage up and deleverage
56
+ * --------------------------------------------
57
+ * Leverage UP: under single-oracle, the contract sees zero forced loss
58
+ * for a perfect swap. The only real sources of difference between
59
+ * snapshot-time prices and execution-time prices are: (a) wei-level share
60
+ * rounding, (b) Redstone update drift between the snapshot RPC and the
61
+ * tx broadcast block. Both are small constants in absolute terms, NOT
62
+ * leverage-scaled. A small flat buffer suffices.
63
+ *
64
+ * DELEVERAGE (full): forced loss comes from intentional swap overshoot
65
+ * (DELEVERAGE_OVERHEAD_BPS) which prevents dust debt by oversizing the
66
+ * collateral→debt swap. This is a real bps-level loss in absolute terms
67
+ * which becomes (L-1) × bps in equity-fraction terms — so the deleverage
68
+ * contract-slippage expansion DOES scale with leverage. Note: the contract
69
+ * returns excess debt token to the user's wallet (BasePositionManager
70
+ * onRedeem lines 482-493), so the economic loss from the overshoot is
71
+ * zero — only the contract's naive equity-loss check sees it as loss.
72
+ */
73
+ const LEVERAGE = {
74
+ /** Max leverage cap: fraction of theoretical max the user can select.
75
+ * Prevents boundary singularity at exact max leverage. Independent of
76
+ * the slippage buffers below — protects post-op position health, not
77
+ * in-op slippage. */
78
+ MAX_LEVERAGE_FACTOR: (0, decimal_js_1.default)(0.995),
79
+ /** Flat BPS buffer added to leverage-up DEX/swapSafe slippage tolerance.
80
+ * Under single-oracle, the only forced loss at the swap level comes from
81
+ * wei-level share rounding plus possible Redstone price drift between
82
+ * snapshot RPC and tx broadcast block. Both are small constants.
83
+ *
84
+ * Fee handling: each call site expands both action.slippage (by feeBps,
85
+ * so _swapSafe doesn't treat the fee as MEV) and contractSlippage (by
86
+ * (L-1) × feeBps, so checkSlippage doesn't fire from equity-fraction
87
+ * amplification). This buffer covers rounding/drift only. */
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
+ };
24
102
  class CToken extends Calldata_1.Calldata {
25
103
  provider;
26
104
  address;
@@ -82,7 +160,7 @@ class CToken extends Calldata_1.Calldata {
82
160
  // to account for share rounding and fee losses that prevent reaching the exact max.
83
161
  const theoretical = (0, decimal_js_1.default)(this.cache.maxLeverage).div(helpers_1.BPS);
84
162
  const factor = theoretical.sub(1);
85
- 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));
86
164
  }
87
165
  get canLeverage() { return this.leverageTypes.length > 0; }
88
166
  get totalAssets() { return this.cache.totalAssets; }
@@ -99,8 +177,15 @@ class CToken extends Calldata_1.Calldata {
99
177
  virtualConvertToAssets(shares) {
100
178
  return (shares * this.totalAssets) / this.totalSupply;
101
179
  }
102
- virtualConvertToShares(assets) {
103
- 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;
104
189
  }
105
190
  getLeverage() {
106
191
  if (this.getUserCollateral(true).equals(0)) {
@@ -385,11 +470,17 @@ class CToken extends Calldata_1.Calldata {
385
470
  async fetchPrice(asset = false, getLower = false, inUSD = true) {
386
471
  const priceForAddress = asset ? this.asset.address : this.address;
387
472
  const price = await this.market.oracle_manager.getPrice(priceForAddress, inUSD, getLower);
388
- if (getLower) {
389
- this.cache.sharePriceLower = price;
473
+ if (asset) {
474
+ if (getLower)
475
+ this.cache.assetPriceLower = price;
476
+ else
477
+ this.cache.assetPrice = price;
390
478
  }
391
479
  else {
392
- this.cache.sharePrice = price;
480
+ if (getLower)
481
+ this.cache.sharePriceLower = price;
482
+ else
483
+ this.cache.sharePrice = price;
393
484
  }
394
485
  return price;
395
486
  }
@@ -619,18 +710,43 @@ class CToken extends Calldata_1.Calldata {
619
710
  return this.market.reader.hypotheticalRedemptionOf(signer.address, this, shares);
620
711
  }
621
712
  /**
622
- * Compute slippage BPS for the contract's checkSlippage modifier when leveraging up.
623
- * Share rounding (vault + cToken) causes equity loss ≈ 20bps × (leverage - 1).
624
- * The user's swap slippage is preserved for DEX protection; this adds a buffer
625
- * 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.
626
745
  */
627
746
  _leverageUpSlippage(slippage, leverage) {
628
- const leverageFactor = leverage.sub(1);
629
- if (leverageFactor.lte(0))
747
+ if (leverage.lte(1))
630
748
  return slippage;
631
- // ~20bps per unit of leverage factor for rounding losses
632
- const buffer = BigInt(leverageFactor.mul(20).ceil().toFixed(0));
633
- return slippage + buffer;
749
+ return slippage + LEVERAGE.LEVERAGE_UP_BUFFER_BPS;
634
750
  }
635
751
  previewLeverageUp(newLeverage, borrow, depositAmount) {
636
752
  const currentLeverage = this.getLeverage() ?? (0, decimal_js_1.default)(0);
@@ -644,29 +760,34 @@ class CToken extends Calldata_1.Calldata {
644
760
  const collateralInUsd = this.convertTokensToUsd(collateralAvail, false);
645
761
  const currentDebt = this.market.userDebt;
646
762
  const notional = collateralInUsd.sub(currentDebt);
647
- // Cap effective leverage slightly below target to account for protocol
648
- // leverage fee and rounding losses. The fee reduces collateral gained
649
- // relative to debt incurred, causing equity loss ≈ fee% × (leverage-1).
650
- // Capping at 98% of the leverage factor ensures the on-chain slippage
651
- // check passes even at max leverage.
652
763
  const leverageFactor = newLeverage.sub(1);
653
764
  const borrowPrice = borrow.getPrice(true);
654
- // Raw borrow amount — what the user actually owes as debt
655
765
  const rawDebtInUsd = notional.mul(newLeverage).sub(notional);
656
- const rawBorrowAmount = rawDebtInUsd.sub(currentDebt).div(borrowPrice);
657
- // Reduced borrow amount what we send to the contract to avoid
658
- // tripping the on-chain slippage check at max leverage
659
- const effectiveLeverage = (0, decimal_js_1.default)(1).add(leverageFactor.mul((0, decimal_js_1.default)(0.99)));
660
- const effectiveDebtInUsd = notional.mul(effectiveLeverage).sub(notional);
661
- const borrowAmount = effectiveDebtInUsd.sub(currentDebt).div(borrowPrice);
662
- const newCollateralInUsd = notional.add(rawDebtInUsd);
766
+ const borrowAmount = rawDebtInUsd.sub(currentDebt).div(borrowPrice);
767
+ // Fee preview: queried from the configured fee policy.
768
+ const borrowAssets = FormatConverter_1.default.decimalToBigInt(borrowAmount, borrow.asset.decimals);
769
+ const feeBps = setup_1.setup_config.feePolicy.getFeeBps({
770
+ operation: 'leverage-up',
771
+ inputToken: borrow.asset.address,
772
+ outputToken: this.asset.address,
773
+ inputAmount: borrowAssets,
774
+ currentLeverage,
775
+ targetLeverage: newLeverage,
776
+ });
777
+ const feeAssets = borrowAmount.mul((0, decimal_js_1.default)(Number(feeBps))).div((0, decimal_js_1.default)(10000));
778
+ const feeUsd = feeAssets.mul(borrowPrice);
779
+ // Subtract fee from displayed collateral — the fee reduces swap
780
+ // output, so the user receives less collateral than rawDebtInUsd.
781
+ const newCollateralInUsd = notional.add(rawDebtInUsd).sub(feeUsd);
663
782
  return {
664
783
  borrowAmount,
665
- rawBorrowAmount,
666
784
  newDebt: rawDebtInUsd,
667
785
  newDebtInAssets: borrow.convertUsdToTokens(rawDebtInUsd, true),
668
786
  newCollateral: newCollateralInUsd,
669
- newCollateralInAssets: this.convertUsdToTokens(newCollateralInUsd, true)
787
+ newCollateralInAssets: this.convertUsdToTokens(newCollateralInUsd, true),
788
+ feeBps,
789
+ feeAssets,
790
+ feeUsd,
670
791
  };
671
792
  }
672
793
  previewLeverageDown(newLeverage, currentLeverage, borrow) {
@@ -685,6 +806,25 @@ class CToken extends Calldata_1.Calldata {
685
806
  const collateralAssetReductionUsd = collateralInUsd.sub(targetCollateralUsd);
686
807
  const collateralAssetReduction = FormatConverter_1.default.decimalToBigInt(collateralAssetReductionUsd.div(this.getPrice(true)), this.asset.decimals);
687
808
  const leverageDiff = (0, decimal_js_1.default)(1).sub(newLeverage.div(currentLeverage));
809
+ // Fee preview: queried from the configured fee policy. The fee is
810
+ // taken on the collateral→debt swap; size of the swap depends on
811
+ // whether this is a partial or full deleverage. We use
812
+ // collateralAssetReductionUsd as the swap notional approximation
813
+ // (exact for partial; for full deleverage the actual swap is sized
814
+ // by leverageDown using the snapshot, but the preview is close enough
815
+ // for display purposes).
816
+ const feeBps = borrow ? setup_1.setup_config.feePolicy.getFeeBps({
817
+ operation: 'leverage-down',
818
+ inputToken: this.asset.address,
819
+ outputToken: borrow.asset.address,
820
+ inputAmount: collateralAssetReduction,
821
+ currentLeverage,
822
+ targetLeverage: newLeverage,
823
+ }) : 0n;
824
+ const feeUsd = collateralAssetReductionUsd.mul((0, decimal_js_1.default)(Number(feeBps))).div((0, decimal_js_1.default)(10000));
825
+ const feeAssets = this.getPrice(true).gt(0)
826
+ ? feeUsd.div(this.getPrice(true))
827
+ : (0, decimal_js_1.default)(0);
688
828
  return {
689
829
  collateralAssetReduction,
690
830
  collateralAssetReductionUsd,
@@ -692,7 +832,10 @@ class CToken extends Calldata_1.Calldata {
692
832
  newDebt: newDebtUsd,
693
833
  newDebtInAssets: borrow ? borrow.convertUsdToTokens(newDebtUsd, true) : undefined,
694
834
  newCollateral: targetCollateralUsd,
695
- newCollateralInAssets: this.convertUsdToTokens(targetCollateralUsd, true)
835
+ newCollateralInAssets: this.convertUsdToTokens(targetCollateralUsd, true),
836
+ feeBps,
837
+ feeAssets,
838
+ feeUsd,
696
839
  };
697
840
  }
698
841
  async leverageUp(borrow, newLeverage, type, slippage_ = (0, decimal_js_1.default)(0.05), simulate = false) {
@@ -701,18 +844,48 @@ class CToken extends Calldata_1.Calldata {
701
844
  const slippage = this._leverageUpSlippage(FormatConverter_1.default.percentageToBps(slippage_), newLeverage);
702
845
  const manager = this.getPositionManager(type);
703
846
  let calldata;
847
+ await this._getLeverageSnapshot(borrow);
704
848
  const { borrowAmount } = this.previewLeverageUp(newLeverage, borrow);
705
849
  switch (type) {
706
850
  case 'simple': {
707
- 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);
851
+ const borrowAssets = FormatConverter_1.default.decimalToBigInt(borrowAmount, borrow.asset.decimals);
852
+ const feeBps = setup_1.setup_config.feePolicy.getFeeBps({
853
+ operation: 'leverage-up',
854
+ inputToken: borrow.asset.address,
855
+ outputToken: this.asset.address,
856
+ inputAmount: borrowAssets,
857
+ currentLeverage: this.getLeverage() ?? (0, decimal_js_1.default)(1),
858
+ targetLeverage: newLeverage,
859
+ });
860
+ const feeReceiver = feeBps > 0n ? setup_1.setup_config.feePolicy.feeReceiver : undefined;
861
+ 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);
862
+ // _swapSafe measures value loss as (valueIn - valueOut) / valueIn
863
+ // where valueIn is the FULL input (pre-fee). KyberSwap deducts
864
+ // the fee before swapping, so _swapSafe sees feeBps as "slippage"
865
+ // even though it's a known, deterministic cost. Expand
866
+ // action.slippage by feeBps so the fee doesn't consume the user's
867
+ // MEV protection budget. The KyberSwap quote already used the
868
+ // user's raw slippage for minReturnAmount (DEX-level protection).
869
+ if (feeBps > 0n) {
870
+ action.slippage = FormatConverter_1.default.bpsToBpsWad(slippage + feeBps);
871
+ }
872
+ // The fee also reduces swap output, which checkSlippage sees
873
+ // as equity loss amplified by (L-1) — same pattern as
874
+ // deleverage. Expand the contract-level tolerance to absorb it.
875
+ const contractSlippage = feeBps > 0n
876
+ ? slippage + BigInt(newLeverage.sub(1)
877
+ .mul(Number(feeBps))
878
+ .ceil()
879
+ .toFixed(0))
880
+ : slippage;
708
881
  calldata = manager.getLeverageCalldata({
709
882
  borrowableCToken: borrow.address,
710
883
  borrowAssets: FormatConverter_1.default.decimalToBigInt(borrowAmount, borrow.asset.decimals),
711
884
  cToken: this.address,
712
- expectedShares: this.virtualConvertToShares(BigInt(quote.min_out)),
885
+ expectedShares: this.virtualConvertToShares(BigInt(quote.min_out), LEVERAGE.SHARES_BUFFER_BPS),
713
886
  swapAction: action,
714
887
  auxData: "0x",
715
- }, FormatConverter_1.default.bpsToBpsWad(slippage));
888
+ }, FormatConverter_1.default.bpsToBpsWad(contractSlippage));
716
889
  break;
717
890
  }
718
891
  case 'native-vault':
@@ -755,24 +928,92 @@ class CToken extends Calldata_1.Calldata {
755
928
  const slippage = (0, helpers_1.toBps)(slippage_);
756
929
  const manager = this.getPositionManager(type);
757
930
  let calldata;
931
+ const snapshot = await this._getLeverageSnapshot(borrowToken);
758
932
  const { collateralAssetReduction } = this.previewLeverageDown(newLeverage, currentLeverage);
759
933
  const isFullDeleverage = newLeverage.equals(1);
760
- const repay_balance = isFullDeleverage ? await borrowToken.fetchDebtBalanceAtTimestamp(100n, false) : null;
761
934
  switch (type) {
762
935
  case 'simple': {
763
936
  let swapCollateral = collateralAssetReduction;
937
+ // Resolve fee policy once for this operation. The fee bps
938
+ // contributes to the deleverage overhead because KyberSwap
939
+ // deducts the fee from the swap input before swapping —
940
+ // effective swap input = swapCollateral × (1 - feeBps).
941
+ // We must oversize swapCollateral to compensate, otherwise
942
+ // the post-fee swap underdelivers and dust debt remains.
943
+ //
944
+ // Order-of-operations note: we pass collateralAssetReduction
945
+ // as the inputAmount estimate. For partial deleverage this
946
+ // is the actual swap size; for full deleverage the actual
947
+ // size is computed below from the snapshot and is slightly
948
+ // larger. flatFeePolicy ignores inputAmount, so this is
949
+ // exact for current callers. Future notional-tiered policies
950
+ // should be aware that for full deleverage the inputAmount
951
+ // passed here is an underestimate.
952
+ const feeBps = setup_1.setup_config.feePolicy.getFeeBps({
953
+ operation: 'leverage-down',
954
+ inputToken: this.asset.address,
955
+ outputToken: borrowToken.asset.address,
956
+ inputAmount: collateralAssetReduction,
957
+ currentLeverage: currentLeverage,
958
+ targetLeverage: newLeverage,
959
+ });
960
+ const feeReceiver = feeBps > 0n ? setup_1.setup_config.feePolicy.feeReceiver : undefined;
764
961
  if (isFullDeleverage) {
765
- const initialQuote = await config.dexAgg.quote(manager.address, this.asset.address, borrowToken.asset.address, collateralAssetReduction, slippage);
766
- if (initialQuote.out < repay_balance) {
767
- swapCollateral = collateralAssetReduction * repay_balance * 1005n / (initialQuote.out * 1000n);
962
+ // Use exact projected debt from snapshot to size the swap.
963
+ // debtTokenBalance is in debt-token native decimals, projected
964
+ // forward by bufferTime. Convert to collateral-asset terms via
965
+ // snapshot prices (lower-bound collateral, standard debt — both
966
+ // conservative, overshooting slightly). Overhead covers DEX
967
+ // routing impact + oracle drift + fee deduction.
968
+ const debtDecimals = 10n ** borrowToken.asset.decimals;
969
+ const collDecimals = 10n ** this.asset.decimals;
970
+ const debtInCollateral = (snapshot.debtTokenBalance * snapshot.debtAssetPrice * collDecimals) / (snapshot.collateralAssetPrice * debtDecimals);
971
+ // Total overhead = base overhead (DEX impact + drift) + fee bps.
972
+ // Additive approximation is accurate to sub-bp at typical
973
+ // fee+overhead magnitudes (< 100 bps combined).
974
+ const overheadBps = LEVERAGE.DELEVERAGE_OVERHEAD_BPS + feeBps;
975
+ swapCollateral = debtInCollateral * (10000n + overheadBps) / 10000n;
976
+ const maxCollateral = this.virtualConvertToAssets(this.cache.userCollateral);
977
+ if (swapCollateral > maxCollateral) {
978
+ swapCollateral = maxCollateral;
768
979
  }
769
980
  }
770
- const { action, quote } = await config.dexAgg.quoteAction(manager.address, this.asset.address, borrowToken.asset.address, swapCollateral, slippage);
771
- const minRepay = isFullDeleverage ? 1n : quote.out - (BigInt((0, decimal_js_1.default)(quote.out).mul(.05).toFixed(0)));
772
- // For full deleverage, add 50bps buffer to the contract-level slippage
773
- // check to account for oracle price variance in the oracleRoute multicall.
774
- const contractSlippage = isFullDeleverage
775
- ? slippage + 50n
981
+ else if (feeBps > 0n) {
982
+ // Partial deleverage: inflate swap size to compensate
983
+ // for fee deduction on input. KyberSwap deducts feeBps
984
+ // from input before swapping, so without compensation
985
+ // the swap underdelivers and actual leverage is slightly
986
+ // higher than target.
987
+ swapCollateral = swapCollateral * 10000n / (10000n - feeBps);
988
+ }
989
+ const { action, quote } = await config.dexAgg.quoteAction(manager.address, this.asset.address, borrowToken.asset.address, swapCollateral, slippage, feeBps, feeReceiver);
990
+ // _swapSafe: expand action.slippage by feeBps so the
991
+ // deterministic fee cost doesn't eat the user's MEV budget.
992
+ // Same rationale as leverageUp — see comment there.
993
+ if (feeBps > 0n) {
994
+ action.slippage = FormatConverter_1.default.bpsToBpsWad(slippage + feeBps);
995
+ }
996
+ const minRepay = isFullDeleverage ? 1n : quote.min_out;
997
+ // checkSlippage measures equity-fraction loss. Both the
998
+ // intentional swap overshoot (full deleverage only) and the
999
+ // DEX fee (always) are real equity losses amplified by
1000
+ // leverage. Expand contractSlippage to absorb them so the
1001
+ // user's `slippage` budget is preserved for variable
1002
+ // DEX impact + oracle drift.
1003
+ //
1004
+ // Full: (L-1) × (overhead + fee) — overshoot + fee
1005
+ // Partial: (ΔL) × fee — fee only, no overshoot
1006
+ const leverageDelta = isFullDeleverage
1007
+ ? currentLeverage.sub(1)
1008
+ : currentLeverage.sub(newLeverage);
1009
+ const forcedBps = isFullDeleverage
1010
+ ? LEVERAGE.DELEVERAGE_OVERHEAD_BPS + feeBps
1011
+ : feeBps;
1012
+ const contractSlippage = forcedBps > 0n
1013
+ ? slippage + BigInt(leverageDelta
1014
+ .mul(Number(forcedBps))
1015
+ .ceil()
1016
+ .toFixed(0))
776
1017
  : slippage;
777
1018
  calldata = manager.getDeleverageCalldata({
778
1019
  cToken: this.address,
@@ -812,18 +1053,42 @@ class CToken extends Calldata_1.Calldata {
812
1053
  const manager = this.getPositionManager(type);
813
1054
  let calldata;
814
1055
  const depositAssets = FormatConverter_1.default.decimalToBigInt(depositAmount, this.asset.decimals);
1056
+ await this._getLeverageSnapshot(borrow);
815
1057
  const { borrowAmount } = this.previewLeverageUp(multiplier, borrow, depositAssets);
816
1058
  switch (type) {
817
1059
  case 'simple': {
818
- 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);
1060
+ const borrowAssets = FormatConverter_1.default.decimalToBigInt(borrowAmount, borrow.asset.decimals);
1061
+ const feeBps = setup_1.setup_config.feePolicy.getFeeBps({
1062
+ operation: 'deposit-and-leverage',
1063
+ inputToken: borrow.asset.address,
1064
+ outputToken: this.asset.address,
1065
+ inputAmount: borrowAssets,
1066
+ currentLeverage: this.getLeverage() ?? (0, decimal_js_1.default)(1),
1067
+ targetLeverage: multiplier,
1068
+ });
1069
+ const feeReceiver = feeBps > 0n ? setup_1.setup_config.feePolicy.feeReceiver : undefined;
1070
+ 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);
1071
+ // _swapSafe: expand action.slippage by feeBps so the
1072
+ // deterministic fee cost doesn't eat the user's MEV budget.
1073
+ // Same rationale as leverageUp — see comment there.
1074
+ if (feeBps > 0n) {
1075
+ action.slippage = FormatConverter_1.default.bpsToBpsWad(slippage + feeBps);
1076
+ }
1077
+ // Fee amplification: same pattern as leverageUp.
1078
+ const contractSlippage = feeBps > 0n
1079
+ ? slippage + BigInt(multiplier.sub(1)
1080
+ .mul(Number(feeBps))
1081
+ .ceil()
1082
+ .toFixed(0))
1083
+ : slippage;
819
1084
  calldata = manager.getDepositAndLeverageCalldata(FormatConverter_1.default.decimalToBigInt(depositAmount, this.asset.decimals), {
820
1085
  borrowableCToken: borrow.address,
821
- borrowAssets: FormatConverter_1.default.decimalToBigInt(borrowAmount, borrow.asset.decimals),
1086
+ borrowAssets: borrowAssets,
822
1087
  cToken: this.address,
823
- expectedShares: this.virtualConvertToShares(BigInt(quote.min_out)),
1088
+ expectedShares: this.virtualConvertToShares(BigInt(quote.min_out), LEVERAGE.SHARES_BUFFER_BPS),
824
1089
  swapAction: action,
825
1090
  auxData: "0x",
826
- }, FormatConverter_1.default.bpsToBpsWad(slippage));
1091
+ }, FormatConverter_1.default.bpsToBpsWad(contractSlippage));
827
1092
  break;
828
1093
  }
829
1094
  case 'native-vault':
@@ -845,6 +1110,7 @@ class CToken extends Calldata_1.Calldata {
845
1110
  }
846
1111
  if (simulate)
847
1112
  return this.simulateOracleRoute(calldata, { to: manager.address });
1113
+ await this._checkPositionManagerApproval(manager);
848
1114
  return this.oracleRoute(calldata, { to: manager.address });
849
1115
  }
850
1116
  catch (error) {
@@ -1008,7 +1274,7 @@ class CToken extends Calldata_1.Calldata {
1008
1274
  if (remainingCollateral == 0n)
1009
1275
  throw new Error(collateralCapError);
1010
1276
  if (remainingCollateral > 0n) {
1011
- const shares = this.virtualConvertToShares(depositAssets);
1277
+ const shares = this.virtualConvertToShares(depositAssets, LEVERAGE.SHARES_BUFFER_BPS);
1012
1278
  if (shares > remainingCollateral) {
1013
1279
  throw new Error(collateralCapError);
1014
1280
  }
@@ -1061,7 +1327,13 @@ class CToken extends Calldata_1.Calldata {
1061
1327
  }
1062
1328
  convertTokensToUsd(tokenAmount, asset = true) {
1063
1329
  const price = this.getPrice(asset, false, false);
1064
- return FormatConverter_1.default.bigIntTokensToUsd(tokenAmount, price, this.decimals);
1330
+ // Pair the price with the matching decimals: asset price ↔ asset
1331
+ // decimals, share price ↔ share decimals. Falls back to share
1332
+ // decimals if asset.decimals is somehow unset (cToken share decimals
1333
+ // always equal asset decimals on current Curvance markets, so the
1334
+ // fallback is value-equivalent).
1335
+ const decimals = asset ? (this.asset.decimals ?? this.decimals) : this.decimals;
1336
+ return FormatConverter_1.default.bigIntTokensToUsd(tokenAmount, price, decimals);
1065
1337
  }
1066
1338
  async fetchConvertTokensToUsd(tokenAmount, asset = true) {
1067
1339
  // Reload cache
@@ -1075,7 +1347,9 @@ class CToken extends Calldata_1.Calldata {
1075
1347
  }
1076
1348
  convertAssetsToUsd(tokenAmount) {
1077
1349
  const price = this.getPrice(true, false, false);
1078
- const decimals = this.decimals;
1350
+ // Asset price ↔ asset decimals (with fallback to share decimals,
1351
+ // which equal asset decimals on current Curvance markets).
1352
+ const decimals = this.asset.decimals ?? this.decimals;
1079
1353
  return FormatConverter_1.default.bigIntTokensToUsd(tokenAmount, price, decimals);
1080
1354
  }
1081
1355
  async convertSharesToUsd(tokenAmount) {