Abstract
Denim addsaddress(this) as a second trigger of the existing
InvalidReceiver(address receiver) error. transfer, transferFrom, their memo variants, mint,
mintWithMemo, batchMint, and seizeWithMemo revert InvalidReceiver(to) when to is the
token’s own address. The guard covers the IB20 transfer, mint, and seize paths plus
IB20Asset.batchMint, so it applies to both B20 Asset and B20 Stablecoin.
A holder sending to themselves (from == to) still succeeds. An issuer can still recover tokens
already credited to the token address: seizeWithMemo with from == address(token) succeeds.
This change is breaking for any integration that sends to the token address today. It adds no new
functions, events, errors, or selectors.
Motivation
Users often paste the token address instead of the recipient’s address. A B20 token is a precompile with no holder key, so it cannot calltransfer on itself. After a credit lands at the token
address, the sender cannot recover it; only the issuer can, through seizeWithMemo.
There is no valid use case for a B20 token to hold its own tokens. Denim reverts on that destination
so the mistaken send fails instead of locking the funds.
What Changed
Receiver Guard
InvalidReceiver already fires for address(0) (ERC-6093). Denim extends the same guard, at the
same position in the revert order:
Before (Cobalt)
After (Denim)
Revert Order
The check runs at the existing invalid-receiver step. Canonical order is otherwise unchanged:
The guard checks
to only. from may equal address(this), so a seize that drains the token
address into a treasury still succeeds.
Examples
A holder transfer to the token reverts:Transfer to Token Address
Mint or Seize to Token Address
Unaffected Paths
Design Decisions and Alternatives Considered
Denim reusesInvalidReceiver and compares to against address(this) on every credit path. The
destination is invalid for the same reason address(0) is: no holder can spend the credited units.
Wallets that already treat InvalidReceiver as “do not send here” keep the same revert handling.
Reject Any B20-Prefix Address
A prefix check cannot tell a B20 token from a user-controlled account in the same address space, such as a multisig. Rejecting the whole prefix would revert valid transfers, so Denim checksaddress(this) only. Sends to other B20 tokens still succeed.
Call isB20Initialized(to)
This would reject only live tokens, but it adds a factory call on every credit path.
New SelfSend(address) Error
A dedicated error would read more clearly in traces, but it adds ABI surface for a condition
InvalidReceiver already describes.
Also Reject from == address(this)
Blocking spends from the token address would close the only recovery path for balances already
sitting there.
Migration
- Treat the token’s own address as an invalid recipient in wallets, custodians, and indexers, the
same way you treat
address(0). - After Denim activates, expect
InvalidReceiverfrom any transfer, mint, or seize toaddress(token)that succeeded before. - Recover a balance credited to the token address before activation with
seizeWithMemo(address(token), treasury, amount, memo). The caller must holdSEIZE_ROLE, and the token must be seizable underSEIZE_EXEMPT_POLICY. - No change is needed for self-transfers, approvals, burns, or sends to other B20 tokens.
seizeWithMemo and
transfer reference pages describe
behavior before Denim activates.