Eip-6909-multi-token-standard

,

I have one suggestion for the ERC after implementing my own version of the code

the term ‘sender’ is easily mistakable with ‘spender’ phonetically, what it makes s bit more confusing is the fact that usually msg.sender becomes the spender not the sender

My suggestion is to intead of using ‘sender’ using the term ‘from’ or ‘origin’ so they don’t clash this much

my seccond suggestion is to use the term operato to whoever is triggers the execution and allowed operator for the setOperator and isOperator functions

so the functions would become

allowance(owner, operator)
isAllowedOperator(owner, operator)

What are yout toughts @jtriley-eth ?

Re first suggestion: While variable names are technically constrained by the standard in the yaml spec, it’s functionally equivalent, so either would be acceptable.

Re second suggestion: Same as above for the variable names, contracts don’t account for variable names, only function names and argument types. Though I prefer isOperator over isAllowedOperator for simplicity. I think they both convey the same message given the usage of ‘operators’ in the past.

1 Like

I’ve put together a first draft of a proposal: ERC-6909 Usability & Security Extensions. As discussed before this is not intended for ERC-6909 itself, but it could become its own ERC if others think something like this is a good idea and necessary. Feedback is welcome!

1 Like

Great proposal. I’ve been experimenting the past month+ using ERC6909 for a few multi-token ideas and I love it. It’s clean, consistent, expressive, and flexible. It’s the multi-token standard of the future.

Metadata Extension Feedback

The one point of inconsistency is in the Metadata Extension — static name(), static symbol(), but dynamic decimals(uint256) does not make sense imo. I struggle to find a use case that would benefit from dynamic decimal values based on token ID, but not also from dynamic names/symbols.

NFTs

For the case of ERC721-style NFTs, decimals always equal 1, and there is a single, unifying name and symbol for the collection. So, doesn’t benefit afaict from dynamic anything here.

Token Wrappers

For an ERC20 token wrapper, which I believe was the initial inspiration for supporting dynamic decimals values per token ID, it will also be useful to have dynamic names/symbols. Let’s take as an example, a single 6909 contract wrapping USDC, DAI, FRAX, etc — the former could return 6 decimals, the latter two 18.

But how is a user or integrator to disambiguate between which ERC20 is being wrapped by which token ID? That is a perfect use case for name/symbol — “xyzUSDC”, “xyzDAI”, “xyzFRAX”, etc. Otherwise, for this use case, we would be leaving it up individual implementors to decide how to map token ID to an explanatory name or other indicator of what token is being wrapped (perhaps the underlying ERC20 contract address). Which then puts pressure on downstream integrators and marketplaces to support custom implementations to display this info to end users.

DeFi Derivatives

For a similar use case of a derivative or some kind of synthetic asset, developers may be more likely to want to define a consistent scalar unit and return that from decimals(), but give an expressive name/symbol to each token ID. For example, take a tokenized options protocol — decimals() might always return 18 but depending on the underlying asset(s) and instrument type of a given token ID, both name and symbol might return “xyz-ETH-USDC-1DEC23-2100-C”.

It’s important that name(id), symbols(id), and decimals(id) are all dynamic based on token ID, for any use case that looks like, “combine multiple ERC20 contract deploys into a single, unified multi-token contract.”

Again, without this, we rely on each implementor to disambiguate the semantic meaning of various token IDs in their own way, which fragments the common usage of ERC6909 and creates headaches for downstream integrators and marketplaces.

DeFi LP Tokens

This is another use case where it is more likely for a developer to be okay with static decimals but want dynamic names/symbols. Think of Uniswap v4’s planned ERC1155 balance accounting (or perhaps ERC6909 =), or any singleton AMM design — the name/symbol can specify what the LP token is.

Gaming

For gaming, the original motivation behind ERC1155, the content of in-game items may be best suited to definition in URI JSON. And potentially this is a use case where a developer would want a single unifying name and symbol for their collection, and dynamic decimals values (eg, in-game collectible items vs. in-game currency). Although still I’d argue that varying decimal values between token IDs makes them conceptually and fundamentally different enough, such that having a way to disambiguate them that is more onchain-native than having to parse JSON is important for developer ergonomics.

Aside on Syntax

Semantics aside, there is one syntactic wrinkle yet in both the current Metadata Extension and my proposal, but I don’t think it’s a big deal —

Because decimals is already plural, when the function is decimals(id), we pedantically might be tempted to call it whatever the plural of decimals is (?), or something like decimalss, decimalz, etc =). But in practice, imo it’s idiomatic enough that developers can understand the expectations and semantics.

tl;dr

Excellent minimal multi-token spec. But for consistency and flexibility across sectors/use cases, the Metadata Extension names/symbols/decimals should either be (1) all dynamic based on token ID, or (2) all static and not take token ID as a parameter. Imo the former is significantly more powerful and more likely to provide great DevEx for future developers and integrators, while still keeping with the ‘minimalist’ spirit of this EIP.

Proposed Metadata Extension Interface

(And of course, these could be implemented as some combination of private state variables and public/external functions, which contain some business logic or just return constant values, such that not all returned values need come directly from storage.)

interface IERC6909Metadata is IERC6909 {
    function names(uint256 id) external view returns (string memory);
    function symbols(uint256 id) external view returns (string memory);
    function decimals(uint256 id) external view returns (uint8);
}

so expanding on this, 6909 is trying to accommodate two very different use cases, namely singleton defi architectures and nft collections. both have different needs in terms of metadata, ie:

a defi protocol that wraps external tokens will need token-level metadata: name, symbol, and decimals

an nft collection where all token ids are a part of the same collection would need collection-level metadata: name and symbol

what are thoughts on breaking these out into two separate interfaces?

// nft collection-level metadata
interface IERC6909CollectionMetadata is IERC6909 {
    function name() external pure returns (string memory);
    function symbol() external pure returns (string memory);
}

// defi token-level metadata
interface IERC6909TokenMetadata is IERC6909 {
    function name(uint256) external view returns (string memory);
    function symbol(uint256) external view returns (string memory);
    function decimals(uint256) external view returns (uint8);
}
1 Like

I like this approach. It should work well for both high-level use cases (NFT collection and DeFi singleton), while keeping the core of the spec consistent. Had the same idea as a morning shower thought =)

And how about the inheritance graph for MetadataURI? With this approach of dedicated interfaces for collection metadata and token metadata, imo in the reference implementation we can treat MetadataURI as a standalone interface/contract.

EDIT — because I could see either use case potentially needing tokenURI(id)

Not a fan of the approach because it fragments the 6909 ecosystem into two incompatible interfaces. I would favor metadata methods with a tokenId parameter, where the token is free to return the same value for all ids.

Note that for collection-level metadata the standard is lacking in another way: see OpenSea’s Contract-level metadata documentation, which proposes a contractURI() function where 6909 only has tokenURI(tokenId). I think this is potentially more important than collection-level name() and symbol().

1 Like

It’s a good point re contractURI(). Included it below.

Setting aside inheritance, and whether it’s a good idea to have multiple extensions based on high-level use case, below is the metadata and URI functionality we’re discussing, plus a point about total supply.

Any thought on how we might bring back totalSupply(id) as optional? Gas optimization is important of course, but aren’t we concerned that many of the “combined ERC20” or “combined ERC4626” use cases absolutely need a standard totalSupply(id) function?

I bet we can find a way to rationalize all of this, while keeping the spec clean + minimal + flexible.

EDIT — to be clear, I’m not proposing this many discrete extensions, just laying it all out

Metadata

// collection-level metadata
interface IERC6909CollectionMetadata {
    function name() external pure returns (string memory);
    function symbol() external pure returns (string memory);
}

// token-level metadata
interface IERC6909TokenMetadata {
    function name(uint256) external view returns (string memory);
    function symbol(uint256) external view returns (string memory);
    function decimals(uint256) external view returns (uint8);
}

Content URI

// collection-level URI
interface IERC6909CollectionURI {
    function contractURI() external view returns (string memory);
}

// token-level URI
interface IERC6909TokenURI {
    function tokenURI(uint256) external view returns (string memory);
}

Supply

// collection-level supply
interface IERC6909CollectionSupply {
    function totalSupply() external view returns (uint256);
}

// token-level supply
interface IERC6909TokenSupply {
    function totalSupply(uint256) external view returns (uint256);
}

Summarizing from discussion @jtriley-eth and I had over the past couple of days.

Here’s where we netted out:

// metadata (can be either collection- or token-level)
interface IERC6909Metadata {
    function name(uint256) external view returns (string memory);
    function symbol(uint256) external view returns (string memory);
    function decimals(uint256) external view returns (uint8);
}

// content uri (covers both collection- and token-level)
interface IERC6909ContentURI {
    function contractURI() external view returns (string memory);
    function tokenURI(uint256) external view returns (string memory);
}

// token-level supply
interface IERC6909TokenSupply {
    function totalSupply(uint256) external view returns (uint256);
}

Thought Process

  • Taking into account @frangio feedback re interface fragmentation, there is a single Metadata interface — and a dev could choose to implement the IERC6909Metadata functions w/ unnamed parameters, always returning the same value for name/symbol/decimals
  • We considered splitting contentURI and tokenURI(uint256) into separate interfaces, but is there really a use case where a dev wants rich media to show up for a token/NFT, but Not show up on their OpenSea or other marketplace collection page? Does not seem likely
  • Rename MetadataURI to ContentURI — (1) this is typically more the ‘content/data’ vs. the ‘metadata’ in practice (for NFT collections especially), and (2) this naming enables us to minimize inheritance, so implementors can pick from one or more of these extension interfaces, a la carte
  • Remove ‘Token’ from Metadata and ContentURI extensions, bc there’s nothing to disambiguate, but keep ‘Token’ in Supply, bc that is specifically for use cases needing to specify total supply at token-level
  • We could do IERC6909CollectionSupply.totalSupply() as a fourth interface, for onchain use case of ‘how many NFTs in this collection are there’, but does not seem critical

I agree that on-chain supply for non-fungible tokens doesn’t seem important like it does for fungible tokens. For the record, this has the issue that Etherscan currently doesn’t display supply of e.g. ERC721 tokens unless they implement a totalSupply() function, but Idon’t see a reason why they couldn’t fix that. I see that the Transfer events for minting and burning are now specified as “MUST” which is great in that sense.