Zum Inhalt

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

uint256 dailyFee = gameTreasury.takeFeeForDay(dayId);

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()

function forceMarkDaySettled(uint256 dayId) external onlyOwner
  • 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.