LFG

Oracle Signing

How the tipping oracle signs claim attestations, and the exact digest layout.

Naming note

This page's URL says 'eip712' for continuity with how the feature is commonly described, but the deployed contract actually verifies a plain EIP-191 personal_sign-style digest — not a eth_signTypedData / structured EIP-712 digest. There's no on-chain DOMAIN_SEPARATOR or typed struct; this page documents the real, exact scheme the contract checks.

Every claimTips call on OmniTippingEscrow requires a valid signature from the contract's configured verifier address — an oracle key held by LFG Pad's backend, never exposed to the client. This page documents the exact digest that signature has to cover, so any backend or integration replicating it produces byte-for-byte compatible signatures.

The digest

OmniTippingEscrow.solsolidity
bytes32 digest = MessageHashUtils.toEthSignedMessageHash(
    keccak256(abi.encodePacked(
        address(this),          // this OmniTippingEscrow deployment
        block.chainid,          // binds the signature to one chain
        key,                    // creatorKey(platform, handle)
        claimerAddress,         // who is authorized to receive the sweep
        claimNonce[key]         // pre-increment nonce for this creatorKey
    ))
);

if (ECDSA.recover(digest, signature) != verifier) revert InvalidSignature();
  • `address(this)` + `block.chainid` — binds a signature to one specific escrow deployment on one specific chain, preventing a valid signature on one deployment from being replayed against another.
  • `key` — the creatorKey(platform, handle) being claimed; see Universal Social Tipping for how it's derived.
  • `claimerAddress` — the wallet the sweep pays out to. The oracle only ever signs this after independently verifying, via OAuth, that the caller controls handle on platform.
  • `claimNonce[key]` — read *before* incrementing, so the very first claim for a fresh creatorKey signs against nonce 0. Bumped by the contract on successful verification, so the exact same signature can never be replayed twice.

Reproducing the digest off-chain

The signing route replicates this exactly using viem: it lowercases platform/handle with an ASCII-only fold (deliberately not String.prototype.toLowerCase(), which case-folds the full Unicode range and could diverge from Solidity's byte-level lowercasing for non-ASCII input), computes creatorKey the same way the contract does, packs the five fields with encodePacked, hashes with keccak256, and signs the raw digest — which viem's signMessage({ message: { raw: digest } }) wraps in the same EIP-191 prefix toEthSignedMessageHash applies on-chain.

app/api/tipping/sign-claim/route.tstypescript
const key = creatorKeyFor(platform, handle); // keccak256(lower(platform) + ":" + lower(handle))

const digest = keccak256(
  encodePacked(
    ["address", "uint256", "bytes32", "address", "uint256"],
    [escrowAddress, BigInt(chainId), key, claimerAddress, nonce]
  )
);

const signature = await account.signMessage({ message: { raw: digest } });

Warning

If OmniTippingEscrow is ever upgraded to inherit a real EIP-712 domain, this page (and the signing route it documents) will need a corresponding update — the two must always match exactly, since a mismatched digest simply fails ECDSA.recover's comparison against verifier with no partial-credit failure mode.