AGENT_OKRW_TRANSFER_LIMIT_POLICY

component compliance

에이전트 지갑의 트랜잭션당 OKRW 전송 상한을 강제합니다. 상한 값은 에이전트의 온체인 TransferLimit 메타데이터에서 읽으며, 에이전트가 직접 발신자인 경우뿐 아니라 전역 범위에서 에이전트가 시작 주체(principal)일 때에도 적용됩니다.

에이전트 지갑이 시작한 단일 OKRW 전송에 상한을 강제하며, 상한 값은 ERC-8004 IdentityRegistry의 온체인 메타데이터 getMetadata(agentId, "TransferLimit")에서 읽습니다. 정책 struct 자체에는 설정 가능한 필드가 없으며, 상한은 에이전트 소유자가 메타데이터 슬롯에 기록한 값입니다. 평가기는 에이전트가 직접 msg.sender인 경우와, 전역 범위 정책에서 PCL 프록시 훅이 전달한 시작 주체(principal)가 에이전트인 경우 모두 전송을 에이전트에게 귀속시킵니다. 따라서 컨트랙트가 에이전트 principal의 자금을 인출하는 흐름도 동일한 상한에 걸립니다. 귀속된 값이 설정된 TransferLimit을 초과하면 트랜잭션은 ExceededAgentTransferLimit(maxLimit, value)로 revert됩니다.

파라미터 struct

이 템플릿에는 사용자가 설정할 파라미터가 없습니다. Solidity가 빈 struct를 허용하지 않으므로 AgentOkrwTransferLimitPolicy는 체인이 무시하는 reserved 필드 하나만 갖습니다. 0을 인코딩하고 넘어갑니다.
import { AgentOkrwTransferLimitPolicy, PolicySet } from "@maroo-chain/contracts/precompiles/pcl/IPcl.sol";

AgentOkrwTransferLimitPolicy memory p = AgentOkrwTransferLimitPolicy({ reserved: 0 });

PolicySet memory ps = PolicySet({
    templateId: "AGENT_OKRW_TRANSFER_LIMIT_POLICY",
    policy: abi.encode(p),
    selector: ""
});

상한 값의 출처

상한은 PolicySet에 저장되지 않고, 0x8004000000000000000000000000000000000001 주소의 ERC-8004 IdentityRegistry preinstall에 에이전트별로 저장됩니다. "TransferLimit" 키의 값은 정확히 32바이트 빅엔디언인 abi.encode(uint256)이어야 하며, 상한을 atokrw 기본 단위로 담습니다(소수점 18자리이므로 1,000만 OKRW는 10000000000000000000000000). 에이전트 소유자가 IdentityRegistry의 setMetadata로 기록하며, viem에서는 pad(toHex(10_000_000n * 10n ** 18n), { size: 32 })로 값을 만듭니다. 메타데이터 슬롯이 비어 있거나 정확히 32바이트가 아니면, 호출은 IPcl ABI의 타입 지정 오류 AgentTransferLimitMetadataInvalid(string reason)로 revert됩니다(사유는 "empty metadata value" 또는 "expected 32-byte uint256, got N bytes"). ExceededAgentTransferLimit과 마찬가지로 IPcl ABI로 디코드합니다.

전송이 귀속되는 주체

평가기는 먼저 실행 전에 호출 자체(전역 정책이면 트랜잭션 자체의 value, PCL 프록시를 거치는 호출이면 프록시가 preCall에 넘기는 value)를 검사하고, 이어서 실행 후(after-execution) 단계에서 트랜잭션이 생성한 각 OKRW 전송을 검사하며, 다음 두 경우에 전송을 에이전트에게 귀속시킵니다.

  • 직접 발신자: 에이전트 지갑이 직접 OKRW를 내보내는 함수를 호출한 경우입니다(전형적인 MAWS 흐름).
  • 전역 범위 시작 주체(principal): 컨트랙트가 출금 전송을 수행하지만, PCL 프록시 훅이 시작 주체(규제 경로에 진입한 지갑)를 함께 전달한 경우입니다. 그 principal이 에이전트라면 전역 범위 평가는 여전히 전송을 에이전트에게 귀속시킵니다.


컨트랙트 범위 평가에서는 직접 발신자 귀속만 적용됩니다. 컨트랙트 범위 정책은 이 템플릿에 대해 principal 재귀속을 수행하지 않습니다.

실패 동작

귀속된 값이 TransferLimit을 초과하면 트랜잭션은 타입 지정 커스텀 오류 ExceededAgentTransferLimit(uint256 maxLimit, uint256 value)로 revert됩니다. 클라이언트에서 디코드하여 초과 금액과 설정된 상한을 함께 표시할 수 있습니다.
import { BaseError, ContractFunctionRevertedError, decodeErrorResult } from "viem";
import { iPclAbi as pclAbi } from "@maroo-chain/contracts/abi/precompiles/pcl/IPcl";

try {
  await wallet.writeContract({ /* 에이전트가 시작한 1,000만 OKRW 전송 */ });
} catch (err) {
  // viem은 revert 데이터를 err가 아니라 원인 체인의 ContractFunctionRevertedError에 둡니다.
  const revert = err instanceof BaseError ? err.walk((e) => e instanceof ContractFunctionRevertedError) : null;
  if (revert instanceof ContractFunctionRevertedError && revert.raw) {
    const decoded = decodeErrorResult({ abi: pclAbi, data: revert.raw });
    if (decoded.errorName === "ExceededAgentTransferLimit") {
      const [maxLimit, value] = decoded.args as [bigint, bigint];
      console.error(`에이전트 전송 상한 초과: max=${maxLimit} value=${value}`);
    } else {
      throw err; // 다른 정책의 거절은 그대로 던집니다
    }
  } else {
    throw err; // revert가 아닌 오류(네트워크 오류, 잘못된 ABI)는 그대로 던집니다
  }
}

다른 상한과의 조합

이 템플릿은 에이전트 메타데이터만 읽으며 attestation, denylist, 주기 윈도는 평가하지 않습니다. 이런 검사(attestation 요구는 EAS_POLICY, denylist는 DENYLIST_POLICY, 주기 윈도는 PERIODIC_VOLUME_POLICY)를 더하려면 템플릿이 LOGICAL_POLICY이고 정량자가 And인 PolicySet 하나로 템플릿들을 묶습니다. 모든 자식이 통과해야 하므로, 에이전트 메타데이터 상한이든 DENYLIST_POLICY 같은 다른 자식이든 하나라도 실패하면 전송이 거절됩니다. 각 자식의 selector는 비워 두고, selector는 바깥 PolicySet에 지정합니다(PCL 조합 정책 참고).

템플릿들을 같은 selector의 별도 PolicySet 항목으로 나열하면 안 됩니다. changeContractPolicies와 setGlobalPolicies는 한 selector에 항목이 두 개인 policies 배열을 거절하며, InvalidParameter(bytes input)로 revert됩니다. 이때 input은 해당 selector를 텍스트로 담은 값으로, 0x 없는 소문자 16진수(예: "a9059cbb"의 바이트)이며 빈 selector는 __all__입니다. 별도 항목으로 나누려면 항목마다 selector가 달라야 합니다.
import {
    AgentOkrwTransferLimitPolicy,
    DenylistPolicy,
    LogicalPolicy,
    LogicalQuantifier,
    PolicySet
} from "@maroo-chain/contracts/precompiles/pcl/IPcl.sol";

// TODO: 프로덕션 배포 전 실제로 차단할 상대방 주소로 교체하십시오.
address[] memory blocked = new address[](1);
blocked[0] = 0x8F3ac2B1d9E74c05A6B18FE27Dc4913e5A0F7b62;

// LOGICAL_POLICY의 자식은 selector를 비워 둡니다.
PolicySet[] memory children = new PolicySet[](2);
children[0] = PolicySet({
    templateId: "AGENT_OKRW_TRANSFER_LIMIT_POLICY",
    policy: abi.encode(AgentOkrwTransferLimitPolicy({ reserved: 0 })),
    selector: ""
});
children[1] = PolicySet({
    templateId: "DENYLIST_POLICY",
    policy: abi.encode(DenylistPolicy({ addresses: blocked })),
    selector: ""
});

// selector 하나에 PolicySet 하나: 모든 자식이 통과해야 합니다.
PolicySet memory combined = PolicySet({
    templateId: "LOGICAL_POLICY",
    policy: abi.encode(LogicalPolicy({ quantifier: LogicalQuantifier.And, children: children })),
    selector: ""
});
ESC
검색어를 입력하세요