Zum Inhalt

06 - PrizeVaultV3

Gewinn-Claims via Merkle Proof, Lost Wallet Recovery


Übersicht

PrizeVaultV3 ist ein State-Only Vault - er hält keine USDT, sondern speichert Merkle Roots und trackt Claims. Die tatsächliche Auszahlung läuft über Settlement → ReserveVault.

Datei: contracts/contracts/PrizeVaultV3.sol


State Variables

mapping(uint256 => bytes32) public merkleRootByDay;    // Merkle Root pro Tag
mapping(uint256 => uint256) public totalPayoutByDay;    // Gesamtpayout pro Tag
mapping(uint256 => mapping(address => bool)) public claimed;  // Claim-Status
mapping(address => address) public reassignedTo;        // Lost Wallet Recovery

ISettlementV5 public settlement;
address public operator;
address public trustedClaimer;   // Multicall3 Adresse

Merkle Leaf Struktur

bytes32 leaf = keccak256(abi.encode(
    dayId,           // uint256 - verhindert Cross-Day Replay
    originalWallet,  // address - kanonische Identität
    amount           // uint256 - Gesamtpayout für diesen Spieler an diesem Tag
));

Pro Tag, pro Wallet

Ein Spieler hat pro Tag genau einen Merkle Leaf mit dem Gesamt-Gewinnbetrag aller seiner Tickets an diesem Tag.


claimMerkle() - Hauptfunktion

function claimMerkle(
    uint256 dayId,
    address originalWallet,
    address paidTo,
    uint256 amount,
    bytes32[] calldata proof
) external nonReentrant

Ablauf (6 Schritte)

Schritt 1: Access Control

require(
    msg.sender == originalWallet ||
    msg.sender == trustedClaimer,  // Multicall3 für Batch Claims
    "Unauthorized"
);

Schritt 2: Reassignment prüfen

address effectiveWallet = originalWallet;
if (reassignedTo[originalWallet] != address(0)) {
    effectiveWallet = reassignedTo[originalWallet];
}

Schritt 3: Payout-Ziel erzwingen

require(paidTo == effectiveWallet, "Invalid paidTo");
// Verhindert Umleitung von Funds an fremde Adressen

Schritt 4: Merkle Root prüfen

bytes32 root = merkleRootByDay[dayId];
require(root != bytes32(0), "No root for day");

Schritt 5: Merkle Proof verifizieren

bytes32 leaf = keccak256(abi.encode(dayId, originalWallet, amount));
require(MerkleProof.verifyCalldata(proof, root, leaf), "Invalid proof");

Schritt 6: Payout via Settlement

require(!claimed[dayId][originalWallet], "Already claimed");
// Settlement markiert claimed und ruft ReserveVault.payPrize() auf
settlement.payoutFromPrizeVault(dayId, originalWallet, paidTo, amount);


Lost Wallet Recovery

adminReassignWallet()

function adminReassignWallet(
    address oldWallet,
    address newWallet,
    uint256 caseId
) external onlyOperator nonReentrant

Szenario: Spieler verliert Zugang zu seinem Web3Auth Wallet.

Ablauf: 1. Off-chain: KYC-Verifizierung des Spielers 2. Operator ruft adminReassignWallet(oldWallet, newWallet, caseId) auf 3. Alle zukünftigen Claims für oldWallet gehen an newWallet 4. Merkle Proof verwendet weiterhin oldWallet (Root ändert sich nicht)

Sicherheitseigenschaften: - Einmalig: reassignedTo[oldWallet] kann nur einmal gesetzt werden - Operator-gated: Nur autorisierter Operator - Audit Trail: Event mit caseId, timestamp, operator - Unveränderlich: Nach Reassignment keine Änderung möglich

require(reassignedTo[oldWallet] == address(0), "Already reassigned");
reassignedTo[oldWallet] = newWallet;
emit WalletReassigned(oldWallet, newWallet, caseId, block.timestamp, msg.sender);

setMerkleRootForDay()

function setMerkleRootForDay(uint256 dayId, bytes32 root, uint256 totalPayout)
    external onlySettlement
  • Aufgerufen von Settlement während commitMerkleSettlement()
  • Root ist immutable nach dem Setzen
  • require(merkleRootByDay[dayId] == bytes32(0), "Root already set")

Access Control

Funktion Owner (SAFE) Operator Settlement TrustedClaimer Anyone
claimMerkle - - - ✅ (eigene Claims)
adminReassignWallet - - - -
setMerkleRootForDay - - - -
setOperator - - - -
setTrustedClaimer - - - -

Events

event MerkleRootSet(uint256 dayId, bytes32 root, uint256 totalPayout);
event PrizeClaimed(uint256 dayId, address originalWallet, address paidTo, uint256 amount);
event WalletReassigned(address oldWallet, address newWallet, uint256 caseId, uint256 timestamp, address operator);

Audit-Hinweise

Immutable Merkle Roots

Einmal gesetzte Merkle Roots können nie geändert werden. Ein fehlerhafter Root würde bedeuten, dass Gewinner nicht claimen können. Die Korrektheit wird off-chain im Settlement-Backend sichergestellt.

Double-Claim Prevention

Die claimed Mapping wird in Settlement gesetzt (nicht in PrizeVault selbst), da Settlement die Auszahlung koordiniert.

TrustedClaimer = Multicall3

Der trustedClaimer ist die Multicall3-Adresse, die für Batch Claims via ClaimRouter verwendet wird. So kann der ClaimRouter im Namen des Spielers claimen.