Zum Inhalt

04 - GameTreasuryV2

Accounting Layer, Fee Splits, Rollover-Berechnung


Übersicht

GameTreasuryV2 ist der reine Accounting-Contract (Option B). Er hält keine USDT, sondern berechnet und trackt die Aufteilung jedes Ticket-Kaufs in Pot, Fee, Rollover und Jackpot Reserve.

Datei: contracts/contracts/GameTreasuryV2.sol


Fee Split Constants (Immutable)

uint256 public constant POT_BP       = 6750;   // 67.5% → Prize Pool
uint256 public constant FEE_BP       = 2750;   // 27.5% → Affiliate + Charity + Net Fee
uint256 public constant ROLLOVER_BP  = 500;    // 5.0%  → Nächster Tag Rollover
uint256 public constant JACKPOT_BP   = 4000;   // 40.0% → Jackpot Reserve (vom Pot)
uint256 public constant BP_BASE      = 10000;

Verifikation: 6750 + 2750 + 500 = 10000

Constants = unveränderlich

Die Fee-Split-Ratios sind als constant definiert und können nicht geändert werden - weder vom Owner noch per Upgrade.


State Variables

// Per-Pool Accounting
mapping(uint8 => uint256) public potAcc;              // Aktueller Pot pro Pool
mapping(uint8 => uint256) public rolloverAcc;          // Rollover pro Pool
mapping(uint8 => uint256) public jackpotReserveAcc;    // Jackpot Reserve pro Pool

// Per-Day Accounting
mapping(uint256 => uint256) public feeAcc;             // Gesamte Fee pro Tag

// Contract References
address public gameManager;
address public settlement;
address public reserveVault;
address public feeSafe;
address public operatorFeeReceiver;

depositFromPlayer() - Ticket-Kauf Aufteilung

function depositFromPlayer(
    address player,
    uint256 dayId,
    uint8 poolId,
    uint256 ticketAmount,
    uint256 gasAmount
) external onlyGameManager nonReentrant

Berechnungs-Flow

Beispiel: 100 USDT Ticket

1. Fee:      100 × 2750 / 10000 = 27.50 USDT  → feeAcc[dayId]
2. Rollover: 100 × 500  / 10000 =  5.00 USDT  → rolloverAcc[poolId]
3. Rest:     100 - 27.50 - 5.00 = 67.50 USDT

4. Jackpot:  67.50 × 4000 / 10000 = 27.00 USDT → jackpotReserveAcc[poolId]
5. Net Pot:  67.50 - 27.00         = 40.50 USDT → potAcc[poolId]

Summe: 27.50 + 5.00 + 27.00 + 40.50 = 100.00 ✅

Aktionen nach Berechnung

// 1. USDT vom Spieler in ReserveVault
reserveVault.depositFrom(player, ticketAmount + gasAmount, "ticket_purchase");

// 2. Gas-Kosten sofort weiterleiten
if (gasAmount > 0) {
    reserveVault.payOperatorFee(operatorFeeReceiver, gasAmount);
}

applySettlement() - Nach Settlement

function applySettlement(uint8 poolId, uint256 payoutAmount)
    external onlySettlement nonReentrant

Rollover-Neuberechnung

uint256 combined = potAcc[poolId] + rolloverAcc[poolId];
require(payoutAmount <= combined, "Payout exceeds pool");

uint256 rest = combined - payoutAmount;
uint256 newRollover = (rest * ROLLOVER_BP) / BP_BASE;
uint256 newPot = rest - newRollover;

potAcc[poolId] = newPot;
rolloverAcc[poolId] = newRollover;

Mathematischer Beweis (Conservation of Mass)

Vor Settlement:
    potAcc = 100, rolloverAcc = 50, combined = 150

Settlement mit payout = 60:
    rest = 150 - 60 = 90
    newRollover = 90 × 500 / 10000 = 4.50
    newPot = 90 - 4.50 = 85.50

Verifikation:
    Vorher:  100 + 50 = 150
    Nachher: 85.50 + 4.50 + 60 = 150 ✅

applyJackpotSettlement()

function applyJackpotSettlement(uint8 poolId, uint256 payoutAmount)
    external onlySettlement nonReentrant
  • Validiert: payoutAmount <= jackpotReserveAcc[poolId]
  • Setzt: jackpotReserveAcc[poolId] = 0 nach Payout
  • Separates Accounting vom regulären Pool

takeFeeForDay()

function takeFeeForDay(uint256 dayId)
    external onlySettlement returns (uint256 fee)
  • Gibt feeAcc[dayId] zurück
  • Setzt feeAcc[dayId] = 0
  • Settlement teilt Fee auf in: Affiliate + Charity + Net Fee

Access Control

Funktion Owner (SAFE) GameManager Settlement Anyone
depositFromPlayer - - -
applySettlement - - -
applyJackpotSettlement - - -
takeFeeForDay - - -
setGameManager - - -
setSettlement - - -
setReserveVault - - -

Events

event PlayerDeposit(address player, uint256 dayId, uint8 poolId, uint256 amount, uint256 gasAmount);
event SettlementApplied(uint8 poolId, uint256 payoutAmount, uint256 newPot, uint256 newRollover);
event JackpotSettlementApplied(uint8 poolId, uint256 payoutAmount);

Kritische Invariante

∀ poolId:
    potAcc[poolId] + rolloverAcc[poolId] + jackpotReserveAcc[poolId]
    <= ReserveVault.balance - Σ(andere Verpflichtungen)

Die Treasury-Buchungen müssen zu jedem Zeitpunkt durch den ReserveVault-Saldo gedeckt sein.


Edge Cases

Szenario Verhalten
Kein Gewinner (payout = 0) Rest bleibt vollständig im Pool, Rollover wird neu berechnet
Voller Drain (payout = combined) potAcc = 0, rolloverAcc = 0
Payout > combined require reverted - Settlement muss korrekte Werte liefern
Jackpot Hit jackpotReserveAcc[pool] = 0 nach Payout