03 - GameManagerV3¶
Ticket-Kauf, EIP-712 BuyPermit, RNG-Generierung, Affiliate Binding
Übersicht¶
GameManagerV3 ist der Einstiegspunkt für alle Ticket-Käufe. Spieler signieren einen EIP-712 BuyPermit, der vom Relayer (Backend) on-chain submitted wird. Der Contract generiert Zufallszahlen (RNG) und speichert Tickets gas-optimiert.
Datei: contracts/contracts/GameManagerV3.sol
State Variables¶
// Ticket Storage
struct Ticket {
address player; // 20 bytes
uint8 poolIdRaw; // 1 byte (gepackt)
uint8 tipCount; // 1 byte (gepackt)
uint8 bonusTipCount; // 1 byte (gepackt)
uint256 dayId;
bytes32 tipsHash; // Hash der Tip-Array (Gas-Optimierung)
bytes rngNumbers; // Gepackte uint8[] der RNG-Zahlen
}
mapping(uint256 => Ticket) public tickets;
mapping(uint256 => mapping(uint8 => uint256[])) public ticketsByDayAndPool;
mapping(address => address) public affiliateOf;
mapping(address => mapping(bytes16 => bool)) public usedNonce; // Replay Protection
uint256 public nextTicketId;
uint256 public lastDayWithTickets;
uint256 public vrfSeed; // Von VRF Callback
uint256 public drandSeed; // Optional DRAND (aktuell 0)
mapping(uint8 => bool) public poolUsesVRF;
mapping(uint8 => uint8) public minTipsPerPool;
mapping(uint8 => uint8) public rngCountPerPool;
Pool-Konfiguration¶
| Pool ID | Preis pro Tip | VRF | RNG Count | Beschreibung |
|---|---|---|---|---|
| 0 | 0.25 USDT | Nein | 6 | Low Risk |
| 1 | 0.50 USDT | Ja | 6 | Standard |
| 2 | 1.00 USDT | Ja | 6 | Medium |
| 3 | 5.00 USDT | Ja | 6 | High |
| 4 | 10.00 USDT | Ja | 6 | Premium |
Bonus-Tip Berechnung¶
| Gekaufte Tips | Bonus Tips | Gesamt |
|---|---|---|
| 1-4 | 0 | 1-4 |
| 5 | 3 | 8 |
| 10 | 7 | 17 |
| 15 | 11 | 26 |
| 20 | 15 | 35 |
buyTicketFor() - Hauptfunktion¶
function buyTicketFor(
address buyer,
uint8 poolId,
uint8 tipCount,
bytes32 tipsHash,
address affiliate,
uint256 gasCostUsdt,
uint64 deadline,
bytes16 nonce,
bytes calldata signature
) external onlyOperator nonReentrant
Ablauf (8 Schritte)¶
Schritt 1: EIP-712 Signatur-Verifikation
bytes32 structHash = keccak256(abi.encode(
BUYPERMIT_TYPEHASH,
buyer, poolId, tipCount, tipsHash, affiliate,
gasCostUsdt, deadline, nonce
));
address signer = ECDSA.recover(domainSeparator, structHash, signature);
require(signer == buyer, "Invalid signature");
Schritt 2: Nonce-Check (Replay Protection)
Schritt 3: Deadline-Check
Schritt 4: Settlement Guard
if (lastDayWithTickets > 0 && lastDayWithTickets < currentDay) {
require(settlement.daySettled(lastDayWithTickets), "Previous day not settled");
}
Schritt 5: Affiliate Binding
if (affiliateOf[buyer] == address(0) && affiliate != address(0)) {
affiliateOf[buyer] = affiliate; // Einmalig, immutable
}
Schritt 6: Treasury Deposit
gameTreasury.depositFromPlayer(buyer, dayId, poolId, ticketAmount, gasCostUsdt);
// → Treasury ruft ReserveVault.depositFrom() auf
// → USDT.transferFrom(buyer → ReserveVault)
Schritt 7: RNG-Generierung
// Seed-Auswahl basierend auf Pool-Konfiguration
if (poolUsesVRF[poolId]) {
require(vrfSeed != 0, "VRF seed missing");
baseSeed = keccak256(abi.encodePacked(vrfSeed, drandSeed, blockhash(block.number - 1)));
} else {
baseSeed = blockhash(block.number - 1);
}
// Ticket-spezifische Entropy
entropy = keccak256(abi.encodePacked(baseSeed, ticketId, buyer, poolId));
// Generiere 6 unique Zahlen (1-40)
for (uint i = 0; i < rngCount; i++) {
seed = keccak256(abi.encodePacked(seed, i));
number = (uint8(seed) % 40) + 1;
// Duplikat-Check, Safety Limit: 100 Versuche
}
Schritt 8: Ticket Storage
tickets[ticketId] = Ticket({
player: buyer,
poolIdRaw: poolId,
tipCount: tipCount,
bonusTipCount: bonusTipCount,
dayId: dayId,
tipsHash: tipsHash,
rngNumbers: packedRngNumbers
});
ticketsByDayAndPool[dayId][poolId].push(ticketId);
emit TicketCreated(ticketId, buyer, dayId, poolId, tipCount, bonusTipCount);
EIP-712 BuyPermit¶
// Domain Separator
{
name: "ChainbetsTicketBuy",
version: "2",
chainId: 421614, // Arbitrum Sepolia
verifyingContract: gameManagerAddress
}
// Typehash
BUYPERMIT_TYPEHASH = keccak256(
"BuyPermit(address buyer,uint8 poolId,uint8 tipCount,bytes32 tipsHash,"
"address affiliate,uint256 gasCostUsdt,uint64 deadline,bytes16 nonce)"
);
RNG-Architektur¶
Entropy-Quellen¶
| Quelle | Sicherheit | Status |
|---|---|---|
| Chainlink VRF | Kryptografisch verifizierbar | Aktiv (Pool 1-4) |
| DRAND | Distributed Randomness Beacon | Geplant (aktuell 0) |
| prevrandao | Block-Randomness (L2) | Fallback (Pool 0) |
Hybrid-Seed-Berechnung¶
baseSeed = keccak256(vrfSeed ∥ drandSeed ∥ blockhash)
ticketEntropy = keccak256(baseSeed ∥ ticketId ∥ player ∥ poolId)
number[i] = (keccak256(ticketEntropy ∥ i) % 40) + 1
RNG-Sicherheitsanalyse¶
| Angriff | Machbarkeit | Mitigation |
|---|---|---|
| Validator Manipulation | L2-Sequencer kontrolliert prevrandao | VRF + DRAND als unabhängige Quellen |
| VRF DoS | Chainlink Callback könnte blockiert werden | try/catch im VRFReceiver, prevrandao Fallback |
| Operator Timing | Operator wählt Zeitpunkt des Ticket-Buys | Seed ist bereits committed bevor Ticket kommt |
Access Control¶
| Funktion | Owner (SAFE) | Operator | VRFReceiver | Anyone |
|---|---|---|---|---|
| buyTicketFor | - | ✅ | - | - |
| setVrfSeed | ✅ | - | ✅ | - |
| setDrandSeed | - | ✅ | - | - |
| setOperator | ✅ | - | - | - |
| setPoolUsesVRF | ✅ | - | - | - |
| setRngCountPerPool | ✅ | - | - | - |
Events¶
event TicketCreated(uint256 ticketId, address player, uint256 dayId, uint8 poolId, uint8 tipCount, uint8 bonusTipCount);
event VrfSeedUpdated(uint256 newSeed);
event AffiliateSet(address player, address affiliate);
event DrandSeedUpdated(uint256 newSeed);
Audit-Hinweise¶
Settlement Guard
Ohne Settlement Guard könnten Tickets für zukünftige Tage gekauft werden, während vergangene Tage noch nicht abgerechnet sind. Dies würde die Pot-Berechnung verfälschen.
Tips als Hash
Die tatsächlichen Tip-Zahlen werden als tipsHash gespeichert (nicht die Zahlen selbst). Die Verifizierung erfolgt off-chain im Settlement-Backend. On-chain wird nur der Hash verglichen.
Gas-Optimierung
Das Ticket-Struct nutzt Slot-Packing: player (20 bytes) + poolIdRaw + tipCount + bonusTipCount (je 1 byte) = 23 bytes in einem 32-byte Slot. Spart ~15k-55k Gas pro Ticket.