IPcl.changeContractPolicies
changeContractPolicies(ContractPolicyConfig calldata policy) external Upserts the ContractPolicyConfig for a target address. The target must be either a PCL-registered proxy (deployed via deployPclProxy) OR a binary-supported policy-aware precompile — currently only the Privacy precompile at 0x100000000000000000000000000000000000000b. msg.sender must be the target's current contract admin — for a proxy, the account that called deployPclProxy (it became the admin in that same call); for a policy-aware precompile, the admin the chain installed — and the new admin value in the payload replaces the old one atomically with the policy replacement. Passing an empty policies array to a policy-aware precompile is rejected — those targets must always carry at least one PolicySet. Ordinary PCL proxies may carry an empty policies array (equivalent to no contract-scope rules). The chain-wide policyAdmin cannot mutate these configs unless it also happens to be the stored contract admin.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
policy | ContractPolicyConfig | ✓ | The new configuration: _contract is the target address (a registered PCL proxy or a policy-aware precompile address), admin is the next gatekeeper for this target's policy config, and policies is the full replacement PolicySet[]. If _contract is neither — an EOA, an agent wallet, a raw implementation or a contract deployed some other way has no contract policy binding — the call reverts with ContractPolicyNotRegistered(contractAddress). If _contract is a policy-aware precompile and policies is empty, the call reverts with CannotEmpty("policy-aware precompile policies"). |
Returns
void No return value. Emits ContractPoliciesChanged(contractAddress, admin, policies) on success.
Errors
| Code | Name | Description |
|---|---|---|
Unauthorized | Unauthorized | Reverts when msg.sender is not the target's currently stored admin — including on the first call for a new proxy, whose admin is already its deployer. The chain-wide policyAdmin does not bypass this check. |
ContractPolicyNotRegistered | ContractPolicyNotRegistered | Reverts when _contract has no contract policy binding — it was not deployed through deployPclProxy and is not a policy-aware precompile (an EOA, an agent wallet, a raw implementation, a contract deployed some other way). This is the first check after the input is decoded. Encoded as ContractPolicyNotRegistered(address contractAddress). |
PclProxyNotRegistered | PclProxyNotRegistered | Reverts when _contract has a contract policy binding (for example one set in genesis) but is neither a registered PCL proxy nor a policy-aware precompile. A target with no binding gets ContractPolicyNotRegistered instead. Encoded as PclProxyNotRegistered(address contractAddress). |
CannotEmpty | CannotEmpty | Reverts when the target is a policy-aware precompile but policies is empty. Encoded as CannotEmpty(string field) with the field value policy-aware precompile policies. Policy-aware precompiles must always carry at least one PolicySet. |
UnknownPolicyType | UnknownPolicyType | Reverts when a PolicySet.templateId in policies is not a known policy type. Encoded as UnknownPolicyType(string templateId); it is raised while the input is decoded, before the admin check. |
PolicyNotRegistered | PolicyNotRegistered | Reverts when a policy in policies (a nested LOGICAL_POLICY / FOR_EACH_POLICY child included) uses a template that is not registered on this network — the policy admin never registered it, or removed it. Encoded as PolicyNotRegistered(string templateId). |
Examples
Bind a denylist policy to a PCL-wrapped ERC20 proxy
Standard usage against a PCL-registered proxy. msg.sender must be the proxy's stored admin — right after deployment, the account that called deployPclProxy.
import { createWalletClient, http, encodeAbiParameters, toHex } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { iPclAbi as pclAbi } from "@maroo-chain/contracts/abi/precompiles/pcl/IPcl";
const PCL = "0x1000000000000000000000000000000000000005" as const;
const wallet = createWalletClient({
account: privateKeyToAccount(process.env.ADMIN_KEY as `0x${string}`),
transport: http("https://rpc-testnet.maroo.io"),
});
// TODO: replace with the real proxy and admin addresses before production.
const proxyAddress = "0x8F3ac2B1d9E74c05A6B18FE27Dc4913e5A0F7b62";
const adminAddress = "0x2c7f09B81a6D3FF1e5A0d4c6bC2a8f7E19dc3a4B";
// abi.encode(DenylistPolicy({ addresses })) — a tuple, as the chain decodes it
const denylistBytes = encodeAbiParameters(
[{ type: "tuple", components: [{ type: "address[]", name: "addresses" }] }],
[{ addresses: [
"0x5AB7c1e40b8DA46F9C7E29d3fA614e97b8F0ac21",
"0x6BC8d2A03e91fFa5b8C4E0F3bd7A0e15c24Bda8E",
] }],
);
await wallet.writeContract({
address: PCL,
abi: pclAbi,
functionName: "changeContractPolicies",
args: [{
_contract: proxyAddress,
admin: adminAddress,
policies: [{
templateId: "DENYLIST_POLICY",
policy: denylistBytes,
selector: toHex("", { size: 0 }),
}],
}],
}); Attach a policy set to the Privacy precompile (policy-aware precompile)
Policy-aware precompiles use the same entry point as PCL proxies, but the target address is the precompile itself. Empty policy sets are rejected — swap the rules by submitting a new non-empty PolicySet[] rather than clearing and re-adding.
import { encodeAbiParameters, toHex } from "viem";
const PCL = "0x1000000000000000000000000000000000000005" as const;
const PRIVACY = "0x100000000000000000000000000000000000000b" as const;
// TODO: replace with the real privacy-admin multisig before production.
const privacyAdmin = "0x2c7f09B81a6D3FF1e5A0d4c6bC2a8f7E19dc3a4B";
// Example: a DENYLIST_POLICY applied to every Privacy call.
// abi.encode(DenylistPolicy({ addresses })) — a tuple, as the chain decodes it
const denylistBytes = encodeAbiParameters(
[{ type: "tuple", components: [{ type: "address[]", name: "addresses" }] }],
[{ addresses: ["0x5AB7c1e40b8DA46F9C7E29d3fA614e97b8F0ac21"] }],
);
await wallet.writeContract({
address: PCL,
abi: pclAbi,
functionName: "changeContractPolicies",
args: [{
_contract: PRIVACY,
admin: privacyAdmin,
policies: [{
templateId: "DENYLIST_POLICY",
policy: denylistBytes,
selector: toHex("", { size: 0 }),
}],
}],
});
// NOTE: passing an empty policies array here reverts with
// CannotEmpty("policy-aware precompile policies"). To rotate rules,
// always submit a non-empty replacement set. Decode the CannotEmpty revert when clearing is attempted
The revert data is ABI-encoded with the CannotEmpty(string field) selector; the payload distinguishes this case from other empty-field violations elsewhere in PCL.
import { BaseError, ContractFunctionRevertedError, decodeErrorResult } from "viem";
try {
await wallet.writeContract({
address: PCL,
abi: pclAbi,
functionName: "changeContractPolicies",
args: [{
_contract: PRIVACY,
admin: privacyAdmin,
policies: [], // empty — will revert for policy-aware precompiles
}],
});
} catch (err) {
// viem keeps the revert data on the ContractFunctionRevertedError in the cause chain, not on err.
const revert = err instanceof BaseError ? err.walk((e) => e instanceof ContractFunctionRevertedError) : null;
if (revert instanceof ContractFunctionRevertedError && revert.raw) {
const decoded = decodeErrorResult({ abi: pclAbi, data: revert.raw });
// decoded.errorName === "CannotEmpty"
// decoded.args[0] === "policy-aware precompile policies"
console.error(`changeContractPolicies reverted: ${decoded.errorName}`, decoded.args);
} else {
throw err;
}
}