Beginner FriendlyFoundryNFT
100 EXP
View results
Submission Details
Severity: medium
Valid

Users can be prevented from entering the raffle

Summary

It is possible to prevent users from entering the raffle under various conditions. These can arise due to the intended use of the protocol or via an attacker.

The severity was selected as high since an attacker could easily prevent this contract from minting NFT tokens in the future, potentially affecting the value of previous winner's tokens.

Vulnerability Details

Under all scenarios, the reason why this occurs is because there will be at least two 'holes', or two zero address entries, in the players storage array. This will fail the duplicates check in enterRaffle -- shown below:

for (uint256 i = 0; i < players.length - 1; i++) {
for (uint256 j = i + 1; j < players.length; j++) {
require(players[i] != players[j], "PuppyRaffle: Duplicate player");
}
}

Some scenarios that produce this state include:

  1. At least two users successfully refund.

  2. A user enters the raffle with a zero address & at least one user requests a refund.

Test scenarios below:

function test_TwoRefunds_PreventsEnterRaffle() public {
// Setup address array: players
address[] memory players = createPlayers(2); // helper function
// Enter raffle with players
puppyRaffle.enterRaffle{value: players.length * entranceFee}(players);
// Players request refunds successfully
startHoax(players[0]);
puppyRaffle.refund(0);
startHoax(players[1]);
puppyRaffle.refund(1);
vm.stopPrank();
// Setup address array: newPlayers
address[] memory newPlayers = createPlayers(1);
// ExpectRevert: attempt to enter raffle with new players
vm.expectRevert();
puppyRaffle.enterRaffle{value: newPlayers.length * entranceFee}(newPlayers);
}
function test_EnterWithZeroAddressAndRefund_PreventsEnterRaffle() public {
// Setup address array: players with a zero address entry
address[] memory players = new address[](2);
players[0] = address(0);
players[1] = makeAddr("alice");
// Enter the raffle with players
puppyRaffle.enterRaffle{value: players.length * entranceFee}(players);
// Alice successfully requests a refund
startHoax(players[1]);
puppyRaffle.refund(1);
// Setup address array: newPlayers
address[] memory newPlayers = createPlayers(1);
// ExpectRevert: attempt to enter raffle with new players
vm.expectRevert();
puppyRaffle.enterRaffle{value: newPlayers.length * entranceFee}(newPlayers);
}
// Helper function to create address array
function createPlayers(uint256 numPlayers) internal returns (address[] memory) {
address[] memory players = new address[](numPlayers);
uint256 lowerBound = uint256(vm.load(address(puppyRaffle), bytes32(uint256(11)))); // players.length at slot 11
uint256 upperBound = lowerBound + numPlayers;
for (uint256 i = lowerBound; i < upperBound; ++i) {
players[i - lowerBound] = makeAddr(string(abi.encodePacked("cyborg ", vm.toString(i))));
}
return players;
}

Impact

Two scenarios before users are prevented from entering the raffle:

  1. Less than 4 players have entered:
    The raffle can never end/restart since selectWinner cannot be called (requires more than 4 players). While users that have entered, are able to refund their ETH, the PuppyRaffle contract will not be able to mint any tokens in the future. Would have to consider redeploying the contract.

  2. More than 4 players have entered:
    selectWinner can be called and the raffle can be restarted.

Tools Used

Foundry.

Recommendations

Alter storage layout of contract.

  • Use a mapping instead of an array of addresses to track the players that have entered the raffle (or refunded). Example: mapping(address => bool) playerEntered;.

  • Use a variable to track the number of players entered (this can be slot packed to save gas) for determining the winnerIndex in selectWinner instead of players.length: Example: uint16 totalPlayers;

Updates

Lead Judging Commences

Hamiltonite Lead Judge about 2 years ago
Submission Judgement Published
Validated
Assigned finding tags:

denial-of-service-in-enter-raffle

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!