PCL Policy Structure
Three-tier hierarchy — PolicyTemplate (registered template type) → PolicySet (template + ABI-encoded params + optional selector) → PolicyConfig (the bag of PolicySets that get applied).
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). Earlier PCL drafts called the middle tier PolicyRef; the canonical name in the IPcl interface is PolicySet (with its policy field being the ABI-encoded parameter bytes).
The three Solidity structs
Straight from
IPcl.sol:struct PolicyTemplate {
string templateId; // e.g. "DENYLIST_POLICY"
string name;
string description;
bytes paramSchema; // self-describing schema (rarely consumed by callers)
}
struct PolicySet {
string templateId; // which template this is an instance of
bytes policy; // abi.encode(<template-specific struct>)
bytes selector; // optional 4-byte function selector; empty bytes = applies to all calls
}
struct GlobalPolicyConfig {
PolicySet[] policies;
}
struct ContractPolicyConfig {
address _contract; // the contract this applies to
address admin; // who can change this config later
PolicySet[] policies;
} The key field is
PolicySet.policy — it's bytes carrying the ABI encoding of the template-specific parameter struct. Don't construct it as JSON; use abi.encode(<struct>) from the Solidity / ethers / viem side.How callers construct a PolicySet
Each template defines its own parameter struct (see
1. Build the template-specific struct with concrete values.
2. Encode it as bytes via
3. Wrap in a
Example — denylisting two addresses:
pcl-policy-templates for the full list). To register a policy you:1. Build the template-specific struct with concrete values.
2. Encode it as bytes via
abi.encode.3. Wrap in a
PolicySet with the template id and (optionally) a 4-byte function selector.Example — denylisting two addresses:
import { IPcl, PolicySet, ContractPolicyConfig, DenylistPolicy } from "@maroo-chain/contracts/IPcl.sol";
DenylistPolicy memory dl = DenylistPolicy({
addresses: new address[](2)
});
dl.addresses[0] = 0xAaaA...;
dl.addresses[1] = 0xBbBb...;
PolicySet memory ps = PolicySet({
templateId: "DENYLIST_POLICY",
policy: abi.encode(dl),
selector: "" // empty → applies to all calls on the target contract
}); Then attach to a
ContractPolicyConfig and submit via IPcl.registerContractPolicies(...).Global vs Contract policies
Two scopes:
When a transaction comes in: PCL evaluates all policies in the global config, AND if the call targets a contract with a registered ContractPolicyConfig and uses the regulated path, all policies in that contract config too. Any single failure rejects the whole transaction with the corresponding ReasonCode.
GlobalPolicyConfig— applied by the AnteHandler to every transaction on the chain. Managed by the chain-wide policy admin (seepcl-policy-admin). Typical contents: a denylist, a periodic-volume cap on un-attested users, etc.ContractPolicyConfig— applied only when a transaction targets a specific contract address (via the regulated path /runOnPcl). Each contract config carries its ownadminso the contract owner can update its own policies independently of the chain-wide admin.
When a transaction comes in: PCL evaluates all policies in the global config, AND if the call targets a contract with a registered ContractPolicyConfig and uses the regulated path, all policies in that contract config too. Any single failure rejects the whole transaction with the corresponding ReasonCode.
The selector field
Each
PolicySet carries an optional selector (4-byte function selector encoded as bytes). When non-empty, the policy only applies to calls invoking a function with that selector on the target contract. This lets a single contract apply different rules to different functions:// rule applies only to calls of `foo(uint256)`:
bytes memory fooSel = abi.encodePacked(bytes4(keccak256("foo(uint256)")));
PolicySet memory ps = PolicySet({
templateId: "VOLUME_POLICY",
policy: abi.encode(volumePolicy),
selector: fooSel
}); Empty
selector ("") means "any call to this contract."