IPcl.postCall
postCall(
bytes32 sessionId,
address contractAddress,
address principal,
bytes calldata data,
uint256 value,
bool workable
) external returns (bytes memory) Closes the PCL enforcement session opened by preCall. Given a sessionId returned by preCall, postCall re-runs the post-execution half of the policy evaluation — recording periodic volume, enforcing after-call rules, and emitting PolicyCheckPassed — using the session's stored context. The precompile verifies that the immediate caller (msg.sender of postCall), the target contractAddress, the forwarded principal, and the leading 4-byte selector of data all match the values captured when the session was opened; any divergence reverts with Unauthorized. The PCL-wrapped proxy invokes this hook automatically after the inner call returns; direct callers must ensure the tuple passed to postCall is byte-identical to the tuple passed to preCall, or the session cannot be closed.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
sessionId | bytes32 | ✓ | The session identifier returned by the paired preCall. Consumed exactly once. |
contractAddress | address | ✓ | The target contract address. MUST equal the contractAddress passed to the paired preCall; a mismatch reverts with Unauthorized. |
principal | address | ✓ | The actual caller of the proxy (EOA or AA smart account) that PCL uses as the policy caller. MUST equal the principal passed to preCall; a mismatch reverts with Unauthorized. |
data | bytes | ✓ | The calldata that was forwarded to the inner contract. Only its leading 4-byte selector is compared against the session's captured selector; a mismatch (including empty data when the session captured a non-empty selector, or vice versa) reverts with Unauthorized. |
value | uint256 | ✓ | Native OKRW value forwarded with the inner call. Used to record periodic volume in aokrw (base denom) after execution when the session ran in UserOp mode. |
workable | bool | ✓ | Whether the inner call completed successfully. When false, PCL closes the session without recording volume or emitting PolicyCheckPassed. |
Returns
bytes Opaque return payload reserved for the hook protocol. dApps should not depend on its shape; the PCL-wrapped proxy discards it.
Errors
| Code | Name | Description |
|---|---|---|
Unauthorized | Unauthorized | Reverts when the tuple (msg.sender, contractAddress, principal, data[:4]) diverges from the values captured by the paired preCall. All four fields are compared; a mismatch in any one — for example, a caller other than the PCL-registered proxy that opened the session, a different target contract, a different principal, or a different function selector — triggers this revert. |
InDenylist | InDenylist | Reverts when the principal is on a denylist evaluated during the after-call phase. |
ExceededPeriodicVolume | ExceededPeriodicVolume | Reverts when recording the inner call's transferred value would push the principal over a PERIODIC_VOLUME_POLICY or OKRW_EAS_PERIODIC_VOLUME_LIMIT_POLICY limit that applies to this session. |
VolumeAboveMaxLimit | VolumeAboveMaxLimit | Reverts when a VOLUME_POLICY upper bound is exceeded during the after-call check. |
Examples
How the PCL-wrapped proxy pairs preCall / postCall
The proxy captures target, principal, data, and value before the inner call and re-uses the same variables afterwards. Rebinding any of them — for example, computing a new calldata slice for the postCall side — would produce a divergent tuple and revert with Unauthorized.
// 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");
}
} Decoding a session-mismatch revert on the client
Unauthorized here means the tuple did not match the session — not a general permission failure. When integrating a custom proxy, log the four inputs on both sides to diagnose the divergence quickly.
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;
}
}