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

Weak randomness in the `RapBattle::_battle` function

Summary

The function RapBattle::_battle uses block.timestamp and block.prevrandao to generate random number. This random number is then used to determine the winner of the battle. But relying on the block.timestamp and block.prevrandao for randomness can be risky and the generated random number can be predicted rather than random.

Vulnerability Details

The RapBattle::_battle function determines the winner of the battle based on random number and rapper skills of the participants.

function _battle(uint256 _tokenId, uint256 _credBet) internal {
address _defender = defender;
require(defenderBet == _credBet, "RapBattle: Bet amounts do not match");
uint256 defenderRapperSkill = getRapperSkill(defenderTokenId);
uint256 challengerRapperSkill = getRapperSkill(_tokenId);
uint256 totalBattleSkill = defenderRapperSkill + challengerRapperSkill;
uint256 totalPrize = defenderBet + _credBet;
@> uint256 random =
@> uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao, msg.sender))) % totalBattleSkill;
// Reset the defender
defender = address(0);
emit Battle(msg.sender, _tokenId, random < defenderRapperSkill ? _defender : msg.sender);
// If random <= defenderRapperSkill -> defenderRapperSkill wins, otherwise they lose
if (random <= defenderRapperSkill) {
// We give them the money the defender deposited, and the challenger's bet
credToken.transfer(_defender, defenderBet);
credToken.transferFrom(msg.sender, _defender, _credBet);
} else {
// Otherwise, since the challenger never sent us the money, we just give the money in the contract
credToken.transfer(msg.sender, _credBet);
}
totalPrize = 0;
// Return the defender's NFT
oneShotNft.transferFrom(address(this), _defender, defenderTokenId);
}

But the generation of random is based on block.timestamp, block.prevrandao, msg.sender which is not recommended.
Relying on block.timestamp for randomness is risky because the validator selected for a transaction has the power to hold or delay the transaction until a more favorable time or reject the transaction because the timestamp isn't favorable.

Timestamp manipulation has become less of an issue on Ethereum, since the merge, but it isn't perfect. Other chains, such as Arbitrum can be vulnerable to several seconds of slippage putting randomness based on block.timestamp at risk.

block.prevrandao replaced the block.difficulty. This is a system to choose random validators. The security issues using this value for randomness are described in the EIP-4399 documentation: https://eips.ethereum.org/EIPS/eip-4399#predictability

Additionally, in the Solidity documentation is written:

Do not rely on block.timestamp or blockhash as a source of randomness, unless you know what you are doing.

https://docs.soliditylang.org/en/latest/units-and-global-variables.html

Impact

The function RapBattle::_battle uses block.timestamp, block.prevrandao and msg.sender to generate a pseudo-random number for determining the winner of the battle. This is considered weak because miners have some control over the block.timestamp and block.prevrandao, which could potentially be exploited to manipulate the outcome of the battle. The block.timestamp influences the keccak256 function, and subsequently the _battle function, by being part of the seed that generates the pseudo-random number. In the _battle function, the seed for the keccak256 function is created by packing block.timestamp, block.prevrandao and msg.sender together using abi.encodePacked.

To demonstrate the weak randomness in the _battle function, we can write a test case that manipulates the block.timestamp and block.prevrandao using the vm.warp and vm.prevrandao functions in Foundry. The vm.warp and vm.prevrandao functions in Foundry give control over the simulated blockchain environment in the tests. These functions don't directly influence the calculation of the winner in the _battle function, but they do allow to manipulate the values that are used in that calculation. Add the following test case in file OneShot.t.test.sol and execute it with the Foundry command: forge test --match-test "testWeakRandomness" -vvvvv.

function testWeakRandomness() public mintRapper {
//Set block.timestamp and block.prevrandao
vm.warp(block.timestamp);
//vm.roll(block.prevrandao);
vm.prevrandao(bytes32(uint256(42)));
// User calls goOnStageOrBattle
vm.startPrank(user);
oneShot.approve(address(rapBattle), 0);
rapBattle.goOnStageOrBattle(0, 0);
// User's rapper skill
uint256 finalSkillUser = rapBattle.getRapperSkill(0);
vm.stopPrank();
vm.startPrank(challenger);
// Challenger mints a rapper
oneShot.mintRapper();
vm.recordLogs();
// Challenger calls goOnStageOrBattle
rapBattle.goOnStageOrBattle(1, 0);
// Challenger's rapper skill
uint256 finalSkillChallenger = rapBattle.getRapperSkill(1);
vm.stopPrank();
// Calculate the totalBattleSkill
uint256 totalBattleSkill = finalSkillUser + finalSkillChallenger;
// Simulate the random number from the _battle function
uint256 random = uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao, challenger))) % totalBattleSkill;
// Display the random number, there is also a console.log of random in _battle function.
console.log("test:", random);
// Get address of winner1
Vm.Log[] memory entries = vm.getRecordedLogs();
address winner1 = address(uint160(uint256(entries[0].topics[2])));
// Repeat the battle one more time to proof that the winner and random number will be the same
vm.warp(block.timestamp);
//vm.roll(block.prevrandao);
vm.prevrandao(bytes32(uint256(42)));
vm.startPrank(user);
oneShot.approve(address(rapBattle), 0);
// User calls the goOnStageOrBattle
rapBattle.goOnStageOrBattle(0, 0);
vm.stopPrank();
vm.startPrank(challenger);
oneShot.mintRapper();
vm.recordLogs();
// Challenger calls goOnStageOrBattle
rapBattle.goOnStageOrBattle(1, 0);
vm.stopPrank();
// Simulate the random number from the _battle function, the totalBattleSkill is the same as in the prevous battle
uint256 random1 = uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao, challenger))) % totalBattleSkill;
console.log("test:", random1);
// Get address of winner2
Vm.Log[] memory entries2 = vm.getRecordedLogs();
address winner2 = address(uint160(uint256(entries2[0].topics[2])));
// Assert that in two battles the winner is the same
assertEq(winner1, winner2);
}

The test function testWeakRandomness demonstrates that in two battles with the same participants the generated random number and respectively the winner are the same.

Tools Used

Manual Review, Foundry

Recommendations

You might consider using a dedicated randomness oracle such as Chainlink VRF. https://blog.chain.link/random-number-generation-solidity/

Updates

Lead Judging Commences

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

Weak Randomness

Support

FAQs

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