Part 2

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

Refund Underflow in Swap Refund Logic Leading to Locked Funds


Summary and Impact

The vulnerability lies in the refund logic within the refundSwap function in the StabilityBranch.sol contract. In this function, the code subtracts a configured base fee (baseFeeUsd) from the user’s deposited amount (amountIn) without verifying that the deposit is at least equal to the fee. When a user creates a swap request with an amountIn lower than the baseFeeUsd, the subtraction underflows. As Solidity 0.8+ performs automatic overflow/underflow checks, the underflow triggers a revert, leaving the swap request unprocessed and the user’s funds permanently locked in the contract.

Impact:

  • User Funds Locked: Users depositing amounts smaller than the base fee cannot reclaim their funds.

  • Denial of Service: This issue could be exploited to cause a denial-of-service for low–value swap requests.

  • Protocol Invariants: The flaw violates the expected invariant that every swap request should eventually be either completed or refunded. Instead, underflow conditions cause the request to remain unresolved.

The flaw is significant because it directly conflicts with the protocol’s expectation that all user deposits are either executed or safely returned. In this case, the refund calculation fails, thereby trapping user funds and undermining confidence in the swap functionality.


Vulnerability Details

  • Description:
    In the refundSwap function, the contract attempts to calculate the refund by subtracting the base fee from the deposited amount:

    uint256 refundAmountUsd = depositedUsdToken - baseFeeUsd;

    If depositedUsdToken (i.e. amountIn) is less than baseFeeUsd, the subtraction underflows, triggering a revert due to Solidity’s built-in overflow/underflow protection. Consequently, the swap request remains unprocessed and the user’s funds are trapped indefinitely.

  • Code Snippet:

The problematic portion of the code in StabilityBranch.sol is as follows:

function refundSwap(uint128 requestId, address engine) external {
// load swap data
UsdTokenSwapConfig.Data storage tokenSwapData = UsdTokenSwapConfig.load();
// load swap request
UsdTokenSwapConfig.SwapRequest storage request = tokenSwapData.swapRequests[msg.sender][requestId];
// if request already procesed revert
if (request.processed) {
revert Errors.RequestAlreadyProcessed(msg.sender, requestId);
}
// if dealine has not yet passed revert
uint120 deadlineCache = request.deadline;
if (deadlineCache > block.timestamp) {
revert Errors.RequestNotExpired(msg.sender, requestId);
}
// set precessed to true
request.processed = true;
// load Market making engine config
MarketMakingEngineConfiguration.Data storage marketMakingEngineConfiguration =
MarketMakingEngineConfiguration.load();
// get usd token for engine
address usdToken = marketMakingEngineConfiguration.usdTokenOfEngine[engine];
// cache the usd token swap base fee
uint256 baseFeeUsd = tokenSwapData.baseFeeUsd;
// cache the amount of usd token previously deposited
uint128 depositedUsdToken = request.amountIn;
// transfer base fee too protocol fee recipients
marketMakingEngineConfiguration.distributeProtocolAssetReward(usdToken, baseFeeUsd);
// cache the amount of usd tokens to be refunded
uint256 refundAmountUsd = depositedUsdToken - baseFeeUsd;
// transfer usd refund amount back to user
IERC20(usdToken).safeTransfer(msg.sender, refundAmountUsd);
emit LogRefundSwap(
msg.sender,
requestId,
request.vaultId,
depositedUsdToken,
request.minAmountOut,
request.assetOut,
deadlineCache,
baseFeeUsd,
refundAmountUsd
);
}
  • Test Code Snippet:
    Below is an excerpt from the Foundry test case that demonstrates the issue:

    // Simulate a swap request with amountIn = 50 (less than baseFeeUsd = 100)
    _setSwapRequest(user, TEST_REQUEST_ID, 50);
    // Expect the refundSwap call to revert due to underflow (50 - 100 underflows)
    vm.prank(user);
    vm.expectRevert();
    mockStability.testRefundSwap(TEST_REQUEST_ID, address(0));

    This test confirms that if a user submits a swap request with an amountIn smaller than the required base fee, the refund calculation underflows and reverts, leaving the funds locked.

  • Technical Walkthrough:

    1. Swap Request Creation: A user submits a swap request with a deposit (amountIn) that is less than the configured baseFeeUsd.

    2. Refund Attempt: When the user (or a system keeper) later attempts to execute refundSwap, the contract retrieves the amountIn from the swap request.

    3. Underflow Calculation: The refund is computed as amountIn - baseFeeUsd. If amountIn (e.g., 50) is less than baseFeeUsd (e.g., 100), the arithmetic underflows.

    4. Reversion: Solidity’s checked arithmetic causes the transaction to revert, preventing the refund logic from completing and leaving the swap request unprocessed.

    5. Result: The user’s funds remain locked in the contract with no mechanism for recovery.


Tools Used

  • Manual Review

  • Foundry


Recommendations

Enforce a Minimum Deposit:
At the time of swap request creation, add an invariant check to require that amountIn >= baseFeeUsd. This would prevent requests with insufficient deposits from being created in the first place.

require(amountIn >= tokenSwapData.baseFeeUsd, "Deposit must be >= base fee");

Updates

Lead Judging Commences

inallhonesty Lead Judge 6 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.