14 - Settlement Lifecycle¶
Day Definition, State Machine, Settlement Guard, No-Winner Handling
Day Definition¶
Ein "Tag" (Day) wird UTC-basiert berechnet:
| 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
commitMerkleSettlementerfordertmerkleRoot != 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)¶
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).