SAFETY

No stranded funds. No drain path.

Nine structural guarantees. Contract code is non-upgradeable from deploy. Pause/unpause can be permanently frozen after BurnBomb #001 settles.

01Pull-payment pattern everywhere

Winners call withdrawPayout(roundId) to claim. Seed dividend recipients call withdrawSeedDividend(roundId). Refunders call withdrawRefund(roundId, slotIndex) (per slot). The contract never iterates external payouts. Zero DoS surface.

02Bounded Chainlink VRF retries

Randomness comes from Chainlink VRF v2.5, ETH-native(no LINK token). The contract owns its own VRF subscription and self-funds it (1% of every pot tops up the sub).

If the VRF callback fails to land within 30 minutes of settle(), anyone can call requestRandomnessAgain(roundId) to fire a fresh request and earn a small bounty (0.0001 Ξ per call). The contract caps total VRF requests at 3 per round (1 initial + 2 retries).

If all 3 requests fail to deliver, the round is recovered via the 72-hour emergency refund path (Guarantee 3). The caller of a retry cannot influence the random word, the bounty is bounded, and the attack surface is zero.

03Two refund paths · normal is instant

Normal refund (instant):if the 6h seed cap passes without crossing the round's threshold (creator-set per round in v1.1, 0.015 → 1 Ξ, default 0.05 Ξ), the round auto-enters refund state. Every claimer calls withdrawRefund(roundId, slotIndex) immediately, one call per slot owned. No waiting.

Emergency refund (72h delay · last resort): if a round is genuinely stuck (e.g., VRF cap hit without a successful callback), the owner can call emergencyResolveStuckRound(roundId) to flip it to refund-eligible. The 72-hour clock starts from the stuck timestamp (e.g., firstVrfRequestedAt for a randomness-stuck round). Even here, owner CANNOT drain:

  • Owner has no recipient parameter.
  • Owner cannot specify amounts.
  • Function only enables per-claimer refunds.
  • Strict requirement: relevant stuck timestamp ≥ 72h old AND round still unsettled.
  • Emergency refund path survives freezeAdmin() · this is the permanent recovery hatch.

04Pause/unpause freezable after #001

Contract ships with two pause-related admin functions: pause() / unpause(). Plus the emergency refund flip described above. Owner runs BurnBomb #001 end-to-end to validate every state transition in production.

Owner then calls freezeAdmin() · a one-way function that permanently disables pause() and unpause(). After freeze, the contract cannot be paused or unpaused again, ever.

What survives freezeAdmin(): owner identity (owner remains owner, but the only privileged action left is emergencyResolveStuckRound which itself has no drain path). What freezes: pause/unpause.

05Pause only blocks new entries

Even during round #001, the pause mechanism is limited:

  • Owner can pause enter() / enterBatch() if a critical bug is discovered.
  • Existing rounds keep settling. Refund still works.
  • Withdraw still works. requestRandomnessAgain() still works.
  • Owner can never pause your money flowing back to you.

06Reentrancy guards · CEI ordering

Every external payable function uses OpenZeppelin's ReentrancyGuard. Internal state updates happen BEFORE external calls (checks-effects-interactions). External ETH transfers use .call{value: ...}with full gas forwarding · safe because every external recipient is either the user's own pull-payment claim or a contract whose re-entry is blocked by the guard.

07Bounded iteration

finalizeSettlement(roundId) iterates at most the 100-slot count. No unbounded loops anywhere in the contract. Withdraw is pull-based, so no iteration risk on payout.

08Per-address entry cap (50)

Each round caps a single address at 50 entries (MAX_ENTRIES_PER_ADDRESS). Single-wallet carpet-bombing is bounded. Real attacks require multi-wallet coordination with real gas overhead. Batch enterBatch() is capped at 10 slots per tx (MAX_ENTRIES_PER_TX).

09Zero stranded ETH

Rounding dust: percentage splits (72.5 / 6.5 / 3.5 / 10 / 5 / 0.5 / 1 / 1) can truncate by a few wei per slice in integer math. The contract handles this by computing the winner share as pot - (allOtherSlices). Every wei in the pot leaves the contract during settle. No accumulating dust.

Direct ETH sends are rejected. The contract's receive() function reverts. Any ETH arriving outside of enter() / topUpVrfReserve() is refused at the EVM level. Prevents arbitrary funds from getting stranded.


What this contract CANNOT do

  • Cannot be upgraded · code is non-upgradeable from deploy.
  • Cannot transfer $BURN tokens (no $BURN held by this contract; only routes ETH to the live $BURN strategy contract via the 5% rake).
  • Cannot interact with TTT NFTs or the BurnToken Strategy proxy beyond plain ETH transfer.
  • Cannot drain user funds via the emergency path · the emergency function only flips refund eligibility and has no recipient or amount parameter.
  • Cannot bypass the 50-entry per-address cap (no admin override).
  • Cannot extract pot funds for the owner · no owner-controlled recipient on any payout.
  • Cannot guarantee VRF will always succeed · if Chainlink fails 3 requests in a row, recovery is the 72-hour emergency refund (no honest player loses their entry).

Current deployment status

Minnow contract deployed at 0x9e16f09e…2f8c. Verified on Etherscan and Blockscout. Chainlink VRF subscription owned by the contract; VRF health-check transaction confirmed delivery in ~47s on mainnet.

Currently pre-activation · BET button is locked until owner fires activate(). After activation, permissionless createRound() opens the first round.