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
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.