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 래핑 프록시는 내부 호출이 반환된 뒤 이 훅을 자동으로 호출합니다. 직접 호출하는 경우에는 postCall에 넘기는 튜플이 preCall에 넘긴 튜플과 바이트 단위로 동일해야 하며, 그렇지 않으면 세션을 종료할 수 없습니다.
파라미터
| 이름 | 타입 | 필수 | 설명 |
|---|---|---|---|
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 값입니다. 세션이 UserOp 모드로 동작한 경우 실행 후 aokrw(base denom) 단위로 주기 거래량을 기록하는 데 사용됩니다. |
workable | bool | ✓ | 내부 호출이 성공적으로 완료되었는지 여부입니다. false이면 PCL은 거래량 기록이나 PolicyCheckPassed 발행 없이 세션을 종료합니다. |
반환값
bytes 훅 프로토콜용으로 예약된 불투명 반환 페이로드입니다. dApp이 형태에 의존해서는 안 되며, PCL 래핑 프록시는 이를 버립니다.
에러
| 코드 | 이름 | 설명 |
|---|---|---|
Unauthorized | Unauthorized | 튜플 (msg.sender, contractAddress, principal, data[:4])가 짝을 이룬 preCall이 기록한 값과 어긋날 때 revert됩니다. 네 필드 모두를 비교하며, 세션을 연 PCL 등록 프록시가 아닌 호출자, 다른 대상 컨트랙트, 다른 principal, 다른 함수 selector 중 어느 하나라도 다르면 이 revert가 발생합니다. |
InDenylist | InDenylist | 사후 단계에서 평가된 denylist에 principal이 포함되어 있을 때 revert됩니다. |
ExceededPeriodicVolume | ExceededPeriodicVolume | 내부 호출의 이체 금액을 기록하면 해당 세션에 적용되는 PERIODIC_VOLUME_POLICY 또는 OKRW_EAS_PERIODIC_VOLUME_LIMIT_POLICY 한도를 초과하게 될 때 revert됩니다. |
VolumeAboveMaxLimit | VolumeAboveMaxLimit | 사후 검사에서 VOLUME_POLICY의 상한을 초과할 때 revert됩니다. |
예제
PCL 래핑 프록시가 preCall / postCall을 짝짓는 방식
프록시는 내부 호출 전에 target, principal, data, value를 확보하고, 이후에 동일한 변수를 다시 사용합니다. 이 중 하나라도 다시 바인딩하면(예: postCall용으로 새로운 calldata slice를 계산) 튜플이 어긋나 Unauthorized로 revert됩니다.
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.22;
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. The tuple passed to postCall MUST be byte-identical to
/// the tuple passed to preCall — otherwise the session fails to close 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(target, principal, data, value);
(bool ok, ) = target.call{value: value}(data);
// Same target, principal, data, value — anything else reverts Unauthorized.
PCL.postCall(sid, target, principal, data, value, ok);
require(ok, "inner call failed");
}
} 클라이언트에서 세션 불일치 revert 디코딩
여기서의 Unauthorized는 일반적인 권한 실패가 아니라 튜플이 세션과 일치하지 않았다는 뜻입니다. 커스텀 프록시를 통합할 때는 양쪽에서 네 입력값을 로깅하면 어긋난 원인을 빠르게 진단할 수 있습니다.
import { createWalletClient, http, decodeErrorResult } from "viem";
import { privateKeyToAccount } from "viem/accounts";
const PCL = "0x1000000000000000000000000000000000000005" as const;
const pclAbi = [
{ type: "function", name: "postCall", stateMutability: "nonpayable",
inputs: [
{ name: "sessionId", type: "bytes32" },
{ name: "contractAddress", type: "address" },
{ name: "principal", type: "address" },
{ name: "data", type: "bytes" },
{ name: "value", type: "uint256" },
{ name: "workable", type: "bool" },
],
outputs: [{ type: "bytes" }] },
{ type: "error", name: "Unauthorized", inputs: [] },
] 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: pclAbi,
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: any) {
if (err?.data) {
const decoded = decodeErrorResult({ abi: pclAbi, data: err.data });
// decoded.errorName === "Unauthorized" when the caller / target / principal /
// selector diverges from the preCall session.
console.error("postCall rejected:", decoded.errorName);
} else {
throw err;
}
}