> ## Documentation Index
> Fetch the complete documentation index at: https://base-a060aa97-docs-sync-code-change-1505323.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# B20: Seize Surface and burnBlocked Deprecation

> The B20 seize surface at Cobalt and the deprecation of burnBlocked. Migration notes for teams integrated against the Beryl seize path.

> **Audience:** teams integrated against the base B20 surface on Beryl (live today) that perform
> administrative balance removal, today via the deprecated `burnBlocked`. This note covers only the
> seize surface landing at the Cobalt hardfork and what it means for `burnBlocked`. The surface is
> shared, so it applies to both B20 Asset and B20 Stablecoin.

## Summary

At Cobalt, the base B20 surface gains a first-class seize operation. `seizeWithMemo(from, to,
amount, memo)` reassigns a holder's balance to a destination in one admin call, gated by a new
`SEIZE_ROLE`, a new `SEIZE` pause vector, and two new policy slots (`SEIZE_EXEMPT_POLICY`,
`SEIZE_RECEIVER_POLICY`).

Nothing you call today breaks: every Beryl selector, event topic, and error keeps its exact 4-byte
selector or topic0 and stays dialable at Cobalt. In particular, `burnBlocked` is deprecated but
unchanged (same selector, same events, same behavior) and remains callable.

To migrate, move administrative balance removal from `burnBlocked` to `seizeWithMemo`: seize to a
treasury or self address, then call `burn` if you want the supply destroyed.

Seize is opt-in per token. The surface exists at Cobalt, but seize does nothing until the issuer
configures `SEIZE_EXEMPT_POLICY`. With the slot unset (always-allow), no account is seizable, and
every `seizeWithMemo` call reverts `AccountNotSeizable`. An issuer that never sets the policy has, in
effect, no seize capability on that token.

Cobalt hasn't gone live yet. Until it activates, only the Beryl surface exists on-chain, and every
`seize*`/`SEIZE_*` selector below is undialable.

## Mapping Table

The selectors and topic0s below are the real values from the frozen ABIs:
`crates/common/precompiles/src/common/abi/v1.rs` for Beryl,
`crates/common/precompiles/src/common/abi/v2.rs` for Cobalt. Every Beryl symbol keeps its selector
at Cobalt. Seize lives on the shared `IB20` surface, so it's identical across Asset and Stablecoin.

### Functions

| Beryl symbol (selector)                     | Cobalt (selector)                                             | Status                 | Why                                                                                                                                                  |
| ------------------------------------------- | ------------------------------------------------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `burnBlocked(address,uint256)` `0xec0cf3dc` | `burnBlocked(address,uint256)` `0xec0cf3dc`                   | deprecated-dialable    | Kept unchanged for backward compatibility. Prefer `seizeWithMemo` then `burn`. Destroys supply and reads `TRANSFER_SENDER_POLICY`.                   |
| `BURN_BLOCKED_ROLE()` `0x32ad9be8`          | `BURN_BLOCKED_ROLE()` `0x32ad9be8`                            | carried over unchanged | Still gates `burnBlocked` only.                                                                                                                      |
| —                                           | `seizeWithMemo(address,address,uint256,bytes32)` `0xf916d81b` | new                    | Admin balance reassignment. A transfer, not a burn.                                                                                                  |
| —                                           | `SEIZE_ROLE()` `0x3c7e9ba5`                                   | new                    | Required to call `seizeWithMemo`. Value `keccak256("SEIZE_ROLE")` = `0x3469b8b0d89e9604f8510ed143f74a8336d22955d4f83e23bf53d9414e27f432`.            |
| —                                           | `SEIZE_EXEMPT_POLICY()` `0xfeb346ec`                          | new                    | Policy slot checked against `from`. Value `keccak256("SEIZE_EXEMPT_POLICY")` = `0xedb5da348cfb67af08746d3afd1be81034b50d5c8576f31aff688f39dfd540ed`. |
| —                                           | `SEIZE_RECEIVER_POLICY()` `0xb31da27f`                        | new                    | Policy slot checked against `to`. Value `keccak256("SEIZE_RECEIVER_POLICY")` = `0xbf15b19caf5c77422c038bc25f26b8b815c3a14f6d04c6616076b81bcfe07b3d`. |

### Events

| Beryl event (topic0)                                                                                          | Cobalt (topic0)                                                                                                | Status                   | Why                                                                                     |
| ------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | ------------------------ | --------------------------------------------------------------------------------------- |
| `BurnedBlocked(address,address,uint256)` `0x0b552e96653fd6842da37c477005d3b5c08a8c7d3631b1f43787b2dc9a1006a3` | unchanged                                                                                                      | deprecated-still-emitted | Still emitted by `burnBlocked` alongside `Transfer(from, address(0), amount)`.          |
| —                                                                                                             | `Seized(address,address,address,uint256)` `0xa9aec5d8b86e2fa2fd6ac3af62f2622e3dfdab1967d4cbbb56a5df7d74cb887c` | new                      | Emitted by `seizeWithMemo` after `Transfer(from, to, amount)` and `Memo(caller, memo)`. |

### Errors

| Beryl error (selector)                    | Cobalt (selector)                          | Status                   | Why                                                                                                      |
| ----------------------------------------- | ------------------------------------------ | ------------------------ | -------------------------------------------------------------------------------------------------------- |
| `AccountNotBlocked(address)` `0x64a5cb46` | unchanged                                  | present on Beryl already | Thrown by `burnBlocked` when `from` is authorized under `TRANSFER_SENDER_POLICY` (that is, not blocked). |
| —                                         | `AccountNotSeizable(address)` `0x91dbbc8d` | new                      | Thrown by `seizeWithMemo` when `from` is authorized under `SEIZE_EXEMPT_POLICY` (that is, not seizable). |

### Pause Features

`PausableFeature` is append-only. Cobalt adds one ordinal.

| Beryl ordinals                   | Cobalt addition | Storage bit  | Why                                                                                          |
| -------------------------------- | --------------- | ------------ | -------------------------------------------------------------------------------------------- |
| `TRANSFER=0`, `MINT=1`, `BURN=2` | `SEIZE=3`       | `1 << 3 = 8` | Independent pause vector for `seizeWithMemo`. `ALL_FEATURES_PAUSED` becomes `15` (`0b1111`). |

`seizeWithMemo` is gated by the new `SEIZE` vector, not `BURN`. `burnBlocked` stays under `BURN`.

## New at Cobalt: Adopt These

### `seizeWithMemo(from, to, amount, memo)`

This is the canonical administrative balance-removal path. It's a transfer: the balance moves from
`from` to `to`, and `totalSupply` is unchanged. It runs as an admin operation that skips allowance
and the transfer policies (`TRANSFER_SENDER/RECEIVER/EXECUTOR_POLICY`). It emits, in order:

1. `Transfer(from, to, amount)`
2. `Memo(caller, memo)` (a memo of `bytes32(0)` is allowed)
3. `Seized(caller, from, to, amount)`

Requirements and guards:

* **Role**: the caller must hold `SEIZE_ROLE`, or the call reverts `AccessControlUnauthorizedAccount`.
* **Pause**: `SEIZE` must not be paused, or the call reverts `ContractPaused(SEIZE)`.
* **Addresses**: `to != address(0)`, `to != address(this)`, and `from != to`, or the call reverts
  `InvalidReceiver`. `from != address(0)`, or the call reverts `InvalidSender`. Note that `from`
  *may* equal `address(this)`: seizing from the token's own address is permitted so that any balance
  accidentally credited there can be recovered to a treasury.
* **Holder gate**: `from` must be blocked under `SEIZE_EXEMPT_POLICY`, that is, not authorized by
  it, or the call reverts `AccountNotSeizable`. An unset slot reads as always-allow, so no account
  is seizable until an issuer configures `SEIZE_EXEMPT_POLICY`.
* **Destination gate**: `to` must be authorized under `SEIZE_RECEIVER_POLICY`, which mirrors
  `MINT_RECEIVER_POLICY` and is always enforced. But an unset slot is always-allow, so a token can
  seize to any destination (a treasury doesn't need to be allowlisted) until the slot is set.
* **Balance**: `from`'s balance must be `>= amount`, or the call reverts `InsufficientBalance`.

When multiple guards would fail, they take this precedence: holder gate, then destination gate,
then balance. That is, `AccountNotSeizable` fires before `PolicyForbids(SEIZE_RECEIVER_POLICY,
...)`, which fires before `InsufficientBalance`.

## `burnBlocked` Is Deprecated, but Unchanged and Still Dialable

`burnBlocked(from, amount)` keeps working exactly as it does on Beryl:

* It destroys `amount` from a `from` blocked under `TRANSFER_SENDER_POLICY`, without spending an
  allowance. It emits `Transfer(from, address(0), amount)` and `BurnedBlocked(caller, from, amount)`
  (no `Memo`).
* It's gated by `BURN_BLOCKED_ROLE` and the `BURN` pause vector.
* It reverts `AccountNotBlocked` when `from` is authorized under `TRANSFER_SENDER_POLICY`.

To migrate, replace `burnBlocked(from, amount)` with `seizeWithMemo(from, treasury, amount, memo)`,
then call `burn(amount)` from the treasury if you still want the supply destroyed. This crosses two
policy, role, and pause domains (see the edge cases below), so it isn't a drop-in selector swap.

## Guarantees and Edge Cases

**Q: Does seize change `totalSupply`? Is it a burn?**
No. Seize is a transfer: it reassigns `amount` from `from` to `to` and leaves `totalSupply`
untouched. `burnBlocked` is the burn: it sends to `address(0)` and reduces supply. To reproduce the
old burn-blocked outcome, seize to a treasury or self address, then call `burn`.

**Q: `seizeWithMemo` and `burnBlocked` both target "bad" accounts. Do they read the same set?**
No, and this is deliberate. `seizeWithMemo` reads `SEIZE_EXEMPT_POLICY`. `burnBlocked` reads
`TRANSFER_SENDER_POLICY`. A token can define a seizable set that's distinct from its
transfer-blocked set. In both cases, "eligible" means not authorized by the relevant policy, and an
unset policy (always-allow) means nobody is eligible.

**Q: Can I pause seize without pausing burns, or vice versa?**
Yes. `SEIZE` (ordinal 3) and `BURN` (ordinal 2) are independent pause bits. Pausing `BURN` doesn't
stop `seizeWithMemo`, and pausing `SEIZE` doesn't stop `burn`, `burnWithMemo`, or `burnBlocked`.

**Q: Do `SEIZE_ROLE` and `BURN_BLOCKED_ROLE` overlap?**
No. `seizeWithMemo` requires `SEIZE_ROLE`. `burnBlocked` requires `BURN_BLOCKED_ROLE`. Granting one
doesn't grant the other.

**Q: I never configured the seize policies. What happens if I call `seizeWithMemo`?**
It reverts `AccountNotSeizable(from)` for every `from`, because an unset `SEIZE_EXEMPT_POLICY` is
always-allow, so no account is seizable. You must configure `SEIZE_EXEMPT_POLICY` to designate
seizable holders before seize does anything. (Leaving `SEIZE_RECEIVER_POLICY` unset simply permits
any destination.)

**Q: Does seize consult the transfer policies or spend an allowance?**
No. It's an admin operation: it bypasses `TRANSFER_SENDER/RECEIVER/EXECUTOR_POLICY` and allowances,
and enforces only `SEIZE_EXEMPT_POLICY` (on `from`) and `SEIZE_RECEIVER_POLICY` (on `to`).

**Q: Why does `seizeWithMemo` reject `to == address(this)` but allow `from == address(this)`?**
Crediting a balance to the token's own address would lock it permanently — the token is a precompile
with no holder key, so no one could move the funds out. Seizing *from* `address(this)` is permitted
precisely to recover any balance that ended up there (for example, via an earlier bug or external
credit). Together these rules ensure stuck funds can be recovered while preventing new funds from
becoming stuck.

**Q: Is seize available on B20 Stablecoin as well as B20 Asset?**
Yes. It's defined on the shared `IB20` surface, so both variants expose the identical
`seizeWithMemo` selector, `Seized` topic0, `AccountNotSeizable` selector, `SEIZE_*` getters, and
`SEIZE` pause bit at Cobalt.
