PCL Dual-Track Transaction Model

mechanism compliance

Every EVM call on Maroo runs on one of two tracks — an open track for direct calls, and a regulated track where a PCL-wrapped proxy invokes preCall / postCall on the PCL precompile around the underlying execution.

Maroo separates transaction execution into two contexts. In the open track, a call goes directly to a target contract and only the global GlobalPolicyConfig (evaluated at the transaction entry point) applies — no contract-scoped policies fire. In the regulated track, users transact through a PCL-registered proxy address; that proxy's hook path calls IPcl.preCall(...) before the underlying execution and IPcl.postCall(...) after, so any ContractPolicyConfig bound to the proxy is enforced atomically around the call. Contract admins choose the track by publishing either the raw implementation address (open) or the PCL-registered proxy address (regulated) as their user-facing entry point.

Architecture

flowchart LR
    U["User / dApp"]:::evm
    IMPL["Implementation contract"]:::evm
    PROXY["PCL-registered proxy"]:::evm
    PCL["IPcl precompile<br/>0x…0005"]:::precompile
    U -->|"open track:<br/>direct call"| IMPL
    U -->|"regulated track"| PROXY
    PROXY -->|"preCall(...)"| PCL
    PROXY -->|"delegatecall"| IMPL
    PROXY -->|"postCall(...)"| PCL
    classDef evm fill:#0096AA,stroke:#0096AA,color:#fff;
    classDef precompile fill:#FF8C50,stroke:#FF8C50,color:#fff;

Open track: user calls the implementation directly; only global policy applies. Regulated track: user calls the PCL-registered proxy, whose hook path invokes `preCall` and `postCall` on the PCL precompile around the delegatecall to the implementation.

Open track — direct EVM calls

A transaction whose to field is a plain contract address executes with standard EVM semantics. The transaction-entry policy check still evaluates the global GlobalPolicyConfig (denylists, cross-contract periodic caps, etc.), but no ContractPolicyConfig is loaded because the target is not a PCL-registered proxy. This is the appropriate track for utility contracts that do not need per-function compliance gates.

Regulated track — PCL-wrapped proxy

The regulated track is entered by calling a proxy address that was deployed through IPcl.deployPclProxy(...) and is therefore present in the PCL registry (see pcl-proxy). Every call through such a proxy is bracketed by two calls to the PCL precompile:

1. preCall(contractAddress, principal, data, value) — evaluates all applicable policies (global + the proxy's ContractPolicyConfig) and returns a session id.
2. The proxy forwards to the implementation and executes the requested function.
3. postCall(sessionId, contractAddress, principal, data, value, workable) — finalises periodic-volume accounting and emits PolicyCheckPassed on success.

If preCall reverts, the underlying execution never runs. If postCall reverts, the whole transaction rolls back. This block is a shape sketch, not compilable source — it shows the call sequence, not code to paste.
// Simplified sketch of what a PCL-wrapped proxy's hook path does around a
// user call. Real proxy bytecode is embedded in the chain binary and
// deployed via IPcl.deployPclProxy; users never write this themselves.
interface IPcl {
    function preCall(address contractAddress, address principal, bytes calldata data, uint256 value)
        external returns (bytes32 sessionId);
    function postCall(bytes32 sessionId, address contractAddress, address principal, bytes calldata data, uint256 value, bool workable)
        external returns (bytes memory);
}

address constant PCL = 0x1000000000000000000000000000000000000005;

function _regulatedForward(address impl, bytes calldata data) internal returns (bytes memory) {
    bytes32 sid = IPcl(PCL).preCall(address(this), msg.sender, data, msg.value);
    (bool ok, bytes memory ret) = impl.delegatecall(data);
    IPcl(PCL).postCall(sid, address(this), msg.sender, data, msg.value, ok);
    require(ok, "call failed");
    return ret;
}

Choosing a track

The choice is made by publishing the address you want end users to call:

TrackUser-facing addressGlobal policyContract policy
OpenImplementation contract addressEnforced at tx entryNot evaluated
RegulatedPCL-registered proxy addressEnforced at tx entryEnforced by preCall / postCall

A regulated dApp typically deploys a stock implementation (an OpenZeppelin ERC20, an AMM pool, etc.), wraps it with IPcl.deployPclProxy(...), binds a ContractPolicyConfig to the proxy via IPcl.changeContractPolicies(...), and lists the proxy address in its docs and front-end. Direct calls to the implementation skip contract-scoped rules, so the proxy address must be treated as the canonical entry point.

Failure surface

A regulated-track failure produces a typed PCL revert. The most common ReasonCodes seen at the boundary are EasNoAttestationReceived, EasAttestationRevoked, EasAttestationExpired, InDenylist, ExceededPeriodicVolume, VolumeAboveMaxLimit, and ExceededAgentTransferLimit. Decode with decodeErrorResult against the PCL ABI to route the failure to a useful user message.
import { createWalletClient, http, parseEther, decodeErrorResult } from "viem";
import { pclAbi, erc20Abi } from "./abis";

// TODO: replace with the real PCL-registered proxy address before production.
const PROXY = "0x8F3ac2B1d9E74c05A6B18FE27Dc4913e5A0F7b62" as const;

const wallet = createWalletClient({ transport: http("https://rpc-testnet.maroo.io") });

try {
  await wallet.writeContract({
    address: PROXY,
    abi: erc20Abi,
    functionName: "transfer",
    // TODO: replace with the real recipient before production.
    args: ["0x2c7f09B81a6D3FF1e5A0d4c6bC2a8f7E19dc3a4B", parseEther("10000000")],
  });
} catch (err: any) {
  if (err?.data) {
    const decoded = decodeErrorResult({ abi: pclAbi, data: err.data });
    console.error("regulated-track rejection:", decoded.errorName, decoded.args);
  } else {
    throw err;
  }
}
ESC
Type to search