Skip to main content
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_HOLDER_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_HOLDER_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

Events

Errors

Pause features

PausableFeature is append-only. Cobalt adds one ordinal. 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) and from != to, or the call reverts InvalidReceiver. from != address(0), or the call reverts InvalidSender.
  • Holder gate: from must be blocked under SEIZE_HOLDER_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_HOLDER_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_HOLDER_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_HOLDER_POLICY is always-allow, so no account is seizable. You must configure SEIZE_HOLDER_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_HOLDER_POLICY (on from) and SEIZE_RECEIVER_POLICY (on to). 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.