Policy-Aware Precompile
A precompile that carries its own ContractPolicyConfig — bound directly, without a PCL proxy — and whose policy set can never be empty or cleared.
Most contract-scope PCL enforcement runs through a PCL-wrapped proxy: the proxy's preCall / postCall hooks evaluate the ContractPolicyConfig bound to the proxy address. A policy-aware precompile is different — it is a binary-supported precompile whose code evaluates PCL policy internally at its own precompile boundary. Currently the only policy-aware precompile is the Privacy precompile at 0x100000000000000000000000000000000000000b. Because a precompile is not a deployed contract, it cannot be registered as a PCL proxy and does not go through the proxy hook path; instead its address is bound directly to a ContractPolicyConfig through IPcl.changeContractPolicies, and the precompile reads that configuration on every state-changing call.
Architecture
flowchart LR U["User / dApp"]:::evm PRIVACY["Privacy precompile<br/>0x…000b"]:::precompile PCL["PCL precompile<br/>0x…0005"]:::precompile CFG["ContractPolicyConfig<br/>keyed by 0x…000b"]:::precompile STATE["Shielded state"]:::precompile U -->|"deposit / transfer / withdraw"| PRIVACY PRIVACY -->|"read policies"| PCL PCL --> CFG PRIVACY -->|"apply if all rules pass"| STATE classDef evm fill:#0096AA,stroke:#0096AA,color:#fff; classDef precompile fill:#FF8C50,stroke:#FF8C50,color:#fff;
The Privacy precompile reads its own ContractPolicyConfig from PCL on every state-changing call. There is no proxy hop.
What makes a precompile policy-aware
ContractPolicyConfig at call time and enforces the resulting rules before performing state changes. In the current release, the sole policy-aware precompile is the Privacy precompile — every state-changing privacy call (deposit, transfer, withdraw, their WithAuthorization variants, and both batch variants) passes PCL policy evaluation at that boundary.Registration rules that differ from PCL proxies
ContractPolicyConfig entry point (IPcl.changeContractPolicies) accepts a policy-aware precompile address as _contract — you do NOT call deployPclProxy for a precompile, and attempts to register a policy-aware precompile as a PCL proxy are rejected at the chain layer. Two additional rules apply that do not apply to ordinary PCL proxies:- The
policiesarray must be non-empty. Submittingpolicies: []reverts withCannotEmpty("policy-aware precompile policies"). removeContractPolicieson a policy-aware precompile address reverts with the sameCannotEmptyerror. The only way to change the policies is to submit a replacement non-empty set throughchangeContractPolicies.
These rules exist because a policy-aware precompile is always in the active call path — leaving it without a policy would silently disable an enforcement point that the chain expects to always be evaluated.
// Bind rules to the Privacy precompile. First call: the payload's `admin`
// becomes the future gatekeeper. Subsequent calls must come from that admin.
const PCL = "0x1000000000000000000000000000000000000005";
const PRIVACY = "0x100000000000000000000000000000000000000b";
await wallet.writeContract({
address: PCL,
abi: pclAbi,
functionName: "changeContractPolicies",
args: [{
_contract: PRIVACY,
admin: privacyAdmin, // TODO: replace with real admin
policies: [nonEmptyPolicySet], // must be non-empty
}],
}); Admin authority is separate from the chain-wide policy admin
admin field stored under a policy-aware precompile's ContractPolicyConfig is the sole gatekeeper for future updates to that config. The chain-wide policyAdmin — the address returned by IPcl.policyAdmin() — does not automatically have authority here. Only the currently stored contract admin (initially the first caller who submitted the config) can rotate the admin or replace the policy set, and the two happen atomically in the same changeContractPolicies call.Global policy still applies
ContractPolicyConfig runs in addition to the chain-wide GlobalPolicyConfig. Every call to the precompile passes global policy evaluation first (the same way any other transaction does), and then the precompile evaluates its own contract-scope policies. Either scope can reject the call independently.