Summary
The getPremiumDiscountFactor function can return 0 due to uninitialized or improperly configured curve parameters. This leads to amountOutX18 also being 0, effectively blocking swaps and causing a DoS for the protocol.
Vulnerability Details
The premium/discount curve values (pdCurveXMinX18, pdCurveXMaxX18, pdCurveYMinX18, pdCurveYMaxX18, pdCurveZX18)
are not initialized in the codebase. If initialized manually, the function’s calculation may still result in premiumDiscountFactorX18 = 0, preventing swaps.
Example Configuration:
UD60x18 pdCurveXMinX18 = ud60x18(0.5e18);
UD60x18 pdCurveXMaxX18 =UD60x18_UNIT;
UD60x18 pdCurveYMinX18 = ud60x18(0.3e18);
UD60x18 pdCurveYMaxX18 = UD60x18_UNIT;
UD60x18 pdCurveZX18 = UD60x18_UNIT;
If getPremiumDiscountFactor
is modified to use these values:
/src/market-making/leaves/UsdTokenSwapConfig.sol:97
97: function getPremiumDiscountFactor(
98: Data storage self,
99: UD60x18 vaultAssetsValueUsdX18,
100: SD59x18 vaultDebtUsdX18
101: )
102: internal
103: view
104: returns (UD60x18 premiumDiscountFactorX18)
105: {
106:
107:
108:
109: UD60x18 vaultDebtTvlRatioAbs = vaultDebtUsdX18.abs().intoUD60x18().div(vaultAssetsValueUsdX18);
110:
111:
112: UD60x18 pdCurveXMinX18 = ud60x18(0.5e18);
113:
114: UD60x18 pdCurveXMaxX18 =UD60x18_UNIT;
115:
116:
117:
118: if (vaultDebtTvlRatioAbs.lte(pdCurveXMinX18)) {
119: premiumDiscountFactorX18 = UD60x18_UNIT;
120: return premiumDiscountFactorX18;
121: }
122:
123:
124:
125: UD60x18 pdCurveXX18 = vaultDebtTvlRatioAbs.gte(pdCurveXMaxX18) ? pdCurveXMaxX18 : vaultDebtTvlRatioAbs;
126:
127:
128: UD60x18 pdCurveYMinX18 = ud60x18(0.3e18);
129:
130: UD60x18 pdCurveYMaxX18 = UD60x18_UNIT;
131:
132:
133: UD60x18 pdCurveZX18 = UD60x18_UNIT;
134:
135: UD60x18 pdCurveYX18 = pdCurveYMinX18.add(
136: pdCurveYMaxX18.sub(pdCurveYMinX18).mul(
137: pdCurveXX18.sub(pdCurveXMinX18).div(pdCurveXMaxX18.sub(pdCurveXMinX18)).pow(pdCurveZX18)
138: )
139: );
140:
141:
142:
143: premiumDiscountFactorX18 =
144: vaultDebtUsdX18.lt(SD59x18_ZERO) ? UD60x18_UNIT.sub(pdCurveYX18) : UD60x18_UNIT.add(pdCurveYX18);
145: }
When vaultDebtUsdX18 is negative, UD60x18_UNIT.sub(pdCurveYX18) can result in 0, leading to a failed swap.
/src/market-making/leaves/UsdTokenSwapConfig.sol:144
144: premiumDiscountFactorX18 =
145: vaultDebtUsdX18.lt(SD59x18_ZERO) ? UD60x18_UNIT.sub(pdCurveYX18) : UD60x18_UNIT.add(pdCurveYX18);
146:
If premiumDiscountFactorX18 = 0, amountOutX18 will always return zero, blocking the swap process.
/src/market-making/branches/StabilityBranch.sol:119
119:
120:
121:
122:
123: UD60x18 premiumDiscountFactorX18 =
124: UsdTokenSwapConfig.load().getPremiumDiscountFactor(vaultAssetsUsdX18, vaultDebtUsdX18);
125:
126:
127: amountOutX18 = usdAmountInX18.div(indexPriceX18).mul(premiumDiscountFactorX18);
128:
129: }
Consider following proof of concept:
POC
function test_it_Will_revert()
external
whenCallerIsKeeper
whenRequestWasNotYetProcessed
whenSwapRequestNotExpired
{
uint256 vaultId = 100;
uint256 marketId = 100;
uint256 vaultAssetsBalance = 5000e22;
uint256 swapAmount = 1000e18;
uint128 vaultDebtAbsUsd = uint128(2.33e30);
TestFuzz_WhenSlippageCheckPassesAndThePremiumOrDiscountIsNotZero_Context memory ctx;
ctx.fuzzVaultConfig = getFuzzVaultConfig(vaultId);
ctx.oneAsset = 10 ** ctx.fuzzVaultConfig.decimals;
changePrank({ msgSender: users.owner.account });
marketMakingEngine.configureUsdTokenSwapConfig(1, 30, type(uint96).max);
marketMakingEngine.workaround_setVaultDebt(uint128(ctx.fuzzVaultConfig.vaultId),-int128(vaultDebtAbsUsd));
marketMakingEngine.workaround_setVaultDepositedUsdc(uint128(ctx.fuzzVaultConfig.vaultId),0);
changePrank({ msgSender: users.naruto.account });
deal({
token: address(ctx.fuzzVaultConfig.asset),
to: ctx.fuzzVaultConfig.indexToken,
give: vaultAssetsBalance
});
swapAmount = vaultAssetsBalance;
deal({ token: address(usdToken), to: users.naruto.account, give: swapAmount });
ctx.fuzzPerpMarketCreditConfig = getFuzzPerpMarketCreditConfig(marketId);
ctx.engine = IMockEngine(perpMarketsCreditConfig[ctx.fuzzPerpMarketCreditConfig.marketId].engine);
ctx.minAmountOut = 0;
UD60x18 priceUsdX18 = IPriceAdapter(vaultsConfig[ctx.fuzzVaultConfig.vaultId].priceAdapter).getPrice();
ctx.priceData = getMockedSignedReport(ctx.fuzzVaultConfig.streamId, priceUsdX18.intoUint256());
ctx.usdTokenSwapKeeper = usdTokenSwapKeepers[ctx.fuzzVaultConfig.asset];
UD60x18 negativeDebtAmountOut =
marketMakingEngine.getAmountOfAssetOut(ctx.fuzzVaultConfig.vaultId, ud60x18(swapAmount), priceUsdX18);
vm.assume(negativeDebtAmountOut.intoUint256() > 0);
}
Impact
Swaps cannot proceed due to amountOutX18 = 0. Protocol disruption: Vaults cannot rebalance assets effectively.
Tools Used
Manual Review, Unit Testing
Recommendations
Add a safety check at the end of getPremiumDiscountFactor to prevent zero values:
if (premiumDiscountFactorX18.isZero()) {
premiumDiscountFactorX18 = UD60x18_UNIT;
}
This ensures premiumDiscountFactorX18 never returns 0, allowing swaps to function correctly.