Part 2

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

Missing Input Validation in Swap Requests Can Lead to Trapped User Funds

Summary

The StabilityBranch.sol contract lacks input validation for swap amounts in relation to base fees. Users can initiate swaps with amounts smaller than the required base fee, resulting in their funds becoming permanently trapped due to refund calculation underflows. A simple validation check should be added to prevent accepting swap requests with insufficient amounts.

Links to affected code

  • StabilityBranch.sol:L197~490

Vulnerability details

Finding description and impact

In StabilityBranch.sol, when users initiate swaps using the initiateSwap() function, there is a critical validation missing that could lead to user funds becoming permanently trapped in the protocol.

The core issue lies in the relationship between swap amounts and base fees:

  1. The initiateSwap() function accepts swap requests without validating if the input amount is sufficient to cover the base fee

  2. When users attempt to refund failed swaps via refundSwap(), the function calculates the refund amount as:

    uint256 refundAmountUsd = depositedUsdToken - baseFeeUsd;
  3. If the original amountIn was less than baseFeeUsd, this calculation will revert due to underflow

This creates a scenario where:

  • Users can submit swap requests with amounts below the base fee

  • These requests cannot be fulfilled (due to insufficient funds for fees)

  • Users cannot refund them (due to underflow)

  • The funds become permanently trapped in the protocol

This vulnerability affects the protocol's user experience and could result in loss of user funds in edge cases where small amounts are swapped without proper validation.

Recommended mitigation steps

Add an explicit validation in initiateSwap() to ensure the input amount is sufficient to cover the base fee:

function initiateSwap(
uint128[] calldata vaultIds,
uint128[] calldata amountsIn,
uint128[] calldata minAmountsOut
)
external
{
..........................................................................
for (uint256 i; i < amountsIn.length; i++) {
// for all but first iteration, refresh the vault and enforce same collateral asset
if (i != 0) {
currentVault = Vault.load(vaultIds[i]);
// revert for swaps using vaults with different collateral assets
if (currentVault.collateral.asset != ctx.initialVaultCollateralAsset) {
revert Errors.VaultsCollateralAssetsMismatch();
}
// refresh current vault balance in native precision of ctx.initialVaultCollateralAsset
ctx.vaultAssetBalance = IERC20(ctx.initialVaultCollateralAsset).balanceOf(currentVault.indexToken);
}
// cache the expected amount of assets acquired with the provided parameters
// amountsIn[i] and ctx.collateralPriceX18 using zaros internal precision
ctx.expectedAssetOut =
getAmountOfAssetOut(vaultIds[i], ud60x18(amountsIn[i]), ctx.collateralPriceX18).intoUint256();
// revert if the slippage wouldn't pass or the expected output was 0
if (ctx.expectedAssetOut == 0) revert Errors.ZeroOutputTokens();
if (ctx.expectedAssetOut < minAmountsOut[i]) {
revert Errors.SlippageCheckFailed(minAmountsOut[i], ctx.expectedAssetOut);
}
// if there aren't enough assets in the vault to fulfill the swap request, we must revert
if (ctx.vaultAssetBalance < ctx.expectedAssetOut) {
revert Errors.InsufficientVaultBalance(vaultIds[i], ctx.vaultAssetBalance, ctx.expectedAssetOut);
}
// transfer USD: user => address(this) - burned in fulfillSwap
ctx.usdTokenOfEngine = IERC20(configuration.usdTokenOfEngine[currentVault.engine]);
ctx.usdTokenOfEngine.safeTransferFrom(msg.sender, address(this), amountsIn[i]);
// get next request id for user
ctx.requestId = tokenSwapData.nextId(msg.sender);
// load swap request
UsdTokenSwapConfig.SwapRequest storage swapRequest = tokenSwapData.swapRequests[msg.sender][ctx.requestId];
+ require(amountsIn[i] >= tokenSwapData.baseFeeUsd, "amountIn is less than baseFee");
// Set swap request parameters
swapRequest.minAmountOut = minAmountsOut[i];
swapRequest.vaultId = vaultIds[i];
swapRequest.assetOut = ctx.initialVaultCollateralAsset;
ctx.deadlineCache = uint120(block.timestamp) + ctx.maxExecTime;
swapRequest.deadline = ctx.deadlineCache;
swapRequest.amountIn = amountsIn[i];
emit LogInitiateSwap(
msg.sender,
ctx.requestId,
vaultIds[i],
amountsIn[i],
minAmountsOut[i],
ctx.initialVaultCollateralAsset,
ctx.deadlineCache
);
}
}
Updates

Lead Judging Commences

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

initiateSwap can be called with amount < than base fee, making the refund function revert due to underflow - funds stuck

Support

FAQs

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