PCL Precompile
EVM surface for the Programmable Compliance Layer at 0x1000…0005. Read the policy graph, register or change templates and configs, deploy PCL-wrapped proxies, and expose the preCall/postCall hook path that regulated proxies use around every call.
The PCL Precompile is a stateful precompile at a fixed EVM address that bridges Solidity code to Maroo's compliance engine. It exposes read views (getParams, globalPolicies, contractPolicies, policyTemplate, periodic-volume queries) and admin-guarded writes (registerPolicyTemplate, setGlobalPolicies, changeContractPolicies, and their remove counterparts). It also owns the regulated execution path: deployPclProxy deploys a canonical PCL-wrapped proxy whose hook path calls preCall before, and postCall after, the underlying execution — a proxied contract's contract-scoped policies fire in this hook. A trusted ERC-4337 entrypoint can also call preCall / postCall, and the policy-aware Privacy precompile evaluates its own contract policies at its own boundary.
Architecture
flowchart LR
User[EOA / dApp]:::evm --> Proxy[PCL Proxy<br/>Transparent or UUPS]:::evm
Proxy --> Pre[IPcl.preCall]:::precompile
Pre --> Logic[Logic Contract]:::evm
Logic --> Post[IPcl.postCall]:::precompile
Post --> Result[Return / Revert<br/>with ReasonCode]:::evm
Admin[Contract Admin]:::evm -.-> ChangeCfg[IPcl.changeContractPolicies]:::precompile
Reader[Any address]:::evm -.-> Views[IPcl views<br/>contractPolicies / globalPolicies / policyTemplate]:::precompile
classDef evm fill:#0096AA,stroke:#0096AA,color:#fff;
classDef precompile fill:#FF8C50,stroke:#FF8C50,color:#fff; Regulated execution flows through a PCL proxy that brackets each call with preCall and postCall. Contract policy configuration is done by the contract admin via changeContractPolicies; policy-graph views are open to any reader.
Address and interface
IPcl in @maroo-chain/contracts; the constant PCL_PRECOMPILED_ADDRESS in that package pins the address so contracts do not hard-code it.// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.28;
import {IPcl, PCL_CONTRACT, PCL_PRECOMPILED_ADDRESS} from
"@maroo-chain/contracts/precompiles/pcl/IPcl.sol";
contract Example {
IPcl constant pcl = PCL_CONTRACT; // 0x1000…0005
address constant PCL = PCL_PRECOMPILED_ADDRESS; // same address as a raw literal
} Method groups
- Params & templates —
getParams,policyAdmin,policyTemplate,registerPolicyTemplate,removePolicyTemplate. - Policy configuration —
globalPolicies,setGlobalPolicies,removeGlobalPolicies,contractPolicies,changeContractPolicies,removeContractPolicies. - Periodic-volume queries —
globalPeriodicVolume,contractPeriodicVolume,globalPeriodicList,contractPeriodicListfor reading current usage against periodic caps. - Regulated execution —
deployPclProxy,pclProxy, and the hook methodspreCall/postCallthat PCL-registered proxies call around every user call.
There is no single-call entry point that both checks policy and executes a target function on the PCL precompile itself. Contract-scoped enforcement for a proxied contract flows through the proxy's hook path; a policy-aware precompile such as Privacy evaluates its own contract policies at its own boundary.
Enforcement points
preCall / postCall hooks, and the policy-aware Privacy precompile evaluates its own contract policies at its own boundary, before and after its state change — see privacy-policy-aware-precompile):1. Transaction entry and exit — the global
GlobalPolicyConfig is evaluated for every EVM transaction, regardless of target (except the policy admin's own registerPolicyTemplate / removePolicyTemplate / setGlobalPolicies / removeGlobalPolicies calls sent directly to PCL, which skip it — see pcl-policy-admin): before execution (at RPC submission and again in the chain's pre-execution checks), and again after a successful execution, over the sender and every call the transaction made. A violation after execution fails the transaction — its state is reverted, but it is still mined and its gas is charged.2. Proxy hook path — when a call targets a PCL-registered proxy (see
pcl-proxy-hook), the proxy calls IPcl.preCall(...) before the underlying execution and IPcl.postCall(...) after. This is where a proxy's ContractPolicyConfig fires.Calls that target a contract's implementation directly (bypassing the proxy) only see the global config, so publishing the proxy address as the user-facing entry point is what makes the regulated track effective.
Reading current state
eth_call. On-chain callers (a contract that reads PCL state during its own execution) pay normal gas.import { createPublicClient, http } from "viem";
const PCL = "0x1000000000000000000000000000000000000005" as const;
const pclAbi = [
{ type: "function", name: "policyAdmin", stateMutability: "view",
inputs: [], outputs: [{ type: "address" }] },
{ type: "function", name: "globalPolicies", stateMutability: "view",
inputs: [], outputs: [{ type: "tuple", components: [
{ name: "policies", type: "tuple[]", components: [
{ name: "templateId", type: "string" },
{ name: "policy", type: "bytes" },
{ name: "selector", type: "bytes" },
] },
] }] },
] as const;
const client = createPublicClient({ transport: http("https://rpc-testnet.maroo.io") });
const admin = await client.readContract({ address: PCL, abi: pclAbi, functionName: "policyAdmin" });
const global = await client.readContract({ address: PCL, abi: pclAbi, functionName: "globalPolicies" });
console.log("policy admin:", admin, "active global policies:", global.policies.length); Failure surface
IPcl. Common ReasonCodes: EasNoAttestationReceived, EasAttestationRevoked, EasAttestationExpired, InDenylist, ExceededPeriodicVolume, VolumeAboveMaxLimit, ExceededAgentTransferLimit. Configuration and admin-guard failures use Unauthorized, UnknownPolicyType (a templateId that is not a known policy type), ContractPolicyNotRegistered, and PclProxyNotRegistered. PolicyTemplateNotFound comes only from the policyTemplate(templateId) view, for an ID that is not registered. A duplicate selector inside a single ContractPolicyConfig (or the global config) is a typed error too: InvalidParameter(bytes input), where input is the selector as text — its lowercase hex without 0x, or __all__ for the empty selector.