Skip to content

fix: cap jackpot payout to the pot size at the winner's own commit (prevents delayed-reveal pot sniping)#11

Open
eltociear wants to merge 1 commit into
profullstack:masterfrom
eltociear:fix/cap-payout-at-commit-time-pot
Open

fix: cap jackpot payout to the pot size at the winner's own commit (prevents delayed-reveal pot sniping)#11
eltociear wants to merge 1 commit into
profullstack:masterfrom
eltociear:fix/cap-payout-at-commit-time-pot

Conversation

@eltociear

Copy link
Copy Markdown

Fixes #10.

_handleWin currently pays out WIN_PERCENTAGE_BP of the live currentPot at the moment revealShot is actually called — which can legally be up to 256 blocks after the win was already determined (see #10 for the full writeup). This lets a player who already knows off-chain that their pending shot wins simply sit on it while other players' shots inflate the pot, then reveal for a much bigger payout than the pot that existed when their win was actually locked in.

Fix

  • Added potAtCommit to PendingShot, snapshotting currentPot right after each shot's own contribution is added (both commitShot and commitFirstShot).
  • _handleWin now takes potAtCommit and pays out min(currentPot, potAtCommit) * WIN_PERCENTAGE_BP / BASIS_POINTS instead of the raw live pot. Only the awarded amount is deducted from currentPot (currentPot -= potAmount, not currentPot = 0), so any excess pot growth from later shots correctly rolls over into the next round instead of being wiped or handed to this winner.
  • revealShot now reads shot.amount and shot.potAtCommit into locals before delete pendingShots[msg.sender]. This also fixes a pre-existing bug in the same lines: shot is a storage pointer, so the old code's emit ShotRevealed(msg.sender, shot.amount, won) — which ran after the delete — always logged amount = 0, since delete zeroes the storage that shot points at.

Verification

  • npx hardhat compile — compiles clean (evm target paris, no warnings from the new code).
  • Wasn't able to run test/contracts/EthShot.test.js in my environment — it fails on a pre-existing, unrelated ESM/CJS interop error (import { ethers } from 'hardhat' against Hardhat's CommonJS build), not something introduced by this change. Happy to add a dedicated test for the capped-payout behavior if you point me at the right test setup for this repo (or once that import issue is sorted).

No changes to canWin, the RNG source, reveal timing, or any other behavior — this only changes how much of the pot a win is allowed to draw from.

revealShot() can legally be called anywhere from commitBlock+2 up to
commitBlock+MAX_REVEAL_DELAY (256 blocks) later, and once the reveal
block's hash is fixed the win/lose outcome only depends on values that
are readable off-chain (secret is known to the caller; nonce/
playerNonces are plain storage, "private" only hides them from other
Solidity contracts, not from external inspection). A player who commits,
waits two blocks, and checks off-chain that they've already won can
simply hold their pending shot open and only call revealShot() once the
pot has been inflated by other players committing in the meantime,
collecting a payout far larger than the pot that existed when their own
win was actually determined.

Fix: snapshot the pot size right after each shot's own commit
(potAtCommit) and cap _handleWin's payout to min(currentPot, potAtCommit).
Pot growth from shots committed after the winner's own commit is excluded
from the payout and rolls over into the next round instead of being
deducted-to-zero or handed to this winner.

Also fixes a related bug in the same function: shot.amount was read from
the storage-pointer `shot` *after* `delete pendingShots[msg.sender]`, so
ShotRevealed's amount argument always logged 0 regardless of the actual
shot size. Needed to capture values out of `shot` before the delete
anyway for potAtCommit, so fixed both in the same pass.

Verified with `npx hardhat compile` (compiles clean, evm target paris).
Could not run the existing contract test suite in this environment —
test/contracts/EthShot.test.js hits a pre-existing ESM/CJS import error
unrelated to this change (`import { ethers } from 'hardhat'` against a
CommonJS build).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant