PCL Built-in Policy Templates
The policy templates Maroo ships with — denylists, per-tx volume bands, cumulative periodic caps, EAS attestation gates, per-agent transfer caps, and the two composite templates that combine them.
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 five leaf templates
IPcl.sol and the parameter struct that fills each one's PolicySet.policy bytes; the two composite templates, LOGICAL_POLICY and FOR_EACH_POLICY, are covered below. Two templates that previously existed — OKRW_EAS_TRANSFER_LIMIT_POLICY and OKRW_EAS_PERIODIC_VOLUME_LIMIT_POLICY — are no longer policy types on the chain (the maroo-testnet-1 registry still lists them, but a PolicySet naming either reverts with UnknownPolicyType); the same behavior can be composed with LogicalPolicy / ForEachPolicy around VOLUME_POLICY, PERIODIC_VOLUME_POLICY, and EAS_POLICY.| templateId | Parameter struct | Purpose |
|---|---|---|
EAS_POLICY | EasPolicy(address easContract, address indexContract, bytes32 schemaUid) | Require a valid EAS attestation for the given schema on the caller. |
DENYLIST_POLICY | DenylistPolicy(address[] addresses) | Reject an operation whose from or to address (the sender, or the call target or transfer recipient) is in the list. It does not resolve agent owners; wrap it in FOR_EACH_POLICY to check a sending agent wallet's owners. |
VOLUME_POLICY | VolumePolicy(string[] tokens, VolumeUnitPolicy[] limits) | Per-transaction min/max amount band by token denom. |
PERIODIC_VOLUME_POLICY | PeriodicVolumePolicy(string[] tokens, UnitPeriodicVolumePolicy[] limits) | Cumulative cap per denom over a resetPeriodSeconds window. |
AGENT_OKRW_TRANSFER_LIMIT_POLICY | AgentOkrwTransferLimitPolicy(uint256 reserved) | Per-transfer cap read from the agent's on-chain TransferLimit metadata; the struct field is ignored (empty structs are invalid in Solidity).What each template actually checks
EAS_POLICY— Reads the EAS Indexer for(schemaUid, caller)and rejects with an EAS-family ReasonCode (EasNoAttestationReceived,EasAttestationLookupFailed,EasAttestationRevokedorEasAttestationExpired) if a valid attestation is not found. It checks only the sender, so an agent wallet passes only if the wallet itself is attested. To accept an agent wallet on its owners' attestations, wrapEAS_POLICYinFOR_EACH_POLICYover the wallet's agent owners (AgentOwners): withAny, one attested owner is enough; withEvery, every owner must be attested. The testnet's global policy does this.DENYLIST_POLICY— Membership check only. No attestation reading, no volume accounting.VOLUME_POLICY— Per-transaction band. Rejects withVolumeBelowMinLimitorVolumeAboveMaxLimit.PERIODIC_VOLUME_POLICY— Cumulative tracking keyed by(scope, contract, sender, selector, asset, resetPeriodSeconds); rejects withExceededPeriodicVolumeincluding the current period'sresetAt.AGENT_OKRW_TRANSFER_LIMIT_POLICY— Reads the agent metadataTransferLimit; rejects withExceededAgentTransferLimitorAgentTransferLimitMetadataInvalid. Only meaningful when the caller wallet is an agent.
Only
EAS_POLICY evaluates an attestation. If a rule must combine an attestation check with a volume cap or denylist, express that as a composite (see pcl-composite-policies).Composite templates for combined rules
OKRW_EAS_* templates are now expressed as LogicalPolicy / ForEachPolicy trees around the leaf templates above. For example, a rule of the form "un-attested senders are capped at 10,000,000 OKRW per 24 h" becomes an OR: attest OR stay under the periodic cap. Composite structural policies (LogicalPolicy, ForEachPolicy) have their own depth and nesting rules — see pcl-composite-policies.import {
EasPolicy, PolicySet, LogicalPolicy, ForEachPolicy, LogicalQuantifier, ForEachQuantifier,
ForEachSubject, VolumePolicy, VolumeUnitPolicy, PeriodicVolumePolicy, UnitPeriodicVolumePolicy,
ContractPolicyConfig, GlobalPolicyConfig, DenylistPolicy
} from "@maroo-chain/contracts/precompiles/pcl/IPcl.sol";
// Composite: caller must EITHER be attested OR stay under the 10,000,000 OKRW / 24h cap.
EasPolicy memory eas = EasPolicy({
easContract: 0x1000000000000000000000000000000000000007,
indexContract: 0x1000000000000000000000000000000000000008,
schemaUid: 0x5f3a2b0e1d5c9a7f6e3b4a1d2c8f7b6a5e4d3c2b1a0f9e8d7c6b5a4938271605
});
string[] memory tokens = new string[](1);
tokens[0] = "atokrw";
UnitPeriodicVolumePolicy[] memory limits = new UnitPeriodicVolumePolicy[](1);
limits[0] = UnitPeriodicVolumePolicy({
maxAmount: 10_000_000 ether, // 10,000,000 OKRW in atokrw
resetPeriodSeconds: 86_400 // 24 h
});
PeriodicVolumePolicy memory cap = PeriodicVolumePolicy({ tokens: tokens, limits: limits });
PolicySet[] memory children = new PolicySet[](2);
children[0] = PolicySet({ templateId: "EAS_POLICY", policy: abi.encode(eas), selector: "" });
children[1] = PolicySet({ templateId: "PERIODIC_VOLUME_POLICY", policy: abi.encode(cap), selector: "" });
LogicalPolicy memory anyOf = LogicalPolicy({
quantifier: LogicalQuantifier.Or,
children: children
});
// wrap `anyOf` in a `PolicySet` with templateId "LOGICAL_POLICY" and attach to the target config. Verifying which templates are registered
PolicySet for "DENYLIST_POLICY", call IPcl.policyTemplate("DENYLIST_POLICY") — it returns the descriptor if registered and reverts with the typed error PolicyTemplateNotFound(string templateId) otherwise. This is the correct way to sanity-check a script that will run on multiple networks (each network's policy admin decides its own template set).import { BaseError, ContractFunctionRevertedError, createPublicClient, http, decodeErrorResult } from "viem";
import { iPclAbi as pclAbi } from "@maroo-chain/contracts/abi/precompiles/pcl/IPcl";
const PCL = "0x1000000000000000000000000000000000000005" as const;
const client = createPublicClient({ transport: http("https://rpc-testnet.maroo.io") });
try {
const t = await client.readContract({
address: PCL,
abi: pclAbi,
functionName: "policyTemplate",
args: ["DENYLIST_POLICY"],
});
console.log("registered:", t.templateId);
} catch (err) {
// viem keeps the revert data on the ContractFunctionRevertedError in the cause chain, not on err.
const revert = err instanceof BaseError ? err.walk((e) => e instanceof ContractFunctionRevertedError) : null;
if (!(revert instanceof ContractFunctionRevertedError) || !revert.raw) throw err;
const decoded = decodeErrorResult({ abi: pclAbi, data: revert.raw });
// decoded.errorName === "PolicyTemplateNotFound"
console.error("template not registered on this network:", decoded.args);
}