DENYLIST_POLICY

component compliance

Rejects any transaction whose principal — the EOA signer or the smart-account sender of an ERC-4337 userOp — appears in the configured address list.

DENYLIST_POLICY is the simplest built-in PCL policy template: it carries a list of addresses that are not allowed to originate value-moving transactions. PCL evaluates the principal of each call — the EOA that signed a normal transaction, or the smart-account sender field of an ERC-4337 userOp — against the list. When the principal matches, the transaction is rejected before execution with the InDenylist reason code. The list can be attached as a global rule via GlobalPolicyConfig (chain-wide, admin-managed) or as a contract-scoped rule via ContractPolicyConfig.

Parameter struct

The template parameter struct comes verbatim from IPcl.sol. addresses is the flat list of principals to block; there is no allow-list companion field.
struct DenylistPolicy {
    address[] addresses;
}
// ABI tuple: (address[])

Building a PolicySet

Encode the struct with abi.encode and wrap it in a PolicySet with template id "DENYLIST_POLICY". Leave selector empty to apply the rule to every call on the target contract:
import { IPcl, PolicySet, DenylistPolicy } from "@maroo-chain/contracts/IPcl.sol";

address[] memory blocked = new address[](2);
blocked[0] = 0xAaAa000000000000000000000000000000000001; // replace before production
blocked[1] = 0xBbBb000000000000000000000000000000000002; // replace before production

DenylistPolicy memory dl = DenylistPolicy({ addresses: blocked });

PolicySet memory ps = PolicySet({
    templateId: "DENYLIST_POLICY",
    policy:     abi.encode(dl),
    selector:   ""   // empty = applies to all calls
});

Global vs contract scope

A denylist attached via setGlobalPolicies blocks the listed principals from originating any transaction on the chain — this is typically how sanctions lists (chain-wide policy admin) are applied. A denylist attached via registerContractPolicies only blocks calls that target that specific contract; useful when a single application wants its own blocklist without touching global state.

Principal resolution — EOAs and ERC-4337 smart accounts

PCL evaluates the transaction principal, not just the immediate caller. For a directly signed transaction the principal is the EOA from. For an ERC-4337 userOp the principal is the smart account address in userOp.sender — not the bundler that submitted handleOps, and not the owner EOA that signed the userOp. Denylisting a smart-account address therefore blocks all userOps whose sender is that account, regardless of which bundler broadcasts them.
// Denylist a smart account address so no bundler can submit a userOp on its behalf.
address[] memory blocked = new address[](1);
blocked[0] = smartAccountAddr; // the AA wallet's on-chain address

DenylistPolicy memory dl = DenylistPolicy({ addresses: blocked });
IPcl(0x1000000000000000000000000000000000000005).setGlobalPolicies(
    GlobalPolicyConfig({
        policies: _wrapAsPolicySet("DENYLIST_POLICY", abi.encode(dl))
    })
);

Rejection surface — where the revert appears

How the rejection surfaces at the client depends on whether the denylisted address is the transaction signer / userOp.sender (pre-execution filter) or an inner-call recipient (post-execution enforcement):

ScenarioWhere it rejectsWhat the client sees
EOA from is denylistedBroadcast filterRPC error on send; no txhash, no receipt
userOp.sender (smart account) is denylistedBroadcast filterRPC error on eth_sendRawTransaction for the outer handleOps; no txhash, no receipt
Denylisted address is only referenced as an inner-call recipient / calldata argumentPost-executionhandleOps mines with status = 0; receipt is present; decode revert against IPcl ABI to see InDenylist

The practical UX implication: an integrator wrapping ERC-4337 must handle two failure paths — a hard RPC error before submission, and a mined-but-failed handleOps receipt after.

Decoding the rejection

When a receipt is present (post-execution case), decode the revert payload against the IPcl ABI. InDenylist carries the offending sender address so the UI can show which party was blocked:
import { decodeErrorResult } from "viem";

try {
  await walletClient.writeContract({
    address: entryPointAddress,
    abi: entryPointAbi,
    functionName: "handleOps",
    args: [userOps, beneficiary],
  });
} catch (err: any) {
  // Broadcast-reject path: no err.data / no receipt.
  if (!err?.data) {
    console.log("PCL blocked at broadcast — likely denylisted signer or userOp.sender");
    return;
  }
  // Post-execution path: revert payload is ABI-encoded IPcl error.
  const decoded = decodeErrorResult({ abi: pclAbi, data: err.data });
  if (decoded.errorName === "InDenylist") {
    console.log("blocked principal:", decoded.args[0]);
  }
}
Source: maroo
ESC
Type to search