DENYLIST_POLICY
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):
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.
| Scenario | Where it rejects | What the client sees |
|---|---|---|
EOA from is denylisted | Broadcast filter | RPC error on send; no txhash, no receipt |
userOp.sender (smart account) is denylisted | Broadcast filter | RPC error on eth_sendRawTransaction for the outer handleOps; no txhash, no receipt |
| Denylisted address is only referenced as an inner-call recipient / calldata argument | Post-execution | handleOps 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]);
}
}