Skip to main content

Abstract

Denim adds address(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 call transfer 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 and seize to the token address revert the same way:
Mint or Seize to Token Address
A self-send and a recovery seize both still succeed:
Unaffected Paths

Design Decisions and Alternatives Considered

Denim reuses InvalidReceiver 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 checks address(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

  1. Treat the token’s own address as an invalid recipient in wallets, custodians, and indexers, the same way you treat address(0).
  2. After Denim activates, expect InvalidReceiver from any transfer, mint, or seize to address(token) that succeeded before.
  3. Recover a balance credited to the token address before activation with seizeWithMemo(address(token), treasury, amount, memo). The caller must hold SEIZE_ROLE, and the token must be seizable under SEIZE_EXEMPT_POLICY.
  4. No change is needed for self-transfers, approvals, burns, or sends to other B20 tokens.
The seizeWithMemo and transfer reference pages describe behavior before Denim activates.