PCL 프록시 훅

mechanism compliance

마루가 업그레이드 가능한 컨트랙트를 감싸 모든 호출이 PCL을 거치도록 만드는 방식을 다룹니다. 체인이 배포한 프록시가 구현체 delegatecall 주위에 preCall/postCall 훅을 삽입하며, 사용자가 호출할 수 있는 우회 경로는 없습니다.

PCL로 래핑된 프록시는 IPcl.deployPclProxy(...)를 통해서만 배포할 수 있는 TransparentUpgradeableProxy 변형입니다. 이 프록시의 주소가 PCL 정책 조회 키가 되며, 프록시에 등록된 ContractPolicyConfig는 구현체 업그레이드와 무관하게 동일하게 적용됩니다. 관리자가 아닌 모든 호출에서 프록시의 fallback은 preCall로 PCL 세션을 열고, 원본 calldata로 현재 구현체를 delegatecall한 뒤 postCall로 세션을 닫습니다. PCL 거절은 revert로 그대로 노출됩니다. 관리자 권한 경로는 upgradeToAndCall 하나뿐이며, 프록시 관리자의 다른 호출은 ProxyDeniedAdminAccess로 revert됩니다.

아키텍처

flowchart TD
    A[Caller EOA / AA] -->|call| P[PclTransparentUpgradeableProxy]
    P -->|msg.sender == admin<br/>and sig == upgradeToAndCall| U[OZ upgrade dispatch<br/>PCL bypassed]
    P -->|every other call| H[hookedDelegate]
    H -->|preCall proxy, principal, data, value| PCL[IPcl precompile<br/>0x1000...0005]
    H -->|delegatecall| I[Implementation]
    H -->|postCall proxy, principal, data, value, ok| PCL
    U -->|delegatecall| I

    classDef evm fill:#0096AA,stroke:#0096AA,color:#fff;
    classDef precompile fill:#FF8C50,stroke:#FF8C50,color:#fff;
    class A,P,U,H,I evm;
    class PCL precompile;

PclTransparentUpgradeableProxy 내부의 fallback 분기 흐름입니다. PCL을 우회하는 경로는 관리자의 `upgradeToAndCall` 하나뿐이며, 그 외 모든 호출은 발신자와 무관하게 프록시 주소를 대상으로 preCall과 postCall을 실행합니다. 사용자가 호출할 수 있는 우회 경로는 없습니다.

Fallback 라우팅

프록시의 _fallback은 다음 두 경우를 순서대로 처리합니다.

1. Admin 경로msg.sender == _proxyAdmin()이면 upgradeToAndCall만 디스패치합니다(표준 OZ 업그레이드 흐름이며 PCL은 우회합니다). 다른 관리자 호출은 ProxyDeniedAdminAccess로 revert됩니다.
2. User 경로 — 나머지 모든 호출은 PclProxyHook.hookedDelegate로 이동하며, 구현체 delegatecall이 preCall / postCall로 감싸집니다.

허가 없이 사용할 수 있는 우회 경로는 존재하지 않습니다. 이전 버전에는 컨트랙트 범위 PCL 검사를 건너뛰고 구현체를 delegatecall할 수 있는 nonPolicyExec(bytes) 우회 진입점이 있었으나 제거되었습니다. 이제 사용자 측 호출은 모두 동일한 정책 강제 경로를 통과합니다.

Selector 예약

디스패치는 upgradeToAndCall selector를 관리자용으로 예약하며, 표준 transparent proxy와 동일한 주의사항을 따릅니다. 이 프록시 뒤에 배치되는 구현체는 해당 selector와 충돌하는 함수를 노출해서는 안 됩니다.
function _fallback() internal virtual override {
    if (msg.sender == _proxyAdmin()) {
        if (msg.sig != ITransparentUpgradeableProxy.upgradeToAndCall.selector) {
            revert ProxyDeniedAdminAccess();
        }
        _dispatch(); // OZ 업그레이드 경로
        return;
    }

    // 관리자가 아닌 모든 호출(빈 calldata의 순수 ETH 전송 포함)은
    // PCL 강제 훅을 통과합니다.
    PclProxyHook.hookedDelegate(address(this), ERC1967Utils.getImplementation());
}

단일 훅 — hookedDelegate

라이브러리 PclProxyHook은 진입점을 정확히 하나 제공합니다. hookedDelegateprincipal(실제 EOA / AA 호출자)을 확보하고 — PCL이 보는 직접 호출자(msg.sender)는 프록시 자신이므로 sender / principal denylist 검사가 발동하려면 principal을 명시적으로 전달해야 합니다 —, preCall로 세션을 연 뒤 msg.data로 구현체를 delegatecall하고, postCall로 세션을 닫습니다. delegatecall의 ok 플래그는 workable로 그대로 전달되며, 구현체가 revert되면 PCL은 사후 처리를 건너뛰고 프록시는 원본 revert 데이터를 그대로 전파합니다.
// PclProxyHook.sol 발췌
function hookedDelegate(address addrForPolicy, address impl) internal {
    address principal = msg.sender;
    bytes32 sessionId = PCL_CONTRACT.preCall(
        addrForPolicy, principal, msg.data, msg.value
    );

    (bool ok, bytes memory ret) = impl.delegatecall(msg.data);

    PCL_CONTRACT.postCall(
        sessionId, addrForPolicy, principal, msg.data, msg.value, ok
    );

    if (!ok) { assembly { revert(add(ret, 0x20), mload(ret)) } }
    assembly { return(add(ret, 0x20), mload(ret)) }
}

건너뛰기 플래그 없음

짝을 이루는 IPcl.preCallIPcl.postCall 시그니처에는 더 이상 checkContractPolicy 플래그가 없습니다. 프록시 주소에 ContractPolicyConfig가 등록되어 있으면 컨트랙트 범위 정책 평가가 항상 실행되며, 이를 건너뛰는 프록시 내부 토글은 존재하지 않습니다.

프록시 주소가 정책 키인 이유

컨트랙트 범위 정책은 구현체가 아닌 프록시 주소를 키로 사용합니다. 따라서 구현체를 업그레이드해도 정책 그래프가 초기화되거나 어긋나지 않습니다. v1을 다루던 동일한 DENYLIST_POLICY, EAS_POLICY, 주기 거래량 정책이 프록시가 v2 로직을 가리키는 즉시 그대로 v2에도 적용됩니다. 각 ContractPolicyConfigadmin 필드는 changeContractPolicies / removeContractPolicies를 호출할 수 있는 유일한 키이며, 프록시의 업그레이드 관리자와는 별개입니다. 정책 관리와 코드 업그레이드는 서로 독립된 두 권한입니다.

배포 — 프리컴파일 전용

이 프록시는 사용자 코드에서 직접 배포할 수 없습니다. 정식 바이트코드는 체인에 내장되어 있으며, IPcl.deployPclProxy(kind, value, initData)가 이를 인스턴스화하는 동시에 결과 주소를 체인 측 프록시 레지스트리에 등록합니다(IPcl.pclProxy(address)로 조회). 다른 경로로 배포한 컨트랙트는 PCL이 허용된 진입점으로 인식하지 않으며, 해당 컨트랙트의 preCall / postCall 호출은 Unauthorized로 revert됩니다.
import { encodeAbiParameters } from "viem";

const PCL = "0x1000000000000000000000000000000000000005";

// Transparent proxy: initData = abi.encode(logic, initialOwner, initializer)
const initData = encodeAbiParameters(
  [
    { type: "address", name: "logic" },
    { type: "address", name: "initialOwner" },
    { type: "bytes",   name: "initializer" },
  ],
  [logicAddress, ownerAddress, "0x"],
);

const { result: proxyAddress } = await publicClient.simulateContract({
  address: PCL,
  abi: pclAbi,
  functionName: "deployPclProxy",
  args: [1 /* PclProxyKind.Transparent */, 0n, initData],
  account: deployer,
});
소스: maroo
ESC
검색어를 입력하세요