DENYLIST_POLICY

component compliance

Reject any transaction whose observed call route touches a denylisted address, even for zero-value calls.

DENYLIST_POLICY is a leaf policy template whose parameter is a list of addresses. PCL evaluates it against every observed call route captured during execution — both direct calls and nested routes that pass through a PCL-wrapped proxy — and against the sender and recipient of each transfer the transaction makes. A route need not move value: even a zero-value call that produces no Transfer log will trip the policy if the route touches a denylisted address. When a match is found the transaction reverts with InDenylist(address sender).

Parameter struct

Straight from IPcl.sol — a flat list of addresses. There is no per-address expiry or reason field; store additional metadata off-chain if you need it.
struct DenylistPolicy {
    address[] addresses;
}
// ABI tuple: (address[])

Evaluation covers call routes, not only transfers

After a successful EVM transaction, PCL's post-transaction hook checks the global denylist against every call route the transaction made, except calls that reverted. For ERC-4337 UserOperations run through the trusted EntryPoint, the postCall hook that the EntryPoint calls does this instead. It checks every observed CALL route (excluding delegate calls, whose execution context stays with the caller) against the global and the contract-scoped denylist. For each route the denylist is checked against the sender and the target. The transfers the transaction makes are checked too, against each transfer's sender and recipient, so an ERC-20 transfer to a denylisted recipient is rejected even though the recipient is never called. This is deliberately independent of whether the route moved any value: a nested zero-value call to a router that ultimately targets a denylisted address will reject the whole transaction. Previously the check was gated on the route having produced at least one native or ERC-20 Transfer log — that gate was removed so that denylist coverage is not bypassable by routing through zero-value calls.

Where the policy applies

Two scopes:

  • Global (GlobalPolicyConfig) — evaluated for every transaction on the chain, so a globally denylisted address cannot be reached from any regulated call path.
  • Contract-scoped (ContractPolicyConfig) — evaluated only when the transaction reaches the target through its PCL-wrapped proxy. Calls that hit the implementation contract directly (bypassing the proxy) do NOT trigger the contract-scoped denylist; publish the proxy address as the canonical entry point.


Attach a policy the usual way — wrap the encoded parameter in a PolicySet with templateId "DENYLIST_POLICY":
import { IPcl, PolicySet, ContractPolicyConfig, DenylistPolicy } from "@maroo-chain/contracts/precompiles/pcl/IPcl.sol";

DenylistPolicy memory dl = DenylistPolicy({ addresses: new address[](2) });
dl.addresses[0] = 0x8F3ac2B1d9E74c05A6B18FE27Dc4913e5A0F7b62;
dl.addresses[1] = 0x2c7f09B81a6D3FF1e5A0d4c6bC2a8f7E19dc3a4B;

PolicySet memory ps = PolicySet({
    templateId: "DENYLIST_POLICY",
    policy:     abi.encode(dl),
    selector:   ""  // empty → applies to every call on the target
});

Rejection payload

A denylist match reverts with the typed error InDenylist(address sender). The sender argument is the address that made the denylist match blocked — the observed route's principal or target, or a transfer's sender or recipient, depending on which side matched. Decode on the client with decodeErrorResult against the IPcl ABI:
import { BaseError, ContractFunctionRevertedError, decodeErrorResult } from "viem";
import { iPclAbi as pclAbi } from "@maroo-chain/contracts/abi/precompiles/pcl/IPcl";

try {
  await wallet.writeContract({ /* call the proxy... */ });
} catch (err) {
  // viem keeps the revert data on the ContractFunctionRevertedError in the cause chain, not on err.
  const revert = err instanceof BaseError ? err.walk((e) => e instanceof ContractFunctionRevertedError) : null;
  if (!(revert instanceof ContractFunctionRevertedError) || !revert.raw) throw err;
  const decoded = decodeErrorResult({ abi: pclAbi, data: revert.raw });
  if (decoded.errorName === "InDenylist") {
    // decoded.args[0] is the offending address; surface it in the UI.
  } else {
    throw err; // a different revert: not the case this example handles
  }
}

Composing with other templates

DENYLIST_POLICY is a leaf — it never checks attestations or volume. To add those checks — an attestation requirement (EAS_POLICY), a per-transaction cap (VOLUME_POLICY), a periodic window (PERIODIC_VOLUME_POLICY) — combine the templates in one PolicySet whose template is LOGICAL_POLICY with the And quantifier. Every child must pass, so a transaction is rejected as soon as one child fails, with that child's ReasonCode. Each child carries an empty selector; the selector goes on the outer PolicySet (see PCL composite policies).

Do not list the templates as separate PolicySet entries with the same selector: changeContractPolicies and setGlobalPolicies reject two entries for one selector with InvalidParameter(bytes input), where input is the selector as text (__all__ for the empty selector). Separate entries work only when each has a distinct selector. The OKRW_EAS_TRANSFER_LIMIT_POLICY and OKRW_EAS_PERIODIC_VOLUME_LIMIT_POLICY templates are still listed on the testnet, but a config that uses them reverts with UnknownPolicyType.
ESC
Type to search