Part 2

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

Missing Expiry Check in fulfillSwap in StabilityBranch.sol

Summary

function fulfillSwap(
address user,
uint128 requestId,
bytes calldata priceData,
address engine
)
external
onlyRegisteredSystemKeepers
{
// load request for user by id
UsdTokenSwapConfig.SwapRequest storage request = UsdTokenSwapConfig.load().swapRequests[user][requestId];
// revert if already processed
if (request.processed) {
revert Errors.RequestAlreadyProcessed(user, requestId);
}
// working data
FulfillSwapContext memory ctx;
// if request dealine expired revert
ctx.deadline = request.deadline;
if (ctx.deadline < block.timestamp) {
revert Errors.SwapRequestExpired(user, requestId, ctx.deadline);
}
// set request processed to true
request.processed = true;
// load market making engine config
MarketMakingEngineConfiguration.Data storage marketMakingEngineConfiguration =
MarketMakingEngineConfiguration.load();
// load vault data
ctx.vaultId = request.vaultId;
Vault.Data storage vault = Vault.loadLive(ctx.vaultId);
// get usd token of engine
ctx.usdToken = UsdToken(marketMakingEngineConfiguration.usdTokenOfEngine[engine]);
// load Stability configuration data
StabilityConfiguration.Data storage stabilityConfiguration = StabilityConfiguration.load();
// get price from report in 18 dec
ctx.priceX18 = stabilityConfiguration.verifyOffchainPrice(priceData);
// get amount out asset
ctx.amountIn = request.amountIn;
ctx.amountOutBeforeFeesX18 = getAmountOfAssetOut(ctx.vaultId, ud60x18(ctx.amountIn), ctx.priceX18);
// gets the base fee and swap fee for the given amount out before fees
(ctx.baseFeeX18, ctx.swapFeeX18) = getFeesForAssetsAmountOut(ctx.amountOutBeforeFeesX18, ctx.priceX18);
// cache the collateral asset address
ctx.asset = vault.collateral.asset;
// load the collateral configuration storage pointer
Collateral.Data storage collateral = Collateral.load(ctx.asset);
// subtract the fees and convert the UD60x18 value to the collateral's decimals value
ctx.amountOut =
collateral.convertUd60x18ToTokenAmount(ctx.amountOutBeforeFeesX18.sub(ctx.baseFeeX18.add(ctx.swapFeeX18)));
// slippage check
ctx.minAmountOut = request.minAmountOut;
if (ctx.amountOut < ctx.minAmountOut) {
revert Errors.SlippageCheckFailed(ctx.minAmountOut, ctx.amountOut);
}
// calculates the protocol's share of the swap fee by multiplying the total swap fee by the protocol's fee
// recipients' share.
ctx.protocolSwapFeeX18 = ctx.swapFeeX18.mul(ud60x18(marketMakingEngineConfiguration.totalFeeRecipientsShares));
// the protocol reward amount is the sum of the base fee and the protocol's share of the swap fee
ctx.protocolReward = collateral.convertUd60x18ToTokenAmount(ctx.baseFeeX18.add(ctx.protocolSwapFeeX18));
// update vault debt
vault.marketsRealizedDebtUsd -= int128(ctx.amountIn);
// burn usd amount from address(this)
ctx.usdToken.burn(ctx.amountIn);
// transfer the required assets from the vault to the mm engine contract before distributions
// note: as the swap fee stays in the ZLP Vault, it is technically a net gain to share holders, i.e it is auto
// accumulated to the contract
IERC20(ctx.asset).safeTransferFrom(vault.indexToken, address(this), ctx.amountOut + ctx.protocolReward);
// distribute protocol reward value
marketMakingEngineConfiguration.distributeProtocolAssetReward(ctx.asset, ctx.protocolReward);
// transfers the remaining amount out to the user, discounting fees
// note: the vault's share of the swap fee remains in the index token contract, thus, we don't need transfer
// it anywhere. The end result is that vaults have an amount of their debt paid off with a discount.
IERC20(ctx.asset).safeTransfer(user, ctx.amountOut);
emit LogFulfillSwap(
user,
requestId,
ctx.vaultId,
ctx.amountIn,
ctx.minAmountOut,
request.assetOut,
ctx.deadline,
ctx.amountOut,
ctx.baseFeeX18.intoUint256(),
ctx.swapFeeX18.intoUint256(),
ctx.protocolReward
);
}

The function does not validate whether block.timestamp has exceeded the deadline before marking a request as processed.

This could allow keepers to execute stale swaps after the intended execution window, leading to potential price manipulation.

Vulnerability Details

The fulfillSwap function does not verify whether the deadline has expired before marking the swap request as processed. This oversight allows a keeper to execute a swap after the intended execution window, potentially manipulating execution timing for financial gain.

This means:

  • Users could receive an unfair price if the swap is executed under different market conditions than expected.

  • Market manipulation is possible if keepers selectively execute only profitable swaps while ignoring unfavorable ones.

Impact

A malicious keeper or delayed execution due to network congestion could result in stale swaps being processed under changed market conditions.

Users may suffer losses if the execution price deviates significantly from their expectations.

Tools Used

Manual Review

Recommendations

Enforce a Deadline Check Before Processing the Swap

Modify the fulfillSwap function to ensure expired swap requests revert before being processed:

if (block.timestamp > ctx.deadline) {
revert Errors.SwapRequestExpired(user, requestId, ctx.deadline);
}
Updates

Lead Judging Commences

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

Support

FAQs

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