PCL 컨트랙트 Admin 바인딩

mechanism compliance

모든 컨트랙트 범위 정책 집합에는 온체인 admin이 존재합니다. 이 바인딩은 프록시에 대한 첫 ContractPolicyConfig가 기록될 때 한 번 생성되며, 이후에는 이양만 가능하고 재초기화되지 않습니다.

PCL은 컨트랙트 범위 정책을 PCL에 등록된 프록시(IPcl.deployPclProxy로 배포된 컨트랙트)와 바이너리가 지원하는 정책 인식 프리컴파일에 대해서만 강제합니다. 이러한 대상은 각각 정확히 하나의 컨트랙트 정책 admin을 가지며, 이 주소만 changeContractPolicies와 removeContractPolicies를 호출할 수 있습니다. admin은 이후 호출이 아니라 대상이 만들어질 때 정해집니다. deployPclProxy는 같은 호출 안에서 호출자(배포자 또는 팩토리)를 프록시의 admin으로 기록하고, 정책 인식 프리컴파일의 admin은 체인이 설정합니다. 그 뒤의 모든 changeContractPolicies / removeContractPolicies 호출은 저장된 admin이 보내야 하며, 다른 발신자는 Unauthorized로 거절됩니다. 권한을 회전하려면 현재 admin이 페이로드의 admin 값을 다른 주소로 바꾸어 changeContractPolicies를 다시 호출합니다. 정책 교체와 admin 이양이 같은 호출 안에서 원자적으로 이루어집니다.

아키텍처

flowchart LR
  D["dApp / factory"] -->|"deployPclProxy(kind, 0, initData)"| P{"PCL precompile"}
  P -->|"deploy proxy bytecode"| X["PCL-wrapped proxy"]
  P -->|"write admin=msg.sender"| R[("Contract-admin record")]
  P -->|"sync projection"| E[("pclProxy registry entry<br/>kind + admin + proxy")]
  A["Current admin"] -->|"changeContractPolicies(newAdmin, ...)"| P
  P -->|"update admin"| R
  P -->|"sync projection"| E
  classDef evm fill:#0096AA,stroke:#0096AA,color:#fff;
  classDef precompile fill:#FF8C50,stroke:#FF8C50,color:#fff;
  class D,A,X,R,E evm;
  class P precompile;

컨트랙트 관리자 레코드가 기본 진실 원본이며, 프록시 레지스트리 엔트리는 PCL 래핑 프록시에 대해 admin을 반영하고 admin이 변경될 때마다 갱신됩니다.

PCL 등록 프록시만 ContractPolicyConfig를 가질 수 있습니다

changeContractPolicies와 removeContractPolicies는 정책 상태를 건드리기 전에 대상의 컨트랙트 정책 바인딩부터 조회합니다. 기본 구현체 주소, EOA, 에이전트 지갑, 다른 방식으로 배포된 컨트랙트에는 바인딩이 없으므로, 이런 대상으로 호출하면 타입 지정 오류 ContractPolicyNotRegistered(address contractAddress)로 revert됩니다. 먼저 IPcl.deployPclProxy로 컨트랙트를 프록시 뒤에 배포해야 합니다. dApp이 사용자에게 공개하는 주소이자 이후 모든 정책 관리 호출이 겨냥해야 하는 주소는 반환된 프록시 주소입니다.

초기화는 단일 할당입니다

admin 바인딩은 deployPclProxy가 직접 만듭니다. 프록시를 배포하는 바로 그 호출에서 PCL이 호출자(msg.sender, 즉 사용자의 지갑이나 프록시를 배포한 팩토리 컨트랙트)를 프록시의 컨트랙트 admin으로 기록합니다. admin 슬롯이 비어 있어 최초 호출자가 차지할 수 있는 순간은 없습니다. 새 프록시에 대한 첫 changeContractPolicies도 이미 그 admin이 보내야 하며, 다른 발신자는 Unauthorized로 revert됩니다. 팩토리 컨트랙트가 프록시를 배포했다면 admin은 팩토리이므로, 첫 changeContractPolicies는 팩토리가 직접 호출하며 보통 그 호출에서 admin을 소유자에게 넘깁니다. 이미 바인딩된 프록시를 다시 초기화하는 것은 같은 admin으로도 허용되지 않습니다. 저수준 InitializeContractPolicyAdmin 경로는 admin이 이미 저장되어 있으면 PolicyAlreadyRegistered(contractAddress)로 revert됩니다. 이 인수인계 방지 보장은 의도된 것으로, 컨트랙트가 파괴된 뒤 다른 배포자가 같은 CREATE2 주소에 재배포하더라도 현재 admin이 명시적으로 권한을 이양하지 않는 한 새 배포자는 접근할 수 없습니다.

changeContractPolicies를 통한 이양

별도의 transferAdmin 메서드는 없습니다. admin을 회전하려면 현재 admin이 자기 키로, 페이로드의 admin 필드를 새 주소로 지정하여 changeContractPolicies를 호출합니다. 이 호출은 PolicySet[] 전체 교체와 새 admin 기록을 하나의 상태 전이로 처리하므로, 어느 시점에도 프록시에 admin이 둘 존재하지 않습니다. 컨트랙트에서 호출할 때는 아래처럼 저수준 call을 씁니다. PCL에는 EVM 바이트코드가 없고, Solidity는 반환값이 없는 함수를 일반 문법으로 호출하기 전에 대상의 코드 크기를 확인합니다. 그래서 흔히 쓰는 PCL.changeContractPolicies(...) 형태는 PCL에 닿기 전에 빈 데이터로 revert됩니다.
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.28;

import {
    IPcl,
    ContractPolicyConfig,
    PolicySet
} from "@maroo-chain/contracts/precompiles/pcl/IPcl.sol";

contract PolicyOwner {
    address constant PCL = 0x1000000000000000000000000000000000000005;
    address public immutable owner;

    constructor() {
        owner = msg.sender;
    }

    /// @notice `proxy`의 정책 권한을 기존 정책을 유지한 채 `newAdmin`으로 이양합니다.
    ///         이 컨트랙트의 owner만 호출할 수 있으며, 이 컨트랙트가 프록시의 현재
    ///         admin이므로 PCL이 변경을 받아들입니다.
    function handOver(
        address proxy,
        address newAdmin,
        PolicySet[] calldata keepPolicies
    ) external {
        require(msg.sender == owner, "PolicyOwner: not owner");
        // 저수준 call로 호출합니다. PCL에는 EVM 바이트코드가 없고, Solidity는
        // 반환값이 없는 함수를 일반 호출하기 전에 대상의 코드 크기를 확인하므로
        // `PCL.changeContractPolicies(...)`는 프리컴파일에 닿기 전에 빈 데이터로
        // revert됩니다.
        (bool ok, bytes memory ret) = PCL.call(abi.encodeCall(
            IPcl.changeContractPolicies,
            (ContractPolicyConfig({
                _contract: proxy,
                admin:     newAdmin,
                policies:  keepPolicies
            }))
        ));
        if (!ok) {
            assembly { revert(add(ret, 32), mload(ret)) } // bubble PCL's own error
        }
    }
}

removeContractPolicies는 정책만 비우고 권한은 유지합니다

현재 admin이 removeContractPolicies(proxy)를 호출하면 proxy에 바인딩된 모든 PolicySet이 제거되지만 admin 바인딩은 유지됩니다. 이후 같은 admin이 changeContractPolicies를 호출하면 별도의 재등록 절차 없이 정책을 다시 채울 수 있습니다. 같은 전제 조건이 여기에도 적용됩니다. 대상에 컨트랙트 정책 바인딩이 없으면 removeContractPolicies는 ContractPolicyNotRegistered(address)로, 호출자가 현재 admin이 아니면 Unauthorized로 revert됩니다.

현재 admin 조회

contractPolicies(address)는 순수 view입니다. 대상 주소의 현재 ContractPolicyConfig(admin + 정책)를 반환하며, admin이 없는 주소, 즉 deployPclProxy로 배포되지 않았고 정책 인식 프리컴파일도 아닌 주소에 대해서는 revert 대신 admin이 0이고 policies가 빈 배열인 struct를 반환하며, 이런 주소에 대한 정책 호출은 ContractPolicyNotRegistered로 revert됩니다. 정책 변경을 보내기 전에 어떤 계정이 프록시를 관리하는지 확인할 때 사용합니다.
import { createPublicClient, http } from "viem";
import { iPclAbi as pclAbi } from "@maroo-chain/contracts/abi/precompiles/pcl/IPcl";

const PCL = "0x1000000000000000000000000000000000000005" as const;
// TODO: 실제 프록시 주소로 교체하십시오.
const proxyAddress = "0x8F3ac2B1d9E74c05A6B18FE27Dc4913e5A0F7b62";

const publicClient = createPublicClient({ transport: http("https://rpc-testnet.maroo.io") });

const { admin, policies } = await publicClient.readContract({
  address: PCL,
  abi: pclAbi,
  functionName: "contractPolicies",
  args: [proxyAddress],
});

if (admin === "0x0000000000000000000000000000000000000000") {
  console.log("정책 admin이 없습니다. PCL 프록시나 정책 인식 프리컴파일이 아니므로 changeContractPolicies는 ContractPolicyNotRegistered로 revert됩니다.");
} else {
  console.log("현재 admin:", admin, "정책 개수:", policies.length);
}
ESC
검색어를 입력하세요