PCL 프록시 훅
PCL 래핑 프록시와 신뢰된 진입점이 preCall / postCall을 호출하여 사용자 트랜잭션 주변에서 ContractPolicyConfig를 강제하는 방식을 설명합니다.
컨트랙트 범위 PCL 정책은 PCL 프리컴파일의 훅 두 개를 통해 강제됩니다. preCall은 래핑된 구현 실행 전에, postCall은 실행 후에 호출됩니다. 이 훅은 개방되어 있지 않으며, 프리컴파일이 직접 호출자(msg.sender)의 종류를 작은 허용 목록과 대조하여 그 외 모든 경우를 Unauthorized로 거절합니다. 허용되는 종류는 두 가지입니다. 하나는 deployPclProxy로 등록된 PCL 래핑 프록시이며 대상 컨트랙트 주소와 같아야 합니다. 다른 하나는 PclParams.entrypoints에 등록된 신뢰된 진입점이며, 하나의 진입점이 여러 대상으로 사용자 오퍼레이션을 라우팅하므로 그 제약을 두지 않습니다.
아키텍처
flowchart LR
EOA["User EOA"]:::evm
AA["AA Smart Account"]:::evm
EP["Trusted Entrypoint<br/>(in PclParams.entrypoints)"]:::evm
PX["PCL-wrapped Proxy<br/>(registered)"]:::evm
IMPL["Target Implementation"]:::evm
PCL{{"PCL Precompile<br/>preCall / postCall"}}:::precompile
EOA -->|"call proxy"| PX
AA -->|"user op"| EP
PX -->|"preCall/postCall<br/>target == self"| PCL
EP -->|"preCall/postCall<br/>target = any"| PCL
PCL -->|"admit → delegate"| IMPL
PX -->|"delegatecall"| IMPL
classDef evm fill:#0096AA,stroke:#0096AA,color:#fff;
classDef precompile fill:#FF8C50,stroke:#FF8C50,color:#fff; PCL의 preCall/postCall을 호출할 수 있는 종류는 두 가지뿐입니다. 등록된 PCL 래핑 프록시(자기 자신을 대상으로 해야 함) 또는 신뢰된 진입점(모든 컨트랙트를 대상으로 가능)입니다. 평범한 EOA와 EIP-7702 위임 EOA는 거절됩니다.
PCL이 분류하는 네 가지 호출자 종류
preCall 또는 postCall이 호출되면 PCL은 먼저 직접 호출자의 온체인 코드를 검사하여 종류를 분류합니다.| 종류 | 판별 방법 | 허용 여부 |
|---|---|---|
| 평범한 EOA | 코드 해시가 비어 있음 | ❌ 거절 — Unauthorized |
| EIP-7702 위임 EOA | 코드가 7702 위임 지시자로 파싱됨 | ❌ 거절 — Unauthorized |
| 등록된 PCL 래핑 프록시 | 체인 측 프록시 레지스트리에 존재 | ✅ targetContract == msg.sender인 경우만 |
| 신뢰된 진입점 | PclParams.entrypoints에 포함된 주소 | ✅ 모든 targetContract |
그 외(등록된 프록시도 아니고 신뢰된 진입점도 아닌 임의의 컨트랙트) 역시 거절됩니다. dApp이 자체 컨트랙트에서
preCall / postCall을 직접 호출하여 강제에 "참여"할 수 없는 이유가 이것입니다. 지원되는 진입 경로는 PCL 래핑 프록시 또는 정책 관리자가 신뢰한 ERC-4337 형태의 진입점 두 가지뿐입니다.허용되는 두 경로의 제약이 다른 이유
PCL 래핑 프록시는
반면 진입점은 사용자 오퍼레이션을 여러 대상 컨트랙트로 라우팅합니다.
deployPclProxy로 배포되며 단일 구현체와 1:1로 대응합니다. 사용자가 프록시를 호출하면 래퍼 바이트코드가 preCall(proxyAddress, principal, data, value)를 호출하고 이후 postCall(sessionId, proxyAddress, principal, data, value, workable)을 호출합니다. 프록시가 정확히 하나의 구현체를 대상으로 자기 자신에게 위임하므로, PCL은 targetContract == msg.sender를 요구합니다. 다른 대상 주소를 주장하는 프록시 호출은 형식이 잘못된 것이므로 Unauthorized로 거절됩니다.반면 진입점은 사용자 오퍼레이션을 여러 대상 컨트랙트로 라우팅합니다.
targetContract == msg.sender 제약을 두면 진입점 경로를 사용할 수 없게 됩니다. 대신 정책 관리자가 PclParams.entrypoints에 진입점 주소를 추가하여 입장을 허용하고, 진입점 자체가 훅에 올바른 targetContract와 principal(진입점이 아니라 기저 사용자)을 전달하도록 신뢰됩니다.정책 호출자는 msg.sender가 아니라 principal
두 훅은 명시적인
이 파라미터가 없으면 PCL은
principal 인자를 받습니다. PCL은 preCall / postCall의 직접 호출자가 아니라 principal에 대해 정책을 평가합니다. 이는 매우 중요합니다.- 프록시 호출의 경우
principal은 프록시를 호출한 EOA(또는 AA 스마트 계정)입니다. - 진입점 호출의 경우
principal은 진입점이 대신 오퍼레이션을 실행하는 기저 사용자입니다.
이 파라미터가 없으면 PCL은
msg.sender에서 프록시 또는 진입점 주소만 볼 수 있고, 발신자 기반 검사(denylist, EAS attestation 조회, 에이전트 전송 한도 메타데이터, 주기 거래량 카운터)가 모두 잘못된 계정으로 해석됩니다.// 프록시 훅 래퍼(개념적 예시 — 실제 바이트코드는 체인에 내장됨).
// 여기서 `msg.sender`는 사용자(EOA 또는 AA 계정)이며,
// `principal`로 PCL에 명시적으로 전달됩니다. PCL은 이 값에 대해 정책을 평가합니다.
address constant PCL = 0x1000000000000000000000000000000000000005;
function _pclWrappedCall(bytes calldata data) external payable {
bytes32 sessionId = IPcl(PCL).preCall(
address(this), // 프록시의 경우 targetContract == msg.sender
msg.sender, // principal — 사용자를 명시적으로 전달
data,
msg.value
);
(bool ok, bytes memory ret) = _implementation().delegatecall(data);
IPcl(PCL).postCall(
sessionId,
address(this),
msg.sender,
data,
msg.value,
ok // false이면 postCall은 정책 평가를 건너뜁니다
);
if (!ok) { assembly { revert(add(ret, 32), mload(ret)) } }
} EIP-7702 위임 EOA가 거절되는 이유
EIP-7702는 EOA가 23바이트 위임 지시자를 계정 코드로 설치하여 일반 호출이 지정된 구현체에 대해 실행되도록 합니다. 원시 코드 해시만 보면 이 계정은 컨트랙트처럼 보이지만, 주소는 여전히 EOA의 것이므로 PCL 등록 프록시 또는 신뢰된 진입점이 될 수 없습니다. 이 경우를 구분하기 위해 PCL은 계정의 원시 코드를 읽어 7702 위임 지시자로 파싱되는지 확인합니다. 파싱되면 7702 EOA로 분류하여
dApp 빌더를 위한 실무 규칙은 다음과 같습니다. EOA나 7702 위임 계정에서
Unauthorized로 거절합니다.dApp 빌더를 위한 실무 규칙은 다음과 같습니다. EOA나 7702 위임 계정에서
preCall / postCall을 절대 직접 호출하지 마십시오. 규제 대상 호출은 해당 컨트랙트에 공개된 PCL 래핑 프록시 주소 또는 계정 추상화를 연동한다면 신뢰된 진입점 주소를 통해 라우팅하십시오.신뢰된 진입점 등록
신뢰된 진입점 목록은
PclParams의 일부이며 체인 전역 정책 관리자(pcl-policy-admin 참고)가 관리합니다. dApp이 스스로 추가할 수 없습니다. 특정 진입점이 현재 신뢰되어 있는지 확인하려면 PCL 프리컴파일의 getParams()를 읽고 entrypoints에 해당 주소가 있는지 확인합니다.import { createPublicClient, http } from "viem";
const PCL = "0x1000000000000000000000000000000000000005" as const;
const pclParamsAbi = [{
name: "getParams", type: "function", stateMutability: "view",
inputs: [],
outputs: [{
type: "tuple", components: [
{ name: "policyAdmin", type: "address" },
{ name: "entrypoints", type: "address[]" },
],
}],
}] as const;
const publicClient = createPublicClient({ transport: http("https://rpc-testnet.maroo.io") });
const params = await publicClient.readContract({
address: PCL,
abi: pclParamsAbi,
functionName: "getParams",
});
// TODO: AA 스택이 사용하는 진입점 주소로 교체하십시오.
const candidate = "0x4337084d9e255ff0702461cf8895ce9e3b5ff108";
const isTrusted = params.entrypoints
.map((a) => a.toLowerCase())
.includes(candidate.toLowerCase());
console.log("진입점 신뢰 여부:", isTrusted);