정책 인지 프리컴파일 래퍼
모든 변경성 privacy 호출은 프리컴파일 경계에서 PCL 컨트랙트 정책 생명주기(사전 평가 → 실행 → 사후 평가 → 기록)를 거칩니다. 별도의 runOnPcl 진입점을 호출할 필요가 없습니다.
0x100000000000000000000000000000000000000b의 Privacy 프리컴파일은 내부 실행 로직을 EVM에 직접 노출하지 않습니다. 대신 체인이 실행 로직을 정책 인지 프리컴파일 래퍼로 감싸고, 이 래퍼가 네이티브 액션 스냅샷을 소유하며 모든 변경성 호출에 대해 PCL 컨트랙트 정책 생명주기를 완전히 수행합니다. 각 호출은 실효 발신자, 수신자, 자산, 금액을 담은 컨트랙트 범위의 PolicyOperation 하나를 정확히 만들어 냅니다. PCL은 실행 전에 이 operation에 대해 정책을 평가하고, 실행 후에 다시 평가한 다음 operation을 기록합니다. 조회 전용 읽기와 가스 회계도 같은 래퍼를 통과하므로, dApp이 PCL 강제를 선택적으로 켜는 방식이 아니라 이 경로가 privacy 표면에 도달하는 유일한 방법입니다.
아키텍처
flowchart LR
dapp[dApp Contract]:::evm
wrap[PolicyAwarePrecompile<br/>at 0x1000...000b]:::precompile
exec[Privacy Executor<br/>Prepare / ExecutePrepared]:::precompile
pcl[IPcl policies<br/>evaluate + record]:::precompile
outcome[Success or PCL ReasonCode revert]:::evm
dapp -->|IPrivacy call| wrap
wrap -->|1. Prepare| exec
wrap -->|2. EvaluatePolicyBeforeExecution| pcl
wrap -->|3. ExecutePrepared| exec
wrap -->|4. EvaluatePolicyAfterExecution| pcl
wrap -->|5. RecordPolicyAfterExecution| pcl
wrap --> outcome
classDef evm fill:#0096AA,stroke:#0096AA,color:#fff;
classDef precompile fill:#FF8C50,stroke:#FF8C50,color:#fff; 정책 인지 프리컴파일이 네이티브 액션 스냅샷을 소유하며 각 privacy 호출을 PCL 컨트랙트 정책 생명주기 전체로 감쌉니다. dApp은 IPrivacy만 보게 되고, PCL 강제는 체인이 보장합니다.
세 협력자 — Executor, Wrapper, PCLKeeper
래퍼는 세 요소를 조합합니다. Executor는 정책을 인지하지 않는 순수 privacy 실행 로직(deposit, transfer, withdraw, batch 변종)이며,
Prepare와 ExecutePrepared를 노출하지만 의도적으로 PrecompiledContract가 아닙니다. 따라서 래퍼를 거치지 않고는 도달할 수 없습니다. PolicyAwarePrecompile은 privacy 주소에 등록된 유일한 vm.PrecompiledContract이며, 네이티브 스냅샷을 소유하고 생명주기를 주도합니다. PCLKeeper는 EvaluatePolicyBeforeExecution, EvaluatePolicyAfterExecution, RecordPolicyAfterExecution을 제공합니다. 래퍼는 모든 변경성 실행 전후로 이 세 가지를 호출합니다. 키퍼가 없거나 executor가 nil이면 상태를 읽기도 전에 revert됩니다.단일 컨트랙트 범위 PolicyOperation 불변식
executor가 반환하는 각
이 불변식 덕분에 컨트랙트 범위 PCL 정책(예: privacy 주소에 바인딩된
PreparedCall은 정확히 하나의 ContractPolicyOperation()을 노출해야 합니다. 래퍼는 이를 PCL에 넘기기 전에 검증하고, 다음 중 어느 것이라도 어긋나면 호출을 즉시 거절합니다.- Scope가
PolicyScopeContract입니다(전역이 아님). - operation의 컨트랙트 주소가 privacy 프리컴파일 자기 주소와 같습니다.
IsSender플래그가 true입니다(실효 발신자가 정책 호출자여야 함).From과To가 모두 0이 아닌 20바이트 EVM 주소입니다.Asset이 빈 문자열이 아닌 denom입니다.Value가 nil이 아니고 음수가 아닙니다.Selector가 정확히 4바이트입니다.
이 불변식 덕분에 컨트랙트 범위 PCL 정책(예: privacy 주소에 바인딩된
DENYLIST_POLICY)이 메서드별 배선 없이 privacy 호출에 그대로 적용됩니다.각 호출을 감싸는 생명주기
모든 변경성 호출에 대해 래퍼는 다음 순서로 동작합니다.
1.
2.
3.
4.
5.
6.
어느 단계에서든 오류가 나면 네이티브 액션 스냅샷이 되돌려지며 부분 상태는 남지 않습니다.
1.
Prepare(ctx, evm, contract, readonly) — ABI 입력을 파싱하고 검증하여 PreparedCall을 만듭니다.2.
PolicyOperation을 추출해 복제합니다. 위 불변식을 어기면 호출을 거절합니다.3.
EvaluatePolicyBeforeExecution(op) — PCL이 적용 가능한 전역/컨트랙트 정책을 모두 평가합니다. 하나라도 위반되면 타입 지정된 PCL ReasonCode로 revert됩니다.4.
ExecutePrepared(...) — executor가 상태 변경을 수행합니다(commitment 생성, nullifier 소비, 이벤트 발행).5.
EvaluatePolicyAfterExecution(op) — 사후 불변식(예: 주기 거래량 상한)을 갱신된 상태에 대해 다시 확인합니다.6.
RecordPolicyAfterExecution(op) — operation을 PCL 회계 저장소(주기 거래량 카운터 등)에 기록합니다.어느 단계에서든 오류가 나면 네이티브 액션 스냅샷이 되돌려지며 부분 상태는 남지 않습니다.
// dApp은 IPrivacy를 그냥 호출합니다. PCL을 먼저 호출하지 않으며 호출할 수도 없습니다.
// 래퍼는 Solidity에서 보이지 않으며, PCL 강제는 프리컴파일 자체가 보장합니다.
import { IPrivacy, PRIVACY_CONTRACT, PrivacyWithdrawRequest } from "@maroo-chain/contracts/precompiles/privacy/IPrivacy.sol";
contract WithdrawExample {
function withdrawTo(PrivacyWithdrawRequest calldata req) external returns (bool) {
// 어떤 정책이라도 이 호출을 거부하면 실제 withdraw가 실행되기 전에
// PCL ReasonCode(예: InDenylist, ExceededPeriodicVolume)로 revert됩니다.
// 사후 검사에서 걸리면(예: 이 호출을 적용한 뒤 누적 거래량이 상한을 넘음)
// 실행 후에도 PCL ReasonCode로 revert됩니다.
return PRIVACY_CONTRACT.withdraw(req);
}
} PCL 프록시 훅과의 차이
두 경로 모두 컨트랙트 범위 PCL 정책을 강제하지만 대상이 다릅니다. PCL 프록시 훅은 사용자가 배포한 업그레이드 가능 프록시(Transparent / UUPS)를 감싸며, PCL 프리컴파일의
preCall / postCall을 통해 principal을 명시적으로 전달합니다. 반면 이 문서에서 설명하는 정책 인지 프리컴파일 래퍼는 마루 내부용입니다. privacy 프리컴파일 자체를 감싸며 preCall / postCall을 호출자에게 노출하지 않습니다. dApp은 평소처럼 IPrivacy와 상호작용하고, PCL 강제는 호출자가 아니라 체인이 보장합니다. 예를 들어 privacy 주소에 바인딩된 denylist 정책은 별도의 설정 없이 deposit, transfer, withdraw, batch 변종 모두에 자동으로 적용됩니다.호출자가 보게 되는 실패 표면
dApp 관점에서 래퍼는 revert 사유를 통해서만 드러납니다. PCL이 거부한 모든 실패는
클라이언트에서
pcl-reason-codes에 문서화된 것과 동일한 형태로, 타입 지정된 PCL ReasonCode가 revert 데이터에 ABI 인코딩되어 반환됩니다. privacy 호출에서 자주 보는 예는 다음과 같습니다.| ReasonCode | 의미 |
|---|---|
InDenylist(sender) | 실효 발신자 또는 수신자가 컨트랙트 범위 denylist에 있습니다. |
ExceededPeriodicVolume(maxLimit, value, resetAt) | 실행 후 거래량 검사가 상한을 넘었습니다. |
EasAttestationRequired(sender) | EAS 기반 컨트랙트 정책이 요구하는 attestation을 호출자가 갖고 있지 않습니다. |
클라이언트에서
decodeErrorResult(viem) 또는 ethers v6의 동등한 ABI 헬퍼로 디코드해 최종 사용자에게 조치 가능한 메시지를 보여줍니다. PCL이 아닌 실패(잘못된 proof, 이미 소비된 nullifier, 트리 용량 부족 등)는 executor에서 일반 문자열 revert로 나옵니다.