PCL Precompile
On-chain compliance enforcement surface at 0x1000…0005. Now exposes getParams() returning both policyAdmin and the trusted ERC-4337 entrypoints list.
The Programmable Compliance Layer (PCL) precompile lives at the fixed address 0x1000000000000000000000000000000000000005 and is the canonical surface for everything compliance-related: registering policy templates, registering per-contract PolicySets, querying current rules, and executing a call under the regulated path via runOnPcl. Its module parameters are now reachable through a single getParams() view that returns a PclParams struct containing the policyAdmin (the chain-wide admin authorized to register templates and global policies) and entrypoints (the ERC-4337 EntryPoint addresses PCL trusts when recovering the real principal behind account-abstraction calls).
Architecture
graph TD
A[dApp / User] -- EVM call --> B{Your Smart Contract};
B -- "runOnPcl(target, data, value)" --> C[PCL Precompile @ 0x10...05];
C -- all policies pass --> D[Precompile executes the target call];
D -- result bytes --> B;
C -. any policy rejects .-> E[Revert with a typed ReasonCode];
E -.-> B;
B -- returns --> A;
classDef evm fill:#0096AA,stroke:#0096AA,color:#fff;
classDef precompile fill:#FF8C50,stroke:#FF8C50,color:#fff;
class A,B,E evm;
class C,D precompile; A contract hands the call to the PCL precompile via `runOnPcl`. The precompile evaluates the applicable global and contract policies and, if they all pass, executes the target call itself and forwards the result bytes back. If any policy rejects, the whole call reverts with a typed ReasonCode.
Surface area
IPcl.sol exports a PCL_CONTRACT constant so callers don't hard-code the address. The relevant types:import { IPcl, PclParams, PCL_CONTRACT } from "@maroo-chain/contracts/precompiles/pcl/IPcl.sol";
struct PclParams {
address policyAdmin;
address[] entrypoints;
}
// Discovery
function getParams() external view returns (PclParams memory);
function policyAdmin() external view returns (address);
// Templates
function policyTemplate(string calldata templateId) external view returns (PolicyTemplate memory);
function registerPolicyTemplate(string calldata templateId) external;
// Contract policies
function registerContractPolicies(ContractPolicyConfig calldata cfg) external;
function changeContractPolicies(ContractPolicyConfig calldata cfg) external;
function removeContractPolicies(address _contract) external;
function contractPolicies(address _contract) external view returns (ContractPolicyConfig memory);
// Regulated execution
function runOnPcl(address contractAddress, bytes calldata data, uint256 value) external returns (bytes memory);
// Proxy lifecycle (always-regulated contracts)
function deployPclProxy(PclProxyKind kind, uint256 value, bytes calldata initData) external returns (address proxy);
function pclProxy(address proxy) external view returns (PclProxyEntry memory);
// Periodic-volume views
function globalPeriodicVolume(address user, string calldata asset, uint64 resetPeriodSeconds, bool resolveAgentOwners) external view returns (PeriodicVolume memory);
function contractPeriodicVolume(address contractAddress, address user, bytes calldata selector, string calldata asset, uint64 resetPeriodSeconds, bool resolveAgentOwners) external view returns (PeriodicVolume memory); `getParams` vs `policyAdmin`
getParams() and the older policyAdmin() view return the same admin address. Prefer getParams() for new code: it's a single call that also returns the entrypoints array — the trusted ERC-4337 EntryPoint list AA integrators need to check. policyAdmin() remains as a compact accessor.PclParams memory p = PCL_CONTRACT.getParams();
address admin = p.policyAdmin;
address[] memory routes = p.entrypoints; What `entrypoints` does
entrypoints array lists the ERC-4337 EntryPoint contracts PCL trusts. When a call arrives through account abstraction, PCL uses this list to unwrap the userOp and attribute the action to the real principal (the smart account behind the EntryPoint) instead of the bundler — so denylist and volume policies bind to the actual actor. It does not decide whether contract policies run: those are evaluated whenever execution goes through runOnPcl or a PCL-wrapped proxy, regardless of this list (see pcl-dual-track-model).import { createPublicClient, http } from "viem";
const pcl = await client.readContract({
address: "0x1000000000000000000000000000000000000005",
abi: pclAbi,
functionName: "getParams",
});
// The ERC-4337 EntryPoints PCL trusts for principal attribution.
// AA wallets/bundlers should check their EntryPoint is listed here.
console.log("trusted 4337 EntryPoints:", pcl.entrypoints); Who calls what
policyAdmin can call registerPolicyTemplate and the global-policy mutators. Per-contract policy mutators (registerContractPolicies, changeContractPolicies, removeContractPolicies) are gated by the admin field inside each ContractPolicyConfig — that key is set by the contract owner at registration time, independent of the chain-wide admin. External dApps register their own contract policies through this second path.Read paths are free off-chain
getParams, policyAdmin, policyTemplate, and contractPolicies are pure view methods — no state changes. Calling them off-chain via eth_call costs nothing; calling them from a contract on-chain still consumes gas like any call. Use runOnPcl when you need a specific encoded call actually executed under the regulated path.