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 pass postCall the same contractAddress, principal, and leading selector of data they 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. In a proxy session (the PCL proxy hook passes its msg.value), a non-zero value is recorded as periodic volume in atokrw (base denom) after execution; in a UserOp session (EntryPoint caller) postCall ignores this argument and records volume from the calls traced during the UserOperation.
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 immediate caller is neither a PCL-registered proxy nor a trusted EntryPoint (for example, an EOA or a contract you wrote and deployed yourself), when no session is open under sessionId in this transaction, or 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 an address the inner call transferred value from or to (in UserOp mode, also any address a non-delegate call went from or to) is on a denylist evaluated during the after-call phase. The principal itself is checked earlier, by preCall.
ExceededPeriodicVolume ExceededPeriodicVolume Reverts when recording the inner call's transferred value would push the principal over a PERIODIC_VOLUME_POLICY limit that applies to this session. Inside a composite policy the composite decides the outcome: under FOR_EACH_POLICY over agent owners, an agent wallet's transfers count toward each of its owners' volume (a principal with no agent owners counts toward its own), and under a LOGICAL_POLICY with Or this error surfaces only when every child fails, as one of the children of AnyOfRejected. The testnet's global policy nests its daily cap this way, in an Or with an EAS attestation, so there an over-cap transfer reverts with AnyOfRejected unless the attestation check passes.
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 passes its own address (address(this)), principal, data, and value to preCall and re-uses the same values for postCall. PCL compares the caller, the contract address, principal, and the 4-byte selector of data; passing a different address or principal, or data whose leading 4-byte selector differs, reverts with Unauthorized.

// 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");
    }
}

Decoding an Unauthorized revert on the client

postCall reverts with Unauthorized when the caller is neither a PCL-registered proxy nor a trusted EntryPoint (this snippet sends from an EOA, so it stops there), or when the call does not match a session preCall opened: no session is open under that sessionId in this transaction (a wrong ID, or one an earlier postCall already closed), or the caller, contract address, principal or selector differs from the session's. A proxy you write and deploy yourself is never PCL-registered — deployPclProxy registers only the proxies it deploys from the chain's own bytecode — so its calls always stop at the first check. Otherwise, check in a transaction trace that postCall received the sessionId that preCall returned and the same caller, contract address, principal and 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
Type to search