PCL 정책 강제
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/>global policies"}
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 전송을 대상으로 다시 평가하는 전역 규칙입니다. RPC 제출 시점에 거절된 트랜잭션은 브로드캐스트되지 않습니다. 실행 중이나 실행 후의 거절은 트랜잭션 전체를 revert시키지만, 이 트랜잭션은 블록에는 포함되며 가스비도 부과됩니다.
두 가지 강제 범위
- 전역 범위 —
GlobalPolicyConfig는 모든 트랜잭션에 대해 실행 전에 평가되고(RPC 제출 시점과 체인의 실행 전 검사에서 트랜잭션의 발신자,to, value, 4바이트 함수 selector를 기준으로), 실행이 성공한 뒤 트랜잭션이 수행한 호출과 전송을 대상으로 다시 평가됩니다. 대상 컨트랙트와 관계없이 적용됩니다. 실행 후 위반이 발견되면 트랜잭션은 실패하지만 블록에는 포함되며, 상태는 되돌려지고 가스비는 부과됩니다. 전형적인 내용은 체인 전역 denylist, 미인증 사용자에 대한 주기 거래량 상한, KYC 게이트입니다. - 컨트랙트 범위 —
ContractPolicyConfig는 실행 중에, 트랜잭션이 PCL 래핑 프록시를 경유할 때 평가됩니다(pcl-proxy-hook참고). 신뢰된 ERC-4337 진입점도preCall/postCall을 호출할 수 있으며, 정책 인식 프리컴파일인 Privacy 프리컴파일은 자체 경계에서 자신의 컨트랙트 정책을 평가합니다. 프록시의preCall훅이 유효 principal, calldata, value를 담아 PCL을 호출하고, 성공 시 구현체로 전달합니다. 실패 시 전체 트랜잭션이 revert됩니다.
두 범위 모두 빈 selector 항목을 먼저 평가한 뒤 selector 매칭 항목을 평가합니다(
pcl-policy-structure 참고).거절은 타입 지정 revert로 노출됩니다
IPcl의 PCL 오류 selector(예: InDenylist(address sender), ExceededPeriodicVolume(uint256 maxLimit, uint256 value, uint256 resetAt), EasNoAttestationReceived(address sender) 같은 ReasonCode)의 ABI 인코딩입니다. 권한·인코딩 실패는 IPcl이 선언하거나 IPrecompile에서 상속한 다른 오류로 표시됩니다. 두 강제 지점 모두 동일하게 동작합니다. eth_sendRawTransaction의 브로드캐스트 전 검사와 온체인 실행 경계가 같은 revert 형태를 만들어 내므로, 하나의 ABI로 둘 다 디코드할 수 있습니다. @maroo-chain/contracts의 iPclAbi에는 IPcl 오류와 IPrecompile에서 상속한 오류가 모두 들어 있습니다.import { BaseError, decodeErrorResult } from "viem";
import { iPclAbi } from "@maroo-chain/contracts/abi/precompiles/pcl/IPcl";
try {
await walletClient.sendRawTransaction({ serializedTransaction: signedTx });
} catch (err) {
// 노드가 돌려준 revert 데이터는 원인 체인의 RPC 오류에 있으며, viem이 그 오류를 한 번 더 감쌀 수 있습니다.
const rpcError = err instanceof BaseError ? err.walk((e) => typeof (e as { data?: unknown }).data === "string") : null;
const data = (rpcError as { data?: `0x${string}` } | null)?.data;
if (data) {
// iPclAbi 하나에 모든 IPcl ReasonCode와 IPrecompile에서 상속한 오류가 들어 있습니다.
const decoded = decodeErrorResult({ abi: iPclAbi, data });
console.error(`PCL ReasonCode: ${decoded.errorName}`, decoded.args);
} else {
throw err;
}
} RPC 제출 시점의 사전 거절
to, value, 4바이트 함수 selector)에 대해 전역 정책의 실행 전 검사를 수행합니다. 평가가 트랜잭션을 거절하면 eth_sendRawTransaction은 온체인에서 발생했을 것과 동일한 타입 지정 revert 데이터를 반환합니다. 리졸버가 내부의 PCL 오류를 EVM 스타일의 타입 지정 revert 페이로드로 감싸므로, sendRawTransaction 주변의 클라이언트 try/catch가 이유를 다른 revert와 동일한 방식으로 디코드합니다. 두 가지 실용적 의미가 있습니다.- 하나의 디코더가 제출 시점 정책 거절과 실행 시점 정책 거절을 모두 처리하지만, 전달되는 방식은 다릅니다. 제출 시점 거절은
eth_sendRawTransaction이 오류로 반환하고, 실행 시점 거절(프록시 훅 또는 실행 후)은 실패한 트랜잭션으로 블록에 포함되어 가스가 청구되므로 클라이언트는 영수증 상태에서 이를 확인합니다. - 거절이 브로드캐스트 전에 발생하므로 가스가 청구되지 않고, 어떤 블록에도 포함되지 않습니다. 익스플로러에도 나타나지 않습니다.
평가 순서
1. 전역 설정 — 빈 selector 항목이 먼저 평가됩니다(체인 전역 denylist가 여기서 단축 평가로 종료시킬 수 있습니다).
2. 전역 설정 — 트랜잭션의 4바이트 함수 selector와
selector가 일치하는 항목이 있으면 평가됩니다.3. 컨트랙트 설정 — 대상 프록시의 빈 selector 항목입니다.
4. 컨트랙트 설정 — 대상 프록시의 selector 매칭 항목입니다.
5. 전역 설정 —
preCall에서 프록시 호출의 principal과 그 호출의 selector로 다시 평가됩니다(빈 selector 항목이 먼저).6. 구현체 호출이 실행됩니다.
7.
postCall 훅이 PCL에 재진입해 호출이 수행한 전송을 컨트랙트 정책과 전역 정책으로 검사하고 기록합니다(예: 주기 거래량 증가).8. 트랜잭션이 성공하면 체인의 트랜잭션 후처리 훅이 트랜잭션의 호출과 전송을 대상으로 전역 설정을 다시 평가합니다.
어느 단계에서든 실패하면 트랜잭션이 중단됩니다. 3단계 이후의 실패는 온체인에서 발생하므로 트랜잭션은 실패 상태로 블록에 포함되고 가스가 청구됩니다. 프록시를 우회해 구현체를 직접 호출하는 경우 1–2단계와 8단계만 거치며, 컨트랙트 범위 정책은 평가되지 않습니다.
일반 문자열 revert가 발생하는 경우
InvalidParameter(bytes input), input은 selector를 텍스트로 담은 값으로 0x 없는 소문자 16진수이며 빈 selector는 __all__), 필요한 admin이 아닌 호출자(Unauthorized()), policy-aware 프리컴파일에 대한 빈 정책 목록(CannotEmpty(string field))도 모두 타입 지정 오류입니다. 예외는 일반 문자열 revert입니다. deployPclProxy는 프록시를 생성하지 못한 경우(initializer가 revert하는 경우 — Transparent·UUPS 프록시는 비어 있는 경우도 — 또는 코드가 없는 logic 주소)를 Solidity Error(string)의 사유 execution reverted로만 알립니다. viem의 decodeErrorResult는 이 형태도 errorName: "Error"로 디코드하고 사유는 args[0]에 담으므로, catch 분기 하나에서 두 형태를 모두 처리할 수 있습니다.정책 평가 비용은 누가 내는가
설계할 때 감안할 결과는 두 가지입니다.
- 가스 한도는 컨트랙트 호출 비용이 아니라
eth_estimateGas에서 가져오십시오. 추정치는 같은 사후 평가를 수행하므로 정책 작업이 이미 포함되어 있습니다. 호출 비용만으로 계산한 한도는 평가 도중 소진될 수 있습니다. - revert된 호출은 여기까지 오지 않습니다. 사후 평가는 실행이 성공한 경우에만 수행되므로, 실패한 호출에 정책 작업 비용이 추가로 부과되지는 않습니다.