Gas Limit Denial of Service (DoS) via Nested Loops
Description
The function getUserMemorabiliaDetailed implements a nested loop structure to identify and aggregate the memorabilia owned by a specific user. It first iterates through all existing collections nextCollectionId and then, within each collection, iterates through every individual item currentItemId.As the project scales and the number of collections and unique items increases, the cumulative gas required to execute these loops will grow quadratically. Eventually, the gas cost will exceed the Block Gas Limit, causing the function to revert.
Risk
Likelihood:
As the project scales, the number of collections and items will inevitably increase. The complexity of the nested loops ensures the gas cost will eventually exceed the block gas limit.
Impact:
Failure of this function prevents the frontend from querying asset details directly from the chain, breaking the user experience. While it doesn't result in a direct loss of funds, it causes a permanent Denial of Service (DoS) for a core contract feature.
Proof of Concept
When the contract state contains 10,000 memorabilia entries, calling getUserMemorabiliaDetailed triggers an OutOfGas revert.
Proof of Code:
function test_DoS_getUserMemorabiliaDetailed() public {
vm.deal(organizer, 10000 ether);
vm.deal(user1, 1000 ether);
for(uint256 i = 0; i < 10000; i++){
string memory uri = string.concat("ipfs://Qm.../", Strings.toString(i), ".json");
string memory name = string.concat("Memorabilia #", Strings.toString(i));
vm.prank(organizer);
festivalPass.createMemorabiliaCollection(name, uri, 1e18, 100,true);
}
uint256 gasBefore = gasleft();
vm.prank(user1);
festivalPass.getUserMemorabiliaDetailed(user1);
uint256 gasAfter = gasleft();
uint256 gasLeft = gasBefore- gasAfter ;
console2.log(gasLeft);
}
Test output:
├─ [77041] FestivalPass::createMemorabiliaCollection("Memorabilia #7330", "ipfs://Qm.../7330.json", 1000000000000000000 [1e18], 100, true)
│ └─ ← [OutOfGas] EvmError: OutOfGas
└─ ← [Revert] EvmError: Revert
Recommended Mitigation
Instead of using nested loops, the contract should implement proactive ownership tracking by overriding the _update function. By updating a mapping userOwnedTokensduring the mint process, the contract ensures that asset data is pre-compiled, allowing the query function to return results directly without risk of an OutOfGas revert.
+ mapping(address => uint256[]) public userOwnedTokens;
+ function _update(
+ address from,
+ address to,
+ uint256[] memory ids,
+ uint256[] memory values
+ ) internal virtual override {
+ if (from != address(0) && to != address(0)) {
+ for (uint256 i = 0; i < ids.length; i++) {
+ // Check if the token ID belongs to the restricted Pass category
+ if (ids[i] <= 3) {
+ revert("Transfer is not allowed");}
+ }
+
+ super._update(from, to, ids, values);
+ // when mint add to the array
+ if (from == address(0) && to != address(0)) {
+ for (uint256 i = 0; i < ids.length; i++) {
+ userOwnedTokens[to].push(ids[i]);
+ }
+
+ }
- function getUserMemorabiliaDetailed(address user) external view returns (
- uint256[] memory tokenIds,
- uint256[] memory collectionIds,
- uint256[] memory itemIds
- ) {
- // First, count how many memorabilia they own
- uint256 count = 0;
- for (uint256 cId = 1; cId < nextCollectionId; cId++) {
- for (uint256 iId = 1; iId < collections[cId].currentItemId; iId++) {
- ........
-
- }
-
- return (tokenIds, collectionIds, itemIds);
- }
+ function getUserMemorabiliaDetailed(address user)
+ external view returns (uint256[] memory tokenIds){
+ return _userOwnedTokens[user];
+ }