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

Type: 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;
  }
}
ESC
Type to search