Skip to main content

Social-account escrows

A creator may want to launch a coin for someone who has never heard of zk-pad: an artist, an open-source maintainer, a public figure. A social-account escrow commits the fees to a platform account so that only the real owner of that account can ever claim, and the account is not published on-chain.

Coming soon: not live yet

On the BSC mainnet deployment no attestors are registered and attestorThreshold is 0. The FeeVault's proposeBind then always reverts with InsufficientAttestations, so no social-account escrow can be bound or claimed until the owner registers attestors and sets a threshold. The attestor service is not deployed yet, and without its attestorSalt no valid handle commitment can be made, so do not create social-account escrows outside the app until it is. Funds credited to an existing escrow id stay in the FeeVault, but only until its fallback clock runs out: if the escrow has a fallback recipient and no bind is finalized before its fallback delay ends (at least 180 days after registration or first credit), anyone can call sweepToFallback and move the whole balance to the fallback recipient, after which the account owner can no longer claim it. See trust assumptions.

Want the account to be public?

If the coin should openly and verifiably say which account it supports, use a public social recipient. The account is public, and its wallet stays hidden.

Building blocks​

PieceRole
Handle commitmentkeccak256(abi.encode(platformId, userIdHash, attestorSalt, nonce)). A salted hash of the platform's immutable numeric user id, not the handle string.
Encrypted hintThe account details (platform, user id, nonce, attestorSalt), encrypted to the hint key of every attestor of the committee and emitted in TokenLaunched. Lets each attestor recognise and verify the escrow when the owner logs in. Only the ciphertext is on-chain.
AttestorsIndependent services that verify an account login (OAuth) and sign an EIP-712 BindOwner message. The vault requires k of n distinct attestor signatures.
TimelockA bind only takes effect after bindDelay (1 to 30 days). A rebind of an account that already has an owner waits twice as long.
VetoDuring the timelock, the guardian or any single attestor can veto. The current owner can cancel a rebind.

Why the user id and not the handle​

Handles change hands. X resells inactive handles, Telegram usernames are tradable collectibles, and GitHub frees renamed usernames. If the escrow committed to the handle string, whoever holds the handle later could claim. zk-pad commits to the account's permanent numeric id, resolved at launch time.

What hides the account​

Without secret randomness, anyone could hash every famous account's user id and find which one an escrow is for. Every commitment includes a fresh random 32-byte nonce, known only to the creator and the hint's readers, so dictionary attacks do not work.

The attestorSalt (an HMAC of the platform and user id under the issuing attestor's pepper) is not what hides the account. The attestor returns it with the commitment and the creator puts it in the hint, because attestors have independent peppers: without it the other committee members could not recompute the commitment and a multi-attestor bind could never be collected.

Lifecycle​

  1. Launch. The creator's browser asks an attestor for a commitment and encrypts the hint to every attestor of the committee. The coin launches with the escrow id as beneficiary. Fees start accruing immediately.
  2. Login. The account owner opens the bind portal. The browser generates a fresh stealth key. The owner logs in separately with each of the k attestors. The intent (stealth key and escrow ids) is sent in a POST body and bound server-side to a random OAuth state, so it never appears in a URL, and the login cannot be replayed for someone else's key.
  3. Propose. The browser picks one signature deadline and asks every attestor to sign BindOwner with it, because proposeBind checks all k signatures against a single deadline. With k signatures, a relayer calls proposeBind. The vault checks that every signer is a current attestor, that the signers are distinct and sorted, and that the bind nonce matches.
  4. Timelock. The bind waits bindDelay. Anyone watching can see that a bind is pending. Attestors run a veto watcher that (with AUTO_VETO) vetoes proposals for their escrows they did not attest. If the portal finds a bind to another key pending, it explains that this may be the owner's own bind from an earlier visit and contests it only after the owner confirms, one escrow at a time (listing escrows never vetoes anything, so a login cannot publicly group an owner's escrows).
  5. Finalize. After the timelock, anyone can call finalizeBind. The escrow becomes a normal account controlled by the stealth key, and the owner claims like any stealth beneficiary.

Rebinding​

If the owner loses the stealth key, or the platform account is recovered after a hijack, the account can be bound again through the same flow. Because the account already has an owner:

  • the timelock is 2 × bindDelay;
  • the current owner can cancel the rebind with a signed CancelBind;
  • the guardian and every attestor can still veto.

A veto or cancel never moves funds. It only clears the pending bind, and a veto keeps the fallback sweep closed for at least 2 × bindDelay so the owner can be re-attested in time. A finalized rebind discards the previous owner's shield templates.

Supported platforms​

PlatformplatformIdVerification (phase 0)
X1OAuth 2.0 with PKCE, minimal read scopes, token revoked immediately after reading the user id
Telegram2Telegram Login Widget hash verification (a Mini App flow with Telegram's Ed25519 signature is planned)
GitHub3GitHub OAuth app
Farcaster4Sign In With Farcaster (FIP-11): a SIWE message with an attestor-issued nonce, signed by the fid's custody address or an auth address, checked against the IdRegistry / KeyRegistry on Optimism. The user id is the fid.
Discord5OAuth 2.0 authorization code with PKCE, scope identify; the user id is the snowflake; token revoked immediately

Endpoint details: attestor API. The attestor service exists in services/attestor but is not deployed for mainnet yet (no attestor keys or OAuth apps).

Limits you should know about​

  • Any single attestor learns the account. Hints are encrypted with threshold 1: each attestor of the committee opens them alone with its own key (there is no threshold decryption), so every one of them, or anyone who steals one hint key, knows which accounts have escrows. The chain does not learn this.
  • k colluding attestors could bind an escrow to their own key. The timelock, single-attestor veto, guardian veto and contest flow are there to catch it, but they rely on someone noticing. Later phases replace OAuth with zkTLS or on-chain ZK proofs (see the roadmap in docs/research/social-beneficiaries.md).
  • The platform is trusted to report who owns an account. A hijacked account could pass OAuth; the timelock gives the real owner time to contest. For Farcaster the "platform" is the fid's on-chain control: whoever holds the custody key (or an auth key it added), including after a transfer or a recovery by the recovery address, can bind.
  • A paid handle lookup is public. When the creator lets the attestor resolve a handle, the x402 payment ($0.03 USDT, same price on X, GitHub and Farcaster) is an on-chain transfer from the creator's wallet. It links that wallet to "a handle-based escrow" (not the platform or the handle), around the time of the launch. Creators who care pay from another wallet or enter the numeric user ID, which is free and makes no payment (Telegram and Discord escrows always use the numeric ID).
  • The coin itself often names the beneficiary. If a coin is called "Donate to @alice", the escrow hides Alice's wallet, not the fact that the coin is about her.
  • An escrow may never be claimed. Set a fallback recipient so donations are not stranded.