How to Simulate Compliance Checks Before Sending

integration intermediate

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.
ESC
Type to search