An ERC-721 extension for tokens that hold assets in a container of their own: nothing leaves the container while the token exists, and any contract can read what the token was minted with.
It grew out of the two ERC-721 extensions @jay suggested in the ERC-6551 thread: one to read who burned a token, now ERC-8423, and one for the canonical assets of an NFT. This proposal is the second, narrowed to tokens whose assets stay locked until the burn, and it builds on the first.
The problem. ERC-6551’s Security Considerations walk through a case every buyer of a token with an account should know: Alice’s token owns an account holding 10 ETH, Bob offers to buy the token, Alice empties the account and accepts the offer, and Bob receives an empty token. The mitigations it suggests include a list of the assets expected to stay in the account and a lock that keeps the owner from taking them out, and it leaves them to marketplaces and implementations, which keeps ERC-6551 general. This proposal takes up those two suggestions for one kind of token. Without a common interface, a marketplace, an indexer or a wallet cannot tell a lower bound it can verify from a figure in the metadata. It falls back on the balance, which says what is there now, not what is bound to stay.
The idea. A gold ring carries two things at once: the goldsmith’s work, and the gold it is made of. You cannot take the gold without destroying the ring. Picture a collector who brings their own gold to a goldsmith: the piece is made of that gold, and getting the gold back means destroying the piece. On chain, the gold is the native asset or ERC-20 tokens, the piece is an ERC-721 token, and destroying it is the burn. Each token declares at mint what it is made of and keeps it in a container that nobody can operate while the token exists. After the burn, only the address that owned the token at that moment can take the assets out, as recorded by ERC-8423.
The interface.
/// @dev The ERC-165 identifier for this interface is 0xd60a6c7e
interface IERC721Composition /* is IERC721, IERC721BurnRecord, IERC165 */ {
/// @param asset The ERC-20 contract, or 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE (ERC-7528) for the native asset
/// @param amount The declared amount, in the asset's smallest unit, never zero
struct Constituent {
address asset;
uint256 amount;
}
/// @notice Emitted once per token, in the transaction that mints it, batch mints included
event CompositionDeclared(
uint256 indexed tokenId,
address indexed account,
bytes4 accountType,
bytes accountData,
Constituent[] constituents
);
/// @notice The declared composition, fixed at mint, sorted by asset address
function compositionOf(uint256 tokenId) external view returns (Constituent[] memory constituents);
/// @notice The container that holds the constituents, its type, and the data to verify it
function compositionAccountOf(uint256 tokenId)
external
view
returns (address account, bytes4 accountType, bytes memory accountData);
}
accountType is the ERC-165 identifier of the container’s interface, which the container confirms through supportsInterface. For an ERC-6551 account it is 0x6faff5f1, accountData is abi.encode(implementation, salt), and account must be the address the canonical registry computes for this chain, this contract and this token id. Other container types define their own data. The container takes its orders from ERC-8423: burnedBy is how it knows who may open it.
Proposed rules, in short.
- Locked until the burn. While the token exists, the container MUST NOT execute, approve or validate anything that moves its assets out; after the burn, only the address returned by
burnedBycan move them. The rule is about the result, not the mechanism, so any container type can meet it. It cannot be changed later, and on any other chain the container validates nothing. - ERC-6551 accounts spell the rule out on the two calls that are easiest to get wrong: before the burn,
isValidSignerandisValidSignaturenever return their magic values; after the burn, they do only for the burner. - One container per token, used by no other token. A token cannot be transferred to its own container.
- Fixed at mint. Composition and container never change, not even after the burn.
- A lower bound. When the minting transaction ends, the container holds at least each declared amount, and no declared amount exceeds what the container actually received. Anything sent later is surplus: it stays inside and leaves with the rest after the burn.
- One event per token, at mint, batch mints included, with the same data the two functions return.
- No re-minting of a burned id, so the recorded burner of a token never changes.
- No reverts. Both functions answer for any id. An empty composition means “not declared”, never “declared as zero”; after the burn they return the same values as before.
- Constituents are the native asset, through ERC-7528, and ERC-20 tokens, each at most once, sorted by address, with amounts above zero. Tokens whose balance can drop without a transfer by the holder, such as downward rebasing, freezable or seizable tokens, SHOULD NOT be declared: no container can protect their lower bound.
Where each fact is read.
| Fact | Read with | Announced by | Changed by |
|---|---|---|---|
| Declared composition, the lower bound | compositionOf |
CompositionDeclared, at mint |
Nothing, fixed at mint |
| Container, its type and verification data | compositionAccountOf |
CompositionDeclared, at mint |
Nothing, fixed at mint |
| Owner when the token was burned | burnedBy (ERC-8423) |
Transfer to the zero address |
Only the burn, once |
| What the container holds now | The constituents: balanceOf, or the native balance |
Nothing in this proposal | Later deposits, actions by a constituent’s issuer, withdrawals after the burn |
The table comes from the Contracts can’t read events thread: thanks to @Zealynx for raising the question and to @priors-agents for proposing the list.
Out of scope: minting and burning, as in ERC-8423; prices, valuations and oracles; rebalancing and yield; splitting a token into shares; other asset types, such as NFTs inside the container, which could come in a separate proposal.
Related work. Two proposals come closest, and I would rather say so first:
| Proposal | What it does | How this proposal differs |
|---|---|---|
| ERC-3589 (2021, Stagnant) | Assembles ETH and tokens into an NFT and gives them back on burn; the composition is a hash in the token id | One container per token instead of a shared balance; a composition contracts can read; the lock as a rule rather than a property of the implementation |
| ERC-8060 (open PR) | Embeds native ETH in an NFT, redeemable only by burning it | Several assets; a composition fixed at mint and verifiable token by token; the burn stays outside the interface |
Others it builds on or differs from: ERC-6551 provides the container and suggests the mitigations above; ERC-4353 already suggested staking tokens at mint and withdrawing them only at burn; ERC-7527 pays back a premium on unwrap, priced by a function over a shared reserve; ERC-998, ERC-7401, ERC-5700 and ERC-7590 bind contents the owner can take out; ERC-5058, ERC-5753, ERC-6982 and ERC-7066 lock a token’s transfers rather than its contents; ERC-8414 (open PR) reaches the same container approach for a different purpose; ERC-7656 could serve as a custom container; and a shared container such as ERC-4626 cannot fit, because the lower bound must be checkable token by token. Pointers to prior art I missed are welcome.
What verification still needs. The data above tell a reader which container holds the assets and which code governs it; they cannot tell whether that code really keeps the assets in. A reader has to recognise two pieces of code: the container’s, and the token contract’s, because the container opens for whoever burnedBy names. It is the same trust ERC-6551 places in ownerOf. A minimal prototype includes a reader contract that does this for an ERC-6551 account and for a custom container, and that turns away an ordinary account the owner can empty. So the declared composition is a lower bound a reader can check under these conditions, not a statement about what the token is worth.
My question: for a marketplace, an indexer or a wallet, are the address, the type and the verification data returned by compositionAccountOf enough to verify the container on chain, or would you need something else?
The prototype and its tests will go into a public repository, and I will open a PR once ERC-8423 is merged.