PCL 정책 강제

mechanism compliance

PCL은 모든 트랜잭션에 대해 활성 정책을 평가합니다. 전역 규칙은 실행 전에 모든 트랜잭션에 적용되고, 컨트랙트 범위 규칙은 PCL 프록시 훅 경로에서 실행됩니다. 어느 경계에서 발생한 거절이든 ABI 디코드 가능한 타입 지정 revert로 노출됩니다.

모든 마루 트랜잭션은 상태 변경 작업이 실행되기 전에 PCL을 통과합니다. 정책 관리자가 설정한 GlobalPolicyConfig의 전역 정책은 모든 트랜잭션에 대해 실행 전에 평가됩니다. PCL 래핑 프록시에 등록된 ContractPolicyConfig의 컨트랙트 범위 정책은 프록시의 preCall / postCall 훅에서 실행됩니다. 어느 경계에서든 거절이 발생하면 트랜잭션이 중단되며, ABI 인코딩된 PCL ReasonCode(권한·인코딩 실패의 경우 공유 IPrecompile 오류)로 이유가 전달됩니다. 이 형태는 RPC 제출 시점의 거절이든 온체인 실행 시점의 거절이든 동일하므로, 클라이언트 디코더 하나로 두 경우를 모두 처리할 수 있습니다.

아키텍처

flowchart TD
  Tx["Incoming transaction"] --> Ingress{"Global policies<br/>(ingress)"}
  Ingress -->|"reject"| Revert1["revert IPcl error"]
  Ingress -->|"admit"| Target{"Target address"}
  Target -->|"PCL-wrapped proxy"| Hook["preCall / postCall<br/>(contract-scoped policies)"]
  Target -->|"plain contract"| Exec["EVM execution"]
  Hook -->|"reject"| Revert2["revert IPcl error"]
  Hook -->|"admit"| Exec
  Exec --> Sweep{"Post-execution<br/>ERC20 log sweep"}
  Sweep -->|"reject"| Revert3["revert IPcl error"]
  Sweep -->|"admit"| Commit["State commit"]

  classDef evm fill:#0096AA,stroke:#0096AA,color:#fff;
  classDef precompile fill:#FF8C50,stroke:#FF8C50,color:#fff;
  class Tx,Target,Exec,Commit evm;
  class Ingress,Hook,Sweep,Revert1,Revert2,Revert3 precompile;

PCL은 세 지점에서 정책을 평가합니다. 진입 시점의 전역 규칙, PCL 프록시 훅 내부의 컨트랙트 범위 규칙, 그리고 ERC20 Transfer 로그에 대한 실행 후 스윕입니다. 단일 거절이 트랜잭션 전체를 revert시킵니다.

두 개의 강제 지점

PCL은 정확히 두 경계에서 정책을 강제합니다. 정책 평가와 호출 실행을 함께 수행하는 단일 진입점은 존재하지 않습니다.

  • 전역 범위GlobalPolicyConfig는 실행 전 모든 트랜잭션에 대해 평가됩니다. 대상 컨트랙트와 관계없이 트랜잭션의 발신자와 calldata를 기준으로 실행됩니다. 전형적인 내용은 체인 전역 denylist, 미인증 사용자에 대한 주기 거래량 상한, KYC 게이트입니다.
  • 컨트랙트 범위ContractPolicyConfig는 트랜잭션이 PCL 래핑 프록시를 경유할 때만 평가됩니다(pcl-proxy-hook 참고). 프록시의 preCall 훅이 유효 principal, calldata, value를 담아 PCL을 호출하고, 성공 시 구현체로 전달합니다. 실패 시 전체 트랜잭션이 revert됩니다.


두 범위 모두 빈 selector 항목을 먼저 평가한 뒤 selector 매칭 항목을 평가합니다(pcl-policy-structure 참고).

거절은 타입 지정 revert로 노출됩니다

PCL이 트랜잭션을 거절하면 revert 데이터는 IPcl의 PCL 오류 selector(예: InDenylist(address sender), ExceededPeriodicVolume(uint256 maxLimit, uint256 value, uint256 resetAt), EasNoAttestationReceived(address sender) 같은 ReasonCode)의 ABI 인코딩입니다. 권한·인코딩 실패는 공유 IPrecompile 오류 중 하나로 표시됩니다. 두 강제 지점 모두 동일하게 동작합니다. eth_sendRawTransaction의 브로드캐스트 전 검사와 온체인 실행 경계가 같은 revert 형태를 만들어 내므로, 클라이언트가 두 오류 조각을 하나의 ABI에 붙여 두면 일관되게 디코드할 수 있습니다.
import { decodeErrorResult } from "viem";

const pclErrorAbi = [
  { type: "error", name: "InDenylist", inputs: [{ name: "sender", type: "address" }] },
  { type: "error", name: "ExceededPeriodicVolume", inputs: [
    { name: "maxLimit", type: "uint256" },
    { name: "value", type: "uint256" },
    { name: "resetAt", type: "uint256" },
  ] },
  { type: "error", name: "EasNoAttestationReceived", inputs: [{ name: "sender", type: "address" }] },
  { type: "error", name: "EasAttestationRevoked", inputs: [{ name: "sender", type: "address" }] },
  // ...나머지 IPcl ReasonCode와 IPrecompile 공유 오류를 이어서 추가합니다.
] as const;

try {
  await walletClient.sendRawTransaction({ serializedTransaction: signedTx });
} catch (err: any) {
  if (err?.data) {
    const decoded = decodeErrorResult({ abi: pclErrorAbi, data: err.data });
    console.error(`PCL ReasonCode: ${decoded.errorName}`, decoded.args);
  } else {
    throw err;
  }
}

RPC 제출 시점의 사전 거절

RPC mempool은 이제 트랜잭션을 브로드캐스트하기 전에 PCL 정책 평가를 수행합니다. 평가가 트랜잭션을 거절하면 eth_sendRawTransaction은 온체인에서 발생했을 것과 동일한 타입 지정 revert 데이터를 반환합니다. 리졸버가 내부의 PCL 오류를 EVM 스타일의 타입 지정 revert 페이로드로 감싸므로, sendRawTransaction 주변의 클라이언트 try/catch가 이유를 다른 revert와 동일한 방식으로 디코드합니다. 두 가지 실용적 의미가 있습니다.

  • 클라이언트는 "제출 시점 정책 거절"과 "실행 시점 정책 거절"을 구분해 특수 처리할 필요가 없습니다. 하나의 디코더가 둘 다 처리합니다.
  • 거절이 브로드캐스트 전에 발생하므로 가스가 청구되지 않고, 어떤 블록에도 포함되지 않습니다. 익스플로러에도 나타나지 않습니다.

평가 순서

PCL 래핑 프록시에 도달한 호출의 평가 순서는 다음과 같습니다.

1. 전역 설정 — 빈 selector 항목이 먼저 평가됩니다(체인 전역 denylist가 여기서 단축 평가로 종료시킬 수 있습니다).
2. 전역 설정 — 트랜잭션의 4바이트 함수 selector와 selector가 일치하는 항목이 있으면 평가됩니다.
3. 컨트랙트 설정 — 대상 프록시의 빈 selector 항목입니다.
4. 컨트랙트 설정 — 대상 프록시의 selector 매칭 항목입니다.
5. 구현체 호출이 실행됩니다.
6. postCall 훅이 PCL에 재진입해 사후 실행 회계(예: 주기 거래량 증가)를 처리합니다.

어느 단계에서든 실패하면 트랜잭션이 중단됩니다. 프록시를 우회해 구현체를 직접 호출하는 경우 1–2단계만 거치며, 컨트랙트 범위 정책은 평가되지 않습니다.

일반 문자열 revert가 발생하는 경우

대부분의 PCL 실패는 타입 지정 오류를 생성하지만, 일부는 일반 문자열 revert입니다. 호출자가 selector가 겹치는 PolicySet 두 개를 제출할 때 나오는 duplicate selector: <selector>가 대표 사례입니다. decodeErrorResult가 아니라 문자열로 디코드합니다. 마찬가지로 내부 실패가 타입 매핑 없는 표준 SDK 오류("unauthorized", "cannot be empty: <field>")인 경우 revert는 일반 문자열입니다. catch 분기에서 두 형태를 모두 확인합니다.

정책 평가 비용은 누가 내는가

정책 평가는 공짜가 아니고 별도로 청구되지도 않습니다. 호출자의 가스에서 나갑니다. 실행 전 검사는 이미 지불하는 호출의 일부이고, 실행 후 정산은 호출이 성공한 뒤 가스 한도에 남은 몫을 사용합니다. 예전에는 여기에 고정 예산이 따로 있어서 한도를 빠듯하게 잡아도 동작했지만, 그 예산은 없어졌습니다.

설계할 때 감안할 결과는 두 가지입니다.

  • 가스 한도는 컨트랙트 호출 비용이 아니라 eth_estimateGas에서 가져오십시오. 추정치는 같은 사후 평가를 수행하므로 정책 작업이 이미 포함되어 있습니다. 호출 비용만으로 계산한 한도는 평가 도중 소진될 수 있습니다.
  • revert된 호출은 여기까지 오지 않습니다. 사후 평가는 실행이 성공한 경우에만 수행되므로, 실패한 호출에 정책 작업 비용이 추가로 부과되지는 않습니다.
ESC
검색어를 입력하세요