Zum Inhalt

14 - Settlement Lifecycle

Day Definition, State Machine, Settlement Guard, No-Winner Handling


Day Definition

Ein "Tag" (Day) wird UTC-basiert berechnet:

uint256 dayId = block.timestamp / 86400;
Eigenschaft Wert
Dauer 86400 Sekunden (24h)
Start 00:00:00 UTC
Ende 23:59:59 UTC
Beispiel 2026-02-28 = dayId 20512

Tag-State-Machine

SETTLED ──[neuer Tag]──► OPEN ──[Settlement-Zeit]──► CLOSED ──[Settlement]──► SETTLED
   │                       │                                                    │
   │                       │  Tickets können                                   │
   │                       │  gekauft werden                                   │
   │                       │                                                    │
   └───────────────────────┴── Nächster Tag beginnt ◄──────────────────────────┘

States

State Beschreibung Ticket-Kauf Settlement
SETTLED Tag ist abgerechnet - -
OPEN Aktiver Tag, Tickets werden verkauft -
CLOSED Keine neuen Tickets, warte auf Settlement
DEADLOCKED Vortag nicht settled, blockiert alles Muss zuerst Vortag settlen

Settlement Guard

Implementation in GameManagerV3

function _checkSettlementGuard() internal view {
    uint256 currentDay = block.timestamp / 86400;

    if (lastDayWithTickets > 0 && lastDayWithTickets < currentDay) {
        require(
            settlement.daySettled(lastDayWithTickets),
            "Previous day not settled"
        );
    }
}

Zweck

Verhindert Ticket-Käufe für "morgen" bevor "gestern" abgerechnet ist.

Beispiel-Szenarien

Szenario lastDayWithTickets currentDay daySettled? Ergebnis
Normalbetrieb Tag 5 Tag 6 Tag 5: ✅ Ticket OK
Settlement fehlt Tag 5 Tag 6 Tag 5: ❌ REVERT
Erster Tag 0 Tag 6 - Ticket OK (kein Guard)
Gleicher Tag Tag 6 Tag 6 - Ticket OK (kein Guard nötig)

Settlement-Sequenz

Täglicher Ablauf

Backend (Cron Job / Worker):

1. Warte bis Tag X endet (00:00 UTC)

2. Berechne Ergebnisse für Tag X:
   a. Lade alle Tickets von Tag X
   b. Vergleiche Tips mit Ergebnissen
   c. Berechne Gewinne pro Spieler
   d. Berechne Affiliate-Provisionen
   e. Berechne Charity-Anteil

3. Baue Merkle Trees:
   a. Prize Tree: alle Gewinner + Beträge
   b. Affiliate Tree: alle Affiliates + Provisionen

4. Berechne Payouts pro Pool:
   a. poolPayout[0..4]: Reguläre Gewinne
   b. poolJackpotPayout[0..4]: Jackpot-Gewinne (falls Hit)

5. Submit on-chain:
   Settlement.commitMerkleSettlement(
       dayId,
       poolPayout,
       poolJackpotPayout,
       totalPayout,
       merkleRoot,
       jackpotHit,
       affiliateTotal,
       affiliateRoot,
       charityTotal
   )

6. Speichere Merkle Proofs in DB (für spätere Claims)

9 Atomare Schritte im Contract

commitMerkleSettlement() [atomar]:

Step 1: Validierung
    ├── dayId nicht already settled
    ├── dayId > lastSettledDay (sequentiell)
    ├── merkleRoot != 0
    └── Σ payouts == totalPayout

Step 2: Pool Settlements (5x)
    └── gameTreasury.applySettlement(poolId, payout)

Step 3: Jackpot Settlements (5x)
    └── gameTreasury.applyJackpotSettlement(poolId, jackpotPayout)

Step 4: Fee abrufen
    └── dailyFee = gameTreasury.takeFeeForDay(dayId)

Step 5: Fee Split berechnen
    └── netFee = dailyFee - affiliateTotal - charityTotal

Step 6: Payouts ausführen
    ├── reserveVault.payCharity(charityVault, charityTotal)
    └── reserveVault.payFee(feeSafe, netFee)

Step 7: Merkle Roots setzen
    ├── prizeVault.setMerkleRootForDay(dayId, root, totalPayout)
    └── affiliateMerkleRootByDay[dayId] = affiliateRoot

Step 8: Jackpot States updaten
    └── noHitStreak++ oder = 0

Step 9: Finalisierung
    ├── daySettled[dayId] = true
    ├── lastSettledDay = dayId
    └── emit MerkleSettlementCommitted

No-Winner-Day Handling

Szenario

Tag hat Tickets, aber kein Spieler hat gewonnen (keine Treffer).

Problem

  • Kein Gewinner → kein Merkle Tree → kein Root
  • commitMerkleSettlement erfordert merkleRoot != 0
  • Ohne Settlement → Settlement Guard blockiert nächsten Tag

Lösung 1: Dummy Merkle Leaf (Standard)

// Backend generiert Dummy-Leaf
const dummyLeaf = ethers.solidityPackedKeccak256(
    ['uint256', 'address', 'uint256'],
    [dayId, ethers.ZeroAddress, 0]
);

const tree = new MerkleTree([dummyLeaf], ethers.keccak256);

// Settlement mit Zero-Payouts
commitMerkleSettlement(
    dayId,
    [0, 0, 0, 0, 0],           // poolPayout: alles 0
    [0, 0, 0, 0, 0],           // jackpotPayout: alles 0
    0,                           // totalPayout: 0
    tree.getHexRoot(),           // Dummy Root
    [false, false, false, false, false],  // kein Jackpot
    0,                           // affiliateTotal: 0
    tree.getHexRoot(),           // Dummy Affiliate Root
    charityTotal                 // Charity kann trotzdem > 0 sein
);

Effekte: - daySettled[dayId] = true → Nächster Tag unblocked - potAcc und rolloverAcc bleiben erhalten (Rollover für nächsten Tag) - Kein Claim möglich (wallet = address(0), amount = 0) - Fee Split wird trotzdem ausgeführt (Charity + Net Fee)

Lösung 2: forceMarkDaySettled (Notfall)

// SAFE (Owner) only
settlement.forceMarkDaySettled(dayId);

Effekte: - daySettled[dayId] = true → Nächster Tag unblocked - KEINE Merkle Roots gesetzt - KEINE Fee Splits ausgeführt - KEINE Pot/Rollover-Berechnung - Funds bleiben im ReserveVault

Verwendung: Nur wenn Backend komplett ausfällt und Dummy-Merkle nicht möglich.


Jackpot Lifecycle

Jackpot-Akkumulation

Tag 1: Ticket-Käufe → 40% vom Pot → jackpotReserveAcc[pool] += X
Tag 2: Kein Jackpot Hit → noHitStreak = 1
Tag 3: Kein Jackpot Hit → noHitStreak = 2
Tag 4: Kein Jackpot Hit → noHitStreak = 3
...
Tag N: Jackpot Hit! → noHitStreak = 0
       → poolJackpotPayout[pool] = jackpotReserveAcc[pool]
       → jackpotReserveAcc[pool] = 0

Jackpot-Trigger (Backend)

Das Backend entscheidet basierend auf: - noHitStreak (je länger, desto wahrscheinlicher) - Zufallsergebnis (z.B. 6er-Treffer) - Pool-spezifische Regeln


Timing-Diagramm

UTC:  00:00        12:00        24:00        12:00        24:00
       │             │             │             │             │
Tag X: ├─── OPEN ────┤── OPEN ─────┤                           │
       │  Tickets     │  Tickets    │                           │
       │  kaufen      │  kaufen     │                           │
       │              │             │                           │
       │              │             ├── Settlement ──┤          │
       │              │             │   (Backend)    │          │
       │              │             │                │          │
Tag Y: │              │             │                ├── OPEN ──┤
       │              │             │                │ Tickets  │
       │              │             │                │ kaufen   │

Audit-Hinweise

Settlement Timing

Wenn das Settlement für Tag X nicht rechtzeitig durchgeführt wird, können an Tag Y keine Tickets verkauft werden. Das Backend muss zuverlässig settlen.

Dummy Merkle

Die Dummy-Merkle-Lösung für No-Winner-Days ist ein bewusstes Design-Pattern. Der Root ist nicht "leer" sondern enthält einen ungültigen Leaf (address(0), amount 0).

Sequentielle Tage

Falls Tage übersprungen werden (z.B. kein Ticket an Tag 7), muss trotzdem jeder Tag dazwischen settled werden (ggf. via forceMarkDaySettled).