Policy & Compliance
Programmable Compliance Layer (PCL) — covers policy structure, templates, enforcement, and precompile interfaces.
Foundations
What PCL is, why a public chain can still be regulated, and the dual-track model.
The Programmable Compliance Layer (PCL) is a core module of the Maroo network that enables the creation and enforcement of compliance policies directly on the blockchain. It validates EVM transactions against chain-wide global rules before and after they execute. It validates them against contract-specific rules during execution, when a call goes through a PCL-registered proxy or reaches a policy-aware precompile such as Privacy. This allows for the implementation of complex regulatory requirements, such as KYC/AML checks, transfer restrictions, and volume limits, without altering the core logic of smart contracts.
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.
Maroo is a permissionless chain where anyone can create a wallet. However, it dynamically routes transactions through a Regulated Path or an Open Path based on counterparty identity, transaction size, and asset type.
Maroo separates transaction execution into two contexts. In the open track, a call goes directly to a target contract and only the global GlobalPolicyConfig (evaluated at the transaction entry point and again after execution) applies — no contract-scoped policies fire. In the regulated track, users transact through a PCL-registered proxy address; that proxy's hook path calls IPcl.preCall(...) before the underlying execution and IPcl.postCall(...) after, so any ContractPolicyConfig bound to the proxy is enforced atomically around the call. Contract admins choose the track by publishing either the raw implementation address (open) or the PCL-registered proxy address (regulated) as their user-facing entry point.
Maroo processes transactions through two distinct paths: the Open Path for standard, low-risk activities, and the Regulated Path for high-value or compliant asset transfers. The PCL automatically determines the correct path.
Policy model
How policies are structured, composed, administered, evaluated, and how rejections surface.
PCL stores compliance rules as Solidity-defined ABI tuples, not as JSON objects. The hierarchy has three tiers: a PolicyTemplate (the type of rule, registered by the policy admin) is instantiated as a PolicySet (the type ID plus an ABI-encoded parameters blob plus an optional function selector) and bundled into a PolicyConfig (either the global config or a per-contract config). PolicyTemplate itself is metadata-only — it carries templateId, name, and description, so the shape of each template's parameters must come from the template-specific struct in IPcl.sol.
Maroo's PCL ships with seven built-in policy templates: five leaf templates that cover the most common regulatory and business compliance gates, and two composite templates (LOGICAL_POLICY, FOR_EACH_POLICY) that combine them. Each template defines a Solidity parameter struct in IPcl.sol (encoded into PolicySet.policy via abi.encode) and an evaluation rule that runs before execution (at submission and again in the chain's pre-execution checks) and again after execution (for GlobalPolicyConfig), or through the PCL proxy hook path (for ContractPolicyConfig). Administrators instantiate a template as a PolicySet and attach it to a config; the on-chain policy admin decides which templates are actually registered on a given network — read IPcl.policyTemplate(templateId) at runtime rather than assuming. A listed id is not always usable: the maroo-testnet-1 registry still lists the two removed OKRW_EAS_* ids.
The PCL policy admin is a single address stored in the x/pcl module parameters and exposed via IPcl.policyAdmin(). It is the only caller authorized to register or remove policy templates and to set or clear the GlobalPolicyConfig. To ensure the admin can always recover from a misconfigured global policy or denylist that would otherwise reject its own transactions, calls from the admin to a fixed set of PCL control-plane methods bypass PCL policy evaluation entirely at both the pre-execution operations extraction step and the post-execution transfer-scan hook.
Every Maroo transaction is checked by PCL. Global policies (a GlobalPolicyConfig set by the policy admin) are evaluated for all transactions ahead of execution, and again after a successful execution. Contract-scoped policies (a ContractPolicyConfig registered against a PCL-wrapped proxy) run during execution through the proxy's preCall / postCall hooks. A rejection at any of these points aborts the transaction with an ABI-encoded PCL ReasonCode (or, for authorization / encoding failures, one of the shared IPrecompile errors) — the same shape whether the rejection happens at RPC-submission time or at on-chain execution, so a single client decoder handles both.
LogicalPolicy and ForEachPolicy are structural policy templates that wrap other PolicySet entries into a tree instead of carrying their own leaf rule. LogicalPolicy combines its children under an AND (Every) or OR (Any) quantifier; ForEachPolicy takes a single child and applies it once per resolved subject — currently the caller's set of agent owners. Both are subject to a maximum structural depth (MaxDepthExceeded) and to a set of validation errors (LogicalPolicyChildrenEmpty, ForEachChildAbsent, QuantifierUnspecified, ChildSelectorNotEmpty) that reject malformed trees at registration.
Every PCL rejection carries one of the typed Solidity errors declared on IPcl — or, since IPcl now inherits from IPrecompile, one of the shared boundary errors. Wallet and dApp code should decode the revert payload against the IPcl ABI (which transitively includes the IPrecompile errors) and drive UX off the error name plus arguments — never off a free-form string. The codes break into four groups: policy-violation codes (the user's transaction failed a compliance rule), configuration codes (an admin call was malformed or unauthorized), composite / structural codes (a LogicalPolicy or ForEachPolicy combinator rejected), and the inherited boundary errors (invalid address, calldata that does not decode, SDK-level rejections). A small number of failures are still plain-string reverts and are not decodable as typed errors — those are called out explicitly.
Proxy & binding
The execution surface: PCL proxies, pre/post hooks, and contract-admin binding.
Contract-scoped PCL policies are enforced through a pair of hook methods on the PCL precompile: preCall runs before the wrapped implementation executes, and postCall runs after. These hooks are not open — the precompile checks the immediate msg.sender against a small allowlist of caller kinds and rejects everything else with Unauthorized. Two caller kinds are admitted: a PCL-wrapped proxy registered by deployPclProxy (which additionally must be the same address as the target contract), and a trusted entrypoint registered in PclParams.entrypoints (with no such constraint, since an entrypoint routes user operations to many targets).
PCL enforces contract-scoped policies only against PCL-registered proxies — contracts deployed through IPcl.deployPclProxy — and the binary-supported policy-aware precompiles. Each such target has exactly one contract policy admin: the address authorized to call changeContractPolicies and removeContractPolicies against it. The admin is set when the target is created, not by a later call: deployPclProxy records its caller (the deployer or factory) as the proxy's admin in the same call, and a policy-aware precompile's admin is installed by the chain. From then on, every changeContractPolicies / removeContractPolicies must come from the stored admin; any other sender gets Unauthorized. To rotate authority, the current admin calls changeContractPolicies again with a different admin in the payload — the policy replacement and the admin handover happen atomically in the same call.
Policy templates
The built-in policy templates, one page per template.
DENYLIST_POLICY is a leaf policy template whose parameter is a list of addresses. PCL evaluates it against every observed call route captured during execution — both direct calls and nested routes that pass through a PCL-wrapped proxy — and against the sender and recipient of each transfer the transaction makes. A route need not move value: even a zero-value call that produces no Transfer log will trip the policy if the route touches a denylisted address. When a match is found the transaction reverts with InDenylist(address sender).
Enforces per-transaction min/max amount limits for one or more token denoms. Each transaction is evaluated independently against the limits — no rolling window, no cumulative tracking. For period-based cumulative limits, see PERIODIC_VOLUME_POLICY.
Tracks cumulative transaction volume per sender per denom over a configured reset period and rejects transactions that would push the running total above the limit. Distinct from VOLUME_POLICY, which checks each transaction independently. Used for daily / monthly transfer caps and Travel-Rule–style thresholds.
Gates a transaction on whether the sender holds a valid (non-expired, non-revoked) attestation issued under a specific EAS schema. The canonical primitive for KYC, KYB, accreditation, and any credential-based gate. Resolves attestations through the chain's x/eas module, which reads the EAS and Indexer contracts with view calls wherever PCL evaluates the policy — before execution, during execution (in the PCL preCall / postCall hooks or the policy-aware Privacy precompile), or after execution.
OKRW_EAS_TRANSFER_LIMIT_POLICY is no longer a policy type on chain. It was one of the monolithic policy templates that bundled an attestation condition with a cap; upstream removed them so that the same rule is expressed by composing the pieces. A PolicySet that names this id now fails — changeContractPolicies and setGlobalPolicies revert with UnknownPolicyType(string templateId) (a PolicySet with an empty policy value fails first, with CannotEmpty) — and registering it reverts too (on maroo-testnet-1, whose registry still lists it, with DuplicatedPolicyTemplate). This page is kept so that integrations still carrying the id find out what happened and what to write instead.
OKRW_EAS_PERIODIC_VOLUME_LIMIT_POLICY is no longer a policy type on chain. It was one of the monolithic policy templates that bundled an attestation condition with a cap; upstream removed them so that the same rule is expressed by composing the pieces. A PolicySet that names this id now fails — changeContractPolicies and setGlobalPolicies revert with UnknownPolicyType(string templateId) (a PolicySet with an empty policy value fails first, with CannotEmpty) — and registering it reverts too (on maroo-testnet-1, whose registry still lists it, with DuplicatedPolicyTemplate). This page is kept so that integrations still carrying the id find out what happened and what to write instead.
Caps a single OKRW transfer originating from an agent wallet using a limit stored as on-chain metadata on the ERC-8004 IdentityRegistry — specifically getMetadata(agentId, "TransferLimit"). The policy struct itself has no configurable fields; the cap is whatever the agent's owner has written into the metadata slot. The evaluator attributes the transfer to the agent both when the agent is the direct msg.sender and, under global-scope policies, when the agent is the initiating principal forwarded by the PCL proxy hook — so a contract that pulls funds from an agent principal still hits the same cap. If the resulting attributed value exceeds the configured TransferLimit, the transaction reverts with ExceededAgentTransferLimit(maxLimit, value).
Regulatory primitives
Chain-level regulatory mechanisms beyond per-transaction policy checks.
Hacks, phishing, and clearly illegal fund flows happen in real-world payment systems. Maroo provides three bounded recovery primitives — freeze, burn, reissue — that correct state without rolling back the chain. All three operate only when there's a documented legal basis and the prescribed procedure has been followed; every invocation is logged to Observer Nodes and the governance audit trail. These are not always-on intervention tools, and Maroo prefers forward correction (new transactions that override prior state) over backward rewriting wherever the legal basis allows.
Regulations change. Travel-Rule thresholds shift, sanctions lists update, jurisdictional rules evolve. Maroo separates the policy engine (PCL — stable, rarely changes) from the parameter supply (Legal Oracle — updated as laws and regulations evolve). New regulatory requirements are absorbed by adding a parameter — or, at most, registering a new PolicyTemplate — without touching chain core code. On maroo-testnet-1 these parameters — the global policy config — are set by the PCL policy admin, a single account; governance controls the module parameters, including which account is the policy admin. Every change is an on-chain transaction, visible and auditable.