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;
for (uint128 i; i < liveMarketIds.length; i++) {
marketId = liveMarketIds[i];
(address[] memory marketAssets, uint256[] memory feesCollected) =
self.marketMakingEngine.getReceivedMarketFees(marketId);
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);
}
}
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;
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;
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);
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);
}
}