ERC-8423: ERC-721 Burn Record Extension

Add a function to read which address burned a given ERC-721 token.

The problem. A contract that has to decide something in the middle of a transaction cannot know who burned a token. The answer sits in the from field of the Transfer to address(0), but the EVM cannot read logs, so a lending protocol, a marketplace or a redemption contract is blind to it exactly when it matters. Indexers cover everyone except the contracts. One use case I have in mind is a redemption flow where a claim belongs only to whoever burned the token, but burn-to-mint flows and destruction receipts hit the same wall.

/// @dev The ERC-165 identifier for this interface is 0x43470a89
interface IERC721BurnRecord /* is IERC721, IERC165 */ {
    /// @notice Get the address recorded as having burned a token
    /// @dev Returns address(0) when no burn record exists for `tokenId`
    /// @param tokenId The token to query
    /// @return The address that burned the token, or address(0)
    function burnedBy(uint256 tokenId) external view returns (address);
}

Proposed semantics.

  1. address(0) means “no burn record for this tokenId”. It does not mean “not burned”.

  2. The recorded address MUST equal the from of the Transfer event emitted for the burn, so a contract reading this function and an indexer reading the logs never disagree.

  3. Burned means the token no longer exists and a Transfer to address(0) was emitted. A token held by a conventionally dead address still has an owner and is out of scope.

  4. While a token exists, burnedBy MUST return address(0). A contract that re-mints a token id therefore reports its most recent burn only.

The guarantee runs one way, and that is the heart of it: a non-zero answer is proof, a zero answer proves nothing. Combined with ownerOf, which throws for tokens that do not exist, a caller can separate a live token from a burned one with a known burner, and is left with a single ambiguous case: never minted, or burned before the record existed.

On indexing. The burner can of course be derived from Transfer events, and off-chain that is the right tool. Contracts have no access to it. Two Final ERC-721 extensions already made the same call: ERC-7634 emits TransferCountIncreased and also exposes transferCountOf, ERC-6672 emits Redeem and also exposes isRedeemed. In both cases the same fact lives in the log for indexers and in a getter for contracts.

Adjacent work, as far as I could find: ERC-5484 fixes who may burn, ahead of time; ERC-5679 standardises the burn operation itself. Neither keeps a record of who performed it. Pointers to prior art I missed are welcome.

Two notes for integrators. If a router or a marketplace owns the token at the moment of the burn, the record names that contract and not the person behind it. That is an application-level concern, and no choice of rule here avoids it. And a contract that guards value on the strength of this record should read it once, at burn time, and store the result, rather than consulting it on every call.

Deliberately left out: the time of the burn. It would pack into the same storage slot, so cost is not the argument, scope is. It could live in a separate optional interface if there is demand.

My question: is a one-directional guarantee enough for the cases you would use this in, or would a consumer need to tell “never existed” from “burned before the record existed”?

@jay, this is the first of the two threads you suggested. I would value your review of the semantics above whenever you have time.

If the shape holds up, I will open a PR on ethereum/ERCs.

What is the specific use case you need this for?

This would also require that a contract not reuse the same token id?

Thanks for both.

Use case. A token that has assets held for it, where burning is the only way to release them and the claim belongs to whoever burned it. The releasing contract has to authorise inside the same transaction, so an indexer is no help. That part is a separate proposal, not this one; this interface is what it rests on, and it stands on its own. The other cases I know of are burn-to-mint flows and destruction receipts, where a contract has to credit the right address.

Reuse. No, it does not require that. Point 4 is weaker than that: while a token exists burnedBy MUST return address(0), so a re-minted id clears the record and a later burn replaces it. Reuse stays legal, permanence is what you give up. burnedBy is the most recent burn, not a receipt.

I would rather not constrain mint policy here. Minting is not in ERC-721’s scope, the rule would not be verifiable from outside, and it would rule out bridges that re-create the same id.

The sharp edge is worth stating explicitly: if a contract holds value for a token and re-reads the record after a re-mint and a second burn, it will authorise the second burner. That is why the record should be read once, at burn time, and stored. The interface cannot close that on its own, and I do not think it should try.

@OniReimu @randyanto, I cited your two standards in the first post as prior art for a choice this proposal repeats: the same fact kept in the log for indexers and in a getter for contracts. ERC-7634 does it with TransferCountIncreased and transferCountOf, ERC-6672 with Redeem and isRedeemed.

One question, since you made that call before me: what led you to expose the getter alongside the event, rather than leaving that state to indexers?

If an NFT disappeared from my wallet, I’d want to know whether it was burned or whether the app simply couldn’t find its history. When that information is missing, “history unavailable” would be clearer than saying the NFT never existed. Having a link to the relevant transaction, when available, would also help.

If it’s just burn NFT, get assets inside the NFT, then there’s no need for this system, since when a user burns an NFT, they get the assets at that time.

Perhaps what you are talking about is when the NFT has some assets, and there are additional assets in a third party system. Usually this is just handled by the third party contract burning the NFT itself, and return both the NFT’s assets and whatever it was holding on behalf of that NFT.

Thanks. This is the first reply that answers the question in the opening post, and it matches point 1: zero means “no record”, never “never existed”. For a wallet the three cases map cleanly:

  • burnedBy returns an address: burned, by that address.

  • ownerOf returns another address: moved, not burned.

  • ownerOf reverts and burnedBy returns zero: history unavailable.

I will make that explicit in the spec: a zero answer should be shown as “no record”, not as evidence that the token never existed.

On the transaction link: a contract cannot record it, because the EVM does not expose the hash of the transaction it is running in. The closest on-chain proxy is the block number, which is the time-of-burn question I left out on purpose: on some chains, Arbitrum for one, block.number does not identify the block that holds the transaction.

For a wallet this is a shortcut, not a replacement for its indexer. On a contract that implements it, one call answers who burned a token: no log search, no dependence on whether the indexer covers that collection or is behind the chain, and the answer comes from the contract itself rather than from a reconstruction of its events. History and the transaction link still come from the logs.

1 Like

Agreed on the simple case: if one contract holds everything and performs the burn itself, it can read the holder before burning, and this interface adds nothing. My earlier wording did not help either: “inside the same transaction” meant the transaction that makes the claim, not the one that burns.

The record matters when the burn and the claim come apart:

  • More than one contract holds value for the same token, without knowing about each other. Only one of them can perform the burn; the others still need to know, later and independently, whom to release to.

  • Value reaches the token after the burn, such as rewards still accruing to it. A contract that burns and pays in one step can only pay what exists at that moment.

A custodian could keep its own record of who burned, but then there are as many records as custodians, each behind its own interface, and a contract that did not perform the burn has no standard place to look. Standardising is about keeping that fact once, where the burn happens.

Hi antferr, the getter and the event do not create two sources of truth. The stored count is the source of truth while the getter exposes its current value, and the event announces its transitions.

Thanks @OniReimu, that is a clearer way to put it than mine. It maps onto this proposal almost unchanged: the contract’s state is the source of truth, burnedBy is how other contracts read it, and the Transfer to address(0) announces the change. I say state rather than stored value because burnedBy also depends on whether the token exists (point 4). Point 2 only guarantees that the two views never disagree. I will put it in those terms in the Rationale.

The reference implementation and its tests are now public: https://github.com/antferr/erc721-burn-record

Two of the example consumers implement the cases from my reply above, where the burn and the claim come apart:

  • test_IndependentCustodians_ReleaseLater: two custodians that do not know each other hold value for the same token and release it later, in any order, to the owner at the time of the burn;

  • test_ValueAfterBurn_ClaimedByBurner: a redeemer burns the token and pays what it holds, and a second contract settles value that arrives after the burn, to the recorded burner only.

The specification tests run against two implementation variants, and every push runs the suite on GitHub Actions. Review of the code is as welcome as review of the semantics.

This proposal is now a pull request to the ERCs repository: https://github.com/ethereum/ERCs/pull/2032

Compared with the opening post, the Specification keeps the four rules discussed here and adds three:

  1. Every burn that occurs while the contract supports this interface MUST produce a record, which burnedBy MUST return until the token id is minted again. Only burns that occurred before the contract supported this interface MAY lack a record.

  2. burnedBy MUST NOT revert, whatever the tokenId.

  3. A non-zero answer is a guarantee and address(0) proves nothing: consumers MUST treat address(0) as the absence of authorization.

Rule 5 closes a gap: with the first four alone, a contract that always returned zero would have been compliant. Rule 6 makes explicit what the interface already implied, and rule 7 turns the one-way guarantee of the opening post into a requirement. None of them changes the semantics discussed here.

The Rationale also covers the points raised in this thread: the owner rather than the caller, re-minting, how interfaces should present zero, the getter alongside the event, and why receipt proofs do not replace it.

@jay, thanks again for the suggestion that started this.