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¶
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] = 0nach Payout - Separates Accounting vom regulären Pool
takeFeeForDay()¶
- 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 |