Part 2

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

permanent loss of funds in function initiateSwap

Summary

Each swap request should be assigned a unique requestId, incrementing sequentially per user.

This ensures that swapRequests[user][requestId] references a distinct swap, even when multiple swaps are initiated in the same transaction. This is not done in the code as can be seen below.

Vulnerability Details

function initiateSwap(
uint128[] calldata vaultIds,
uint128[] calldata amountsIn,
uint128[] calldata minAmountsOut
)
external
{
// Perform length checks
if (vaultIds.length != amountsIn.length) {
revert Errors.ArrayLengthMismatch(vaultIds.length, amountsIn.length);
}
if (amountsIn.length != minAmountsOut.length) {
revert Errors.ArrayLengthMismatch(amountsIn.length, minAmountsOut.length);
}
// working data
InitiateSwapContext memory ctx;
// cache the vault's index token and asset addresses
Vault.Data storage currentVault = Vault.load(vaultIds[0]);
ctx.initialVaultIndexToken = currentVault.indexToken;
ctx.initialVaultCollateralAsset = currentVault.collateral.asset;
// load collateral data; must be enabled
Collateral.Data storage collateral = Collateral.load(ctx.initialVaultCollateralAsset);
collateral.verifyIsEnabled();
// load market making engine config
MarketMakingEngineConfiguration.Data storage configuration = MarketMakingEngineConfiguration.load();
// load usd token swap data
UsdTokenSwapConfig.Data storage tokenSwapData = UsdTokenSwapConfig.load();
// cache additional common fields
// ctx.collateralPriceX18 in zaros internal precision
ctx.collateralPriceX18 = currentVault.collateral.getPrice();
ctx.maxExecTime = uint120(tokenSwapData.maxExecutionTime);
// ctx.vaultAssetBalance in native precision of ctx.initialVaultCollateralAsset
ctx.vaultAssetBalance = IERC20(ctx.initialVaultCollateralAsset).balanceOf(ctx.initialVaultIndexToken);
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];
// 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];
  • Inside the loop of initiateSwap, the requestId for each swap in the batch is fetched by:

    ctx.requestId = tokenSwapData.nextId(msg.sender); // Inside loop

    The nextId function does not increment on retrieval. The nextId returns the current ID without incrementing, all swaps in the batch receive the same requestId.

    Example: First swap gets requestId=5, the second also gets requestId=5 in the same transaction.**** All swaps in the batch clobber the same swapRequests[user][5] entry, leaving only the last swap’s details.

    Another Example:

    • User calls initiateSwap with 3 swaps (vaultIds.length=3).

    • Current nextId=5 for the user.

    • First Loop (i=0):
      nextId returns 5, assigns requestId=5.
      Sets swapRequests[user][5] = SwapRequest_1.

    • Second Loop (i=1):
      nextId still returns 5 (not incremented).
      Overwrites swapRequests[user][5] = SwapRequest_2.

    • Third Loop (i=2):
      Same requestId=5. Overwrites to SwapRequest_3.

  • Result:

    • Only SwapRequest_3 is stored. SwapRequest_1 and SwapRequest_2 are permanently lost.

    • Users lose USD tokens for the first two swaps, as no entries exist to refund or fulfill them.

Impact

Users cannot refund/fulfill any swap except the last one, leading to permanent loss of funds.

Tools Used

Manual Review

Recommendations

Increment nextId Appropriately: Modify nextId to auto-increment per user on retrieval

// Inside UsdTokenSwapConfig.Data:
function nextId(address user) internal returns (uint128) {
return nextRequestId[user]++; // Auto-increment
}
Updates

Lead Judging Commences

inallhonesty Lead Judge
6 months ago
inallhonesty Lead Judge 6 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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