Part 2

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

Unbounded Iteration in Fee Conversion Upkeep


Summary and Impact

The FeeConversionKeeper contract’s checkUpkeep function iterates over an array of live market IDs without any explicit upper bound. In a scenario where the number of live markets grows significantly, the gas required to iterate over all IDs may approach or exceed the block gas limit. This would cause the upkeep function to revert, halting the fee conversion process. Although the potential for an attacker to create markets is constrained by protocol invariants and market creation controls, a failure in fee conversion can lead to an accumulation of unconverted fees, which may disrupt the protocol’s fee distribution and economic balance. Given the overall design and invariants detailed in the documentation, this vulnerability presents a medium risk: the economic impact is significant but mitigated by system controls and predictable growth patterns.


Vulnerability Details

The vulnerability arises in the core function (herein referred to as checkUpkeep) within the FeeConversionKeeper contract. This function calls a helper (e.g., getLiveMarketIds()) that returns an array of all currently active market IDs. The function then loops over the entire array to process fee conversions for each market. There is no mechanism to limit or batch the iteration. If the array grows too large, the gas consumption will scale linearly with the number of market IDs. Eventually, even if each iteration is inexpensive, the cumulative gas usage can exceed the block gas limit, leading to a revert.

Code Snippet

Below is an illustrative excerpt from the vulnerable contract:

function checkUpkeep(bytes calldata /**/ ) external view returns (bool upkeepNeeded, bytes memory performData) {
FeeConversionKeeperStorage memory self = _getFeeConversionKeeperStorage();
uint128[] memory liveMarketIds = self.marketMakingEngine.getLiveMarketIds();
bool distributionNeeded;
uint128[] memory marketIds = new uint128[](liveMarketIds.length * 10);
address[] memory assets = new address[](liveMarketIds.length * 10);
uint256 index = 0;
uint128 marketId;
// Iterate over markets by id
for (uint128 i; i < liveMarketIds.length; i++) {
marketId = liveMarketIds[i];
(address[] memory marketAssets, uint256[] memory feesCollected) =
self.marketMakingEngine.getReceivedMarketFees(marketId);
// Iterate over receivedMarketFees
for (uint128 j; j < marketAssets.length; j++) {
distributionNeeded = checkFeeDistributionNeeded(marketAssets[j], feesCollected[j]);
if (distributionNeeded) {
// set upkeepNeeded = true
upkeepNeeded = true;
// set marketId, asset
marketIds[index] = marketId;
assets[index] = marketAssets[j];
index++;
}
}
}
if (upkeepNeeded) {
performData = abi.encode(marketIds, assets);
}
}

This snippet clearly demonstrates that the loop iterates over an unbounded array (liveMarketIds) without any batching or limit, risking gas exhaustion when the number of live markets is high.


Tools Used

  • Manual Review


Recommendations

Batch Processing:
Modify checkUpkeep to process a fixed number of markets per call. Track the index of the last processed market across successive calls so that all markets are eventually processed without requiring a single transaction to iterate through all live markets.

Updates

Lead Judging Commences

inallhonesty Lead Judge 6 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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