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 plus arguments — never off a free-form string. Codes break into three groups: policy-violation codes (a compliance rule failed), configuration codes (bad template id, malformed parameters, structural constraint broken), and infrastructure codes (unauthorized caller, ABI decode failure).
Policy-violation ReasonCodes
These are the errors a user sees when their transaction fails an active compliance rule. They map 1:1 to the built-in policy templates. Some can be cleared by user action (complete an attestation, send less, wait for a window reset) — wallets should detect the code and offer the matching remediation.
| ReasonCode | Triggered by | Wallet UX |
|---|---|---|
InDenylist(address sender) | DENYLIST_POLICY matched the sender | Terminal — "this address can't transact". No retry. |
VolumeBelowMinLimit(uint256 minLimit, uint256 value) | Per-transaction VOLUME_POLICY lower bound broken | Suggest a larger amount. |
VolumeAboveMaxLimit(uint256 maxLimit, uint256 value) | Per-transaction VOLUME_POLICY upper bound broken | Suggest a smaller amount or splitting the transfer. |
ExceededPeriodicVolume(uint256 maxLimit, uint256 value, uint256 resetAt) | PERIODIC_VOLUME_POLICY (or its OKRW/EAS variant) window exhausted; resetAt is the unix timestamp at which the current window ends | Show "limit reached — resets at resetAt" with a countdown. |
ReachedLimitOfNonEAS(uint256 maxLimit, uint256 value) | An EAS-gated volume policy tripped its non-attested ceiling | Suggest completing attestation, or a smaller amount. |
EasAttestationRequired(address sender) | EAS_POLICY family — the sender needs an attestation | Route into the attestation / onboarding flow. |
EasNoAttestationReceived(address sender) | EAS_POLICY family — no attestation was found for the sender | Same onboarding flow. |
EasAttestationRevoked(address sender) | EAS_POLICY family — the sender's attestation was revoked | Re-onboarding or escalation; revocation is intentional. |
EasAttestationExpired(address sender) | EAS_POLICY family — the sender's attestation expired | Re-attestation flow. |
EasAttestationLookupFailed(address sender) | EAS_POLICY family — the attestation could not be read during evaluation | Treat as transient; show a generic error and retry. |
AgentKeeperRequired() | AGENT_OKRW_TRANSFER_LIMIT_POLICY evaluated without the agent keeper available | Operator-side issue — surface a generic error. |
ExceededAgentTransferLimit(uint256 maxLimit, uint256 value) | AGENT_OKRW_TRANSFER_LIMIT_POLICY per-agent cap broken | Suggest a smaller amount. |
AgentTransferLimitMetadataInvalid(string reason) | AGENT_OKRW_TRANSFER_LIMIT_POLICY — the agent's limit metadata could not be parsed | Operator-side issue, not a user error. |
AnyOfRejected(bytes[] childReverts) | Every child of a LogicalPolicy with Or quantifier rejected; the array carries each child's raw revert payload | Decode each child payload with the same decoder and surface the most actionable one. |
import { decodeErrorResult } from "viem";
try {
await walletClient.writeContract({
address: tokenAddress,
abi: erc20Abi,
functionName: "transfer",
args: [recipient, 10_000_000n * 10n ** 18n], // 10,000,000 OKRW in aokrw
});
} catch (err: any) {
const decoded = decodeErrorResult({ abi: pclAbi, data: err.data });
console.log("reason:", decoded.errorName, decoded.args);
// e.g. ExceededPeriodicVolume { maxLimit, value, resetAt }
} Configuration ReasonCodes
These are surfaced when an admin (global or contract) submits a bad
PolicySet or attempts an illegal registry operation. External dApps see them when registering a ContractPolicyConfig. The rows from LogicalPolicyChildrenEmpty down are structural checks on composite policy trees — see pcl-composite-policies.| ReasonCode | Triggered when |
|---|---|
InvalidPolicyTemplate(string input) | Template-registration validation failed: the template definition being registered is invalid. |
DuplicatedPolicyTemplate(string templateId) | Registering a template id that already exists. |
PolicyTemplateInUse() | Removing a template that at least one live PolicySet still references. |
UnknownPolicyType(string templateId) | The templateId on a submitted PolicySet does not map to any registered policy type — an unknown PolicySet.templateId surfaces as this error. |
UnknownPolicyConfigType() | The submitted policy config could not be decoded as either a GlobalPolicyConfig or a ContractPolicyConfig. |
PolicyAlreadyRegistered(string policy) | Registration collision: the target contract's admin slot is already bound and the caller is not that admin. Also raised by template-in-use registration guards. |
PolicyNotRegistered(string policy) | Targeting a policy that is not registered under the config being edited. |
PolicyCannotBeNested(string policyType) | A policy that is not permitted inside a composite tree (e.g. OKRW_EAS_PERIODIC_VOLUME_LIMIT_POLICY) was placed as a child of a LogicalPolicy or ForEachPolicy. Composite trees may only nest policy types that support nested evaluation. |
InvalidParameter(bytes input) | The ABI-encoded parameter blob on a PolicySet did not decode against the template's parameter struct. |
InvalidSelector(bytes input) | The PolicySet selector is neither empty nor a valid 4-byte selector. |
LogicalPolicyChildrenEmpty() | A LogicalPolicy was submitted with an empty children list. |
LogicalPolicyChildNil(uint256 index) | The LogicalPolicy child at index is nil. |
ForEachChildAbsent() | A ForEachPolicy was submitted without a child policy. |
ForEachSubjectUnspecified() | The ForEachPolicy subject field is unset. |
UnknownForEachSubject(uint8 subject) | The ForEachPolicy subject value is not a known subject kind. |
QuantifierUnspecified() | A LogicalPolicy was submitted without a quantifier. |
ChildSelectorNotEmpty() | A nested child PolicySet carries a non-empty selector — selectors belong on the outer PolicySet only. |
MaxDepthExceeded(uint8 maxDepth) | The composite policy tree nests deeper than maxDepth allows. |
CannotEmpty(string field) | A required field on the config is empty. |
// Registering a nested-forbidden policy inside a LogicalPolicy tree reverts
// with PolicyCannotBeNested(policyType).
//
// bytes4(keccak256("PolicyCannotBeNested(string)"))
// Handle it in a client the same way as any other IPcl custom error:
// decodeErrorResult({ abi: pclAbi, data: err.data })
// → { errorName: "PolicyCannotBeNested", args: ["OKRW_EAS_PERIODIC_VOLUME_LIMIT_POLICY"] } Infrastructure ReasonCodes
Errors that are neither compliance-rule violations nor bad policy config — they signal a misuse of the precompile itself.
| ReasonCode | Triggered when |
|---|---|
Unauthorized() | The caller is not permitted to invoke this method (e.g. non-policy-admin calling setGlobalPolicies, non-contract-admin calling changeContractPolicies). |
InvalidAddress(string input) | An argument that must be a 20-byte address was not. |
InvalidCall() | The precompile was invoked with malformed calldata. |
InvalidStructType(string got) | An ABI-decoded struct is not the type expected at that argument slot. |
AbiDecodeFailed(string reason) | The argument blob could not be ABI-decoded at all. |
JSONMarshal() / JSONUnmarshal() | Chain-side (de)serialization failed; typically indicates an SDK bug. |
// Only the policy admin may set global policies; any other caller reverts Unauthorized().
try {
await walletClient.writeContract({
address: PCL,
abi: pclAbi,
functionName: "setGlobalPolicies",
args: [{ policies: [] }],
});
} catch (err: any) {
const { errorName } = decodeErrorResult({ abi: pclAbi, data: err.data });
if (errorName === "Unauthorized") {
// Surface as "this action requires the policy admin".
}
} Decoding pattern
Every PCL error is a typed Solidity custom error. Decode the 4-byte selector and remaining bytes against the
IPcl ABI you get from @maroo-chain/contracts; do not string-match. AnyOfRejected carries an array of raw child revert payloads — recurse into it with the same decoder to surface why each branch of an Or composite failed. Code identifiers are stable: new codes are added as new policy templates ship, and existing codes are not renamed, so historic failures stay interpretable. The canonical list is the error … declarations in IPcl.sol.import { decodeErrorResult } from "viem";
import { pclAbi } from "@maroo-chain/contracts/abis/IPcl";
function explain(revertData: `0x${string}`): string {
const { errorName, args } = decodeErrorResult({ abi: pclAbi, data: revertData });
if (errorName === "AnyOfRejected") {
const childReverts = args[0] as `0x${string}`[];
return `AnyOfRejected [${childReverts.map(explain).join(", ")}]`;
}
return `${errorName}(${args.map(String).join(", ")})`;
}