Privacy Precompile

component privacy

Shielded-pool OKRW at 0x100000000000000000000000000000000000000b — deposit / transfer / withdraw notes under Groth16 proofs, with typed revert data for every failure mode.

The Privacy precompile at 0x100000000000000000000000000000000000000b implements a shielded pool for OKRW. Callers deposit transparent OKRW to commit a note, transfer notes to other recipients under a Groth16 proof, or withdraw a note back to a transparent EOA — each with an EIP-712 *WithAuthorization relay variant and a batch form. Every state-changing call passes PCL policy evaluation at the precompile boundary, so shielded flows respect the same compliance rules as transparent ones. Failures now revert with typed custom errors declared on IPrivacy — the ambient shared errors from IPrecompile, the request-lifecycle family (PrivacyNonPayable, PrivacyRequestExpired, PrivacyBatchSizeOutOfRange, PrivacyNativeDenomMismatch), the authorization family (PrivacyInvalidAuthorizationField, PrivacyAuthorizationExpired, PrivacyAuthorizationNonceUsed, PrivacyAuthorizationSignerMismatch, PrivacyAuthorizationRejected, PrivacyInvalidAuthorizationMagic), and the shielded-state family (PrivacyDuplicateNullifier, PrivacyNullifierAlreadySpent, PrivacyDuplicateCommitment, PrivacyCommitmentAlreadyExists, PrivacyMerkleCapacityExceeded, PrivacyTxLimitExceeded, PrivacySenderTxLimitExceeded).

Architecture

flowchart LR
  User["Wallet / dApp"] -->|"deposit / transfer / withdraw"| Priv["IPrivacy<br/>0x…000b"]
  User -->|"*WithAuthorization<br/>(relayed)"| Relayer["Relayer"]
  Relayer -->|"executor tx"| Priv
  Priv -->|"preflight"| PCL["IPcl"]
  Priv -->|"revert with typed IPrivacy error"| User
  classDef evm fill:#0096AA,stroke:#0096AA,color:#fff;
  classDef precompile fill:#FF8C50,stroke:#FF8C50,color:#fff;
  class User,Relayer evm;
  class Priv,PCL precompile;

Every state-changing call passes PCL preflight, then either commits shielded state or reverts with a typed IPrivacy error.

What the surface exposes

Nine state-changing methods, arranged in three pairs plus batch:

ShapeDirect callRelay variant
Value indeposit (payable)
Note-to-notetransfertransferWithAuthorization
Value outwithdrawwithdrawWithAuthorization
BatchbatchTransfer, singleProofBatchTransferbatchTransferWithAuthorization, singleProofBatchTransferWithAuthorization

Only deposit is payable. Every other method rejects any attached value with PrivacyNonPayable(methodName). All shielded amounts are 64-bit — a single operation can move at most 18.446744073709551615 OKRW; hold larger balances as multiple notes.

Request-lifecycle errors

Ambient conditions the request itself violates:

  • PrivacyNonPayable(string methodName) — attached msg.value to a non-payable privacy method.
  • PrivacyRequestExpired(uint64 expiresAtUnix) — the request's expiresAtUnix is in the past. Set it a few blocks into the future to allow for mempool delay.
  • PrivacyBatchSizeOutOfRange(uint256 count, uint256 max)batchTransfer / singleProofBatchTransfer outside the allowed size.
  • PrivacyNativeDenomMismatch(string got, string expected) — the request's denom is not the chain's native denom (aokrw).


Decode these with the shared IPrecompile decode table plus the four selectors above.
import { decodeErrorResult } from "viem";

const privacyLifecycleAbi = [
  { type: "error", name: "PrivacyNonPayable",          inputs: [{ name: "methodName",    type: "string" }] },
  { type: "error", name: "PrivacyRequestExpired",     inputs: [{ name: "expiresAtUnix", type: "uint64" }] },
  { type: "error", name: "PrivacyBatchSizeOutOfRange",inputs: [{ name: "count", type: "uint256" }, { name: "max", type: "uint256" }] },
  { type: "error", name: "PrivacyNativeDenomMismatch",inputs: [{ name: "got",  type: "string" }, { name: "expected", type: "string" }] },
] as const;

try {
  await client.writeContract({ /* privacy call */ });
} catch (err: any) {
  if (err?.data) {
    const decoded = decodeErrorResult({ abi: privacyLifecycleAbi, data: err.data });
    console.error(decoded.errorName, decoded.args);
  }
}

Authorization errors — for the *WithAuthorization relay path

The relay variants (transferWithAuthorization, withdrawWithAuthorization, singleProofBatchTransferWithAuthorization, batchTransferWithAuthorization) accept an EIP-712 signature separated from the executor. See privacy-authorization-eip712-domain for the signed payload — the wire struct is 6 fields but the signed typeHash has 11.

  • PrivacyInvalidAuthorizationField(string field) — a required field is missing / malformed (e.g. deadline unset, selectors wrong length).
  • PrivacyAuthorizationExpired(uint64 deadline)deadline is in the past.
  • PrivacyAuthorizationNonceUsed(address sender, uint256 nonce) — the effective-sender nonce has already been consumed.
  • PrivacyAuthorizationSignerMismatch(address expected, address got) — the recovered signer is not the effective sender.
  • PrivacyAuthorizationRejected(uint8 authorizationKind) — the effective sender is a contract and its ERC-1271 verifier rejected the signature.
  • PrivacyInvalidAuthorizationMagic(bytes4 magic) — the ERC-1271 verifier returned an unrecognised magic value.

Shielded-state errors

Failures that arise from the shielded state machine itself:

  • PrivacyDuplicateNullifier() — the same nullifier appears twice in the request.
  • PrivacyNullifierAlreadySpent() — a nullifier has already been posted on-chain (double-spend attempt).
  • PrivacyDuplicateCommitment() — the same output commitment appears twice in the request.
  • PrivacyCommitmentAlreadyExists() — an output commitment is already present in the Merkle tree.
  • PrivacyMerkleCapacityExceeded(uint256 count) — adding the request's commitments would exceed the tree's capacity.
  • PrivacyTxLimitExceeded(uint256 limit) — chain-wide privacy tx quota per block exceeded.
  • PrivacySenderTxLimitExceeded(address sender, uint256 limit) — per-sender privacy tx quota exceeded.


Most of these mean the client should retry with a freshly-scanned note set — a duplicate nullifier usually indicates a stale view of the pool.

Non-decodable failures

A few checks run before typed encoding and surface as plain-string reverts — decode them as strings, not with decodeErrorResult. Concrete examples: "authorization deadline is required", "authorization effective sender and executor are required", "authorization selectors must be 4 bytes", "expiresAtUnix overflows int64", "invalid batchId", "duplicate privacy batchId in EVM transaction", "prepared privacy deposit denom %q does not match runtime native denom %q". Branch on err.data — when it is present, use the typed decoder; when it is absent, read the string reason from the higher-level error message.
ESC
Type to search