Skip to content

Smart contract: winner can hold a known-winning reveal open to snipe a larger pot inflated by later players #10

Description

@eltociear

Summary (a known-winning shot can be held open to snipe a bigger pot)

revealShot() can legally be called any time from commitBlock + REVEAL_DELAY + 1 up to commitBlock + MAX_REVEAL_DELAY (up to 256 blocks later — contracts/EthShot.sol:141,378). The win/lose outcome is fully determined the moment blockhash(commitBlock + REVEAL_DELAY) exists, since every other input to _checkWin (the player's own secret, nonce, playerNonces[msg.sender], msg.sender) is either already known to the caller or is plain contract storage — private in Solidity only blocks other contracts from reading it, it does not hide it from off-chain inspection (eth_getStorageAt), and the whole computation can trivially be replicated off-chain / via a local eth_call simulation before ever broadcasting the real transaction.

So a player can:

  1. commitShot
  2. Wait until blockhash(commitBlock+1) exists (2 blocks)
  3. Simulate revealShot(secret) off-chain (no gas, no state change) to see whether it wins
  4. If it wins, do nothing yet — just leave the pending shot open
  5. Wait while other players commit shots and inflate currentPot (this doesn't touch nonce, so the already-determined win outcome doesn't change as long as nobody else reveals in the meantime — plausible in a low-traffic period)
  6. Call revealShot right before the 256-block window closes, when the pot is much larger than it was at step 1

_handleWin (contracts/EthShot.sol:614 in the current code) pays out WIN_PERCENTAGE_BP of the live currentPot at the moment of the call, with no cap tied to what the pot was when the win was actually determined. So this lets a player extract value contributed by other players' shots that were committed after the winning outcome was already locked in — those later players never had a fair chance at that portion of the pot.

Note: PR #5 ("Auto-reveal pending shots") only added a client-side bot that reveals promptly for well-behaved users of the official frontend. It doesn't change the contract, so it does nothing to stop someone calling the contract directly (their own script, not the website) from holding a known winner open.

Impact

A sufficiently patient/automated player can systematically extract a larger-than-fair share of the pot at the expense of players who committed shots after the outcome was already secretly known to be a win, without the contract enforcing that the payout reflects the pot as of when randomness was actually fixed.

Fix (PR opened)

Snapshot the pot size right after each shot's own commit (potAtCommit) and cap the eventual payout to min(currentPot, potAtCommit). Any pot growth from shots committed after the winner's own commit stays in currentPot and rolls over to the next round instead of being awarded to a winner who could have strategically delayed their reveal.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions