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

Type: 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;
  }
}
ESC
Type to search