Summary
The function enterRaffle doesn't ensure that address(0) is not passed within the newPlayers array. If selectWinner function will ever pick a address(0) winner, it will fail due to ERC721: mint to the zero address.
Vulnerability Details
The _safeMint function in OZ's ERC721 implementation calls the _mint function from the same place which looks as follows:
function _mint(address to, uint256 tokenId) internal virtual {
require(to != address(0), "ERC721: mint to the zero address");
require(!_exists(tokenId), "ERC721: token already minted");
PoC
function testEnter4Zero() public {
address[] memory players = new address[](4);
players[0] = playerOne;
players[1] = playerTwo;
players[2] = playerThree;
players[3] = address(0);
vm.warp(block.timestamp + duration + 1);
vm.roll(block.number + 1);
puppyRaffle.enterRaffle{value: entranceFee * 4}(players);
puppyRaffle.selectWinner();
}
This fails in 25% of the cases.
forge test --mt testEnter4Zero
Ran 1 test suites: 0 tests passed, 1 failed, 0 skipped (1 total tests)
Failing tests:
Encountered 1 failing test in test/PuppyRaffleTest.t.sol:PuppyRaffleTest
[FAIL. Reason: ERC721: mint to the zero address] testEnter4Zero() (gas: 204625)
Impact
In case of low participation the enterRaffle has a reasonable chance to fail due to address(0) winning the NFT that can't be minted to address(0). Making interested parties to call the function again and again until a non zero address wins.
Tools Used
Manual review
Recommendations
function enterRaffle(address[] memory newPlayers) public payable {
require(msg.value == entranceFee * newPlayers.length, "PuppyRaffle: Must send enough to enter raffle");
for (uint256 i = 0; i < newPlayers.length; i++) {
++ require(newPlayers[i] != address(0), "PuppyRaffle: Address 0 not allowed")
players.push(newPlayers[i]);
}
// Check for duplicates
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");
}
}
emit RaffleEnter(newPlayers);
}