Part 2

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

Reentrancy Vulnerability in fulfillSwap functon 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
);
}

Even though onlyRegisteredSystemKeepers enforces access control, it does not prevent reentrancy if a keeper interacts with a malicious ERC-20 token. Reentrancy Can Still Happen Within a Single Keeper's Execution.

CEI pattern not followed.

No Reentrancy Guard.

Vulnerability Details

The keeper might interact with a malicious ERC20 token. This might reenter the contract and drain out funds.

Malicious ERC-20 tokens could exploit this by calling fulfillSwap in their safeTransferFrom hook. Malicious ERC-20 tokens can weaponize safeTransferFrom to re-enter fulfillSwap before state updates.

Impact

More than one valid withdrawals.

Loss of funds from the vault.

Tools Used

Manual Review

Recommendations

Implement nonReentrant in fulfillSwap.

Ensure state updates happen BEFORE external calls.

Updates

Lead Judging Commences

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

Support

FAQs

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