IPcl.deployPclProxy

deployPclProxy(
  PclProxyKind kind,
  uint256 value,
  bytes calldata initData
) external returns (address proxy)

요청한 kind의 PCL 래핑 프록시를 체인 바이너리에 내장된 표준 바이트코드로 배포하고, 온체인 프록시 레지스트리에 등록하며, 직접 호출자(msg.sender)를 초기 정책 관리자로 설정합니다. 반환된 proxy 주소가 dApp이 표준 컨트랙트 주소로 공개해야 할 주소입니다. 이 등록된 프록시를 경유하는 호출만 컨트랙트 범위 PCL 강제 경로(preCall / postCall)를 발동시키기 때문입니다. 배포 후 IPcl.pclProxy(proxy)는 프록시의 kind, 현재 admin, 프록시 주소 자체를 담은 레지스트리 엔트리를 반환합니다. changeContractPolicies로 컨트랙트 관리자를 인계하면 이 admin 필드도 함께 갱신됩니다.

파라미터

이름 타입 필수 설명
kind PclProxyKind ✓ Transparent(1), UUPS(2), Beacon(3) 중 하나입니다. Unspecified(0)를 전달하면 거절된 kind 바이트를 담은 InvalidParameter(bytes input)로 revert됩니다. Diamond는 예약되어 있으나 구현되어 있지 않습니다.
value uint256 ✓ 프록시 생성자에 전달할 네이티브 OKRW 금액(atokrw 단위)입니다. 프록시 초기화 함수가 payable로 값을 받지 않는다면 보통 0을 전달합니다.
initData bytes ✓ kind별 ABI 인코딩된 생성자 인수입니다. Transparent는 abi.encode(logic, initialOwner, initializer), UUPS는 abi.encode(logic, initializer), Beacon은 abi.encode(beacon, initializer) 형태이며, beacon은 implementation()이 로직 컨트랙트를 반환하는 beacon 컨트랙트입니다(예: OpenZeppelin의 UpgradeableBeacon). initializer는 로직 컨트랙트의 초기화 함수를 호출하는 calldata입니다(예: viem의 encodeFunctionData로 만든 initialize(...)). 프록시가 배포 중에 이 calldata를 실행하므로 초기화된 상태는 프록시에 저장됩니다. Transparent와 UUPS에서는 initializer를 비워 둘 수 없습니다. 두 프록시는 OpenZeppelin 5.6의 ERC1967Proxy를 기반으로 하며, 이 생성자는 빈 initializer를 받으면 ERC1967ProxyUninitialized()로 revert됩니다. deployPclProxy에서는 이 오류가 일반 사유 execution reverted로만 드러납니다. Beacon 프록시(OpenZeppelin BeaconProxy)는 value가 0이면 빈 initializer도 받습니다.

반환값

타입: address

새로 배포되고 등록된 PCL 래핑 프록시 주소입니다. 사용자 트랜잭션이 이 주소를 대상으로 호출하며, changeContractPolicies로 정책을 이 주소에 바인딩합니다.

에러

코드 이름 설명
InvalidParameter InvalidParameter(bytes input) kind가 Unspecified(0)이거나 체인에 표준 프록시 바이트코드가 없는 값일 때 revert됩니다. input은 거절된 kind 바이트입니다. 이 검사는 initData 디코딩보다 먼저 수행되므로, 지원되지 않는 kind는 디코딩 실패가 아니라 이 오류로 드러납니다.
AbiDecodeFailed AbiDecodeFailed initData가 요청한 kind가 기대하는 레이아웃으로 디코딩되지 않을 때 revert됩니다.
InternalError InternalError 모듈이 PCL 상태를 인코딩하거나 디코딩하지 못할 때(예: 프록시를 기록하는 중) InternalError()로 revert됩니다. 프록시 생성 자체가 실패하는 경우(initializer가 revert하는 경우, Transparent나 UUPS에서 initializer가 비어 있는 경우, 또는 logic이나 beacon 주소에 코드가 없는 경우)에는 이 오류가 아니라 일반 Error(string) 사유 execution reverted로 revert되며, viem의 decodeErrorResult는 이를 errorName: "Error"로 반환합니다.

예제

Transparent 프록시 배포 및 admin 확인

initializer는 encodeFunctionData로 만든 실제 calldata이며, 비워 두면 revert됩니다. writeContract는 트랜잭션 해시를 반환하므로 프록시 주소는 영수증의 PclProxyDeployed 이벤트에서 읽습니다. 레지스트리 엔트리는 프록시 배포와 원자적으로 기록되므로, deployPclProxy가 반환한 직후 entry.admin이 deployer.address와 같습니다. 이후 admin을 인계하려면 changeContractPolicies를 사용하며, 이때 이 레지스트리 필드도 함께 갱신됩니다.

import { createWalletClient, createPublicClient, http, encodeAbiParameters, encodeFunctionData, parseEventLogs } from "viem";
import { privateKeyToAccount } from "viem/accounts";

const PCL = "0x1000000000000000000000000000000000000005" as const;

const pclAbi = [
  { type: "function", name: "deployPclProxy", stateMutability: "nonpayable",
    inputs: [
      { name: "kind",     type: "uint8" },
      { name: "value",    type: "uint256" },
      { name: "initData", type: "bytes" },
    ],
    outputs: [{ name: "proxy", type: "address" }] },
  { type: "function", name: "pclProxy", stateMutability: "view",
    inputs: [{ name: "proxy", type: "address" }],
    outputs: [{ type: "tuple", components: [
      { name: "kind",  type: "uint8" },
      { name: "admin", type: "address" },
      { name: "proxy", type: "address" },
    ]}] },
  { type: "event", name: "PclProxyDeployed",
    inputs: [
      { name: "proxy",    type: "address", indexed: true },
      { name: "deployer", type: "address", indexed: true },
      { name: "kind",     type: "uint8",   indexed: false },
    ] },
] as const;

const deployer = privateKeyToAccount(process.env.DEPLOYER_KEY as `0x${string}`);
const wallet = createWalletClient({ account: deployer, transport: http("https://rpc-testnet.maroo.io") });
const pub    = createPublicClient({ transport: http("https://rpc-testnet.maroo.io") });

// TODO: replace with the real ERC20 implementation address before production.
const implAddress  = "0x8F3ac2B1d9E74c05A6B18FE27Dc4913e5A0F7b62";
// TODO: replace with the real proxy admin (multisig) before production.
const initialOwner = "0x2c7f09B81a6D3FF1e5A0d4c6bC2a8f7E19dc3a4B";
// TODO: replace with your implementation's own initializer before production.
const implAbi = [
  { type: "function", name: "initialize", stateMutability: "nonpayable",
    inputs: [{ name: "owner", type: "address" }], outputs: [] },
] as const;

// The proxy runs this call during deployment. It must not be empty:
// with initializer "0x" the proxy constructor reverts, and so does deployPclProxy.
const initializer = encodeFunctionData({
  abi: implAbi,
  functionName: "initialize",
  args: [deployer.address],
});

const initData = encodeAbiParameters(
  [{ type: "address" }, { type: "address" }, { type: "bytes" }],
  [implAddress, initialOwner, initializer],
);

const hash = await wallet.writeContract({
  address: PCL,
  abi: pclAbi,
  functionName: "deployPclProxy",
  args: [1 /* Transparent */, 0n, initData],
});
const receipt = await pub.waitForTransactionReceipt({ hash });

// writeContract returns the transaction hash; the proxy address is in the PclProxyDeployed event.
const [deployed] = parseEventLogs({ abi: pclAbi, eventName: "PclProxyDeployed", logs: receipt.logs });
if (!deployed) throw new Error(`deployPclProxy did not deploy a proxy (status: ${receipt.status})`);
const proxyAddress = deployed.args.proxy;

// The initial admin is msg.sender at deployment time — the `deployer` EOA here.
const entry = await pub.readContract({
  address: PCL, abi: pclAbi, functionName: "pclProxy", args: [proxyAddress],
});
console.log(`kind=${entry.kind} admin=${entry.admin} proxy=${entry.proxy}`);

팩토리가 admin이 되는 경우 — changeContractPolicies로 인계

초기 admin이 직접 호출자(msg.sender)이므로, 사용자를 대신해 프록시를 배포하는 팩토리 컨트랙트가 admin이 됩니다. 같은 트랜잭션에서 changeContractPolicies로 원래 의도한 admin으로 인계할 수 있으며, 프록시 레지스트리 엔트리의 admin 필드도 자동으로 동기화됩니다. 인계는 저수준 call로 합니다. changeContractPolicies는 반환값이 없고 PCL에는 EVM 바이트코드가 없으므로, 흔히 쓰는 pcl.changeContractPolicies(...) 형태는 PCL에 닿기 전에 빈 데이터로 revert됩니다. deployPclProxy는 값을 반환하므로 일반 문법을 그대로 씁니다.

// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.28;

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

contract TokenFactory {
    IPcl constant pcl = IPcl(0x1000000000000000000000000000000000000005);

    /// @notice Deploy a PCL-wrapped proxy and immediately hand admin to `endUser`.
    /// @dev The factory itself becomes the initial admin because msg.sender is the
    ///      immediate caller of the PCL precompile. `changeContractPolicies` (the
    ///      admin-rotation path) can then transfer authority in the same tx.
    function deployAndHandoff(
        bytes calldata initData,
        address endUser,
        PolicySet[] calldata policies
    ) external returns (address proxy) {
        proxy = pcl.deployPclProxy(PclProxyKind.Transparent, 0, initData);

        // Factory is currently the admin -> rotate it to `endUser` while
        // installing the initial policy set. Low-level call: PCL has no EVM
        // bytecode, and Solidity checks the target's code size before a
        // high-level call to a function that returns nothing, so
        // `pcl.changeContractPolicies(...)` would revert with empty data.
        (bool ok, bytes memory ret) = address(pcl).call(abi.encodeCall(
            IPcl.changeContractPolicies,
            (ContractPolicyConfig({
                _contract: proxy,
                admin:     endUser,
                policies:  policies
            }))
        ));
        if (!ok) {
            assembly { revert(add(ret, 32), mload(ret)) } // bubble PCL's own error
        }
    }
}
ESC
검색어를 입력하세요