05 - SettlementV5¶
Tägliche Abrechnung, Merkle Root Commit, Payout-Orchestrierung
Übersicht¶
SettlementV5 ist der Orchestrator der täglichen Abrechnung. Er nimmt die off-chain berechneten Ergebnisse (Merkle Roots, Payouts) entgegen, validiert sie und führt alle on-chain Buchungen atomar durch.
Datei: contracts/contracts/SettlementV5.sol
State Variables¶
uint256 public lastSettledDay;
mapping(uint256 => bool) public daySettled;
// Jackpot State (per Pool)
struct JackpotState {
uint256 noHitStreak; // Aufeinanderfolgende Tage ohne Jackpot
uint256 lastJackpotDayId; // Letzter Tag mit Jackpot-Hit
}
mapping(uint8 => JackpotState) public jackpotStates;
// Affiliate Merkle
mapping(uint256 => bytes32) public affiliateMerkleRootByDay;
mapping(uint256 => uint256) public affiliateTotalByDay;
// Charity Tracking
mapping(uint256 => uint256) public charityTotalByDay;
// Contract References
IGameTreasuryV2 public gameTreasury;
IPrizeVaultV3 public prizeVault;
IAffiliateVaultV2 public affiliateVault;
ICharityVaultV2 public charityVault;
IReserveVaultV2 public reserveVault;
address public feeSafe;
address public operator;
commitMerkleSettlement() - Hauptfunktion¶
function commitMerkleSettlement(
uint256 dayId,
uint256[5] calldata poolPayout, // Payout pro Pool (5 Pools)
uint256[5] calldata poolJackpotPayout, // Jackpot Payout pro Pool
uint256 totalPayout, // Gesamtpayout (Summe)
bytes32 merkleRoot, // Winner Merkle Root
bool[5] calldata jackpotHit, // Jackpot-Hit pro Pool
uint256 affiliateTotal, // Affiliate-Gesamt
bytes32 affiliateRoot, // Affiliate Merkle Root
uint256 charityTotal // Charity-Anteil
) external onlyOperator nonReentrant
8-Phasen Ablauf¶
Phase 1: Validierung¶
require(!daySettled[dayId], "Already settled");
require(dayId > lastSettledDay, "Must be sequential");
require(merkleRoot != bytes32(0), "Empty root");
require(sum(poolPayout) + sum(poolJackpotPayout) == totalPayout, "Payout mismatch");
Phase 2: Pool Settlement¶
for (uint8 poolId = 0; poolId < 5; poolId++) {
if (poolPayout[poolId] > 0) {
gameTreasury.applySettlement(poolId, poolPayout[poolId]);
}
}
Phase 3: Jackpot Settlement¶
for (uint8 poolId = 0; poolId < 5; poolId++) {
if (poolJackpotPayout[poolId] > 0) {
gameTreasury.applyJackpotSettlement(poolId, poolJackpotPayout[poolId]);
}
}
Phase 4: Fee abrufen¶
Phase 5: Fee Split berechnen¶
require(affiliateTotal + charityTotal <= dailyFee, "Fee overflow");
uint256 netFee = dailyFee - affiliateTotal - charityTotal;
Phase 6: Payouts ausführen¶
// Charity → CharityVault
if (charityTotal > 0) {
reserveVault.payCharity(address(charityVault), charityTotal, dayId);
charityVault.recordIncome(dayId, charityTotal);
}
// Net Fee → feeSafe
if (netFee > 0) {
reserveVault.payFee(feeSafe, netFee, dayId);
}
// Affiliate Payouts: PULL-basiert (nicht hier ausgeführt)
// → Affiliates claimen später über AffiliateVault
Phase 7: Merkle Roots setzen¶
prizeVault.setMerkleRootForDay(dayId, merkleRoot, totalPayout);
affiliateMerkleRootByDay[dayId] = affiliateRoot;
affiliateTotalByDay[dayId] = affiliateTotal;
Phase 8: Finalisierung¶
// Jackpot State updaten
for (uint8 poolId = 0; poolId < 5; poolId++) {
if (jackpotHit[poolId]) {
jackpotStates[poolId].noHitStreak = 0;
jackpotStates[poolId].lastJackpotDayId = dayId;
} else {
jackpotStates[poolId].noHitStreak += 1;
}
}
// Tag als settled markieren
daySettled[dayId] = true;
lastSettledDay = dayId;
emit MerkleSettlementCommitted(dayId, merkleRoot, totalPayout);
Payout-Funktionen¶
payoutFromPrizeVault()¶
function payoutFromPrizeVault(
uint256 dayId,
address originalWallet,
address paidTo,
uint256 amount
) external onlyPrizeVault nonReentrant
- Aufgerufen von PrizeVault nach Merkle-Verifikation
- Markiert Claim als ausgeführt
- Ruft
reserveVault.payPrize(paidTo, amount, dayId)auf
payoutFromAffiliateVault()¶
function payoutFromAffiliateVault(
uint256 dayId,
address affiliate,
address to,
uint256 amount
) external onlyAffiliateVault nonReentrant
- Aufgerufen von AffiliateVault nach Merkle-Verifikation
- Ruft
reserveVault.payPrize(to, amount, dayId)auf
Emergency-Funktionen¶
forceMarkDaySettled()¶
- Keine Merkle Roots gesetzt
- Keine Fee Splits ausgeführt
- Nur:
daySettled[dayId] = true - Zweck: Deadlock-Auflösung bei Backend-Ausfall oder No-Winner-Tag
claimFromSettlement() (Backup)¶
function claimFromSettlement(
uint256 dayId,
address originalWallet,
uint256 amount,
bytes32[] calldata proof
) external nonReentrant
- Alternativer Claim-Pfad falls Vaults nicht verfügbar
- Verifiziert Merkle Proof direkt im Settlement
- Führt Payout via ReserveVault aus
Access Control¶
| Funktion | Owner (SAFE) | Operator | PrizeVault | AffiliateVault |
|---|---|---|---|---|
| commitMerkleSettlement | - | ✅ | - | - |
| payoutFromPrizeVault | - | - | ✅ | - |
| payoutFromAffiliateVault | - | - | - | ✅ |
| forceMarkDaySettled | ✅ | - | - | - |
| claimFromSettlement | - | - | - | - (anyone*) |
*claimFromSettlement ist für jeden aufrufbar, aber benötigt gültigen Merkle Proof.
Events¶
event MerkleSettlementCommitted(uint256 dayId, bytes32 merkleRoot, uint256 totalPayout);
event PrizePayoutExecuted(uint256 dayId, address wallet, address paidTo, uint256 amount);
event AffiliatePayoutExecuted(uint256 dayId, address affiliate, address to, uint256 amount);
event FeePayoutExecuted(uint256 dayId, uint256 netFee, uint256 affiliateTotal, uint256 charityTotal);
event JackpotStateUpdated(uint8 poolId, bool hit, uint256 noHitStreak);
event DayForceSettled(uint256 dayId);
Gas-Kosten¶
| Operation | Geschätzt |
|---|---|
| commitMerkleSettlement (5 Pools) | ~500k-800k Gas |
| payoutFromPrizeVault | ~80k Gas |
| forceMarkDaySettled | ~30k Gas |
Audit-Hinweise¶
Atomare Settlement
commitMerkleSettlement ist atomar - bei einem Revert in irgendeiner Phase wird die gesamte Transaktion rückgängig gemacht. Keine Teil-Settlements möglich.
Sequentielle Tage
dayId > lastSettledDay erzwingt strikte Reihenfolge. Tag 5 kann nicht vor Tag 4 settled werden.
Fee Split Constraint
affiliateTotal + charityTotal <= dailyFee stellt sicher, dass nie mehr ausgezahlt wird als eingenommen.