Privacy Precompile
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
| Shape | Direct call | Relay variant |
|---|---|---|
| Value in | deposit (payable) | — |
| Note-to-note | transfer | transferWithAuthorization |
| Value out | withdraw | withdrawWithAuthorization |
| Batch | batchTransfer, singleProofBatchTransfer | batchTransferWithAuthorization, 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
PrivacyNonPayable(string methodName)— attachedmsg.valueto a non-payable privacy method.PrivacyRequestExpired(uint64 expiresAtUnix)— the request'sexpiresAtUnixis in the past. Set it a few blocks into the future to allow for mempool delay.PrivacyBatchSizeOutOfRange(uint256 count, uint256 max)—batchTransfer/singleProofBatchTransferoutside 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
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 zeroeffectiveSenderorexecutor, or adeadlineof 0.fieldnames it.PrivacyAuthorizationExpired(uint64 deadline)—deadlineis 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 statedauthorizationKind: 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
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))
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.