Managing Contract Policies
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
ContractPolicyConfigalready registered - Wallet that holds the
adminaddress recorded on that ContractPolicyConfig
Surface — three calls you'll use
From
There is no "add one PolicySet" or "delete one PolicySet" call —
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 entirePolicySet[](and optionally rotateadmin). Caller must be the currentadmin.removeContractPolicies(address contractAddress)— wipe the config entirely. Caller must be the currentadmin.
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));