PCL Dual-Track Transaction Model
Execute transactions in either an open, permissionless context or a regulated, compliant context — with inner transfers on the regulated track attributed back to the original caller for sender-scoped policy checks.
Maroo's PCL exposes two execution contexts for smart-contract interactions. The 'Open Track' is a standard, direct EVM call that bypasses contract-scoped policy checks (global policies still apply). The 'Regulated Track' is an opt-in flow — either IPcl.runOnPcl(...) or a PCL-wrapped proxy — which enforces every applicable global and contract-scoped policy before executing the wrapped call. Within the regulated frame, PCL also observes inner transfers emitted by the target contract and attributes them back to the original caller for sender-scoped global checks (e.g., EAS attestation) so that a contract-funded payout still requires the principal to satisfy the sender's compliance requirements.
Two tracks, one enforcement engine
IPcl.runOnPcl(target, data, value) for one-shot regulated execution and a PCL-wrapped proxy (deployPclProxy / PclTransparentUpgradeableProxy) for contracts that should always be called regulated.// Open track — direct call. Global policies apply; contract-scoped policies do NOT.
IERC20(token).transfer(recipient, 10_000_000 ether);
// Regulated track — routed through PCL. Global + contract-scoped policies apply,
// and PCL observes inner transfers emitted by `target`.
IPcl(0x1000000000000000000000000000000000000005).runOnPcl(
target,
abi.encodeCall(IPayout.pay, (recipient, 10_000_000 ether)),
0
); Inner transfers and caller attribution
from = actual emitter, to = actual recipient). For sender-scoped global policies (denylist-of-sender, EAS attestation of sender, sender periodic-volume caps), PCL attributes the inner transfer to the original caller — the EOA or AA principal that entered the regulated frame — rather than to the intermediate contract. Without this remap, an attested user calling a payout contract would fail attestation checks whenever the contract funded the outgoing leg, because the contract itself is not attested.Funding source decides accumulation
msg.value or transferFrom(caller, …) the transfer is a caller outflow and is fully counted (including recorded periodic-volume state). If the target contract funds the transfer from its own balance, the transfer is still evaluated against sender-scoped global policies as though the caller were the sender, but periodic-volume state is NOT recorded against the caller for the contract-funded leg (only true caller outflows accumulate).// Caller-funded outflow — counted fully, including periodic-volume accumulation.
// PCL sees this as caller → recipient for every policy scope.
runOnPcl(payout, abi.encodeCall(IPayout.payFromSender, (recipient, 10_000_000 ether)), 10_000_000 ether);
// Contract-funded outflow — the payout contract calls recipient.call{value: X}(...)
// with its own balance. PCL still evaluates sender-scoped GLOBAL policies
// (denylist, EAS attestation, periodic caps) as if the caller were the sender —
// but does not add the amount to the caller's periodic-volume counter. When to choose which track
ContractPolicyConfig you want enforced (KYC-gated pools, compliant off-ramps, regulated stablecoin payouts), or when you want PCL to observe inner transfers and enforce sender-scoped global rules against the principal even when the contract funds the outgoing leg. dApp UIs should surface which track a given action uses so users understand which policies gate the transaction.Client-side check via runOnPcl simulation
IPcl.runOnPcl itself with eth_call — an off-chain simulation persists nothing, so it runs the full global + contract-scope evaluation and surfaces the exact ReasonCode the user would hit, without spending gas (see contract-pcl-run-simulation). Do not call preCall / postCall directly for this: those are the PCL proxy hook's internal pair — preCall reverts Unauthorized unless the immediate caller is a registered PCL proxy (contract-pcl-pre-call).import { createPublicClient, http } from "viem";
const PCL = "0x1000000000000000000000000000000000000005";
const publicClient = createPublicClient({ transport: http(MAROO_RPC) });
try {
// eth_call simulation: full policy evaluation, nothing persisted, no gas spent.
await publicClient.simulateContract({
address: PCL,
abi: pclAbi,
functionName: "runOnPcl",
args: [targetContract, callData, valueWei],
account: principal,
value: valueWei,
});
// Policies pass — safe to send the real transaction.
} catch (err) {
// Decode the typed PCL ReasonCode from the revert data and show it to the user.
}