2.0 RC docsView 1.x docs
Concept · Claim identity

Claim identity and relayed claims

Anyone may submit a Merkle claim. The payout goes to the leaf's account unless that account submits and redirects it.

v2.0.0-rc.1 release candidate
The API is frozen and later candidates carry fixes only, except the receipt-free and Safe create surface of /fhe-airdrop, which is @beta. Published on the next dist-tag; latest stays on 1.6.0 until 2.0.0.

A Merkle claim separates two questions that v1 answered with one address: who sends the transaction, and whose allocation it settles. In v2 the first can be anyone. The second is fixed by the leaf.

What a claim submits#

claim({ entry }) sends everything the campaign builder put in the entry: the encrypted amount handle, its input proof, and the Merkle proof for entry.account's leaf. Every leaf's input is encrypted against (airdrop, airdrop) - the contract verifies it against the instance itself, not against the caller. That is why the caller does not matter.

Who may submit: anyone#

The recipient, a relayer, any third party. There is no role, no allowlist, and nothing to choose when the roster is encrypted: the same entries serve recipients who claim for themselves and a relayer who claims for all of them.

@tokenops/sdk/fhe-airdrop
ts
import { createMerkleAirdropClient } from "@tokenops/sdk/fhe-airdrop";
import { isEncryptedValueZero } from "@zama-fhe/sdk";

const merkle = createMerkleAirdropClient({ publicClient, walletClient, address: airdrop });

// First root: anything delivered means the leaf is settled, and a repeat
// pays an encrypted zero and still costs the fee. After a rotation, see below.
if (!isEncryptedValueZero(await merkle.getClaimedAmount(entry.account))) return;

// Sent from the relayer's wallet. The contract pays entry.account.
await merkle.claim({ entry });

The full relayer loop is in Relay a claim.

Who gets paid: the account, unless it redirects#

The contract pays entry.account (or the to that account itself names, below) and advances entry.account's cumulative delivered total, whoever sent the transaction. A relayer pays the gas and the per-claim fee; it receives nothing.

Only the account may redirect or unwrap#

to defaults to entry.account. Anyone else passing a different to is refused - the SDK checks this client-side and throws UnauthorizedRedirectError, mirroring the contract's UnauthorizedRedirect revert.

claim.ts
ts
// Only entry.account itself may redirect payout. Anyone else passing a
// different `to` is refused client-side before any RPC call.
await merkle.claim({
  entry,
  to: someOtherAddress, // throws UnauthorizedRedirectError unless
                        // the sender IS entry.account
});

claimAndUnwraptakes no claim identity at all: its account is always the sender. Turning a confidential allocation into a public ERC-20 transfer is irreversible, so it stays the recipient's own choice - a third-party unwrap is unrepresentable, not merely rejected.

The first submitter settles the leaf#

Because anyone may submit, a relayer or a stranger can get there before the recipient. Their claim settles the leaf confidentially, paying entry.account. Two things follow:

  • A redirect or unwrap the recipient intended no longer happens - the funds already arrived as a confidential balance.
  • The recipient's own later claim pays an encrypted zero and still costs the fee. The amounts are encrypted, so the contract cannot tell a zero-outstanding claim apart and refuse it.

So check getClaimedAmount(entry.account) before offering a claim. An account that has never been paid returns the zero handle.

The zero test is exact only for an account's first root. The delivered total is cumulative and kept across roots, so after a root rotation an account paid earlier reads non-zero while its top-up is still owed. The recipient decrypts the handle and compares it with their new cumulative total; a relayer, which cannot decrypt it, tracks the (root, account) pairs it already submitted. See Relay a claim.

The relayer cannot read what it settles#

The delivered total is granted to entry.account, never to the caller, and getClaimAmount grants its preview to entry.accounttoo. A relayer can confirm a leaf verifies and settle it without ever learning the amount; its own user-decrypt of the recipient's total is refused. The live walkthrough shows both decrypts side by side.

ECDSA: always sender-bound#

The ECDSA variant has no separate claim-identity parameter. The EIP-712 signature is verified over a struct whose recipient field is always the caller, so there is no third-party submission at all. to only redirects payout, exactly as on Merkle.

See also