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 is evaluated by PCL at the precompile boundary — against the contract policies bound to the Privacy precompile's address, before and after the call executes — on top of the global policies the enclosing EVM transaction meets. A PCL rejection at the precompile boundary reverts with a typed IPcl error; other 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 is checked by PCL before and after it executes, then either commits shielded state or reverts — with a typed IPcl error when PCL rejects it.

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 (atokrw).


iPrivacyAbi from @maroo-chain/contracts holds these four and the shared IPrecompile errors every Maroo precompile inherits, so decode with it — a hand-written list of the four alone cannot decode a shared revert such as InvalidAmount.
import { BaseError, ContractFunctionRevertedError, decodeErrorResult } from "viem";
import { iPrivacyAbi } from "@maroo-chain/contracts/abi/precompiles/privacy/IPrivacy";

try {
  await client.writeContract({ /* privacy call */ });
} catch (err) {
  // viem keeps the revert data on the ContractFunctionRevertedError in the cause chain, not on err.
  const revert = err instanceof BaseError ? err.walk((e) => e instanceof ContractFunctionRevertedError) : null;
  if (revert instanceof ContractFunctionRevertedError && revert.raw) {
    const decoded = decodeErrorResult({ abi: iPrivacyAbi, data: revert.raw });
    console.error(decoded.errorName, decoded.args);
  } else {
    throw err; // not a revert (a network error, a wrong ABI): let it surface
  }
}

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 or zero: a zero effectiveSender or executor, or a deadline of 0. field names it.
  • 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 signature could not be checked for the stated authorizationKind: the kind is not 1 (EOA), 2 (ERC-1271) or 3 (EIP-7702), the signature does not recover, the account's code does not fit the kind (an EOA kind on a contract, ERC-1271 on an account without code, EIP-7702 without a delegation), or the ERC-1271 call failed.
  • 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.

Plain-string failures (Error(string))

A few checks have no typed IPrivacy error and revert with a plain reason, Solidity's Error(string). Examples an EVM caller can receive: "expiresAtUnix overflows int64" (an expiresAtUnix of 2^63 or more), "privacy batchId must be non-zero" (a batch call with a zero batchId), and "duplicate privacy batchId in EVM transaction" (the same caller reusing a batchId within one EVM transaction). They carry revert data like any other revert: decodeErrorResult returns them as errorName: "Error" with the reason in args[0] (viem also exposes it as reason on the ContractFunctionRevertedError), so one catch branch handles both shapes — branch on errorName, not on whether data is present. Missing or zero authorization fields are not among them: a zero effectiveSender or executor, or a deadline of 0, reverts with the typed PrivacyInvalidAuthorizationField(field) above.
ESC
Type to search