PCL 내장 정책 템플릿

component compliance

마루가 기본 제공하는 정책 템플릿을 다룹니다. denylist, 트랜잭션별 거래량 밴드, 누적 주기 상한, EAS attestation 게이트, 에이전트별 전송 한도와 이들을 묶는 두 조합 템플릿을 포함합니다.

마루의 PCL은 일곱 가지 내장 정책 템플릿을 제공합니다. 다섯 가지 리프 템플릿은 가장 흔한 규제 및 비즈니스 컴플라이언스 게이트를 다루고, 두 조합 템플릿(LOGICAL_POLICY, FOR_EACH_POLICY)은 이들을 묶습니다. 각 템플릿은 IPcl.sol에 파라미터 struct를 정의하며(PolicySet.policy에 abi.encode로 인코딩됩니다), 평가 규칙은 GlobalPolicyConfig의 경우 실행 전(제출 시점과 체인의 실행 전 검사)과 실행 후에, ContractPolicyConfig의 경우 PCL 프록시 훅 경로에서 실행됩니다. 관리자는 템플릿을 PolicySet으로 인스턴스화하여 설정에 부착합니다. 특정 네트워크에서 실제로 등록되어 있는 템플릿은 온체인 정책 관리자가 결정하므로, 가정하지 말고 런타임에 IPcl.policyTemplate(templateId)로 확인합니다. 다만 목록에 있다고 모두 쓸 수 있는 것은 아닙니다. maroo-testnet-1의 레지스트리에는 제거된 두 OKRW_EAS_* id가 아직 남아 있습니다.

다섯 가지 리프 템플릿

다음 표는 IPcl.sol이 기본 제공하는 다섯 가지 리프 템플릿과 각 템플릿의 PolicySet.policy bytes를 채우는 파라미터 struct를 정리합니다. 두 조합 템플릿 LOGICAL_POLICY와 FOR_EACH_POLICY는 아래에서 다룹니다. 이전에 있었던 두 템플릿 OKRW_EAS_TRANSFER_LIMIT_POLICY와 OKRW_EAS_PERIODIC_VOLUME_LIMIT_POLICY는 더 이상 체인의 정책 유형이 아닙니다(maroo-testnet-1의 레지스트리에는 아직 남아 있지만, 이를 지정한 PolicySet은 UnknownPolicyType으로 revert됩니다). 동일한 동작은 VOLUME_POLICY, PERIODIC_VOLUME_POLICY, EAS_POLICY를 LogicalPolicy / ForEachPolicy로 감싸 조합 정책으로 표현할 수 있습니다.

templateId파라미터 struct목적
EAS_POLICYEasPolicy(address easContract, address indexContract, bytes32 schemaUid)지정 스키마에 대한 유효 EAS attestation을 호출자에게 요구합니다.
DENYLIST_POLICYDenylistPolicy(address[] addresses)작업의 from 또는 to 주소(발신자, 또는 호출 대상이나 전송 수신자)가 목록에 있으면 거절합니다. 에이전트 소유자는 해석하지 않으므로, 발신 에이전트 지갑의 소유자를 검사하려면 FOR_EACH_POLICY로 감쌉니다.
VOLUME_POLICYVolumePolicy(string[] tokens, VolumeUnitPolicy[] limits)토큰 denom별 트랜잭션 단위 최소·최대 금액 밴드를 검사합니다.
PERIODIC_VOLUME_POLICYPeriodicVolumePolicy(string[] tokens, UnitPeriodicVolumePolicy[] limits)resetPeriodSeconds 윈도 동안 denom별 누적 상한을 강제합니다.
AGENT_OKRW_TRANSFER_LIMIT_POLICYAgentOkrwTransferLimitPolicy(uint256 reserved)에이전트 온체인 TransferLimit 메타데이터에서 읽은 전송 상한을 강제합니다. Solidity가 빈 struct를 허용하지 않아 struct 필드가 존재하며, 체인은 이 필드를 무시합니다.

템플릿별 실제 검사 대상

  • EAS_POLICY — EAS Indexer에서 (schemaUid, 호출자)를 조회하고, 유효한 attestation이 없으면 EAS 계열 ReasonCode(EasNoAttestationReceived, EasAttestationLookupFailed, EasAttestationRevoked, EasAttestationExpired)로 거절합니다. 검사 대상은 발신자뿐이므로, 에이전트 지갑은 지갑 자체에 attestation이 있어야 통과합니다. 에이전트 지갑을 소유자의 attestation으로 통과시키려면 EAS_POLICY를 지갑의 에이전트 소유자(AgentOwners)를 순회하는 FOR_EACH_POLICY로 감쌉니다. Any이면 소유자 한 명의 attestation으로 충분하고, Every이면 모든 소유자에게 attestation이 있어야 합니다. 테스트넷의 전역 정책이 이렇게 구성되어 있습니다.
  • DENYLIST_POLICY — 멤버십 검사만 수행합니다. attestation 조회나 거래량 누적 없이 동작합니다.
  • VOLUME_POLICY — 트랜잭션 단위 밴드를 검사하며, VolumeBelowMinLimit 또는 VolumeAboveMaxLimit으로 거절합니다.
  • PERIODIC_VOLUME_POLICY — (scope, contract, sender, selector, asset, resetPeriodSeconds)를 키로 누적 추적하고, 현재 주기의 resetAt을 포함한 ExceededPeriodicVolume으로 거절합니다.
  • AGENT_OKRW_TRANSFER_LIMIT_POLICY — 에이전트 메타데이터의 TransferLimit을 읽고 ExceededAgentTransferLimit 또는 AgentTransferLimitMetadataInvalid로 거절합니다. 호출자 지갑이 에이전트인 경우에만 의미가 있습니다.


attestation을 평가하는 템플릿은 EAS_POLICY 하나뿐입니다. attestation 검사와 거래량 상한이나 denylist를 함께 요구해야 하는 규칙은 조합 정책으로 표현합니다(pcl-composite-policies 참고).

복합 규칙을 위한 조합 정책

제거된 OKRW_EAS_* 템플릿이 표현하던 비즈니스 규칙은 이제 위의 리프 템플릿을 LogicalPolicy / ForEachPolicy 트리로 감싸 표현합니다. 예를 들어 "미인증 발신자는 24시간에 1,000만 OKRW로 제한" 규칙은 OR 조합으로 표현합니다. attestation을 갖거나, 주기 상한 이내에 머무르면 통과합니다. 조합 구조 정책(LogicalPolicy, ForEachPolicy)에는 자체 깊이·중첩 규칙이 있습니다(pcl-composite-policies 참고).
import {
    EasPolicy, PolicySet, LogicalPolicy, ForEachPolicy, LogicalQuantifier, ForEachQuantifier,
    ForEachSubject, VolumePolicy, VolumeUnitPolicy, PeriodicVolumePolicy, UnitPeriodicVolumePolicy,
    ContractPolicyConfig, GlobalPolicyConfig, DenylistPolicy
} from "@maroo-chain/contracts/precompiles/pcl/IPcl.sol";

// 조합: 호출자가 attestation을 갖거나, 24시간 동안 1,000만 OKRW 상한 내에 머물러야 합니다.
EasPolicy memory eas = EasPolicy({
    easContract:   0x1000000000000000000000000000000000000007,
    indexContract: 0x1000000000000000000000000000000000000008,
    schemaUid:     0x5f3a2b0e1d5c9a7f6e3b4a1d2c8f7b6a5e4d3c2b1a0f9e8d7c6b5a4938271605
});

string[] memory tokens = new string[](1);
tokens[0] = "atokrw";
UnitPeriodicVolumePolicy[] memory limits = new UnitPeriodicVolumePolicy[](1);
limits[0] = UnitPeriodicVolumePolicy({
    maxAmount:          10_000_000 ether,     // 1,000만 OKRW를 atokrw로 표시
    resetPeriodSeconds: 86_400                 // 24시간
});
PeriodicVolumePolicy memory cap = PeriodicVolumePolicy({ tokens: tokens, limits: limits });

PolicySet[] memory children = new PolicySet[](2);
children[0] = PolicySet({ templateId: "EAS_POLICY",             policy: abi.encode(eas), selector: "" });
children[1] = PolicySet({ templateId: "PERIODIC_VOLUME_POLICY", policy: abi.encode(cap), selector: "" });

LogicalPolicy memory anyOf = LogicalPolicy({
    quantifier: LogicalQuantifier.Or,
    children:   children
});
// `anyOf`를 templateId "LOGICAL_POLICY"인 `PolicySet`으로 감싸 대상 설정에 부착합니다.

등록된 템플릿 확인

실제로 활성화된 템플릿은 컴파일 시점 목록이 아니라 온체인 상태입니다. "DENYLIST_POLICY"의 PolicySet을 빌드하기 전에 IPcl.policyTemplate("DENYLIST_POLICY")를 호출합니다. 등록되어 있으면 디스크립터를 반환하고, 아니면 타입 지정 오류 PolicyTemplateNotFound(string templateId)로 revert됩니다. 여러 네트워크에서 실행할 스크립트를 검증하는 표준 방식입니다. 네트워크마다 정책 관리자가 등록 템플릿을 독립적으로 결정합니다.
import { BaseError, ContractFunctionRevertedError, createPublicClient, http, decodeErrorResult } from "viem";
import { iPclAbi as pclAbi } from "@maroo-chain/contracts/abi/precompiles/pcl/IPcl";

const PCL = "0x1000000000000000000000000000000000000005" as const;
const client = createPublicClient({ transport: http("https://rpc-testnet.maroo.io") });

try {
  const t = await client.readContract({
    address:      PCL,
    abi:          pclAbi,
    functionName: "policyTemplate",
    args:         ["DENYLIST_POLICY"],
  });
  console.log("registered:", t.templateId);
} catch (err) {
  // viem은 revert 데이터를 err가 아니라 원인 체인의 ContractFunctionRevertedError에 둡니다.
  const revert = err instanceof BaseError ? err.walk((e) => e instanceof ContractFunctionRevertedError) : null;
  if (!(revert instanceof ContractFunctionRevertedError) || !revert.raw) throw err;
  const decoded = decodeErrorResult({ abi: pclAbi, data: revert.raw });
  // decoded.errorName === "PolicyTemplateNotFound"
  console.error("이 네트워크에 등록되지 않은 템플릿입니다:", decoded.args);
}
ESC
검색어를 입력하세요