IPcl.preCall

preCall(
  address contractAddress,
  address principal,
  bytes calldata data,
  uint256 value
) external returns (bytes32 sessionId)

Opens a PCL enforcement session for a regulated call. The immediate msg.sender must be one of two authorized caller kinds: a PCL-registered proxy invoking its own target (contractAddress == msg.sender), or a governance-trusted ERC-4337 EntryPoint executing a bundle. In both cases PCL evaluates global and contract-scoped policies against the forwarded principal (the EOA or smart-account that originated the call) with the extracted 4-byte selector and value. If any before-execution policy fails the call reverts with the corresponding PCL ReasonCode; otherwise PCL installs an internal tracer, registers a session keyed by a derived sessionId, and returns it. The paired postCall invocation must present the same sessionId to close the session and run after-execution evaluation. A staticcall (readonly) execution returns the zero bytes32 and does not open a session — policy state is evaluated inside a cached context and discarded.

Parameters

Name Type Required Description
contractAddress address ✓ The target contract of the regulated call. When the caller is a PCL proxy this MUST equal msg.sender — otherwise PCL reverts with Unauthorized. When the caller is a trusted EntryPoint this constraint is relaxed because the EntryPoint dispatches into the SmartAccount which then calls the target.
principal address ✓ The semantically relevant caller — the EOA or ERC-4337 smart-account address that originated the call. The proxy/EntryPoint must forward this value verbatim; PCL uses it as the sender for contract-scope denylist and periodic-volume checks. If PCL only saw the immediate caller (proxy or EntryPoint) sender-side rules could never match a real user.
data bytes ✓ The calldata being delegated. PCL slices the leading 4 bytes as the function selector for selector-scoped policies. Data shorter than 4 bytes is treated as no-selector (empty selector).
value uint256 ✓ Native OKRW value attached to the underlying call, in atokrw. Non-zero values feed into VOLUME_POLICY / PERIODIC_VOLUME_POLICY evaluation on the native denom.

Returns

Type: bytes32

A session identifier derived from the call metadata plus a per-transaction sequence. The paired postCall must echo this value. For staticcall, returns the zero hash.

Errors

Code Name Description
Unauthorized Unauthorized Reverts when the immediate caller is neither a registered PCL proxy nor a trusted EntryPoint, or when a PCL-proxy caller passes a contractAddress different from its own address.
InDenylist InDenylist Reverts when the resolved principal (or the target contract) matches a denylist entry in either the contract or global policy config.
VolumeAboveMaxLimit VolumeAboveMaxLimit Reverts when the attached native value alone exceeds a VOLUME_POLICY maximum bound.
ExceededPeriodicVolume ExceededPeriodicVolume Reverts when the attached native value would push the principal over a PERIODIC_VOLUME_POLICY cap for the current window. Inside a composite policy the composite decides the outcome: 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 applies its daily cap to each of the principal's agent owners (or to the principal itself when it has none), paired in an Or with that account's EAS attestation, so there exceeding it reverts with AnyOfRejected unless that attestation check passes.
EasNoAttestationReceived EasNoAttestationReceived Reverts when an EAS_POLICY on the target finds no attestation the principal received under its schema in the EAS Indexer: none was issued, or it was issued but never indexed (index it with the Indexer's indexAttestation(uid)).
EasAttestationRevoked EasAttestationRevoked Reverts when the principal's most recently indexed attestation under the schema was revoked. An older valid one does not count.
EasAttestationExpired EasAttestationExpired Reverts when that attestation's expirationTime has passed.
EasAttestationLookupFailed EasAttestationLookupFailed Reverts when the attestation the EAS Indexer lists for the principal cannot be read from the policy's EAS contract (easContract) — check that address.
EasAttestationRequired EasAttestationRequired Declared in the same error set, but no policy raises it: a principal with no indexed attestation reverts with EasNoAttestationReceived.

Examples

PCL proxy invoking preCall for its own target

Classic PCL proxy hook path. The proxy's own address must match contractAddress; the human user is forwarded as principal.

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

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

contract RegulatedProxy {
    address internal immutable implementation;

    constructor(address impl) { implementation = impl; }

    /// @notice A PCL-wrapped proxy dispatches user calls through preCall/postCall.
    ///         msg.sender at the PCL precompile will be `address(this)` — the proxy itself.
    function forward(bytes calldata data) external payable returns (bytes memory) {
        // The proxy passes itself as `contractAddress` (must equal msg.sender at the precompile),
        // and the real caller as `principal`.
        bytes32 sessionId = PCL_CONTRACT.preCall(address(this), msg.sender, data, msg.value);

        (bool ok, bytes memory ret) =
            implementation.delegatecall(data);

        // Any implementation revert is surfaced to postCall via `workable=false`.
        PCL_CONTRACT.postCall(sessionId, address(this), msg.sender, data, msg.value, ok);

        require(ok, "impl reverted");
        return ret;
    }
}

Decoding preCall reverts on the client

Every preCall failure is a typed error on IPcl. Decode with the interface ABI to branch UX on the specific ReasonCode.

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

const PCL = "0x1000000000000000000000000000000000000005" as const;
const publicClient = createPublicClient({ transport: http("https://rpc-testnet.maroo.io") });

try {
  await publicClient.simulateContract({
    address: "0x8F3ac2B1d9E74c05A6B18FE27Dc4913e5A0F7b62", // TODO: replace with real proxy address
    abi: [/* proxy ABI */],
    functionName: "forward",
    args: ["0x"],
  });
} 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 ReasonCode and the IPrecompile errors it inherits. A composite (an Or) that
    // rejects — how the testnet's global caps reject — arrives as AnyOfRejected, carrying each child's revert.
    const decoded = decodeErrorResult({ abi: iPclAbi, data: revert.raw });
    console.error("PCL preCall rejected:", decoded.errorName, decoded.args);
  } else {
    throw err; // not a revert (a wrong ABI, a network error): let it surface
  }
}
ESC
Type to search