Summary
Informational: The enter_raffle
external function allows participation from the ZERO_ADDRESS
in the Snek Raffle, despite the critical fact that the ZERO_ADDRESS
is incapable of holding an NFT.
Vulnerability Details
Please review the enter_raffle
external function starting from Line 113. There's currently no assert/revert check in place for the ZERO_ADDRESS
.
@external
@payable
def enter_raffle():
"""Enter the raffle by sending the entrance fee."""
------>
assert msg.value == ENTRANCE_FEE, ERROR_SEND_MORE_TO_ENTER_RAFFLE
assert self.raffle_state == RaffleState.OPEN, ERROR_RAFFLE_NOT_OPEN
self.players.append(msg.sender)
log RaffleEntered(msg.sender)
Due to the absence of a ZERO_ADDRESS
check in the enter_raffle
function, the Snek Raffle
lacks a contingency plan to prevent the minting of an NFT and payment of the reward amount to a ZERO_ADDRESS
. This could lead to failure whenever the VRF - fulfillRandomWords
function attempts to mint an NFT for the ZERO_ADDRESS
.
Additionally, there are no checks or sanitization for the ZERO_ADDRESS
even within the VRF - fulfillRandomWords
internal function.
@internal
def fulfillRandomWords(request_id: uint256, random_words: uint256[MAX_ARRAY_SIZE]):
index_of_winner: uint256 = random_words[0] % len(self.players)
recent_winner: address = self.players[index_of_winner]
self.recent_winner = recent_winner
self.players = []
self.raffle_state = RaffleState.OPEN
self.last_timestamp = block.timestamp
rarity: uint256 = random_words[0] % 3
self.tokenIdToRarity[ERC721._total_supply()] = rarity
log WinnerPicked(recent_winner)
ERC721._mint(recent_winner, ERC721._total_supply())
-----------------^
send(recent_winner, self.balance)
Impact
If a Raffle spinning includes a ZERO_ADDRESS
as a participant and that ZERO_ADDRESS
becomes the winner, the Raffle will become indefinitely stuck and won't allow any further sessions to spin the Raffle. This vulnerability could potentially be exploited as a Denial of Service (DoS) attack if a malicious user discovers it within the Raffle. An attacker could exploit (Not Possible on Live chains) this vulnerability by deploying a malicious contract that pushes a ZERO_ADDRESS
as a participant into the Raffle. However, the DoS attack can only occur if the ZERO_ADDRESS
is declared the winner which is not possible into the real world or live chains. Why? Because EVM
automatically selects the msg.sender=deployer or contract
and that can't be a zero address.
DoS with ZERO_ADDRESS
def test_nft_mint_fails_on_zero_address(raffle_boa, vrf_coordinator_boa, entrance_fee):
boa.env.set_balance(ZERO_ADDRESS, STARTING_BALANCE)
with boa.env.prank(ZERO_ADDRESS):
raffle_boa.enter_raffle(value=entrance_fee)
boa.env.time_travel(seconds=INTERVAL + 1)
with boa.reverts("ERC721: mint to the zero address"):
vrf_coordinator_boa.fulfillRandomWords(0, raffle_boa.address)
pytest -v tests/snek_raffle_test.py::test_nft_mint_fails_on_zero_address -s
==================================================================== test session starts =====================================================================
platform linux -- Python 3.10.12, pytest-8.0.2, pluggy-1.4.0 -- /home/theirrationalone/vyperenv/bin/python
cachedir: .pytest_cache
hypothesis profile 'default' -> database=DirectoryBasedExampleDatabase(PosixPath('/home/theirrationalone/first-flights/2024-03-snek-raffle/.hypothesis/examples'))
rootdir: /home/theirrationalone/first-flights/2024-03-snek-raffle
plugins: titanoboa-0.1.8, cov-4.1.0, hypothesis-6.98.17
collected 1 item
tests/snek_raffle_test.py::test_nft_mint_fails_on_zero_address PASSED
===================================================================== 1 passed in 1.56s ======================================================================
As evidenced by the successful passing of the test, it is apparent that the Raffle is vulnerable to a Denial of Service (DoS) Attack (theoretically Possible only) under certain circumstances.
Tools Used
Manual Review, Pytest
Recommendations
Please update the contracts/snek_raffle.vy
file as provided below...
.
.
.
# Errors
ERROR_NOT_ENDED: constant(String[25]) = "SnekRaffle: Not ended"
ERROR_TRANSFER_FAILED: constant(String[100]) = "SnekRaffle: Transfer failed"
ERROR_SEND_MORE_TO_ENTER_RAFFLE: constant(String[100]) = "SnekRaffle: Send more to enter raffle"
ERROR_RAFFLE_NOT_OPEN: constant(String[100]) = "SnekRaffle: Raffle not open"
ERROR_NOT_COORDINATOR: constant(String[46]) = "SnekRaffle: OnlyCoordinatorCanFulfill"
+ ERROR_ZERO_ADDRESS_NOT_ALLOWED: constant(String[50]) = "SnekRaffle: Zero Address not allowed"
.
.
.
# External Functions
@external
@payable
def enter_raffle():
"""Enter the raffle by sending the entrance fee."""
+ assert msg.sender != empty(address), ERROR_ZERO_ADDRESS_NOT_ALLOWED
assert msg.value == ENTRANCE_FEE, ERROR_SEND_MORE_TO_ENTER_RAFFLE
assert self.raffle_state == RaffleState.OPEN, ERROR_RAFFLE_NOT_OPEN
self.players.append(msg.sender)
log RaffleEntered(msg.sender)
.
.
.
Save the file.
Mitigation PoC:
def test_reverts_on_zero_address_entry(raffle_boa, entrance_fee):
boa.env.set_balance(ZERO_ADDRESS, STARTING_BALANCE)
with boa.env.prank(ZERO_ADDRESS):
with boa.reverts("SnekRaffle: Zero Address not allowed"):
raffle_boa.enter_raffle(value=entrance_fee)
pytest -v tests/snek_raffle_test.py::test_reverts_on_zero_address_entry -s
==================================================================== test session starts =====================================================================
platform linux -- Python 3.10.12, pytest-8.0.2, pluggy-1.4.0 -- /home/theirrationalone/vyperenv/bin/python
cachedir: .pytest_cache
hypothesis profile 'default' -> database=DirectoryBasedExampleDatabase(PosixPath('/home/theirrationalone/first-flights/2024-03-snek-raffle/.hypothesis/examples'))
rootdir: /home/theirrationalone/first-flights/2024-03-snek-raffle
plugins: titanoboa-0.1.8, cov-4.1.0, hypothesis-6.98.17
collected 1 item
tests/snek_raffle_test.py::test_reverts_on_zero_address_entry PASSED
===================================================================== 1 passed in 1.55s ======================================================================