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.
- package/dist/abis/ProtocolReader.json +64 -0
- package/dist/chains/monad-mainnet.json +1 -26
- package/dist/classes/CToken.d.ts +33 -6
- package/dist/classes/CToken.d.ts.map +1 -1
- package/dist/classes/CToken.js +325 -51
- package/dist/classes/CToken.js.map +1 -1
- package/dist/classes/DexAggregators/IDexAgg.d.ts +3 -1
- package/dist/classes/DexAggregators/IDexAgg.d.ts.map +1 -1
- package/dist/classes/DexAggregators/Kuru.d.ts +3 -3
- package/dist/classes/DexAggregators/Kuru.d.ts.map +1 -1
- package/dist/classes/DexAggregators/Kuru.js +19 -9
- package/dist/classes/DexAggregators/Kuru.js.map +1 -1
- package/dist/classes/DexAggregators/KyberSwap.d.ts +3 -3
- package/dist/classes/DexAggregators/KyberSwap.d.ts.map +1 -1
- package/dist/classes/DexAggregators/KyberSwap.js +97 -14
- package/dist/classes/DexAggregators/KyberSwap.js.map +1 -1
- package/dist/classes/DexAggregators/MultiDexAgg.d.ts +3 -3
- package/dist/classes/DexAggregators/MultiDexAgg.d.ts.map +1 -1
- package/dist/classes/DexAggregators/MultiDexAgg.js +13 -13
- package/dist/classes/DexAggregators/MultiDexAgg.js.map +1 -1
- package/dist/classes/OracleManager.js.map +1 -1
- package/dist/classes/ProtocolReader.d.ts +10 -0
- package/dist/classes/ProtocolReader.d.ts.map +1 -1
- package/dist/classes/ProtocolReader.js +4 -0
- package/dist/classes/ProtocolReader.js.map +1 -1
- package/dist/classes/Zapper.d.ts.map +1 -1
- package/dist/classes/Zapper.js +39 -2
- package/dist/classes/Zapper.js.map +1 -1
- package/dist/contracts/monad-mainnet.json +1 -1
- package/dist/feePolicy.d.ts +182 -0
- package/dist/feePolicy.d.ts.map +1 -0
- package/dist/feePolicy.js +110 -0
- package/dist/feePolicy.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/retry-provider.js +3 -3
- package/dist/retry-provider.js.map +1 -1
- package/dist/setup.d.ts +8 -1
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +3 -1
- package/dist/setup.js.map +1 -1
- package/package.json +1 -1
- package/dist/chains/arb-sepolia.json +0 -44
- package/dist/classes/DexAggregators/KuruMainnet.d.ts +0 -1
- package/dist/classes/DexAggregators/KuruMainnet.d.ts.map +0 -1
- package/dist/classes/DexAggregators/KuruMainnet.js +0 -228
- package/dist/classes/DexAggregators/KuruMainnet.js.map +0 -1
- package/dist/classes/Kuru.d.ts +0 -59
- package/dist/classes/Kuru.d.ts.map +0 -1
- package/dist/classes/Kuru.js +0 -167
- package/dist/classes/Kuru.js.map +0 -1
- package/dist/classes/KuruMainnet.d.ts +0 -59
- package/dist/classes/KuruMainnet.d.ts.map +0 -1
- package/dist/classes/KuruMainnet.js +0 -167
- package/dist/classes/KuruMainnet.js.map +0 -1
- package/dist/snapshot.d.ts +0 -53
- package/dist/snapshot.d.ts.map +0 -1
- package/dist/snapshot.js +0 -103
- package/dist/snapshot.js.map +0 -1
package/dist/classes/CToken.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
103
|
-
|
|
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 (
|
|
389
|
-
|
|
473
|
+
if (asset) {
|
|
474
|
+
if (getLower)
|
|
475
|
+
this.cache.assetPriceLower = price;
|
|
476
|
+
else
|
|
477
|
+
this.cache.assetPrice = price;
|
|
390
478
|
}
|
|
391
479
|
else {
|
|
392
|
-
|
|
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
|
-
*
|
|
623
|
-
*
|
|
624
|
-
*
|
|
625
|
-
*
|
|
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
|
-
|
|
629
|
-
if (leverageFactor.lte(0))
|
|
747
|
+
if (leverage.lte(1))
|
|
630
748
|
return slippage;
|
|
631
|
-
|
|
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
|
|
657
|
-
//
|
|
658
|
-
|
|
659
|
-
const
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
766
|
-
|
|
767
|
-
|
|
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
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
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
|
|
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:
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|