PCL Built-in Policy Templates
The five policy templates Maroo ships with — denylists, per-tx volume bands, cumulative periodic caps, EAS attestation gates, and per-agent transfer caps.
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 five built-in templates
IPcl.sol and the parameter struct that fills its PolicySet.policy bytes. Two templates that previously existed — OKRW_EAS_TRANSFER_LIMIT_POLICY and OKRW_EAS_PERIODIC_VOLUME_LIMIT_POLICY — have been removed from the chain; 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 transactions whose caller (or resolved agent owner) is in the address list. |
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,EasAttestationRevoked,EasAttestationExpired, orEasAttestationRequired) if a valid attestation is not found. When the caller is an agent wallet, PCL resolves the underlying agent owners and passes as long as any owner is attested.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] = "aokrw";
UnitPeriodicVolumePolicy[] memory limits = new UnitPeriodicVolumePolicy[](1);
limits[0] = UnitPeriodicVolumePolicy({
maxAmount: 10_000_000 ether, // 10,000,000 OKRW in aokrw
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 { createPublicClient, http, decodeErrorResult } from "viem";
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: any) {
const decoded = decodeErrorResult({ abi: pclAbi, data: err.data });
// decoded.errorName === "PolicyTemplateNotFound"
console.error("template not registered on this network:", decoded.args);
}