PCL ReasonCode

mechanism compliance

PCL이 트랜잭션을 거절할 때 발행하는 커스텀 오류 전체와 이제 상속받는 IPrecompile 공유 오류를 함께 다룹니다. 지갑과 SDK는 이 코드들을 기준으로 사용자 경험을 구성합니다.

PCL의 모든 거절은 IPcl에 선언된 커스텀 Solidity 오류 중 하나를 전달합니다. IPcl이 이제 IPrecompile을 상속하므로 공유 경계 오류도 포함됩니다. 지갑과 dApp 코드는 revert 페이로드를 IPcl ABI(전이적으로 IPrecompile 오류를 포함)로 디코드하고, 자유 형식 문자열이 아니라 오류 이름과 인자를 기준으로 사용자 경험을 구성합니다. 코드는 네 그룹으로 나뉩니다. 정책 위반 코드(사용자 트랜잭션이 컴플라이언스 규칙에 실패), 설정 코드(관리자 호출이 잘못되었거나 권한이 없음), 조합/구조 코드(LogicalPolicy 또는 ForEachPolicy 조합기가 거절), 그리고 상속된 경계 오류(잘못된 주소, 디코딩할 수 없는 calldata, SDK 수준 거절)입니다. 일부 실패는 여전히 평문 문자열 revert이며 커스텀 오류로 디코드할 수 없습니다. 해당 항목은 명시적으로 표시합니다.

정책 위반 ReasonCode

사용자 트랜잭션이 리프 정책에 도달했고 그 정책이 거절할 때 발생합니다. 지갑 UX가 가장 관심 있는 코드들이며, 최종 사용자에게 결제가 차단된 이유를 알려줍니다.

오류인자의미
InDenylistaddress sender호출 경로나 전송의 주소가 denylist에 있습니다. 발신자(또는 해석된 주체)일 수도 있고 수신자·호출 대상일 수도 있습니다. 이름과 달리 sender에는 매치된 주소가 담깁니다.
VolumeBelowMinLimituint256 minLimit, uint256 value전송 금액이 트랜잭션당 하한 미만입니다.
VolumeAboveMaxLimituint256 maxLimit, uint256 value전송 금액이 트랜잭션당 상한을 초과합니다.
ExceededPeriodicVolumeuint256 maxLimit, uint256 value, uint256 resetAt기간 상한을 초과합니다. resetAt은 현재 윈도가 재설정되는 시각입니다. 윈도는 롤링이 아닌 고정 구간이며, Unix epoch부터 resetPeriodSeconds의 배수에 맞춰지므로 24시간 윈도는 모든 사용자에게 UTC 00:00에 함께 재설정됩니다.
EasAttestationRequiredaddress sender선언되어 있지만 이 오류를 발생시키는 정책은 없습니다. 인덱싱된 attestation이 없는 발신자는 EAS_POLICY가 EasNoAttestationReceived로 거절합니다.
EasNoAttestationReceivedaddress sender지정된 스키마로 발신자에 대해 인덱서가 attestation을 반환하지 않았습니다.
EasAttestationLookupFailedaddress sender인덱서가 알려 준 attestation을 정책에 지정된 EAS 컨트랙트(easContract)에서 읽지 못했습니다. 그 주소를 확인합니다. 인덱서 조회 자체가 실패한 경우는 이 오류로 보고되지 않습니다.
EasAttestationRevokedaddress senderattestation이 존재하지만 취소되었습니다.
EasAttestationExpiredaddress senderattestation이 존재하지만 expirationTime을 지났습니다.
ExceededAgentTransferLimituint256 maxLimit, uint256 value에이전트의 온체인 TransferLimit 메타데이터 상한을 초과했습니다.
AgentTransferLimitMetadataInvalidstring reason에이전트 메타데이터가 잘못되었습니다.
AgentKeeperRequired—에이전트 정책을 평가했으나 에이전트 모듈이 연결되어 있지 않습니다.

설정 ReasonCode

관리자 작업(registerPolicyTemplate, changeContractPolicies, setGlobalPolicies 등)에서 페이로드가 잘못되었거나 호출자가 권한이 없을 때 발생합니다.

오류인자의미
CannotEmptystring field필수 필드가 비어 있습니다.
Unauthorized—호출자가 요구되는 admin(정책 admin, 컨트랙트 admin, 보호된 호출자)이 아닙니다.
InternalError—모듈 내부에서 state 인코딩/디코딩이 실패했습니다.
InvalidCall—호출 컨텍스트 자체가 잘못되었습니다(예: EVM 없이 deployPclProxy 호출).
InvalidStructTypestring gotABI struct가 잘못된 형태로 디코드되었습니다.
AbiDecodeFailedstring reason원시 ABI 디코드가 실패했습니다(initData 형식 오류 등).
InvalidParameterbytes input파라미터 바이트가 템플릿의 예상 struct와 일치하지 않습니다. 중복 selector에도 발생하며, 이때 input은 해당 selector를 텍스트로 담은 값입니다. 0x 없는 소문자 16진수(예: "a9059cbb"의 바이트)이고, 빈 selector는 __all__입니다.
InvalidSelectorbytes inputselector가 4바이트 값이 아닙니다.
InvalidPolicyTemplatestring input템플릿 ID 문자열이 인식되는 템플릿이 아닙니다.
DuplicatedPolicyTemplatestring templateId이미 존재하는 템플릿을 등록하려고 했습니다.
PolicyTemplateNotFoundstring templateId템플릿 ID가 등록되어 있지 않습니다. policyTemplate(templateId) 뷰에서만 발생하며, 관리자 작업은 등록되지 않은 템플릿을 PolicyNotRegistered로 보고합니다.
PolicyTemplateInUse—선언되어 있지만 발생하지 않습니다. removePolicyTemplate은 PolicySet이 해당 템플릿을 참조하는지 확인하지 않으므로 제거가 성공합니다. 이후 템플릿을 다시 등록할 때까지 그 템플릿을 쓰는 정책을 평가할 때마다 PolicyNotRegistered로 실패합니다.
UnknownPolicyTypestring templateId이 빌드가 평가할 수 있는 템플릿 ID가 아닙니다.
UnknownPolicyConfigType—상위 config 타입이 Global도 Contract도 아닙니다.
PolicyAlreadyRegisteredaddress contractAddress생성 경로에서 이 주소의 ContractPolicyConfig가 이미 존재합니다.
ContractPolicyNotRegisteredaddress contractAddress갱신/제거 경로에서 이 주소의 ContractPolicyConfig가 없습니다.
PclProxyNotRegisteredaddress contractAddress이 주소는 등록된 PCL 래핑 프록시가 아닙니다.
PolicyNotRegisteredstring templateIdPolicySet이 참조하는 템플릿이 등록되어 있지 않습니다.

조합 및 구조 ReasonCode

LogicalPolicy와 ForEachPolicy 조합기(pcl-composite-policies 참고)는 조합기가 어떻게 거절했는지 설명하는 구조적 오류로 실패할 수 있습니다. 특히 중요한 것은 AnyOfRejected이며, 자식 revert들을 재귀적으로 감쌉니다.

오류인자의미
AnyOfRejectedbytes[] childRevertsOr 조합기의 자식들이 모두 거절되었습니다. 각 자식 revert가 원시 ABI 바이트로 보존되어 클라이언트가 개별적으로 디코드할 수 있습니다.
MaxDepthExceededuint8 maxDepth조합기 중첩이 구조적 깊이 제한을 초과했습니다.
ChildSelectorNotEmpty—조합기의 자식 PolicySet은 빈 selector여야 하는데 비어 있지 않은 값이 제공되었습니다.
LogicalPolicyChildrenEmpty—LogicalPolicy.children 배열이 비어 있습니다.
LogicalPolicyChildNiluint256 index주어진 인덱스의 자식이 nil PolicySet으로 디코드되었습니다.
ForEachChildAbsent—ForEachPolicy.child가 설정되지 않았습니다.
ForEachSubjectUnspecified—ForEachPolicy.subject가 Unspecified입니다.
UnknownForEachSubjectuint8 subjectsubject 값이 알려진 ForEachSubject가 아닙니다.
QuantifierUnspecified—조합기의 quantifier가 Unspecified입니다.

AnyOfRejected를 디코드할 때는 childReverts 배열을 순회하며 각 항목을 다시 IPcl ABI로 디코드해 기반이 되는 리프 오류를 노출합니다.

상속된 IPrecompile 오류

IPcl이 이제 IPrecompile을 상속하므로, 모든 공유 경계 오류를 동일한 ABI로 디코드할 수 있습니다. 이 오류들은 정책 코드가 실행되기 전에 발생하며, 프리컴파일 경계의 잘못된 입력이나 SDK 수준 거절을 나타냅니다.

오류인자발생 시점
InvalidAddressstring bad저장된 주소 문자열이 Bech32 주소로 파싱되지 않습니다. 실제로는 모듈의 policyAdmin 파라미터가 설정되지 않은 상태에서 정책 관리자 메서드를 호출할 때만 발생하며, 이때 bad는 빈 문자열입니다. 테스트넷에는 policyAdmin이 설정되어 있습니다. address 인자는 20바이트 EVM 주소로 전달되므로 0 주소는 이 오류로 거절되지 않습니다. 예를 들어 removeContractPolicies(address(0))는 ContractPolicyNotRegistered로 revert됩니다. 인자가 이전에 로컬로 선언되었던 string input이 아니라 string bad라는 점에 주의합니다. selector는 IPrecompile 쪽 값을 사용합니다.
InvalidAmountstring amount선언되어 있지만 PCL의 어떤 코드 경로도 이 오류를 발생시키지 않습니다.
ABISetupFailedstring reasoncalldata가 IPcl ABI로 디코딩되지 않습니다. 알 수 없는 selector(사유 no method with id: 0x…, 대개 오래된 ABI)이거나 디코딩하기에 짧은 인자(ABI 디코더의 메시지)입니다. 마지막 인자 뒤에 붙은 바이트는 무시됩니다.
QueryFailedstring queryMethod, string reason조회가 거절되었습니다. 예를 들어 주기 목록 뷰에서 PageRequest에 key와 offset을 모두 지정한 경우입니다.
InvalidPageRequeststring method, uint256 index, string value선언되어 있지만, 주기 목록 뷰의 잘못된 PageRequest는 이 오류를 반환하지 않습니다. key와 offset을 모두 지정하면 QueryFailed로, 디코딩되지 않는 튜플은 ABISetupFailed로 revert됩니다.
SDKUnauthorized / SDKInvalidAddress / SDKInvalidRequest / SDKNotFound / …—매핑된 SDK 오류 카탈로그를 통해 노출되는 하부 체인 계층 거절입니다.

InvalidNumberOfArgs와 UnknownMethod도 IPrecompile에 선언되어 있지만, PCL은 selector나 인자 개수가 틀려도 이 오류를 발생시키지 않습니다.

이전에 IPcl의 로컬 InvalidAddress(string input) 형태만 매칭하던 클라이언트는 ABI 조각을 IPrecompile의 InvalidAddress(string bad)로 갱신해야 합니다. 인자 이름이 바뀌고 선언 인터페이스가 이동했지만, InvalidAddress(string)의 4바이트 selector는 변하지 않았습니다.
import { BaseError, ContractFunctionRevertedError, decodeErrorResult } from "viem";
import { iPclAbi } from "@maroo-chain/contracts/abi/precompiles/pcl/IPcl";

try {
  await wallet.writeContract({ /* ... PCL 호출 ... */ });
} catch (err) {
  // viem은 revert 데이터를 err가 아니라 원인 체인의 ContractFunctionRevertedError에 둡니다.
  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은 IPcl reason code 중 하나이거나
  // 상속된 IPrecompile 오류(InvalidAddress, ABISetupFailed, QueryFailed, ...)입니다.
  switch (decoded.errorName) {
    case "InDenylist":
    case "ExceededPeriodicVolume":
    case "EasAttestationRevoked":
      // 정책 위반 UX
      break;
    case "Unauthorized":
    case "PolicyTemplateNotFound":
      // admin / 설정 UX
      break;
    case "AnyOfRejected":
      // decoded.args[0]: bytes[]를 재귀적으로 디코드
      break;
    case "InvalidAddress":
    case "ABISetupFailed":
      // 경계 오류입니다. 대개 오래된 ABI 또는 잘못된 인자 타입이 원인입니다.
      break;
    default:
      console.warn("처리되지 않은 PCL revert:", decoded.errorName, decoded.args);
  }
}

커스텀 오류가 아닌 실패

일부 PCL 실패는 IPcl의 타입 지정 오류가 아니라 평문 문자열 이유로 revert됩니다. 그 문자열이 Solidity Error(string)으로 ABI 인코딩되어 있으면(deployPclProxy의 경우처럼) viem의 decodeErrorResult가 errorName: "Error"로 반환하고 이유는 args[0]에 담기므로, 이유 문자열로 매칭합니다.

  • execution reverted — deployPclProxy가 프록시를 생성하지 못했습니다(initializer가 revert하는 경우 — Transparent·UUPS 프록시는 비어 있는 경우도 — 또는 코드가 없는 logic 주소).
  • pcl keeper is not initialized — 이 빌드에 모듈이 연결되어 있지 않습니다. 사용자 오류가 아니라 노드 설정 오류입니다.
  • pcl: parse IPcl.json: <err> — 모듈 초기화 실패입니다. 실제 dApp에는 노출되지 않습니다.


selector 중복은 여기에 해당하지 않습니다. 타입 지정 오류 InvalidParameter(bytes input)로 revert됩니다(위 설정 표 참고). 이 페이지에 나열된 그 외 모든 실패는 커스텀 오류이며 IPcl ABI로 디코드해야 합니다.
ESC
검색어를 입력하세요