IPcl.deployPclProxy

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

Deploys a PCL-wrapped proxy of the requested kind using the canonical bytecode embedded in the chain binary, registers it in the on-chain proxy registry, and sets the immediate EVM caller (msg.sender) as its initial policy admin. The returned proxy address is the address dApps should publish as the canonical contract address, because only calls that go through this registered proxy trigger the contract-scoped PCL enforcement path (preCall / postCall). After deployment, IPcl.pclProxy(proxy) returns the registry entry with the proxy's kind, its current admin, and the proxy address itself; that admin field stays in sync when changeContractPolicies rotates the contract admin.

Parameters

Name Type Required Description
kind PclProxyKind ✓ One of Transparent (1), UUPS (2), or Beacon (3). Unspecified (0) reverts with InvalidParameter(bytes input) carrying the rejected kind byte. Diamond is reserved but not implemented.
value uint256 ✓ Native OKRW value (in atokrw) to forward to the proxy constructor. Typically 0 unless the proxy's initializer takes payable ETH-style value.
initData bytes ✓ Kind-specific ABI-encoded constructor arguments. For Transparent: abi.encode(logic, initialOwner, initializer). For UUPS: abi.encode(logic, initializer). For Beacon: abi.encode(beacon, initializer), where beacon is a beacon contract (for example OpenZeppelin's UpgradeableBeacon) whose implementation() is the logic contract. initializer is the calldata for the logic contract's initializer function, for example initialize(...) built with viem's encodeFunctionData; the proxy runs it during deployment, so the initialized state lives in the proxy. For Transparent and UUPS it must not be empty: both are built on OpenZeppelin 5.6's ERC1967Proxy, whose constructor reverts with ERC1967ProxyUninitialized() on an empty initializer, and deployPclProxy reports that only with the generic reason execution reverted. A Beacon proxy (OpenZeppelin BeaconProxy) accepts an empty initializer when value is 0.

Returns

Type: address

Address of the newly deployed and registered PCL-wrapped proxy. This is the address that users' transactions should target and that policies are bound to via changeContractPolicies.

Errors

Code Name Description
InvalidParameter InvalidParameter(bytes input) Reverts when kind is Unspecified (0) or any value the chain has no canonical proxy bytecode for. input is the rejected kind byte. This check runs before initData is decoded, so an unsupported kind surfaces here rather than as a decode failure.
AbiDecodeFailed AbiDecodeFailed Reverts when initData does not decode against the layout expected for the requested kind.
InternalError InternalError Reverts with InternalError() when the module fails to encode or decode PCL state, for example while recording the proxy. A proxy whose construction fails — a reverting initializer, an empty one for Transparent or UUPS, or a logic (or beacon) address without code — does not produce it: deployPclProxy then reverts with the plain Error(string) reason execution reverted, which viem's decodeErrorResult returns as errorName: "Error".

Examples

Deploy a Transparent proxy and confirm its admin

initializer is real calldata built with encodeFunctionData; an empty one reverts. writeContract returns the transaction hash, so the proxy address comes from the PclProxyDeployed event in the receipt. The registry entry is written atomically with the proxy deployment: entry.admin equals deployer.address immediately after deployPclProxy returns. Rotating that admin later goes through changeContractPolicies (which also updates this registry field).

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}`);

Factories become the admin — hand over via changeContractPolicies

Because the initial admin is msg.sender (the immediate EVM caller), a factory contract that deploys a proxy on a user's behalf becomes the admin. Rotate to the intended admin in the same transaction with changeContractPolicies; the proxy registry entry's admin field is synced automatically. The rotation is a low-level call: changeContractPolicies returns nothing and PCL has no EVM bytecode, so the usual pcl.changeContractPolicies(...) syntax reverts with empty data before PCL sees it. deployPclProxy returns a value, so it keeps the normal syntax.

// 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
Type to search