컨트랙트 정책 관리하기

advanced intermediate

특정 컨트랙트에 부착된 PolicySet을 PCL 프리컴파일로 업데이트하거나 교체·제거하는 방법을 안내합니다. 컨트랙트를 재배포하거나 글로벌 거버넌스를 거치지 않고도 정책을 갱신할 수 있습니다.

사전 요구사항

  • 이미 ContractPolicyConfig가 등록된 배포된 컨트랙트
  • 해당 ContractPolicyConfig에 기록된 admin 주소를 보유한 지갑

Surface — 사용할 세 가지 호출

IPcl.sol에서:

  • registerContractPolicies(ContractPolicyConfig calldata policy) — 최초 등록에 사용하며, 해당 컨트랙트 주소에 이미 설정이 있으면 revert됩니다.
  • changeContractPolicies(ContractPolicyConfig calldata policy)PolicySet[]을 전체 교체합니다(admin 회전을 선택할 수 있습니다). 호출자는 현재 admin이어야 합니다.
  • removeContractPolicies(address contractAddress) — 설정을 완전히 삭제합니다. 호출자는 현재 admin이어야 합니다.


"PolicySet 하나 추가" 또는 "하나 삭제" 같은 호출은 없습니다. 의도적으로 change…가 전체 배열을 교체하도록 설계해, 서명한 내용과 온체인 설정이 일치하게 유지됩니다.
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);
}

새 템플릿으로 교체

템플릿 자체는 등록 후 불변입니다(감사 가능성 확보). 규칙을 "업그레이드"하려면 새 PolicySet[]을 빌드해 changeContractPolicies로 제출하며, 이전 배열은 원자적으로 교체됩니다.
import { encodeAbiParameters } from "viem";

const PCL = "0x1000000000000000000000000000000000000005";

// 건당 0..10,000,000 OKRW를 허용하는 새 VOLUME_POLICY
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,                // 유지하거나 회전을 위해 새 주소
    policies: [{
      templateId: "VOLUME_POLICY",
      policy:     volumePolicy,
      selector:   "0x",
    }],
  }],
});
팁: 컴플라이언스 규칙을 강화하기 전에 사용자에게 알립니다 — 지갑은 마지막으로 받은 ReasonCode 집합을 캐시하므로 실패 UI가 새 코드를 인식하지 못할 수 있습니다.

admin 회전

adminContractPolicyConfig의 한 필드이므로, 회전도 정책 변경과 동일한 흐름을 따릅니다. 새 admin과 (기존 또는 갱신된) policieschangeContractPolicies를 호출하며, 현재 admin이 서명해야 합니다. 제출 후에는 새 admin만 change… / remove…를 호출할 수 있습니다.
await walletClient.writeContract({
  address: PCL,
  abi: pclAbi,
  functionName: "changeContractPolicies",
  args: [{
    _contract: tokenAddress,
    admin:     newAdmin,         // 회전
    policies:  existingPolicies, // 그대로 전달
  }],
});
주의: 별도의 `transferAdmin` 호출은 없습니다 — admin 키를 잃으면 설정 변경/삭제 능력을 잃습니다. 회전은 신중히 계획합니다.

설정 제거

컨트랙트를 다시 비규제 주소처럼(GlobalPolicyConfig만 적용되도록) 되돌리려면 ContractPolicyConfig를 완전히 삭제합니다.
await walletClient.writeContract({
  address: PCL,
  abi: pclAbi,
  functionName: "removeContractPolicies",
  args: [tokenAddress],
});
주의: 이는 컨트랙트 스코프 규칙만 제거합니다. 체인 전역 `GlobalPolicyConfig` (denylist, KYC 계층 등)은 여전히 모든 트랜잭션에 적용됩니다.

현재 활성 상태 확인

contractPolicies view로 현재 설정을 다시 읽어옵니다. 지갑의 정책 검사 UI나 방금 설정한 내용을 확인할 때 활용합니다.
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
검색어를 입력하세요