Managing Contract Policies

advanced intermediate

How to update, swap, or remove the PolicySets attached to a specific contract via the PCL precompile — without redeploying the contract or going through global governance.

Prerequisites

  • Deployed contract with a ContractPolicyConfig already registered
  • Wallet that holds the admin address recorded on that ContractPolicyConfig

Surface — three calls you'll use

From IPcl.sol:

  • registerContractPolicies(ContractPolicyConfig calldata policy) — first-time registration. Reverts if a config already exists for that contract address.
  • changeContractPolicies(ContractPolicyConfig calldata policy) — replace the entire PolicySet[] (and optionally rotate admin). Caller must be the current admin.
  • removeContractPolicies(address contractAddress) — wipe the config entirely. Caller must be the current admin.


There is no "add one PolicySet" or "delete one PolicySet" call — change… is whole-array replacement, by design (so the on-chain config matches what you signed off on).
interface IPcl {
    function registerContractPolicies(ContractPolicyConfig calldata policy) external;
    function changeContractPolicies(ContractPolicyConfig calldata policy) external;
    function removeContractPolicies(address contractAddress) external;
    function contractPolicies(address contractAddress)
        external view returns (ContractPolicyConfig memory);
}

Swap to a new template

Templates themselves are immutable once registered (auditability). To "upgrade" a rule, you build a fresh PolicySet[] and submit it via changeContractPolicies — the old array is replaced atomically.
import { encodeAbiParameters } from "viem";

const PCL = "0x1000000000000000000000000000000000000005";

// new VOLUME_POLICY allowing 0..10,000,000 OKRW per tx
const volumePolicy = encodeAbiParameters(
  [
    { type: "string[]", name: "tokens" },
    {
      type: "tuple[]", name: "limits",
      components: [
        { type: "uint256", name: "minLimit" },
        { type: "uint256", name: "maxLimit" },
      ],
    },
  ],
  [["aokrw"], [{ minLimit: 0n, maxLimit: 10_000_000n * 10n ** 18n }]],
);

await walletClient.writeContract({
  address: PCL,
  abi: pclAbi,
  functionName: "changeContractPolicies",
  args: [{
    _contract: tokenAddress,
    admin:     currentAdmin,                // keep, or set a new one to rotate
    policies: [{
      templateId: "VOLUME_POLICY",
      policy:     volumePolicy,
      selector:   "0x",
    }],
  }],
});
Tip: Always notify your users before tightening compliance rules — wallets cache the last-seen ReasonCode set and your fail UI may need to learn a new code.

Rotate the admin

Because admin is just a field on ContractPolicyConfig, rotating it is the same flow as a policy change: call changeContractPolicies with the new admin (and the same or updated policies). The current admin must sign. Once submitted, only the new admin can call change… or remove….
await walletClient.writeContract({
  address: PCL,
  abi: pclAbi,
  functionName: "changeContractPolicies",
  args: [{
    _contract: tokenAddress,
    admin:     newAdmin,         // rotated
    policies:  existingPolicies, // pass through unchanged
  }],
});
Warning: There is no separate `transferAdmin` call — losing track of the admin key means losing the ability to change or remove the config. Plan rotation carefully.

Remove the config

When you want a contract to behave again like an unregulated address (only the GlobalPolicyConfig applies), wipe its ContractPolicyConfig entirely.
await walletClient.writeContract({
  address: PCL,
  abi: pclAbi,
  functionName: "removeContractPolicies",
  args: [tokenAddress],
});
Warning: This removes only the contract-scoped rules. The chain-wide `GlobalPolicyConfig` (denylists, KYC tiers, etc.) still applies to every transaction.

Verify what's currently active

Read the current config back via the contractPolicies view; useful for a wallet's policy-inspector UI or for asserting what was just set.
const cfg = await publicClient.readContract({
  address: PCL,
  abi: pclAbi,
  functionName: "contractPolicies",
  args: [tokenAddress],
});
console.log(cfg.admin, cfg.policies.length, cfg.policies.map(p => p.templateId));
ESC
Type to search