TwentyOne

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

Contract Lacks Sufficient Ether to Handle Game Logic and Payouts

Summary

The contract fails to process player winnings due to insufficient funds in the contract when the player wins. The test test_Call_PlayerWins() fails because the contract is deployed without enough ether to process the payout. The error occurs when the contract attempts to transfer 2 ether to the winning player but lacks the necessary balance.

Vulnerability Details

When running the test test_Call_PlayerWins(), the following error occurs:

[FAIL: EvmError: Revert] test_Call_PlayerWins() (gas: 1450168)
Logs:
player1: 0x0000000000000000000000000000000000000123
twentyOne balance: 0
contract balance: 1000000000000000000
initialPlayerBalance: 9000000000000000000
Traces:
[705494] TwentyOneTest::setUp()
├─ [659863] → new TwentyOne@0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
│ └─ ← [Return] 3296 bytes of code
├─ [0] VM::deal(0x0000000000000000000000000000000000000123, 10000000000000000000 [1e19])
│ └─ ← [Return]
├─ [0] VM::deal(0x0000000000000000000000000000000000000456, 10000000000000000000 [1e19])
│ └─ ← [Return]
└─ ← [Stop]
[1450168] TwentyOneTest::test_Call_PlayerWins()
├─ [0] VM::startPrank(0x0000000000000000000000000000000000000123)
│ └─ ← [Return]
├─ [0] console::log("player1: ", 0x0000000000000000000000000000000000000123) [staticcall]
│ └─ ← [Stop]
├─ [0] console::log("twentyOne balance: ", 0) [staticcall]
│ └─ ← [Stop]
├─ [346] TwentyOne::startGame{value: 1000000000000000000}()
│ └─ ← [Revert] revert: Not enough ether on contract to start game
└─ ← [Revert] revert: Not enough ether on contract to start game

The contract balance is shown as 0 when it attempts to start the game. The following line of code in the startGame()function causes the revert when trying to transfer 2 ether to the winner:

payable(player).transfer(2 ether); // Transfer the prize to the player

Error Output:

[FAIL: EvmError: Revert] test_Call_PlayerWins() (gas: 1450168)
Logs:
twentyOne balance: 0
contract balance: 1000000000000000000 [1 ether]
initialPlayerBalance: 9000000000000000000 [9 ether]
Traces:
[1450168] TwentyOneTest::test_Call_PlayerWins()
├─ [0] VM::startPrank(0x0000000000000000000000000000000000000123)
│ └─ ← [Return]
├─ [0] console::log("twentyOne balance: ", 0) [staticcall]
│ └─ ← [Stop]
├─ [346] TwentyOne::startGame{value: 1000000000000000000}()
│ └─ ← [Revert] revert: Not enough ether on contract to start game
└─ ← [Revert] revert: Not enough ether on contract to start game

The revert happens because there is not enough ether in the contract to cover the payout of 2 ether.

Impact

  • Loss of Player's Deposit: Since the contract has insufficient funds to cover the payout of 2 ether, the player may lose their 1 ether deposit if the game cannot proceed as expected.

  • Unreliable Game Logic: The game logic will fail if the contract does not have enough ether to continue the game, leading to inconsistent behavior.

  • Failed Transactions: The contract cannot process winnings, which makes the entire contract unusable for players who expect to receive a payout upon winning.

To summarize, players may lose their 1 ether deposit without being able to play or receive winnings due to the contract balance being insufficient. This issue leads to a poor user experience and a loss of trust in the contract's ability to function correctly.

Tools Used

  • Forge (Foundry): Used to compile and run the tests.

  • Solidity: The smart contract is written in Solidity, which is responsible for handling the game's logic and ether transfers.

  • EVM Error Logs: The error log output generated by Forge during test execution, showing the failed transaction and revert reason.

Recommendations

  1. Deploy Contract with Sufficient Ether: Ensure that the contract is deployed with an initial balance of at least 2 ether to cover the player payout when the game starts. You can require this in the constructor:

    constructor() payable {
    require(msg.value >= 1 ether, "Contract must be deployed with at least 1 ether");
    }
  2. Add receive() Function: Allow the contract to accept ether deposits after deployment. This can be done by adding a receive() function to ensure the contract can accept ether and maintain a sufficient balance:

    receive() external payable {}
  3. Balance Check in startGame(): Implement a check in the startGame() function to ensure that the contract has enough ether to process the winnings. This will prevent the game from starting unless there is at least 2 ether in the contract:

    function startGame() public payable returns (uint256) {
    require(address(this).balance >= 2 ether, "Not enough ether on contract to start game");
    // Continue game logic...
    }
  4. Test Contract Behavior: Ensure thorough testing of the contract under different conditions, including cases where ether is deposited after deployment, to verify that the game can proceed and payouts are correctly handled.

Updates

Lead Judging Commences

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

Insufficient balance for payouts / Lack of Contract Balance Check Before Starting Game

Contract Lacks Mechanism to Initialize or Deposit Ether

Support

FAQs

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