Zum Inhalt

13 - EIP-712 Signaturen

BuyPermit, ClaimPermit, Domain Separators, Typehashes


Übersicht

Das System verwendet zwei EIP-712 Signatur-Typen für Meta-Transaktionen:

Signatur Contract Zweck
BuyPermit GameManagerV3 Gasloser Ticket-Kauf
ClaimPermit ClaimRouterV2 Gasloser Batch-Claim

Spieler signieren strukturierte Daten off-chain, der Relayer (Backend) submitted die Transaktionen on-chain.


BuyPermit (GameManagerV3)

Domain Separator

{
    "name": "ChainbetsTicketBuy",
    "version": "2",
    "chainId": 421614,
    "verifyingContract": "<GameManagerV3 Address>"
}

Struct & Typehash

struct BuyPermit {
    address buyer;
    uint8 poolId;
    uint8 tipCount;
    bytes32 tipsHash;
    address affiliate;
    uint256 gasCostUsdt;
    uint64 deadline;
    bytes16 nonce;
}

bytes32 constant BUYPERMIT_TYPEHASH = keccak256(
    "BuyPermit(address buyer,uint8 poolId,uint8 tipCount,bytes32 tipsHash,"
    "address affiliate,uint256 gasCostUsdt,uint64 deadline,bytes16 nonce)"
);

Signatur-Flow

1. Frontend generiert BuyPermit mit allen Feldern
2. Frontend berechnet EIP-712 Hash:
    structHash = keccak256(abi.encode(BUYPERMIT_TYPEHASH, permit))
    digest = keccak256("\x19\x01" || domainSeparator || structHash)
3. Spieler signiert digest mit Web3Auth Wallet
4. Frontend sendet Permit + Signatur an Backend
5. Backend submitted GameManagerV3.buyTicketFor(permit, signature)
6. Contract recovered Signer und prüft: signer == buyer

Felder im Detail

Feld Typ Beschreibung
buyer address Spieler-Wallet (muss == Signer sein)
poolId uint8 Pool 0-4
tipCount uint8 Anzahl gekaufter Tips
tipsHash bytes32 keccak256 der Tip-Zahlen (off-chain Verifizierung)
affiliate address Affiliate-Wallet (oder address(0))
gasCostUsdt uint256 Gas-Kosten die der Spieler akzeptiert
deadline uint64 Ablauf-Timestamp (Unix Seconds)
nonce bytes16 Einmalig pro Spieler (16 Bytes Random)

ClaimPermit (ClaimRouterV2)

Domain Separator

{
    "name": "ChainbetsClaimRouter",
    "version": "2",
    "chainId": 421614,
    "verifyingContract": "<ClaimRouterV2 Address>"
}

Struct & Typehash

struct ClaimPermit {
    address claimer;
    uint256 totalWinnerAmount;
    uint256 totalAffiliateAmount;
    uint256 maxGasCostUsdt;
    uint64 deadline;
    bytes16 nonce;
    bytes32 claimsHash;
}

bytes32 constant CLAIMPERMIT_TYPEHASH = keccak256(
    "ClaimPermit(address claimer,uint256 totalWinnerAmount,"
    "uint256 totalAffiliateAmount,uint256 maxGasCostUsdt,"
    "uint64 deadline,bytes16 nonce,bytes32 claimsHash)"
);

Claims Hash Berechnung

// Winner Claims: [{dayId, originalWallet, amount, proof}, ...]
// Affiliate Claims: [{dayId, affiliate, amount, proof}, ...]

bytes32 claimsHash = keccak256(abi.encode(
    keccak256(abi.encode(winnerClaims)),
    keccak256(abi.encode(affiliateClaims))
));

Zweck: Der Claims Hash stellt sicher, dass der Relayer die Claims nicht austauschen oder manipulieren kann. Der Spieler signiert exakt die Claims, die er erwartet.

Felder im Detail

Feld Typ Beschreibung
claimer address Spieler-Wallet (muss == Signer sein)
totalWinnerAmount uint256 Summe aller Gewinn-Claims
totalAffiliateAmount uint256 Summe aller Affiliate-Claims
maxGasCostUsdt uint256 Max. akzeptierter Gas-Abzug
deadline uint64 Ablauf-Timestamp
nonce bytes16 Einmalig pro Spieler
claimsHash bytes32 Hash aller Einzel-Claims

Replay Protection

Nonce-System

// In GameManagerV3 und ClaimRouterV2:
mapping(address => mapping(bytes16 => bool)) public usedNonce;

// Bei Signatur-Verifizierung:
require(!usedNonce[signer][nonce], "Nonce already used");
usedNonce[signer][nonce] = true;
  • bytes16 Nonce: 16 Bytes Random, vom Frontend generiert
  • Kein sequentieller Counter: Parallele Transaktionen möglich
  • Einmal-Use: Kann nur von false → true wechseln (nie zurück)

Deadline

require(block.timestamp <= deadline, "Permit expired");
  • Typisch: 5-10 Minuten in die Zukunft
  • Verhindert unbegrenzt gültige Signaturen

Chain ID

// Im Domain Separator:
chainId: 421614  // Arbitrum Sepolia
  • Verhindert Cross-Chain Replay
  • Signatur ist nur auf der richtigen Chain gültig

Signatur-Verifikation On-Chain

// Digest berechnen
bytes32 structHash = keccak256(abi.encode(TYPEHASH, ...fields));
bytes32 digest = _hashTypedDataV4(structHash);

// Signer recovern
address signer = ECDSA.recover(digest, signature);

// Prüfen
require(signer == expectedSigner, "Invalid signature");
require(signer != address(0), "Invalid signature");

OpenZeppelin's EIP712 Base-Contract wird für _hashTypedDataV4() verwendet.


Frontend Integration (ethers.js)

BuyPermit signieren

const domain = {
    name: "ChainbetsTicketBuy",
    version: "2",
    chainId: 421614,
    verifyingContract: GAME_MANAGER_ADDRESS
};

const types = {
    BuyPermit: [
        { name: "buyer", type: "address" },
        { name: "poolId", type: "uint8" },
        { name: "tipCount", type: "uint8" },
        { name: "tipsHash", type: "bytes32" },
        { name: "affiliate", type: "address" },
        { name: "gasCostUsdt", type: "uint256" },
        { name: "deadline", type: "uint64" },
        { name: "nonce", type: "bytes16" }
    ]
};

const signature = await signer.signTypedData(domain, types, permit);

ClaimPermit signieren

const domain = {
    name: "ChainbetsClaimRouter",
    version: "2",
    chainId: 421614,
    verifyingContract: CLAIM_ROUTER_ADDRESS
};

const types = {
    ClaimPermit: [
        { name: "claimer", type: "address" },
        { name: "totalWinnerAmount", type: "uint256" },
        { name: "totalAffiliateAmount", type: "uint256" },
        { name: "maxGasCostUsdt", type: "uint256" },
        { name: "deadline", type: "uint64" },
        { name: "nonce", type: "bytes16" },
        { name: "claimsHash", type: "bytes32" }
    ]
};

const signature = await signer.signTypedData(domain, types, permit);

Audit-Hinweise

Domain Separator = Contract-spezifisch

Jeder Contract hat seinen eigenen Domain Separator. BuyPermit-Signaturen funktionieren NUR mit GameManagerV3, ClaimPermit-Signaturen NUR mit ClaimRouterV2.

Version String

Beide verwenden Version "2". Bei einem Contract-Upgrade mit geändertem Signatur-Schema muss die Version erhöht werden, damit alte Signaturen ungültig werden.

Web3Auth Kompatibilität

Web3Auth Wallets unterstützen signTypedData (EIP-712). Die Signatur-Implementierung ist kompatibel mit allen gängigen Wallet-Providern.