PCL ReasonCodes
The complete set of typed errors PCL emits when it rejects a transaction, plus the shared IPrecompile errors it now inherits. Wallets and SDKs key off these codes to drive UX.
Every PCL rejection carries one of the typed Solidity errors declared on IPcl — or, since IPcl now inherits from IPrecompile, one of the shared boundary errors. Wallet and dApp code should decode the revert payload against the IPcl ABI (which transitively includes the IPrecompile errors) and drive UX off the error name plus arguments — never off a free-form string. The codes break into four groups: policy-violation codes (the user's transaction failed a compliance rule), configuration codes (an admin call was malformed or unauthorized), composite / structural codes (a LogicalPolicy or ForEachPolicy combinator rejected), and the inherited boundary errors (invalid address, calldata that does not decode, SDK-level rejections). A small number of failures are still plain-string reverts and are not decodable as typed errors — those are called out explicitly.
Policy-violation ReasonCodes
| Error | Arguments | Meaning |
|---|---|---|
InDenylist | address sender | An address on the call route or in a transfer is on the denylist — the sender (or a resolved principal), or the recipient / call target. Despite its name, sender is whichever address matched. |
VolumeBelowMinLimit | uint256 minLimit, uint256 value | Transfer amount is below the per-transaction floor. |
VolumeAboveMaxLimit | uint256 maxLimit, uint256 value | Transfer amount is above the per-transaction cap. |
ExceededPeriodicVolume | uint256 maxLimit, uint256 value, uint256 resetAt | Periodic cap exceeded; resetAt is when the window resets. Windows are fixed, not rolling — aligned to multiples of resetPeriodSeconds since the Unix epoch, so a 24-hour window resets for everyone at 00:00 UTC. |
EasAttestationRequired | address sender | Declared, but no policy raises it: EAS_POLICY reports a sender with no indexed attestation as EasNoAttestationReceived. |
EasNoAttestationReceived | address sender | Indexer returned no attestation for the sender under the configured schema. |
EasAttestationLookupFailed | address sender | The attestation the Indexer listed could not be read from the EAS contract the policy names (easContract) — check that address. A failed Indexer lookup is not reported as this error. |
EasAttestationRevoked | address sender | Attestation exists but was revoked. |
EasAttestationExpired | address sender | Attestation exists but is past its expirationTime. |
ExceededAgentTransferLimit | uint256 maxLimit, uint256 value | Agent's on-chain TransferLimit metadata cap was exceeded. |
AgentTransferLimitMetadataInvalid | string reason | Agent metadata is malformed. |
AgentKeeperRequired | — | Agent policy evaluated but the agent module is not wired. |
Configuration ReasonCodes
registerPolicyTemplate, changeContractPolicies, setGlobalPolicies, etc.) when the payload is malformed or the caller is not authorized.| Error | Arguments | Meaning |
|---|---|---|
CannotEmpty | string field | A required field was left empty. |
Unauthorized | — | Caller is not the required admin (policy admin, contract admin, or protected caller). |
InternalError | — | State encode / decode failed inside the module. |
InvalidCall | — | The call context itself is invalid (e.g. deployPclProxy called without an EVM). |
InvalidStructType | string got | ABI struct decoded to the wrong shape. |
AbiDecodeFailed | string reason | Raw ABI decode failed (initData malformed, etc.). |
InvalidParameter | bytes input | Parameter bytes did not match the template's expected struct. Also raised for a duplicate selector, with input set to that selector as text — its lowercase hex without 0x (for example the bytes of "a9059cbb"), or __all__ for the empty selector. |
InvalidSelector | bytes input | Selector is not a 4-byte value. |
InvalidPolicyTemplate | string input | Template ID string is not a recognized template. |
DuplicatedPolicyTemplate | string templateId | Registering a template that already exists. |
PolicyTemplateNotFound | string templateId | Template ID is not registered — raised only by the policyTemplate(templateId) view; admin operations report an unregistered template as PolicyNotRegistered. |
PolicyTemplateInUse | — | Declared, but not raised: removePolicyTemplate does not check whether a PolicySet still references the template, so the removal succeeds. Until the template is registered again, every evaluation of a policy that uses it then fails with PolicyNotRegistered. |
UnknownPolicyType | string templateId | Template ID is not one this build knows how to evaluate. |
UnknownPolicyConfigType | — | Enclosing config type is neither Global nor Contract. |
PolicyAlreadyRegistered | address contractAddress | ContractPolicyConfig already exists for this address on a create path. |
ContractPolicyNotRegistered | address contractAddress | No ContractPolicyConfig for this address on an update / remove path. |
PclProxyNotRegistered | address contractAddress | Address is not a registered PCL-wrapped proxy. |
PolicyNotRegistered | string templateId | Template referenced by a PolicySet is not registered. |
Composite and structural ReasonCodes
LogicalPolicy and ForEachPolicy combinators (see pcl-composite-policies) can fail with structural errors that describe how a combinator rejected — most importantly AnyOfRejected, which recursively wraps the child reverts.| Error | Arguments | Meaning |
|---|---|---|
AnyOfRejected | bytes[] childReverts | An Or combinator's children all rejected; each child revert is preserved as raw ABI bytes so clients can decode them individually. |
MaxDepthExceeded | uint8 maxDepth | Combinator nesting exceeded the structural depth limit. |
ChildSelectorNotEmpty | — | A combinator's child PolicySet must carry an empty selector; a non-empty one was provided. |
LogicalPolicyChildrenEmpty | — | LogicalPolicy.children array was empty. |
LogicalPolicyChildNil | uint256 index | A child at the given index decoded to a nil PolicySet. |
ForEachChildAbsent | — | ForEachPolicy.child was not set. |
ForEachSubjectUnspecified | — | ForEachPolicy.subject was Unspecified. |
UnknownForEachSubject | uint8 subject | Subject value is not a known ForEachSubject. |
QuantifierUnspecified | — | A combinator's quantifier was Unspecified. |
When decoding
AnyOfRejected, iterate the childReverts array and re-decode each entry against the IPcl ABI to surface the underlying leaf errors.Inherited IPrecompile errors
IPcl now inherits from IPrecompile, so every shared boundary error is decodable against the same ABI. These fire before any policy code runs — they describe malformed inputs at the precompile edge or SDK-level rejections.| Error | Arguments | When it fires |
|---|---|---|
InvalidAddress | string bad | A stored address string does not parse as a Bech32 address. In practice this happens only when a policy-admin method is called while the module's policyAdmin parameter is unset, so bad is the empty string; on the testnet policyAdmin is set. Address arguments arrive as 20-byte EVM addresses, so a zero address is not rejected with this error: removeContractPolicies(address(0)), for example, reverts with ContractPolicyNotRegistered. Note the argument is now string bad, not the previous locally-declared string input — the selector is the IPrecompile one. |
InvalidAmount | string amount | Declared, but no PCL code path raises it. |
ABISetupFailed | string reason | Calldata does not decode against IPcl's ABI: an unknown selector (reason no method with id: 0x…, most commonly a stale ABI) or arguments too short to decode (the ABI decoder's message). Bytes after the last argument are ignored. |
QueryFailed | string queryMethod, string reason | A query is rejected, for example a PageRequest with both key and offset set in the periodic-list views. |
InvalidPageRequest | string method, uint256 index, string value | Declared, but a malformed PageRequest in the periodic-list views does not return it: both key and offset set reverts with QueryFailed, and a tuple that does not decode reverts with ABISetupFailed. |
SDKUnauthorized / SDKInvalidAddress / SDKInvalidRequest / SDKNotFound / … | — | Underlying chain-layer rejection surfaced through the mapped SDK error catalog. |
InvalidNumberOfArgs and UnknownMethod are declared in IPrecompile as well, but PCL does not raise them for a wrong selector or argument count.Any client that previously matched only the local
InvalidAddress(string input) shape on IPcl must update its ABI fragment to the IPrecompile InvalidAddress(string bad) — the argument name changed and the declaring interface moved, though the 4-byte selector for InvalidAddress(string) is unchanged.import { BaseError, ContractFunctionRevertedError, decodeErrorResult } from "viem";
import { iPclAbi } from "@maroo-chain/contracts/abi/precompiles/pcl/IPcl";
try {
await wallet.writeContract({ /* ... call PCL ... */ });
} catch (err) {
// viem keeps the revert data on the ContractFunctionRevertedError in the cause chain, not on err.
const revert = err instanceof BaseError ? err.walk((e) => e instanceof ContractFunctionRevertedError) : null;
if (!(revert instanceof ContractFunctionRevertedError) || !revert.raw) throw err;
const decoded = decodeErrorResult({ abi: iPclAbi, data: revert.raw });
// decoded.errorName is one of the IPcl reason codes OR an inherited
// IPrecompile error (InvalidAddress, ABISetupFailed, QueryFailed, ...).
switch (decoded.errorName) {
case "InDenylist":
case "ExceededPeriodicVolume":
case "EasAttestationRevoked":
// policy-violation UX
break;
case "Unauthorized":
case "PolicyTemplateNotFound":
// admin / configuration UX
break;
case "AnyOfRejected":
// recursively decode decoded.args[0]: bytes[]
break;
case "InvalidAddress":
case "ABISetupFailed":
// boundary error — usually a stale ABI or wrong argument type
break;
default:
console.warn("unhandled PCL revert:", decoded.errorName, decoded.args);
}
} Failures that are NOT typed errors
IPcl error. Where that string is ABI-encoded as Solidity's Error(string) — as deployPclProxy's is — viem's decodeErrorResult returns it as errorName: "Error" with the reason in args[0]; match on the reason string.execution reverted—deployPclProxycould not construct the proxy (aninitializerthat reverts — or is empty, for a Transparent or UUPS proxy — or alogicaddress without code).pcl keeper is not initialized— the module is not wired on this build; indicates a misconfigured node rather than a user error.pcl: parse IPcl.json: <err>— module init failure; will never surface to a live dApp.
A duplicate selector is not one of them: it reverts with the typed
InvalidParameter(bytes input) (see the configuration table above). All other failures listed on this page are typed errors and should be decoded against the IPcl ABI.