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 aokrw) 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). initializer is the calldata to run against the logic contract after deployment; pass "0x" if no initializer is needed. |
Returns
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 when the underlying proxy deployment or registry write fails on the chain layer. |
Examples
Deploy a Transparent proxy and confirm its admin
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 } from "viem";
import { privateKeyToAccount } from "viem/accounts";
const PCL = "0x1000000000000000000000000000000000000005" as const;
const pclAbi = [
{ type: "function", name: "deployPclProxy", stateMutability: "payable",
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" },
]}] },
] 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";
const initData = encodeAbiParameters(
[{ type: "address" }, { type: "address" }, { type: "bytes" }],
[implAddress, initialOwner, "0x"],
);
const proxyAddress = await wallet.writeContract({
address: PCL,
abi: pclAbi,
functionName: "deployPclProxy",
args: [1 /* Transparent */, 0n, initData],
});
// 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.
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.22;
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.
pcl.changeContractPolicies(ContractPolicyConfig({
_contract: proxy,
admin: endUser,
policies: policies
}));
}
}