PCL Contract Admin Binding
How a contract's PCL admin slot is initialized — automatically at PCL proxy deploy time, or once via legacy registerContractPolicies — and why the binding is single-assignment.
Every ContractPolicyConfig on PCL has an admin field: the only address allowed to later call changeContractPolicies or removeContractPolicies on that contract. This admin slot is bound once per contract address. There are two ways it gets bound: (1) automatically, when the contract is deployed via IPcl.deployPclProxy — the EVM msg.sender of the deploy call is written as admin before the deploy returns; (2) manually, on the legacy path, by the first successful call to registerContractPolicies (now deprecated — prefer changeContractPolicies once the slot is bound). Once written, the slot is anti-takeover: an unrelated caller cannot overwrite it. Handover only happens through changeContractPolicies where the current admin sets a new admin in the config.
Two initialization paths
- Auto-bind on proxy deploy (recommended).
IPcl.deployPclProxy(kind, value, initData)deploys a PCL-wrapped proxy and, before returning, writes the deploy caller (msg.senderat the precompile) into the proxy's admin slot. No separateregisterContractPoliciescall is required to establish the admin — you go straight tochangeContractPoliciesto attach policies. - Legacy explicit registration. For contracts deployed without the PCL proxy factory, the first successful
registerContractPoliciescall binds the admin from theconfig.adminfield. Subsequent updates must usechangeContractPolicies. This path is deprecated and will be removed.
Auto-bind at deploy: what actually happens
deployPclProxy, the precompile deploys the canonical proxy bytecode, then calls the chain-side InitializeContractPolicyAdmin(proxyAddress, msg.sender) before emitting PclProxyDeployed. The admin field on the resulting ContractPolicyConfig is the proxy deployer, not the proxy's logic contract owner and not the initializer target — those are separate concepts.// Deploying the proxy from an EOA binds that EOA as PCL admin.
IPcl pcl = IPcl(0x1000000000000000000000000000000000000005);
address proxy = pcl.deployPclProxy(
PclProxyKind.Transparent,
0,
abi.encode(logicImpl, initialOwner, initializerCalldata)
);
// pcl.contractPolicies(proxy).admin == msg.sender (the EOA that called deployPclProxy)
// Policies are NOT attached yet — call changeContractPolicies next.
pcl.changeContractPolicies(ContractPolicyConfig({
_contract: proxy,
admin: msg.sender, // keep or hand over
policies: policies
})); Factory contracts: the admin becomes the factory
msg.sender of the deployPclProxy call, deploying via a factory contract makes the factory the admin — not the end user who invoked the factory. A factory that wants the end user to own the admin slot has three options:- Have the end user call
deployPclProxydirectly (skip the factory). - Deploy from the factory, then have the factory call
changeContractPoliciesto hand overadminto the end user in the same transaction. - Build a delegation layer in the factory that owns admin permanently and mediates policy changes on the user's behalf.
There is deliberately no
admin parameter on deployPclProxy — the design fixes the initial admin to msg.sender to eliminate spoofing at deploy time.Single-assignment invariant and CREATE2 pre-registration
InitializeContractPolicyAdmin will only ever write the slot if it is empty, or as a no-op if the caller passes the same admin that is already stored. Any other caller trying to initialize a different admin gets PolicyAlreadyRegistered.The same-admin idempotency exists to support CREATE2 pre-registration: a deployer can bind admin against a precomputed proxy address before the proxy exists, and the auto-bind at actual deploy time is a harmless no-op. It also means a contract destroyed and redeployed at the same CREATE2 address by a different deployer is intentionally locked out — the original admin retains authority unless it explicitly hands over via
changeContractPolicies.Handover: change vs. register
changeContractPolicies for every subsequent modification, including admin handover. Set config.admin to the new admin address in the same call that replaces policies; the current admin must be the transaction sender for the check to pass.// Current admin hands over control to a new address in the same call
// that replaces the policy set.
pcl.changeContractPolicies(ContractPolicyConfig({
_contract: proxy,
admin: newAdmin, // handover target
policies: updatedPolicies
}));
// From this point on, only newAdmin can call changeContractPolicies /
// removeContractPolicies on `proxy`. Client-side decision: which entrypoint to call
deployPclProxy, the admin slot is already bound at deploy time — go directly to changeContractPolicies to attach or update policies. Calling registerContractPolicies against such a proxy reverts with PolicyAlreadyRegistered unless the caller is the bound admin — and in that case it performs a full legacy re-registration (policies replaced, the slot set to config.admin), not a no-op. Either way the path is deprecated. For portability, code that manages a contract you own can query IPcl.contractPolicies(contract) first: if admin == address(0), use the legacy registerContractPolicies; otherwise use changeContractPolicies.