ERC-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
1 Like

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.

hey @jtriley-eth

are you discussing this erc in some discord or timgs like that?

@jtriley-eth any idea when this might be finalized? have a project coming up I’d like to use it for

also where’s the best place to view the latest draft – I don’t suppose it’s this?

have received messages and conversations across a few platforms, hard to get every convo into the same forum

the eip website is up to date now.

re finalization: there’s an open pr to move from ‘draft’ to ‘review’ at the time of writing. will move to finalization as soon as possible. though if you need it soon, the current iteration of the core is the final iteration, so there should be no breaking issues here.

I’m asking because I don’t want to reopen discussions that probably have been done before

My current questions are:

  • Why the totalSupply have been moved to an extension? maybe this can be expanded in the rationale
  • Can the decimal function revert? is there an default behaviour for an ID not yet used?

re totalSupply: this is also non-existent in the erc1155 standard. the totalSupply adds an extra disk write on mint and burn, which affects nft mints and defi protocols where wrapping/minting and unwrapping/burning is a common occurrence. totalSupply has a few onchain use cases, so moving it to an extension sacrifices some composability with protocols that need the totalSupply in favor of significantly cheaper gas costs.

re decimals: i hadn’t thought of this; so at the time of writing, tokenURI may revert if the tokenId doesn’t exist, but this is likely called offchain almost exclusively, whereas decimals may be used onchain as well. while these functions are different, i think they should follow the same rule for consistency. i’m open to suggestions here but my initial thought is:

  • decimals must not revert if tokenId does not exist
  • decimals should return 0 if tokenId does not exist
  • tokenURI must not revert if tokenId does not exist
  • tokenURI should return an empty string if tokenId does not exist

the rationale for “should” in regards to returning a default is there may be cases where the decimal amount is hard coded, but in general it should fall back to the default value.

// happy case
mapping(uint256 id => uint8) public decimals;

// edge case
function decimals(uint256) external pure returns (uint8) {
    return 18;
}

Makes sense and I actually really like that

I think he totalSupply part is important to be in the retionalle

Can you point mr the discord you are using if possible?

sounds good, will update the totalSupply bit in rationale

and discord server is here (channel #6909): Ripped Jesus Gang

The notion of “tokenId does not exist” is ambiguous, so it should be avoided or defined. Does it mean the token has no supply? That it has never been minted?

I agree with must not revert, but I’d remove the “should” items, I’m not sure they add value.