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.
import { iPclAbi as pclAbi } from "@maroo-chain/contracts/abi/precompiles/pcl/IPcl";
// Replace the Privacy precompile's policy set. Send it from the precompile's
// current contract admin — any other sender gets `Unauthorized`. The payload's
// `admin` is who may change the set next.
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 installs that admin together with the precompile's initial policy set — no caller claims it by submitting a config first, and every sender other than the stored admin gets Unauthorized(). The chain-wide policyAdmin — the address returned by IPcl.policyAdmin() — has no authority here by virtue of that role (on Maroo Testnet the two happen to be the same account). Only the currently stored contract admin 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 is subject to the global policies the same way any other transaction is — before the transaction executes and again after it. The precompile evaluates its own contract-scope policies at its boundary, before and after it changes state. Either scope can reject the call independently.