PCL ReasonCodes

mechanism compliance

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.

ReasonCodeTriggered byWallet UX
InDenylist(address sender)DENYLIST_POLICY — sender or recipient on the listTerminal — "this address can't transact". No retry.
VolumeBelowMinLimit(uint256 minLimit, uint256 value)VOLUME_POLICYvalue < minLimit for the denomSuggest a higher amount.
VolumeAboveMaxLimit(uint256 maxLimit, uint256 value)VOLUME_POLICYvalue > maxLimitSuggest splitting the transfer.
ExceededPeriodicVolume(uint256 maxLimit, uint256 value, uint256 resetAt)PERIODIC_VOLUME_POLICY or OKRW_EAS_PERIODIC_VOLUME_LIMIT_POLICY window exhaustedShow "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 schemaSame — onboarding flow.
EasAttestationRevoked(address sender)sender had an attestation; it was revokedRe-onboard or escalate; revoke is intentional.
EasAttestationExpired(address sender)sender's attestation expiredRe-attestation flow.
EasAttestationLookupFailed(address sender)reading the attestation failed mid-evaluationTreat 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 variantsSame 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.

ReasonCodeTriggered 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.

ReasonCodeTriggered 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).
Source: maroo
ESC
Type to search