PCL Contract Admin Binding

mechanism compliance

Every contract-scoped policy set has an on-chain admin. The binding is created once when the first ContractPolicyConfig for a proxy is written, and can only be handed over — never re-initialized.

PCL enforces contract-scoped policies only against PCL-registered proxies — contracts deployed through IPcl.deployPclProxy. Each such proxy has exactly one contract policy admin: the address authorized to call changeContractPolicies and removeContractPolicies against it. The binding is single-assignment. The first successful changeContractPolicies call for a given proxy stores policy.admin as that proxy's admin; from that point on, every subsequent call must be signed by the stored admin. To rotate authority, the current admin calls changeContractPolicies again with a different admin in the payload — the policy replacement and the admin handover happen atomically in the same call.

Architecture

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;

The contract-admin record is the primary source of truth; the proxy registry entry mirrors the admin for PCL-wrapped proxies and is refreshed on every admin change.

Only PCL-registered proxies can carry a ContractPolicyConfig

changeContractPolicies and removeContractPolicies both check that the target address is a PCL-registered proxy before touching admin or policy state. A call against a raw implementation address, an EOA, or a contract deployed through some other mechanism reverts with the typed error PclProxyNotRegistered(address contractAddress). Deploy the contract behind IPcl.deployPclProxy first — the returned proxy address is what dApps publish to users, and it is the address every subsequent policy management call must target.

Initialization is single-assignment

The admin binding is created on the first successful changeContractPolicies for a given proxy. On that first call the admin slot is empty, so any caller may set it — policy.admin becomes the future gatekeeper. Every subsequent management call must be signed by the stored admin, otherwise it reverts with Unauthorized. Re-initializing an already-bound proxy — even with the same admin — is not allowed: the low-level InitializeContractPolicyAdmin path reverts with PolicyAlreadyRegistered(contractAddress) when an admin is already stored. The anti-takeover guarantee is intentional: if a contract is destroyed and redeployed at the same CREATE2 address by a different deployer, the new deployer is locked out unless the current admin explicitly hands over authority.

Handover through changeContractPolicies

There is no separate transferAdmin method. To rotate the admin, the current admin calls changeContractPolicies from its own key with a payload whose admin field is the new address. The call replaces the entire PolicySet[] and writes the new admin in a single state transition, so at no point does the proxy have two admins.
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.22;

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

contract PolicyOwner {
    IPcl constant PCL = IPcl(0x1000000000000000000000000000000000000005);

    /// @notice Hand policy authority for `proxy` over to `newAdmin` while
    ///         keeping the existing policies. Callable only by this contract
    ///         (the current admin).
    function handOver(
        address proxy,
        address newAdmin,
        PolicySet[] calldata keepPolicies
    ) external {
        PCL.changeContractPolicies(ContractPolicyConfig({
            _contract: proxy,
            admin:     newAdmin,
            policies:  keepPolicies
        }));
    }
}

removeContractPolicies clears policies, not authority

Calling removeContractPolicies(proxy) from the current admin drops every PolicySet bound to proxy, but the admin binding survives. A follow-up changeContractPolicies from the same admin can re-populate policies without any separate re-registration step. The same PCL-registration precondition applies: removeContractPolicies also reverts with PclProxyNotRegistered(address) if the target is not a registered PCL proxy, and with Unauthorized if the caller is not the current admin.

Reading the current admin

contractPolicies(address) is a pure view: it returns the current ContractPolicyConfig (admin + policies) for the target address. For an address that has never been bound, it returns an empty struct — a zero admin and an empty policies array — rather than reverting. Use this to check whether a proxy is already under admin control before attempting to bind policies.
import { createPublicClient, http } from "viem";

const PCL = "0x1000000000000000000000000000000000000005" as const;
// TODO: replace with the real proxy address.
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("proxy has no policy admin yet — the next changeContractPolicies caller wins.");
} else {
  console.log("current admin:", admin, "policy count:", policies.length);
}
ESC
Type to search