Hoarder Can Deny Other Users from Buying Passes in FestivalPass.sol::buyPass
Description
-
The buyPass
function lets users purchase festival passes by sending the exact ETH price for the selected tier.
-
However, there is no limit on the number of passes one wallet can buy. As a result, one user or bot can buy the entire available supply for a pass tier, preventing other participants from purchasing passes fairly. This creates an economic Denial-of-Service (DoS) condition where fair distribution is impossible.
function buyPass(uint256 collectionId) external payable {
require(collectionId == GENERAL_PASS || collectionId == VIP_PASS || collectionId == BACKSTAGE_PASS, "Invalid pass ID");
require(msg.value == passPrice[collectionId], "Incorrect payment amount");
require(passSupply[collectionId] < passMaxSupply[collectionId], "Max supply reached");
_mint(msg.sender, collectionId, 1, "");
++passSupply[collectionId];
uint256 bonus = (collectionId == VIP_PASS) ? 5e18 : (collectionId == BACKSTAGE_PASS) ? 15e18 : 0;
if (bonus > 0) {
BeatToken(beatToken).mint(msg.sender, bonus);
}
emit PassPurchased(msg.sender, collectionId);
}
Risk
Likelihood:
Impact:
-
Legitimate users are denied access once supply is exhausted by the hoarder
-
The hoarder can resell passes at a markup, harming the fairness and reputation of the festival ecosystem
Proof of Concept
Add this code to your FestivalPassTest.sol
file to confirm the proof
...
address public attacker = makeAddr("attacker");
...
function setUp() public {
...
vm.deal(attacker, 1000 ether);
}
function test_Economic_DoS() public {
vm.startPrank(attacker);
for (uint256 i = 0; i < BACKSTAGE_MAX_SUPPLY; i++) {
festivalPass.buyPass{value: BACKSTAGE_PRICE}(3);
}
vm.stopPrank();
vm.startPrank(user1);
vm.expectRevert("Max supply reached");
festivalPass.buyPass{value: BACKSTAGE_PRICE}(3);
vm.stopPrank();
}
Recommended Mitigation
This change uses a single hasPurchasedTier
flag to ensure each address can only purchase one pass in total, regardless of the tier. Once a buyer purchases any pass, further purchases are blocked, enforcing a strict one-tier-per-user limit.
+ mapping(address => bool) public hasPurchasedTier;
function buyPass(uint256 collectionId) external payable {
require(collectionId == GENERAL_PASS || collectionId == VIP_PASS || collectionId == BACKSTAGE_PASS, "Invalid pass ID");
+ require(!hasPurchasedTier[msg.sender], "Address can only purchase one tier");
require(msg.value == passPrice[collectionId], "Incorrect payment amount");
require(passSupply[collectionId] < passMaxSupply[collectionId], "Max supply reached");
+ hasPurchasedTier[msg.sender] = true;
_mint(msg.sender, collectionId, 1, "");
++passSupply[collectionId];
uint256 bonus = (collectionId == VIP_PASS) ? 5e18 : (collectionId == BACKSTAGE_PASS) ? 15e18 : 0;
if (bonus > 0) {
BeatToken(beatToken).mint(msg.sender, bonus);
}
emit PassPurchased(msg.sender, collectionId);
}