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 aokrw. Non-zero values feed into VOLUME_POLICY / PERIODIC_VOLUME_POLICY evaluation on the native denom. |
Returns
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. |
EasAttestationRequired | EasAttestationRequired | Reverts when an EAS_POLICY-bound target requires an attestation on the resolved principal and none is present. |
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.22;
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 { createPublicClient, http, decodeErrorResult } from "viem";
const PCL = "0x1000000000000000000000000000000000000005" as const;
const pclAbi = [
{ type: "function", name: "preCall", stateMutability: "nonpayable",
inputs: [
{ name: "contractAddress", type: "address" },
{ name: "principal", type: "address" },
{ name: "data", type: "bytes" },
{ name: "value", type: "uint256" },
], outputs: [{ type: "bytes32" }] },
{ type: "error", name: "Unauthorized", inputs: [] },
{ type: "error", name: "InDenylist", inputs: [{ name: "sender", type: "address" }] },
{ type: "error", name: "ExceededPeriodicVolume",
inputs: [
{ name: "maxLimit", type: "uint256" },
{ name: "value", type: "uint256" },
{ name: "resetAt", type: "uint256" },
] },
] 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: any) {
if (err?.data) {
const decoded = decodeErrorResult({ abi: pclAbi, data: err.data });
console.error("PCL preCall rejected:", decoded.errorName, decoded.args);
}
}