49705 sc medium two vectors for unbounded gas consumption due to the normal raffle operations
Submitted on Jul 18th 2025 at 15:49:15 UTC by @blackgrease for Attackathon | Plume Network
Report ID: #49705
Report Type: Smart Contract
Report severity: Medium
Target: https://github.com/immunefi-team/attackathon-plume-network/blob/main/plume/src/spin/Raffle.sol
Impacts:
Unbounded gas consumption
Description
Note
This report outlines a long term issue that is based on the current design of the Raffle contract.
Summary
The Raffle contract has two griefing vectors affecting users and admin roles. This is due to how the contract handles inactive and active prizes forcing removePrize and getPrizeDetails to iterate over a large array potentially resulting in a denial of service where no prize can be removed and no prize details can be retrieved.
Description
The Raffle contract allows users to use their obtained raffles from the Spin rewards system in order to obtain prizes. Prizes are set by an admin.
The Issue:
This means the prizeIds array will hold both active and inactive prizes. Over time, this array will grow in size and there is no limit set for its size.
Impacting functions:
removePrize loops over the
prizeIdsarray to find and remove an id.getPrizeDetails loops over the full
prizeIdsarray and returns details for each entry.
Example: removePrize implementation in the contract
function removePrize(uint256 prizeId) external onlyRole(ADMIN_ROLE) prizeIsActive(prizeId) {
prizes[prizeId].isActive = false;
// Remove from prizeIds array
uint256 len = prizeIds.length;
for (uint256 i = 0; i < len; i++) { //@audit: unbounded gas consumption
if (prizeIds[i] == prizeId) {
prizeIds[i] = prizeIds[len - 1];
prizeIds.pop();
break;
}
}
emit PrizeRemoved(prizeId);
}Example: getPrizeDetails implementation in the contract
function getPrizeDetails() external view returns (PrizeWithTickets[] memory) {
uint256 prizeCount = prizeIds.length;
PrizeWithTickets[] memory prizeArray = new PrizeWithTickets[](prizeCount);
for (uint256 i = 0; i < prizeCount; i++) { //@audit: unbounded gas consumption
uint256 currentPrizeId = prizeIds[i];
Prize storage currentPrize = prizes[currentPrizeId];
prizeArray[i] = PrizeWithTickets({
name: currentPrize.name,
description: currentPrize.description,
value: currentPrize.value,
endTimestamp: currentPrize.endTimestamp,
isActive: currentPrize.isActive,
quantity: currentPrize.quantity,
winnersDrawn: winnersDrawn[currentPrizeId],
totalTickets: totalTickets[currentPrizeId],
totalUsers: totalUniqueUsers[currentPrizeId]
});
}
return prizeArray;
}Alternatively, for both vectors, even if out-of-gas errors are not thrown, the high gas cost may make the functions infeasible to interact with, reducing their purpose/functionality.
Impact
Unbounded gas consumption resulting in a Denial of Service, either by:
out of gas
or infeasibility to invoke due to high gas costs
Affected parties:
Admin: prevents incorrect or undesirable prizes from being removed from the
Rafflecontract.Users: prevents them from identifying what prize to spend their raffle reward on, making them blind to the current prizes in the contract.
As a side effect, users not being able to get prize details may reduce participation in the gamified system and hinder user engagement for the Plume system.
Mitigation
Recommended mitigations:
Once a prize has been fully claimed by winners, the prize should be removed from the
prizeIdsarray. Because events are emitted (e.g., when a prize is fully claimed), historical prize data remains accessible on-chain via explorers.Additionally, enforce a maximum limit to the number of active prizes which will bound the
prizeIdsarray.
These mitigations ensure only active prizes remain in prizeIds, preventing unbounded loops and improving efficiency. For users, returned information will be limited to useful (active) prizes.
Link to Proof of Concept
https://gist.github.com/blackgrease/2682d84574dcc145ddcb53524a9e3c19
Proof of Concept
As proof of the validity of this issue the reporter included a runnable PoC alongside a walk-through. The runnable script displays the gas consumption for both vectors.
Private Gist link: "https://gist.github.com/blackgrease/2682d84574dcc145ddcb53524a9e3c19"
Run both tests with:
forge test --mt testUnboundedGasConsumption -vvv --via-irThe reporter also attached a screenshot of the Foundry console after the test displaying the gas consumption across both vectors.
Was this helpful?