Part 2

Zaros
PerpetualsDEXFoundrySolidity
70,000 USDC
View results
Submission Details
Severity: medium
Invalid

Incorrect Curve Calculation Results in Zero Swap Output

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: // calculate the vault's tvl / debt absolute value, positive means we'll apply a discount, negative means
107: // we'll apply a premium
108:
109: UD60x18 vaultDebtTvlRatioAbs = vaultDebtUsdX18.abs().intoUD60x18().div(vaultAssetsValueUsdX18);
110:
111: // cache the minimum x value of the premium / discount curve
112: UD60x18 pdCurveXMinX18 = ud60x18(0.5e18);//ud60x18(self.pdCurveXMin);
113: // cache the maximum x value of the premium / discount curve
114: UD60x18 pdCurveXMaxX18 =UD60x18_UNIT; //ud60x18(self.pdCurveXMax);
115:
116: // if the vault's debt / tvl ratio is less than or equal to the minimum x value of the premium / discount
117: // curve, then we don't apply any premium or discount
118: if (vaultDebtTvlRatioAbs.lte(pdCurveXMinX18)) {
119: premiumDiscountFactorX18 = UD60x18_UNIT;
120: return premiumDiscountFactorX18;
121: }
122:
123: // if the vault's debt / tvl ratio is greater than or equal to the maximum x value of the premium / discount
124: // curve, we use the max X value, otherwise, use the calculated vault tvl / debt ratio
125: UD60x18 pdCurveXX18 = vaultDebtTvlRatioAbs.gte(pdCurveXMaxX18) ? pdCurveXMaxX18 : vaultDebtTvlRatioAbs;
126:
127: // cache the minimum y value of the premium / discount curve
128: UD60x18 pdCurveYMinX18 = ud60x18(0.3e18); //ud60x18(self.pdCurveYMin);
129: // cache the maximum y value of the premium / discount curve
130: UD60x18 pdCurveYMaxX18 = UD60x18_UNIT;//ud60x18(self.pdCurveYMax);
131:
132: // cache the exponent that determines the steepness of the premium / discount curve
133: UD60x18 pdCurveZX18 = UD60x18_UNIT;//ud60x18(self.pdCurveZ);
134: // calculate the y point of the premium or discount curve given the x point
135: UD60x18 pdCurveYX18 = pdCurveYMinX18.add(
136: pdCurveYMaxX18.sub(pdCurveYMinX18).mul(
137: pdCurveXX18.sub(pdCurveXMinX18).div(pdCurveXMaxX18.sub(pdCurveXMinX18)).pow(pdCurveZX18)
138: )
139: );
140:
141: // if the vault is in credit, we apply a discount, otherwise, we apply a premium
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: // calculate the premium or discount that may be applied to the vault asset's index price
121: // note: if no premium or discount needs to be applied, the premiumDiscountFactorX18 will be
122: // 1e18 (UD60x18 one value)
123: UD60x18 premiumDiscountFactorX18 =
124: UsdTokenSwapConfig.load().getPremiumDiscountFactor(vaultAssetsUsdX18, vaultDebtUsdX18);
125:
126: // get amounts out taking into consideration the CL price and the premium/discount
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);
// working data
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.

Updates

Lead Judging Commences

inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.