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.
lastTimestampis 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 end —
claimPrizeandclearLeftoverAdoptionBonusstay 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()istrue, 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:
- Send entries to the active game until
entrantCount >= minTicketsBeforeEnd(each entry costsentryAmount; topping up from the host wallet is fine — the host is just another entrant). - Wait
deadlineseconds with no further entries.isGameOver()becomestrueand 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 default0.001 ETHentry and a floor of25, that worst-case top-up is0.025 ETH.
What this changes for participants
- A host who configures
minTicketsBeforeEnd = Nis publicly committing (via theGameStartedevent) that the active game cannot end beforeNtickets are sold. - Players can rely on this guarantee: viewing
gameConfigRecord[gameId].minTicketsBeforeEndreveals the locked floor for any past or active game. claimPrizeandclearLeftoverAdoptionBonusboth checkisGameOver(), 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.