Part 2

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

Unbounded Loop in checkUpkeep

Summary

The checkUpkeep function contains an unbounded loop that iterates over all liveMarketIds and their associated marketAssets. If the number of markets or assets is very large, the function could run out of gas, causing the transaction to fail. This is a common issue in smart contracts that process large datasets.

Vulnerability Details

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);
}
}

Impact

  • If there are too many liveMarketIds or marketAssets, the loop could consume more gas than the block gas limit, causing the transaction to fail.

  • Processing a large number of markets and assets in a single transaction is inefficient and could lead to high gas costs.

  • An attacker could intentionally create a large number of markets or assets to make the function unusable. Denial of Service (DoS):

Tools Used

Manual Code Review

Recommendations

To address the unbounded loop issue, consider the following solutions:

Add a gas limit check to ensure the function does not consume too much gas

uint256 gasLimit = 5_000_000; // Example gas limit
uint256 gasUsed = 0;
for (uint128 i; i < liveMarketIds.length; i++) {
if (gasUsed >= gasLimit) {
break;
}
uint128 marketId = liveMarketIds[i];
(address[] memory marketAssets, uint256[] memory feesCollected) =
self.marketMakingEngine.getReceivedMarketFees(marketId);
for (uint128 j; j < marketAssets.length; j++) {
uint256 startGas = gasleft();
distributionNeeded = checkFeeDistributionNeeded(marketAssets[j], feesCollected[j]);
if (distributionNeeded) {
upkeepNeeded = true;
marketIds[index] = marketId;
assets[index] = marketAssets[j];
index++;
}
gasUsed += startGas - gasleft();
if (gasUsed >= gasLimit) {
break;
}
}
}

Pagination

Process markets and assets in smaller batches.

function checkUpkeep(uint128 startMarketId, uint128 batchSize) 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[](batchSize * 10);
address[] memory assets = new address[](batchSize * 10);
uint256 index = 0;
// Iterate over a batch of markets
for (uint128 i = startMarketId; i < startMarketId + batchSize && i < liveMarketIds.length; i++) {
uint128 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) {
upkeepNeeded = true;
marketIds[index] = marketId;
assets[index] = marketAssets[j];
index++;
}
}
}
if (upkeepNeeded) {
performData = abi.encode(marketIds, assets);
}
}
Updates

Lead Judging Commences

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