Minimum Tickets Before End

QuantumRaffle V2 supports an optional, host-configurable minimum ticket floor (minTicketsBeforeEnd) that prevents the inactivity countdown from expiring until enough entries have been sold. This protects against the scenario where a game ends on its deadline before reaching a viable participant pool.

Properties

Property Value
Storage gameConfig.minTicketsBeforeEnd (per-game; copied into gameConfigRecord[gameId] when the game starts)
Units Whole entries (tickets)
Default 0 (disabled — countdown behaves normally)
Maximum none (any uint256)
Set by Host only, via constructor or setNextGameConfig(...) (affects the next game)

Why the floor is locked at game start

Like every other field in gameConfig, minTicketsBeforeEnd is captured into gameConfigRecord[gameId] the moment a game starts. setNextGameConfig only writes to nextGameConfig, so the floor in effect when the game started is the floor every participant plays under for the entire raffle. The host cannot raise or lower the floor on an active game — there is no setter that mutates the active game's config.

This is the same guarantee that protects entrants from mid-game changes to entryAmount, deadline, the log bases, or depositFeeBps.

How the floor affects the game lifecycle

isGameOver() enforces the floor before the deadline check:

function isGameOver() public view returns (bool isOver) {
    if (gameId == 0) return true;
    gameConfig memory config = gameConfigRecord[gameId];
    if (entrantCount[gameId] < config.minTicketsBeforeEnd) return false;
    isOver = (block.timestamp > lastTimestamp[gameId] + config.deadline);
}

Concretely:

  • Below floor: the game is not over regardless of how much time has passed. New entries are accepted as usual; the deadline never has the chance to expire.
  • At/above floor: the deadline behaves as before. lastTimestamp is updated on every entry, so the countdown effectively restarts from the most recent deposit.

Because every entry resets lastTimestamp, the moment the floor is reached the contract has a fresh full deadline window before the game can end — there is no risk of a game ending in the same block the floor is hit.

⚠️ Operational risk: an under-subscribed game freezes the contract

Setting a non-zero floor introduces a liveness risk the host must plan for. Because isGameOver() returns false while a game is below its floor, a game that never reaches minTicketsBeforeEnd:

  • can never endclaimPrize and clearLeftoverAdoptionBonus stay blocked, so all ETH already deposited (including the host's seed) is locked;
  • blocks every future game — a new game can only start once isGameOver() is true, so one stuck game freezes the entire contract.

There is no admin force-end, cancel, or refund. The contract is immutable, so this state is only recoverable by reaching the floor.

Recovery procedure

The host (or anyone) can unstick a below-floor game by buying entries until entrantCount reaches the floor, then letting the inactivity deadline elapse:

  1. Send entries to the active game until entrantCount >= minTicketsBeforeEnd (each entry costs entryAmount; topping up from the host wallet is fine — the host is just another entrant).
  2. Wait deadline seconds with no further entries. isGameOver() becomes true and the game ends normally; prizes and a new game unlock.

Guidance for hosts

  • For a first game with uncertain turnout, prefer a small floor (or 0) so the game can always end, then raise it once demand is proven.
  • If you keep a higher floor (e.g. 25), budget to top the game up to the floor yourself (minTicketsBeforeEnd × entryAmount) so you can always unblock the contract. With the default 0.001 ETH entry and a floor of 25, that worst-case top-up is 0.025 ETH.

What this changes for participants

  • A host who configures minTicketsBeforeEnd = N is publicly committing (via the GameStarted event) that the active game cannot end before N tickets are sold.
  • Players can rely on this guarantee: viewing gameConfigRecord[gameId].minTicketsBeforeEnd reveals the locked floor for any past or active game.
  • claimPrize and clearLeftoverAdoptionBonus both check isGameOver(), so they correctly remain blocked while the floor isn't met.

Events

Event When
GameStarted(..., depositFeeBps, minTicketsBeforeEnd) A new game starts; the locked floor is the trailing field
GameConfigUpdated(..., depositFeeBps, minTicketsBeforeEnd) Host updates nextGameConfig; the value applies to the next game

Backwards compatibility

When the constructor is called with minTicketsBeforeEnd = 0 and the host never raises it, the contract behaves identically to a version without the floor: isGameOver() reduces to the original deadline-only check.

results matching ""

    No results matching ""