PCL ReasonCodes
The complete set of typed errors PCL emits when it rejects a transaction. Wallets and SDKs key off these codes to drive UX.
Every PCL rejection carries one of the typed Solidity errors defined on IPcl. Wallet and dApp code should decode the revert payload against the IPcl ABI and key UX off the error name + arguments — never off a free-form string. Codes break into three groups: policy-violation codes (the user's transaction failed a compliance check; some can be cleared by user action), template-management codes (admin tooling errors raised on register… / set… calls; never seen by end users on the regulated path), and internal codes (chain misconfiguration; surface to operators).
Policy-violation codes
These fire when a transaction reaches PCL evaluation and fails a registered policy. The user can sometimes clear the error (complete KYC, send less, wait for a window reset) — wallets should detect the code and offer the right remediation.
| ReasonCode | Triggered by | Wallet UX |
|---|---|---|
InDenylist(address sender) | DENYLIST_POLICY — sender or recipient on the list | Terminal — "this address can't transact". No retry. |
VolumeBelowMinLimit(uint256 minLimit, uint256 value) | VOLUME_POLICY — value < minLimit for the denom | Suggest a higher amount. |
VolumeAboveMaxLimit(uint256 maxLimit, uint256 value) | VOLUME_POLICY — value > maxLimit | Suggest splitting the transfer. |
ExceededPeriodicVolume(uint256 maxLimit, uint256 value, uint256 resetAt) | PERIODIC_VOLUME_POLICY or OKRW_EAS_PERIODIC_VOLUME_LIMIT_POLICY window exhausted | Show "limit reached; resets at \<resetAt\>". |
EasAttestationRequired(address sender) | EAS_POLICY — generic "sender needs an attestation" | Trigger KYC onboarding. |
EasNoAttestationReceived(address sender) | sender has never been attested under that schema | Same — onboarding flow. |
EasAttestationRevoked(address sender) | sender had an attestation; it was revoked | Re-onboard or escalate; revoke is intentional. |
EasAttestationExpired(address sender) | sender's attestation expired | Re-attestation flow. |
EasAttestationLookupFailed(address sender) | reading the attestation failed mid-evaluation | Treat as transient; surface a generic error and retry. |
ExceededAgentTransferLimit(uint256 maxLimit, uint256 value) | OKRW_EAS_TRANSFER_LIMIT_POLICY (un-attested cap) or AGENT_OKRW_TRANSFER_LIMIT_POLICY (per-agent cap) | Suggest reducing the amount or completing KYC. |
ReachedLimitOfNonEAS(uint256 maxLimit, uint256 value) | reserved historical name for non-EAS limits; you may still see it from older policy variants | Same as the volume codes. |
Template-management codes
These appear on admin calls that mutate the policy graph (
registerPolicyTemplate, setGlobalPolicies, registerContractPolicies, etc.). End users on the regulated path don't trigger these; admin UIs and CI scripts should surface them as raw errors.| ReasonCode | Triggered when |
|---|---|
Unauthorized() | Caller isn't the PolicyAdmin (global) or the per-config admin (contract) for the operation. |
InvalidPolicyTemplate(string input) | Template ID isn't a recognized built-in. |
DuplicatedPolicyTemplate(string templateId) | registerPolicyTemplate called for a templateId already registered. |
PolicyTemplateInUse() | removePolicyTemplate called for a template still referenced by a live PolicySet. |
UnknownPolicyType(string templateId) | A PolicySet.templateId references a value that hasn't been registered on this network. |
UnknownPolicyConfigType() | Decoded config struct doesn't match either GlobalPolicyConfig or ContractPolicyConfig shape. |
PolicyAlreadyRegistered(string policy) | registerContractPolicies called for a contract that already has a config (use change… instead). |
PolicyNotRegistered(string policy) | change… / remove… called for a contract with no existing config. |
InvalidParameter(bytes input) | The policy bytes don't abi.decode cleanly into the template's parameter struct. |
InvalidSelector(bytes input) | PolicySet.selector isn't empty bytes or a 4-byte selector. |
InvalidStructType(string got) | ABI decoding hit an unexpected top-level struct. |
AbiDecodeFailed(string reason) | Generic abi.decode failure (e.g., truncated calldata). |
InvalidAddress(string input) | An address argument is the zero address or otherwise malformed. |
InvalidCall() | Public method invoked with the wrong arguments shape. |
CannotEmpty(string field) | A required field on a config struct was empty. |
JSONMarshal() / JSONUnmarshal() | Internal serialization failure (mostly seen in legacy paths). |
Internal / chain-config codes
Surfaced when something is wired up wrong in the chain itself, not in user input. Treat these as bugs and report to operators.
| ReasonCode | Triggered when |
|---|---|
AgentKeeperRequired() | PCL was invoked without the agent keeper installed (chain misconfiguration). |
AgentTransferLimitMetadataInvalid(string reason) | The agent's TransferLimit metadata exists but isn't a parseable 32-byte uint256. Almost always an admin bug, not a user bug. |
How wallets / SDKs should consume these
Decode the revert payload against the IPcl ABI and switch on the error name. Use the typed arguments (
maxLimit, value, resetAt, sender) for the user-facing copy — never reformat from a string.import { decodeErrorResult } from "viem";
import { iPclAbi } from "@maroo-chain/contracts/abi/precompiles/pcl/IPcl";
try {
await walletClient.writeContract({ /* ... */ });
} catch (err: any) {
const decoded = decodeErrorResult({ abi: iPclAbi, data: err.data });
switch (decoded.errorName) {
case "EasNoAttestationReceived":
case "EasAttestationRequired":
case "EasAttestationExpired":
// route into KYC onboarding
break;
case "EasAttestationRevoked":
// surface escalation flow
break;
case "InDenylist":
// terminal — no retry
break;
case "VolumeAboveMaxLimit":
case "ExceededAgentTransferLimit":
// suggest a smaller amount
break;
case "ExceededPeriodicVolume": {
const [maxLimit, value, resetAt] = decoded.args;
// show window-reset countdown using resetAt (unix seconds)
break;
}
default:
// template-management or internal — show raw error to operator UIs only
break;
}
} Stability guarantee
Code identifiers are stable. New codes are added when new policy templates ship; existing codes don't get renamed or repurposed even when the underlying template is deprecated, so historic transaction failures stay interpretable. The full canonical list is in
IPcl.sol (error … declarations).