PCL Precompile

component compliance

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 — the only mechanism through which contract-scoped policies fire.

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

The PCL precompile lives at a stable EVM address across networks. Its Solidity interface is 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

The surface falls into four groups:

  • Params & templatesgetParams, policyAdmin, policyTemplate, registerPolicyTemplate, removePolicyTemplate.
  • Policy configurationglobalPolicies, setGlobalPolicies, removeGlobalPolicies, contractPolicies, changeContractPolicies, removeContractPolicies.
  • Periodic-volume queriesglobalPeriodicVolume, contractPeriodicVolume, globalPeriodicList, contractPeriodicList for reading current usage against periodic caps.
  • Regulated executiondeployPclProxy, pclProxy, and the hook methods preCall / postCall that 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. All contract-scoped enforcement flows through the proxy's hook path.

Enforcement points

PCL evaluates policies at two places:

1. Transaction entry — the global GlobalPolicyConfig is evaluated for every EVM transaction, regardless of target.
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

All read views are gasless when called off-chain via 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

Every policy failure surfaces as a typed custom error on IPcl. Common ReasonCodes: EasNoAttestationReceived, EasAttestationRevoked, EasAttestationExpired, InDenylist, ExceededPeriodicVolume, VolumeAboveMaxLimit, ExceededAgentTransferLimit. Configuration and admin-guard failures use Unauthorized, PolicyTemplateNotFound, ContractPolicyNotRegistered, and PclProxyNotRegistered. A duplicate selector inside a single ContractPolicyConfig reverts with the plain string duplicate selector: <selector> — this one is not a typed error, so decode it as a string.
ESC
Type to search