정책 및 컴플라이언스
프로그래머블 컴플라이언스 레이어(PCL)를 다룹니다. 정책 구조, 템플릿, 시행 흐름, 프리컴파일 인터페이스를 포함합니다.
컴플라이언스 기초
PCL이 무엇인지, 퍼블릭 체인이 어떻게 규제를 만족하는지, 듀얼 트랙 모델을 소개합니다.
프로그래머블 컴플라이언스 레이어(PCL)는 마루 네트워크의 핵심 모듈로, 블록체인 상에서 직접 컴플라이언스 정책을 생성하고 시행할 수 있게 합니다. 이 모듈은 트랜잭션이 처리되기 전에 가로채어, 전역 및 컨트랙트별 규칙 집합에 대해 유효성을 검사합니다. 이를 통해 스마트 컨트랙트의 핵심 로직을 변경하지 않고도 KYC/AML 확인, 전송 제한, 거래량 한도와 같은 복잡한 규제 요건을 구현할 수 있습니다.
PCL 프리컴파일은 고정된 EVM 주소에 위치한 상태 저장 프리컴파일로, Solidity 코드와 마루의 컴플라이언스 엔진을 연결합니다. 조회 뷰(getParams, globalPolicies, contractPolicies, policyTemplate, 주기 거래량 조회)와 관리자 전용 쓰기(registerPolicyTemplate, setGlobalPolicies, changeContractPolicies 및 각각의 제거 함수)를 제공합니다. 규제 실행 경로도 함께 소유합니다. deployPclProxy는 표준 PCL 래핑 프록시를 배포하며, 이 프록시의 훅 경로가 실제 실행 전후로 preCall과 postCall을 호출합니다. 이 훅이 컨트랙트 범위 정책을 발동시키는 유일한 메커니즘입니다.
마루는 누구나 지갑을 생성할 수 있는 무허가형 체인입니다. 그러나 거래 상대방의 신원, 거래 규모 및 자산 유형에 따라 트랜잭션을 규제 경로(Regulated Path) 또는 개방 경로(Open Path)로 동적으로 라우팅합니다.
마루는 트랜잭션 실행을 두 컨텍스트로 분리합니다. 개방 트랙에서는 호출이 대상 컨트랙트로 직접 전달되며, 트랜잭션 진입점에서 평가되는 전역 GlobalPolicyConfig만 적용됩니다. 컨트랙트 범위 정책은 작동하지 않습니다. 규제 트랙에서는 사용자가 PCL에 등록된 프록시 주소로 트랜잭션을 보냅니다. 프록시의 훅 경로가 실제 실행 전에 IPcl.preCall(...)을, 실행 후에 IPcl.postCall(...)을 호출하므로, 프록시에 바인딩된 ContractPolicyConfig가 호출 전후로 원자적으로 강제됩니다. 컨트랙트 관리자는 사용자 대상 진입점으로 구현 컨트랙트 주소(개방)를 공개할지, PCL 등록 프록시 주소(규제)를 공개할지 선택해 트랙을 결정합니다.
마루는 두 가지 별개의 경로를 통해 트랜잭션을 처리합니다. 표준 저위험 활동을 위한 개방 경로(Open Path)와 고가치 또는 규제 자산 전송을 위한 규제 경로(Regulated Path)입니다. PCL이 올바른 경로를 자동으로 결정합니다.
정책 모델
정책이 어떻게 구성·조합·관리·평가되는지, 거절이 어떤 코드로 나타나는지 다룹니다.
PCL은 컴플라이언스 규칙을 JSON 객체가 아니라 Solidity로 정의된 ABI 튜플로 저장합니다. 구조는 3계층으로, PolicyTemplate(정책 관리자가 등록하는 규칙 타입)이 PolicySet(타입 ID와 ABI 인코딩 파라미터 blob, 선택적 함수 selector)으로 인스턴스화되고, PolicyConfig(전역 또는 컨트랙트별 설정)에 묶입니다. PolicyTemplate 자체는 메타데이터 전용으로 templateId, name, description만 가지므로, 각 템플릿의 파라미터 형태는 IPcl.sol의 템플릿별 struct에서 확인해야 합니다.
마루의 PCL은 가장 흔한 규제 및 비즈니스 컴플라이언스 게이트를 다섯 가지 내장 정책 템플릿으로 제공합니다. 각 템플릿은 IPcl.sol에 파라미터 struct를 정의하며(PolicySet.policy에 abi.encode로 인코딩됩니다), 평가 규칙은 GlobalPolicyConfig의 경우 AnteHandler, ContractPolicyConfig의 경우 PCL 프록시 훅 경로에서 실행됩니다. 관리자는 템플릿을 PolicySet으로 인스턴스화하여 설정에 부착합니다. 특정 네트워크에서 실제로 등록되어 있는 템플릿은 온체인 정책 관리자가 결정하므로, 가정하지 말고 런타임에 IPcl.policyTemplate(templateId)로 확인합니다.
PCL 정책 관리자는 x/pcl 모듈 파라미터에 저장되고 IPcl.policyAdmin()으로 노출되는 단일 주소입니다. 정책 템플릿을 등록·제거하고 GlobalPolicyConfig를 설정·해제할 수 있는 유일한 호출자입니다. 잘못 구성된 전역 정책이나 denylist가 관리자 자신의 트랜잭션을 거절하는 상황에서도 관리자가 항상 복구할 수 있도록, 관리자가 PCL의 특정 제어 평면 메서드를 호출하면 사전 실행 단계의 오퍼레이션 추출과 사후 실행 단계의 전송 스캔 훅 모두에서 PCL 정책 평가를 우회합니다.
모든 마루 트랜잭션은 상태 변경 작업이 실행되기 전에 PCL을 통과합니다. 정책 관리자가 설정한 GlobalPolicyConfig의 전역 정책은 모든 트랜잭션에 대해 실행 전에 평가됩니다. PCL 래핑 프록시에 등록된 ContractPolicyConfig의 컨트랙트 범위 정책은 프록시의 preCall / postCall 훅에서 실행됩니다. 어느 경계에서든 거절이 발생하면 트랜잭션이 중단되며, ABI 인코딩된 PCL ReasonCode(권한·인코딩 실패의 경우 공유 IPrecompile 오류)로 이유가 전달됩니다. 이 형태는 RPC 제출 시점의 거절이든 온체인 실행 시점의 거절이든 동일하므로, 클라이언트 디코더 하나로 두 경우를 모두 처리할 수 있습니다.
LogicalPolicy와 ForEachPolicy는 자체 리프 규칙을 담지 않고 다른 PolicySet들을 트리로 감싸는 구조 정책 템플릿입니다. LogicalPolicy는 자식들을 AND(Every) 또는 OR(Any) 정량자로 결합합니다. ForEachPolicy는 하나의 자식을 취해 해석된 대상마다 한 번씩 적용합니다. 현재 대상은 호출자의 에이전트 소유자 집합입니다. 두 구조 모두 최대 깊이(MaxDepthExceeded)와 검증 오류(LogicalPolicyChildrenEmpty, ForEachChildAbsent, QuantifierUnspecified, ChildSelectorNotEmpty)의 제한을 받으며, 등록 시점에 잘못된 트리를 거절합니다.
PCL의 모든 거절은 IPcl에 선언된 커스텀 Solidity 오류 중 하나를 전달합니다. IPcl이 이제 IPrecompile을 상속하므로 공유 경계 오류도 포함됩니다. 지갑과 dApp 코드는 revert 페이로드를 IPcl ABI(전이적으로 IPrecompile 오류를 포함)로 디코드하고, 자유 형식 문자열이 아니라 오류 이름과 인자를 기준으로 사용자 경험을 구성합니다. 코드는 네 그룹으로 나뉩니다. 정책 위반 코드(사용자 트랜잭션이 컴플라이언스 규칙에 실패), 설정 코드(관리자 호출이 잘못되었거나 권한이 없음), 조합/구조 코드(LogicalPolicy 또는 ForEachPolicy 조합기가 거절), 그리고 상속된 경계 오류(잘못된 주소, 인자 개수 불일치, 알 수 없는 메서드, SDK 수준 거절)입니다. 일부 실패는 여전히 평문 문자열 revert이며 커스텀 오류로 디코드할 수 없습니다. 해당 항목은 명시적으로 표시합니다.
프록시·바인딩
실행 표면입니다. PCL 프록시와 pre/post 훅, 컨트랙트 admin 바인딩을 다룹니다.
컨트랙트 범위 PCL 정책은 PCL 프리컴파일의 훅 두 개를 통해 강제됩니다. preCall은 래핑된 구현 실행 전에, postCall은 실행 후에 호출됩니다. 이 훅은 개방되어 있지 않으며, 프리컴파일이 직접 호출자(msg.sender)의 종류를 작은 허용 목록과 대조하여 그 외 모든 경우를 Unauthorized로 거절합니다. 허용되는 종류는 두 가지입니다. 하나는 deployPclProxy로 등록된 PCL 래핑 프록시이며 대상 컨트랙트 주소와 같아야 합니다. 다른 하나는 PclParams.entrypoints에 등록된 신뢰된 진입점이며, 하나의 진입점이 여러 대상으로 사용자 오퍼레이션을 라우팅하므로 그 제약을 두지 않습니다.
PCL은 컨트랙트 범위 정책을 오직 PCL에 등록된 프록시, 즉 IPcl.deployPclProxy로 배포된 컨트랙트에 대해서만 강제합니다. 이러한 프록시는 각각 정확히 하나의 컨트랙트 정책 admin을 가지며, 이 주소만 changeContractPolicies와 removeContractPolicies를 호출할 수 있습니다. 이 바인딩은 단일 할당입니다. 특정 프록시에 대한 최초 성공 changeContractPolicies 호출이 policy.admin을 admin으로 저장하며, 이후 모든 호출은 저장된 admin이 서명해야 합니다. 권한을 회전하려면 현재 admin이 페이로드의 admin 값을 다른 주소로 바꾸어 changeContractPolicies를 다시 호출합니다. 정책 교체와 admin 이양이 같은 호출 안에서 원자적으로 이루어집니다.
정책 템플릿
내장 정책 템플릿을 템플릿별 한 페이지로 정리합니다.
DENYLIST_POLICY는 주소 목록을 파라미터로 받는 리프 정책 템플릿입니다. PCL은 실행 중에 관찰된 모든 비-PCL 관리 CALL 경로에 대해 이 정책을 평가합니다. 직접 호출뿐 아니라 PCL 래핑 프록시를 거치는 중첩 경로도 포함합니다. 평가는 자금 이동이 아니라 호출 경로 기반이므로, Transfer 로그가 발생하지 않는 0-value 호출이라도 경로가 denylist 주소에 닿으면 정책이 발동합니다. 일치가 발견되면 InDenylist(address sender)로 revert됩니다.
하나 이상의 토큰 denom에 대해 건당 최소·최대 금액 한도를 강제하는 템플릿입니다. 각 트랜잭션은 다른 트랜잭션과 독립적으로 평가되며 롤링 윈도우나 누적 추적은 없습니다. 기간 기반 누적이 필요한 경우 PERIODIC_VOLUME_POLICY 페이지를 참고합니다.
설정된 리셋 주기에 걸쳐 발신자별 denom별 누적 트랜잭션 금액을 추적하고, 누적 합계가 한도를 넘는 트랜잭션을 거절합니다. 각 트랜잭션을 독립적으로 검사하는 VOLUME_POLICY와는 구별됩니다. 일/월 전송 상한 및 Travel Rule 스타일 임계값에 사용합니다.
발신자가 특정 EAS 스키마로 발행된 유효한 (만료되지 않고 폐기되지 않은) attestation을 보유했는지를 트랜잭션 게이트의 기준으로 삼습니다. KYC, KYB, 자격 증명, 자격 기반 게이트를 구현하는 표준 프리미티브입니다. EAS 프리컴파일을 통해 attestation을 해결하므로 AnteHandler 평가 중 조회가 저렴합니다.
OKRW_EAS_TRANSFER_LIMIT_POLICY는 더 이상 체인에 존재하지 않습니다. attestation 조건과 상한을 한 덩어리로 묶었던 정책 템플릿 중 하나였는데, 같은 규칙을 조각을 조합해 표현하도록 상류에서 제거했습니다. 이제 이 템플릿 id를 등록하거나 참조하면 실패하며, changeContractPolicies는 PolicyNotRegistered(string templateId)로 revert됩니다. 아직 이 id를 들고 있는 통합이 무슨 일이 있었고 대신 무엇을 써야 하는지 찾을 수 있도록 이 페이지를 남겨 둡니다.
OKRW_EAS_PERIODIC_VOLUME_LIMIT_POLICY는 더 이상 체인에 존재하지 않습니다. attestation 조건과 상한을 한 덩어리로 묶었던 정책 템플릿 중 하나였는데, 같은 규칙을 조각을 조합해 표현하도록 상류에서 제거했습니다. 이제 이 템플릿 id를 등록하거나 참조하면 실패하며, changeContractPolicies는 PolicyNotRegistered(string templateId)로 revert됩니다. 아직 이 id를 들고 있는 통합이 무슨 일이 있었고 대신 무엇을 써야 하는지 찾을 수 있도록 이 페이지를 남겨 둡니다.
에이전트 지갑이 시작한 단일 OKRW 전송에 상한을 강제하며, 상한 값은 ERC-8004 IdentityRegistry의 온체인 메타데이터 getMetadata(agentId, "TransferLimit")에서 읽습니다. 정책 struct 자체에는 설정 가능한 필드가 없으며, 상한은 에이전트 소유자가 메타데이터 슬롯에 기록한 값입니다. 평가기는 에이전트가 직접 msg.sender인 경우와, 전역 범위 정책에서 PCL 프록시 훅이 전달한 시작 주체(principal)가 에이전트인 경우 모두 전송을 에이전트에게 귀속시킵니다. 따라서 컨트랙트가 에이전트 principal의 자금을 인출하는 흐름도 동일한 상한에 걸립니다. 귀속된 값이 설정된 TransferLimit을 초과하면 트랜잭션은 ExceededAgentTransferLimit(maxLimit, value)로 revert됩니다.
제도 장치
트랜잭션 단위 정책 검사 밖의 체인 수준 규제 장치를 다룹니다.
해킹, 피싱, 명백히 불법인 자금 흐름은 실제 결제 시스템에서 발생합니다. 마루는 체인을 롤백하지 않고도 상태를 교정할 수 있도록 범위 제한된 3개의 recovery primitive를 제공합니다. 동결, 소각, 재발행이 그 세 가지입니다. 셋 모두 문서화된 법적 근거가 있고 정해진 절차가 이행된 경우에만 작동하며, 모든 발동은 Observer Node와 거버넌스 감사 로그에 기록됩니다. 상시 개입 장치가 아니며, 법적 근거가 허용하는 한 마루는 과거 이력 재작성 대신 전방 교정(이전 상태를 무효화하는 새 트랜잭션)을 선호합니다.
규제는 변합니다. Travel Rule 임계 금액이 바뀌고, 제재 리스트가 갱신되고, 관할권 규칙이 진화합니다. 마루는 정책 엔진 (PCL — 안정적, 거의 변하지 않음)과 파라미터 공급 (Legal Oracle — 법·정책 변화에 따라 갱신)을 분리합니다. 새로운 규제 요구는 파라미터 추가로 — 길어야 새 PolicyTemplate 등록으로 — 흡수되며, 체인 코어 코드는 건드리지 않습니다. Legal Oracle의 파라미터 갱신 권한 자체도 거버넌스로 통제됩니다 — 모든 변경은 온체인에서 가시화되고 감사 가능합니다.