Part 2

Zaros
PerpetualsDEXFoundrySolidity
70,000 USDC
View results
Submission Details
Severity: low
Valid

Incorrect Slippage Enforcement in `initiateSwap` Due to Pre-Fee Amount Comparison Can Cause Swaps to Fail During Fulfilment

Summary

The StabilityBranch contract has a flaw in its slippage protection mechanism. The initiateSwap function performs slippage checks against a pre-fee amount, while the actual amount users receive is post-fee. This discrepancy allows swaps to be initiated that are guaranteed to fail during fulfillment, leading to temporarily locked funds until refund is initiated.

Vulnerability Details

The vulnerability stems from an inconsistency in how amounts are compared against the user's minAmountOut parameter between the initiation and fulfillment phases of a swap.

  1. During Initiation (initiateSwap):

  • The code calculates expectedAssetOut using getAmountOfAssetOut()

  • This calculation only takes into account the premiumDiscountFactor but does NOT include fees

  • It then compares this pre-fee amount against the user's minAmountOut

  • If expectedAssetOut >= minAmountOut, the swap is initiated

// Calculate expected output
ctx.expectedAssetOut = getAmountOfAssetOut(vaultIds[i], ud60x18(amountsIn[i]), ctx.collateralPriceX18).intoUint256();
// Check slippage
if (ctx.expectedAssetOut < minAmountsOut[i]) {
revert Errors.SlippageCheckFailed(minAmountsOut[i], ctx.expectedAssetOut);
}
  1. During Fulfillment (fulfillSwap):

  • It calculates amountOutBeforeFeesX18 using the same getAmountOfAssetOut()

  • Then calculates baseFeeX18 and swapFeeX18

  • Subtracts these fees to get the final amountOut

  • Compares this post-fee amount against minAmountOut

// Get amount out before fees
ctx.amountOutBeforeFeesX18 = getAmountOfAssetOut(ctx.vaultId, ud60x18(ctx.amountIn), ctx.priceX18);
// Get fees
(ctx.baseFeeX18, ctx.swapFeeX18) = getFeesForAssetsAmountOut(ctx.amountOutBeforeFeesX18, ctx.priceX18);
// Subtract fees to get final amount
ctx.amountOut = collateral.convertUd60x18ToTokenAmount(ctx.amountOutBeforeFeesX18.sub(ctx.baseFeeX18.add(ctx.swapFeeX18)));
// Check slippage
if (ctx.amountOut < ctx.minAmountOut) {
revert Errors.SlippageCheckFailed(ctx.minAmountOut, ctx.amountOut);
}

The issue is that:

  1. During initiation, it compares: expectedAssetOut (pre-fees) >= minAmountOut

  2. During fulfillment, it compares: amountOut (post-fees) >= minAmountOut

This means a swap could be initiated even though the final amount received after fees would be less than minAmountOut.

Therefore, the check during initiation is incorrect because it uses the pre-fee amount to compare against the user's minAmountOut. This is a flaw because the user's minAmountOut is not properly enforced. The user could set a minAmountOut that's higher than what they will actually receive, leading to the swap being initiated but later failing during fulfillment. Because during fulfillment, the code does check the post-fee amount against minAmountOut, which would revert if it's insufficient. So the user's transaction during initiateSwap would pass, but the fulfillSwap would fail. This leaves the user's funds locked until they can refund, but they have to wait for the deadline.

Example Scenario:

// Initial conditions
WETH price = $2,000
User wants to swap: 100,000 USDz
User sets minAmountOut: 49.9 WETH (expecting at least $1,996/WETH after 0.2% slippage)
// During initiateSwap
expectedAssetOut = 50 WETH (pre-fees)
// Slippage check in initiateSwap (INCORRECT)
if (50 ETH < 49.9 WETH) revert; // Passes because 50 > 49.9
// During fulfillSwap
amountOutBeforeFeesX18 = 50 WETH
// Calculate fees
baseFeeX18 = $1 / $2,000 per WETH = 0.0005 WETH
swapFeeX18 = 50 WETH * 0.3% = 0.15 WETH
totalFees = 0.1505 WETH (~$301)
// Final amount after fees
amountOut = 50 - 0.1505 = 49.8495 WETH
// Slippage check in fulfillSwap (CORRECT)
if (49.8495 ETH < 49.9 ETH) revert; // REVERTS because 49.8495 < 49.9

The transaction would fail because:

  • User expects at least 49.9 ETH (0.2% slippage from 50 ETH)

  • But after fees they get 49.8495 ETH

  • 49.8495 ETH < 49.9 ETH, so fulfillSwap reverts

Impact

Code does not properly enforce slippage checks during initiateSwap which can cause the swap to fail during fulfilSwap.

Tools Used

Manual Review

Recommendations

Modify the initiateSwap function to perform slippage checks against the post-fee amount:

function initiateSwap(
uint128[] calldata vaultIds,
uint128[] calldata amountsIn,
uint128[] calldata minAmountsOut
) external {
// ...
// Calculate expected output
UD60x18 expectedAssetOutX18 = getAmountOfAssetOut(
vaultIds[i],
ud60x18(amountsIn[i]),
ctx.collateralPriceX18
);
// Calculate fees
(UD60x18 baseFeeX18, UD60x18 swapFeeX18) = getFeesForAssetsAmountOut(
expectedAssetOutX18,
ctx.collateralPriceX18
);
// Calculate post-fee amount
uint256 expectedAssetOutAfterFees = collateral.convertUd60x18ToTokenAmount(
expectedAssetOutX18.sub(baseFeeX18.add(swapFeeX18))
);
// Check slippage against post-fee amount
if (expectedAssetOutAfterFees < minAmountsOut[i]) {
revert Errors.SlippageCheckFailed(minAmountsOut[i], expectedAssetOutAfterFees);
}
// ...
}

This change ensures that:

  1. Slippage checks are consistent between initiation and fulfillment

  2. Users can accurately set their minimum output expectations

  3. Swaps that would fail during fulfillment are rejected during initiation

  4. User funds are not unnecessarily locked

Updates

Lead Judging Commences

inallhonesty Lead Judge 5 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Missing basefee and settlement fee deduction before slippage check in the Stablitybranch contract.

Appeal created

0xshoonya Submitter
5 months ago
inallhonesty Lead Judge
4 months ago
inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Missing basefee and settlement fee deduction before slippage check in the Stablitybranch contract.

Support

FAQs

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