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 intercepts transactions before they are processed, validating them against a set of global and contract-specific rules. 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 — the only mechanism through which contract-scoped policies fire.
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) 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 five built-in policy templates that cover the most common regulatory and business compliance gates. Each template defines a Solidity parameter struct in IPcl.sol (encoded into PolicySet.policy via abi.encode) and an evaluation rule that runs at the AnteHandler (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.
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 filtered through PCL before any state-changing work runs. Global policies (a GlobalPolicyConfig set by the policy admin) are evaluated for all transactions ahead of execution. Contract-scoped policies (a ContractPolicyConfig registered against a PCL-wrapped proxy) run through the proxy's preCall / postCall hooks. A rejection at either boundary 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, wrong argument count, unknown method, 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. Each such proxy has exactly one contract policy admin: the address authorized to call changeContractPolicies and removeContractPolicies against it. The binding is single-assignment. The first successful changeContractPolicies call for a given proxy stores policy.admin as that proxy's admin; from that point on, every subsequent call must be signed by the stored admin. 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 non-PCL-managed CALL route captured during execution — both direct calls and nested routes that pass through a PCL-wrapped proxy. Evaluation is call-route based, not transfer based: 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 EAS precompile so lookups stay cheap during AnteHandler evaluation.
OKRW_EAS_TRANSFER_LIMIT_POLICY no longer exists 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. Registering or referencing this template id now fails — changeContractPolicies reverts with PolicyNotRegistered(string templateId). 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 no longer exists 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. Registering or referencing this template id now fails — changeContractPolicies reverts with PolicyNotRegistered(string templateId). 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. The Legal Oracle's authority to update parameters is itself governance-controlled — every change is visible on-chain and auditable.