PCL Policy Enforcement

mechanism compliance

How PCL evaluates active policies against every transaction — at the AnteHandler for global rules, and via the regulated EVM path for contract-scoped rules.

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. Contract-scoped policies (a ContractPolicyConfig registered by a contract admin) only apply when a call targets that contract through the regulated path. A failure on any single policy rejects the whole transaction with a typed PCL ReasonCode — no compliance check can be silently skipped.

The two enforcement points

PCL fires in two distinct places per transaction:

1. AnteHandler — global policies, every tx. Before any state work, the chain runs the GlobalPolicyConfig PolicySets against the transaction's sender, target, and value. This is non-bypassable for any transaction the validators accept.
2. Regulated EVM call path — contract-scoped policies. When a caller routes through IPcl.runOnPcl(contractAddress, data, value) (or any path the chain wires to that), PCL additionally evaluates the ContractPolicyConfig registered for contractAddress. Plain EVM calls don't trigger contract-scoped policies — only the regulated path does, by design.

Both use the same evaluation logic against the same template implementations, just with different parameter scopes.

What evaluation looks like

Either path resolves to the same loop: read the active PolicySets for that scope, then for each PolicySet dispatch on templateId to the matching evaluator (DENYLIST_POLICY → address-list check, EAS_POLICY → attestation lookup, VOLUME_POLICY → per-token amount check, etc.). The first failure short-circuits and returns an ABI-encoded ReasonCode like InDenylist(sender) or EasAttestationRequired(sender) — see pcl-reason-codes for the full list and per-template behavior in pcl-policy-templates.

Reading remaining quota at runtime

Period-based templates (PERIODIC_VOLUME_POLICY, OKRW_EAS_PERIODIC_VOLUME_LIMIT_POLICY) accumulate per (scope, sender) in PeriodicVolume records. dApps can read the current accumulator without sending a transaction — useful for showing "you have X OKRW left until reset":
IPcl pcl = IPcl(0x1000000000000000000000000000000000000005);
PeriodicVolume memory pv = pcl.globalPeriodicVolume(user, "aokrw");
// pv.amount, pv.maxAmount, pv.resetAt
There are matching reads for contract-scoped accumulators (contractPeriodicVolume, globalOkrwEasPeriodicVolume, contractOkrwEasPeriodicVolume).

Pre-flight simulation

Wallets can preview whether a transaction would be admitted by performing an EVM static call to IPcl.runOnPcl with the same from, target, calldata, and value the user is about to sign. A successful static call means PCL would admit; a revert means it would reject with the encoded ReasonCode. See simulating-pcl-checks for the integration pattern.

Error handling

PCL never returns a generic "transaction failed". Each failure carries one of the typed ReasonCodes (InDenylist, EasAttestationRequired, VolumeAboveMaxLimit, ExceededPeriodicVolume, etc.). Wallets and frontends should decode the revert payload against the IPcl ABI and show a code-specific UX — e.g., "complete KYC" for an EAS-family code vs. "split the transfer" for a volume code. Treating all errors as opaque defeats the point of the typed surface.
Source: maroo
ESC
Type to search