IPcl.postCall

postCall(
  bytes32 sessionId,
  address contractAddress,
  address principal,
  bytes calldata data,
  uint256 value,
  bool workable
) external returns (bytes memory)

preCall이 연 PCL 강제 세션을 종료합니다. preCall이 반환한 sessionId를 받아, 세션에 저장된 컨텍스트를 사용해 정책 평가의 사후 단계를 실행합니다. 주기 거래량을 기록하고, 사후 규칙을 강제하며, PolicyCheckPassed를 발행합니다. 프리컴파일은 postCall의 직접 호출자(msg.sender), 대상 contractAddress, 전달된 principal, 그리고 data의 선행 4바이트 selector가 모두 세션 개시 시점에 저장된 값과 일치하는지 확인하며, 하나라도 어긋나면 Unauthorized로 revert됩니다. PCL 래핑 프록시는 내부 호출이 반환된 뒤 이 훅을 자동으로 호출합니다. 직접 호출하는 경우에는 preCall에 넘긴 것과 같은 contractAddress, principal, data의 선행 selector를 postCall에 넘겨야 하며, 그렇지 않으면 세션을 종료할 수 없습니다.

파라미터

이름 타입 필수 설명
sessionId bytes32 ✓ 짝을 이룬 preCall이 반환한 세션 식별자입니다. 정확히 한 번만 소비됩니다.
contractAddress address ✓ 대상 컨트랙트 주소입니다. 짝을 이룬 preCall에 전달한 contractAddress와 반드시 일치해야 하며, 다르면 Unauthorized로 revert됩니다.
principal address ✓ PCL이 정책 호출자로 사용하는 프록시의 실제 호출자입니다(EOA 또는 AA 스마트 계정). preCall에 전달한 principal과 반드시 일치해야 하며, 다르면 Unauthorized로 revert됩니다.
data bytes ✓ 내부 컨트랙트에 전달된 calldata입니다. 선행 4바이트 selector만 세션에 저장된 selector와 비교되며, 다르면(세션이 비어 있지 않은 selector를 기록했는데 data가 비어 있거나 그 반대 포함) Unauthorized로 revert됩니다.
value uint256 ✓ 내부 호출과 함께 전달된 네이티브 OKRW 값입니다. 프록시 세션(PCL 프록시 훅이 자신의 msg.value를 전달)에서는 0이 아닌 값이 실행 후 atokrw(base denom) 단위의 주기 거래량으로 기록됩니다. UserOp 세션(EntryPoint 호출자)에서는 postCall이 이 인자를 사용하지 않으며, UserOperation 실행 중 추적한 호출에서 거래량을 기록합니다.
workable bool ✓ 내부 호출이 성공적으로 완료되었는지 여부입니다. false이면 PCL은 거래량 기록이나 PolicyCheckPassed 발행 없이 세션을 종료합니다.

반환값

타입: bytes

훅 프로토콜용으로 예약된 불투명 반환 페이로드입니다. dApp이 형태에 의존해서는 안 되며, PCL 래핑 프록시는 이를 버립니다.

에러

코드 이름 설명
Unauthorized Unauthorized 직접 호출자가 PCL 등록 프록시도 신뢰된 EntryPoint도 아닐 때(예: EOA나 직접 작성해 배포한 컨트랙트), 이 트랜잭션에 sessionId로 열린 세션이 없을 때, 또는 튜플 (msg.sender, contractAddress, principal, data[:4])가 짝을 이룬 preCall이 기록한 값과 어긋날 때 revert됩니다. 네 필드 모두를 비교하며, 세션을 연 PCL 등록 프록시가 아닌 호출자, 다른 대상 컨트랙트, 다른 principal, 다른 함수 selector 중 어느 하나라도 다르면 이 revert가 발생합니다.
InDenylist InDenylist 내부 호출이 값을 보내거나 받은 주소(UserOp 모드에서는 delegatecall이 아닌 각 호출의 출발·도착 주소도 포함)가 사후 단계에서 평가된 denylist에 포함되어 있을 때 revert됩니다. principal 자체는 그보다 앞서 preCall에서 검사됩니다.
ExceededPeriodicVolume ExceededPeriodicVolume 내부 호출의 이체 금액을 기록하면 해당 세션에 적용되는 PERIODIC_VOLUME_POLICY 한도를 초과하게 될 때 revert됩니다. 조합 정책 안에서는 조합 정책이 결과를 정합니다. 에이전트 소유자를 순회하는 FOR_EACH_POLICY 아래에서는 에이전트 지갑의 이체가 각 소유자의 거래량에 합산되고(에이전트 소유자가 없는 principal은 자신의 거래량에 합산됩니다), 정량자가 Or인 LOGICAL_POLICY 아래에서는 모든 자식이 실패할 때만 이 오류가 AnyOfRejected의 자식 중 하나로 나타납니다. 테스트넷의 전역 정책은 일일 상한을 이렇게 중첩해 EAS attestation과 Or로 묶어 두므로, 테스트넷에서 상한을 넘는 이체는 attestation 검사를 통과하지 않는 한 AnyOfRejected로 revert됩니다.
VolumeAboveMaxLimit VolumeAboveMaxLimit 사후 검사에서 VOLUME_POLICY의 상한을 초과할 때 revert됩니다.

예제

PCL 래핑 프록시가 preCall / postCall을 짝짓는 방식

프록시는 자신의 주소(address(this)), principal, data, value를 preCall에 넘기고, postCall에도 같은 값을 다시 사용합니다. PCL은 호출자, 컨트랙트 주소, principal, data의 4바이트 selector를 비교하므로, 다른 주소나 principal, 또는 선행 4바이트 selector가 다른 data를 넘기면 Unauthorized로 revert됩니다.

// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.28;

import {IPcl} from "@maroo-chain/contracts/precompiles/pcl/IPcl.sol";

/// @dev Illustrative sketch of the flow the PCL-wrapped proxy performs on every
///      inbound call. postCall MUST come from the same caller and carry the same
///      contract address, principal and 4-byte selector of data as preCall —
///      otherwise it reverts with `Unauthorized`.
abstract contract PclHookedProxy {
    IPcl constant PCL = IPcl(0x1000000000000000000000000000000000000005);

    function _forward(address target, address principal, bytes calldata data, uint256 value) internal {
        bytes32 sid = PCL.preCall(address(this), principal, data, value);

        (bool ok, ) = target.delegatecall(data);

        // Same caller, address(this), principal and 4-byte selector of data as preCall — any divergence reverts Unauthorized.
        PCL.postCall(sid, address(this), principal, data, value, ok);

        require(ok, "inner call failed");
    }
}

클라이언트에서 Unauthorized revert 디코딩

postCall은 호출자가 PCL 등록 프록시도 신뢰된 EntryPoint도 아니면 Unauthorized로 revert됩니다(이 스니펫은 EOA에서 보내므로 여기서 멈춥니다). preCall이 연 세션과 맞지 않을 때도 마찬가지입니다. 이 트랜잭션에 해당 sessionId로 열린 세션이 없거나(잘못된 ID이거나 앞선 postCall이 이미 닫은 ID), 호출자·컨트랙트 주소·principal·selector 중 하나가 세션과 다른 경우입니다. 직접 작성해 배포한 프록시는 PCL 등록 프록시가 될 수 없습니다. deployPclProxy는 자신이 체인 바이너리에 내장된 바이트코드로 배포한 프록시만 등록하므로, 그런 프록시의 호출은 항상 첫 번째 검사에서 멈춥니다. 그 밖의 경우에는 트랜잭션 트레이스에서 postCall이 preCall이 반환한 sessionId와 같은 호출자·컨트랙트 주소·principal·selector를 받았는지 확인합니다.

import { BaseError, ContractFunctionRevertedError, createWalletClient, http, decodeErrorResult } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { iPclAbi } from "@maroo-chain/contracts/abi/precompiles/pcl/IPcl";

const PCL = "0x1000000000000000000000000000000000000005" as const;
const wallet = createWalletClient({
  account: privateKeyToAccount(process.env.KEY as `0x${string}`),
  transport: http("https://rpc-testnet.maroo.io"),
});

try {
  await wallet.writeContract({
    address: PCL,
    abi: iPclAbi,
    functionName: "postCall",
    // TODO: replace with the real session tuple before production.
    args: [
      "0x0000000000000000000000000000000000000000000000000000000000000001",
      "0x8F3ac2B1d9E74c05A6B18FE27Dc4913e5A0F7b62", // contractAddress
      "0x2c7f09B81a6D3FF1e5A0d4c6bC2a8f7E19dc3a4B", // principal
      "0xa9059cbb", // data — leading 4 bytes must match the preCall session
      0n,
      true,
    ],
  });
} catch (err) {
  // viem keeps the revert data on the ContractFunctionRevertedError in the cause chain, not on err.
  const revert = err instanceof BaseError ? err.walk((e) => e instanceof ContractFunctionRevertedError) : null;
  if (revert instanceof ContractFunctionRevertedError && revert.raw) {
    // iPclAbi lists every IPcl error, the IPrecompile ones it inherits included
    const decoded = decodeErrorResult({ abi: iPclAbi, data: revert.raw });
    // decoded.errorName === "Unauthorized": this EOA is neither a PCL proxy nor a trusted
    // EntryPoint. From one of those, it means no session is open under sessionId or the
    // caller / target / principal / selector diverges from it.
    console.error("postCall rejected:", decoded.errorName);
  } else {
    throw err;
  }
}
ESC
검색어를 입력하세요