How to Simulate Compliance Checks Before Sending
A focused guide on statically simulating a call to a PCL-wrapped proxy so PCL evaluates every applicable policy before the user signs, improving dApp user experience and preventing wasted gas.
Maroo lets you find out whether a transaction will pass the network's compliance rules before a user signs and sends it. The technique is an ordinary static call to a PCL-wrapped proxy: the proxy's hook calls
IPcl.preCall before delegating to the logic contract, so a simulated call runs the full global + contract-scoped policy evaluation and reverts with the exact ReasonCode the user would have hit — without spending gas or persisting anything. This guide walks through it from a JavaScript/TypeScript frontend.Prerequisites
- Knowing which address is the PCL-wrapped proxy your users call.
- A client-side setup with Ethers.js or a similar library.
1. The Goal: Pre-flight Checks
Imagine a user wants to transfer a large amount of a regulated token. If they submit the transaction and it fails due to a PCL policy (like a transfer limit), they still pay for gas. This is a poor user experience. Our goal is to simulate the call against the PCL-wrapped proxy first and give the user immediate feedback, letting them adjust the amount or abort without wasting funds.
2. Setting Up the Contracts
You need a contract instance bound to the proxy address using the logic contract's ABI (e.g. an ERC20 token's) — that is the address your users actually call. Keep the
IPcl ABI around too: PCL's typed ReasonCodes are what a failed simulation reverts with, and you decode them with that ABI.import { ethers } from 'ethers';
import PCL_ABI from './PclAbi.json';
import TOKEN_ABI from './TokenAbi.json';
const PCL_ADDRESS = '0x1000000000000000000000000000000000000005';
const TOKEN_PROXY = '0x...'; // PCL-wrapped proxy in front of the token logic
// Assume 'provider' is an ethers Provider instance
const pclContract = new ethers.Contract(PCL_ADDRESS, PCL_ABI, provider); // for introspection + error decoding
const tokenContract = new ethers.Contract(TOKEN_PROXY, TOKEN_ABI, provider); // what users call
3. Confirm the Target Is PCL-Wrapped
A static call only performs a policy evaluation if the address you call actually carries the PCL hook.
IPcl.pclProxy(address) returns that contract's registry entry; every field is zero when the address is not a registered PCL proxy, so a kind of Unspecified (0) means a simulation against it would tell you nothing about contract-scoped policies. Do this check once at load time rather than per transaction.// PclProxyKind: 0 = Unspecified, 1 = Transparent, 2 = UUPS, 3 = Beacon
const entry = await pclContract.pclProxy(TOKEN_PROXY);
if (Number(entry.kind) === 0) {
// Not a registered PCL proxy — contract-scoped policies will not be evaluated.
// Global policies still apply to the real transaction.
}
4. Performing the Static Call
Simulate by calling the method on the proxy with a static call: it executes on a node without creating a transaction or changing state, and the hook runs regardless. Crucially, provide the
from address — PCL evaluates sender-scoped policies against the principal, and the hook forwards it, so a simulation without from checks the wrong actor.// ethers v6 — call .staticCall on the method, not the v5 contract.callStatic
async function isTransferAllowed(userAddress, recipient, amount) {
try {
await tokenContract.transfer.staticCall(
recipient,
amount,
{ from: userAddress } // This is the most important part!
);
console.log('Simulation passed. The transaction is allowed.');
return true;
} catch (error) {
console.error('Simulation failed. Transaction would be blocked.');
// PCL custom errors are ABI-encoded in error.data; decode with the IPcl ABI.
if (error.data) {
console.error('ReasonCode:', pclContract.interface.parseError(error.data));
}
return false;
}
}
// Usage:
isTransferAllowed(user.address, recipient, ethers.parseEther('50000'));
Note: Use
<method>.staticCall(...) (ethers v6) or eth_call via JSON-RPC. Ethers v5's contract.callStatic.x(...) was removed in v6 — newly built dApps should use the v6 form. Conclusion
By integrating this simulation logic into your dApp, you can create a more intelligent and responsive user interface. You can disable a 'Send' button, show a warning message, or suggest a valid transaction amount, all based on the real-time feedback from the PCL before any gas is spent.