How the SolMask banlist works

A mechanics-level walk-through of the on-chain banlist: the registry layout, the deposit-time check, and the off-chain pipeline that maintains the list.

The registry account

Each banned pubkey corresponds to a single program-derived BanEntry account. The seed scheme is [b"ban", banned_pubkey.as_ref()], which yields a deterministic address: any client that knows a pubkey can compute the BanEntry PDA for that pubkey locally and check whether the account exists with a single RPC call. There is no separate registry account to scan and no per-list pagination.

BanEntry accounts store metadata about the listing — the source (e.g. cipherowl), the listing timestamp, and a small free-form reason field that the listing authority can populate (e.g. an OFAC reference number). Storage is intentionally minimal: the value of the registry is that the account exists, not what is in it.

The deposit-time check

The deposit instruction in programs/solmask/src/instructions/deposit.rs takes the depositor's signing pubkey and the BanEntry PDA derived from it as part of the accounts list. The handler runs roughly:

// Derive expected BanEntry PDA for the depositor.
let (expected, _bump) = Pubkey::find_program_address(
    &[b"ban", ctx.accounts.depositor.key().as_ref()],
    ctx.program_id,
);
require_keys_eq!(ctx.accounts.ban_entry.key(), expected);

// If the account exists (non-zero data), the depositor is banned.
if !ctx.accounts.ban_entry.data_is_empty() {
    return err!(SolMaskError::DepositorBanned);
}

The check happens before any token movement or commitment-tree update, so a banned deposit consumes only the transaction fee and leaves no other on-chain footprint. A banned user cannot incrementally probe the banlist in a way that pollutes the pool's state.

The off-chain pipeline

New listings come from Cipherowl's aggregated risk feeds (which themselves ingest Chainalysis, TRM, and OFAC lists). A scheduled worker pulls the delta since the last sync and submits a transaction that creates one new BanEntry per newly listed pubkey. To control RPC cost the worker caches risk responses with a multi-week TTL — sanctions lists change slowly enough that polling per-pubkey on every deposit would be wasteful and slow.

Removals follow the same path in reverse. If a pubkey is delisted upstream (de-sanctioned, false-positive corrected) the worker closes the corresponding BanEntry, which returns the rent to the listing authority and allows the previously banned address to deposit again. Every list change is a regular Solana transaction signed by the listing authority, so the entire history of the banlist is reconstructible from the on-chain log.

What this design buys and what it doesn't

It buys auditable, structural enforcement at the entry point — the highest- leverage place to enforce screening, because it gates whether funds enter the privacy set at all. It does not turn SolMask into a compliance platform in any larger sense; the protocol cannot freeze in-pool funds, cannot unwind a withdraw, cannot identify a withdrawer from on-chain state. Those properties are deliberate. The banlist is the one place where a regulator- facing constraint is implemented on-chain, and that scope is the whole point.

See also: the compliance pillar page for the broader posture, and audits & ceremony for the verification record on the program code itself.

FAQ

Where in the program is the banlist enforced?
Inside the deposit instruction handler (deposit.rs). The instruction derives the BanEntry PDA for the depositor's signing pubkey using the seed scheme described above and includes the account in the instruction's accounts list as a non-mutable, optional account. The handler aborts with a `DepositorBanned` error if that account exists on-chain.
Why aren't withdraws checked against the banlist?
Withdraws are authorised by knowledge of a note secret, not by a signing pubkey. The withdraw transaction can be submitted by any address (including the protocol relayer) and pays out to an address the prover specifies inside the proof. There is no on-chain identity at the withdraw step to check against. Screening happens at entry — once funds are in the pool, they are governed by the note commitment.
Who can add or remove entries?
The listing authority — a multisig owned by the SolMask core team — is the only account permitted to create a BanEntry. Removal follows the same authority. The authority pubkey is recorded in the program's config account and is publicly verifiable on-chain.
What happens if Cipherowl flags an address after a deposit?
The address can still withdraw — there is no on-chain identity at the withdraw step. What changes is that any future deposit from that address will fail. The banlist is forward-looking from the moment of listing; it does not retroactively freeze in-pool balances. That would require a different design (with very different trade-offs).
How the SolMask banlist works · SolMask