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 non-PCL-managed CALL route captured during execution — both direct calls and nested routes that pass through a PCL-wrapped proxy. Evaluation is call-route based, not transfer based: 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 is call-route based, not transfer based

PCL's postCall hook inspects every observed non-PCL-managed CALL route (excluding delegate calls, whose execution context stays with the caller) captured during transaction execution. For each route the denylist is checked against the sender and the target. 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 enforcement points, both driven by the same call-route observation model:

  • 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 depending on which side matched. Decode on the client with decodeErrorResult against the IPcl ABI:
import { decodeErrorResult } from "viem";

try {
  await wallet.writeContract({ /* call the proxy... */ });
} catch (err: any) {
  const decoded = decodeErrorResult({ abi: pclAbi, data: err.data });
  if (decoded.errorName === "InDenylist") {
    // decoded.args[0] is the offending address; surface it in the UI.
  }
}

Composing with other templates

DENYLIST_POLICY is a leaf — it never checks attestations or volume. Compose it with the attestation and cap templates when you need broader coverage:

  • Attestation-only checks live in EAS_POLICY, OKRW_EAS_TRANSFER_LIMIT_POLICY, and OKRW_EAS_PERIODIC_VOLUME_LIMIT_POLICY.
  • Value caps live in VOLUME_POLICY and PERIODIC_VOLUME_POLICY.
  • Multiple PolicySet entries in the same enclosing PolicyConfig are combined with AND semantics — any single failure rejects the whole transaction with that policy's ReasonCode.
ESC
Type to search