Skip to main content

Abstract

Denim adds address(this) as a second trigger of the existing InvalidReceiver(address receiver) error. Any call to transfer, transferFrom, their memo variants, mint, mintWithMemo, batchMint, or seizeWithMemo that names the token’s own address as the recipient reverts. A B20 token is a precompile with no holder key; a credit to that address is not recoverable by the sender. Holder self-sends (from == to) are unaffected, and seizeWithMemo from the token address remains allowed so issuers can recover balances already stuck there.

Motivation

Users occasionally paste the token contract address instead of a recipient address. For standard ERC-20 tokens the tokens are stranded but potentially recoverable via governance. For B20 tokens — precompiles with no holder key — the sender cannot recover the funds at all. Only the issuer can, through seizeWithMemo. There is no valid use case for a B20 token to hold its own tokens, so the correct handling is an immediate revert rather than a silent lock. InvalidReceiver(address receiver) already fires for address(0) (ERC-6093). Denim extends it to cover address(this) using the same check, at the same position in the revert order, with no new selector, event, or function.

What Changed

Revert condition added to InvalidReceiver

The shared receiver guard that previously rejected only address(0) now also rejects address(this):
Before (Cobalt)
After (Denim)
No new error, selector, or event is introduced. The two conditions share one revert path and one error type.

Affected functions and revert order

The check runs at the existing invalid-receiver position in each function’s revert sequence: from may equal address(this) in seizeWithMemo. The check applies only to the to argument.

Code examples

Transfer to the token address reverts:
Revert on transfer to token address
Mint and seize to the token address revert the same way:
Revert on mint or seize to token address
A holder sending to themselves still succeeds:
Self-send still succeeds
Recovery of a balance already sitting at the token address still succeeds:
Seize from token address to treasury

Migration

1

Treat the token address as an invalid recipient

Update wallets, custodians, and indexers to reject address(token) as a destination the same way you already reject address(0). This applies before Denim activates.
2

Expect InvalidReceiver after Denim activation

Any transfer, mint, or seize to address(token) that succeeded before Denim will revert InvalidReceiver(address(token)) after activation. Update integrations that send to the token address accordingly.
3

Recover stuck balances with seizeWithMemo

If tokens were credited to the token address before Denim activation, recover them with:
Recovery call
The caller must hold SEIZE_ROLE. The token must be seizable under SEIZE_EXEMPT_POLICY.
4

Verify unaffected paths are unchanged

Holder-to-holder self-transfers, approvals, burns, and sends to other B20 tokens are not affected. Do not change handling for those cases.

Alternatives Considered

Reject any B20-prefix address as recipient. This would also block transfers to other B20 token addresses. Rejected because a prefix check cannot distinguish a B20 precompile from a user-controlled account in the same address space (for example, a multisig). Denim compares against address(this) only. Call isB20Initialized(to) on each credit path. This would reject only live tokens. Rejected because it adds a factory call on every credit path and does not address the paste-error use case precisely. Introduce a new error such as SelfSend(address). A dedicated error would make traces clearer. Rejected because it adds ABI surface for a condition already covered by InvalidReceiver — “this destination is invalid.” Also reject from == address(this) in seizeWithMemo. This would close the only recovery path for balances already sitting at the token address. Rejected.

Test Cases

Key scenarios validated by the Denim test suite:
  • transfer to address(token) reverts InvalidReceiver(address(token))
  • transferFrom to address(token) reverts InvalidReceiver(address(token))
  • mint to address(token) reverts InvalidReceiver(address(token))
  • batchMint with any element targeting address(token) reverts InvalidReceiver(address(token))
  • seizeWithMemo with to == address(token) reverts InvalidReceiver(address(token))
  • seizeWithMemo with from == address(token) and to == treasury succeeds
  • Holder self-send (from == to) succeeds
  • Sends to other B20 token addresses succeed