Trick or Treat

First Flight #27
Beginner FriendlyFoundry
100 EXP
View results
Submission Details
Severity: medium
Invalid

Unbounded Array Iteration in Constructor Causes Contract Deployment Failure Due to Block Gas Limit

Vulnerability Details

The constructor accepts an unbounded array of treats and iterates through each element to call the public addTreat() function, which performs storage operations (mapping updates and array pushes) for each treat. Since there's no limit on the input array size and each iteration consumes significant gas through storage operations, passing a large number of treats during deployment could cause the transaction to exceed Ethereum's block gas limit (currently ~30M gas). This would render the contract undeployable and could be exploited as a denial-of-service vector by malicious actors who intentionally provide an oversized array.

constructor(Treat[] memory treats) ERC721("SpookyTreats", "SPKY") {
nextTokenId = 1;
@> for (uint256 i = 0; i < treats.length; i++) {
@> addTreat(treats[i].name, treats[i].cost, treats[i].metadataURI);
@> }
}

Impact

The impact is that malicious actors could permanently prevent the SpookyTreats contract from being deployed by providing an excessively large array of treats that exceeds the block gas limit, effectively denying the entire service/protocol from ever launching on-chain.

Proof of Concept

To create a test folder and a file named TestTrickOrTreat.t.sol for your Foundry project, you can follow these steps:

  • Create the test folder: Navigate to your project directory and create a test folder.

  • Create the TestTrickOrTreat.t.sol file: Inside the test folder, create a file named TestTrickOrTreat.t.sol.

  • Add the Foundry test code: Here’s an example of what you might include in your TestTrickOrTreat.t.sol file:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test, console} from "forge-std/Test.sol";
import {SpookySwap} from "../src/TrickOrTreat.sol";
contract TrickOrTreatTest is Test {
SpookySwap public spookySwap;
address owner;
function setUp() public {
owner = address(this);
}
function testDOSConstructor() public {
// Create an array with large number of treats to exceed block gas limit
// Most EVM networks have ~30M block gas limit
// Each storage operation (mapping + array push) costs ~20k gas
// So around 1500 treats should exceed the block gas limit
SpookySwap.Treat[] memory maliciousTreats = new SpookySwap.Treat[]();
// Fill the array with treats
for (uint256 i = 0; i < 1500; i++) {
maliciousTreats[i] = SpookySwap.Treat({
name: string(abi.encodePacked("MaliciousTreat", vm.toString(i))),
cost: 0.01 ether,
metadataURI: string(abi.encodePacked("ipfs://QmEvil/", vm.toString(i)))
});
}
// Try to deploy contract with malicious treats array
// This should revert due to exceeding block gas limit
vm.expectRevert();
spookySwap = new SpookySwap(maliciousTreats);
// Log gas usage (if it somehow didn't revert)
uint256 gasUsed = gasleft();
console.log("Gas Used:", gasUsed);
}
}

Tools Used

Manual Review

Recommendations

Mitigate DOS risk by removing constructor's unbounded loop and introducing a size-limited batch addition function for treats

+ uint256 public constant BATCH_SIZE = 100;
constructor(Treat[] memory treats) ERC721("SpookyTreats", "SPKY") {
nextTokenId = 1;
- for (uint256 i = 0; i < treats.length; i++) {
- addTreat(treats[i].name, treats[i].cost, treats[i].metadataURI);
- }
}
+ function addTreatBatch(Treat[] calldata treats) external onlyOwner {
+ require(treats.length <= BATCH_SIZE, "Batch too large");
+ for (uint256 i = 0; i < treats.length; i++) {
+ _addTreat(treats[i].name, treats[i].cost, treats[i].metadataURI);
+ }
+ }
Updates

Appeal created

bube Lead Judge 10 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.