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
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
}
}