Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ERC-998 Composable Non-Fungible Token Standard #998

Closed
Tracked by #5111
mattlockyer opened this issue Apr 14, 2018 · 63 comments
Closed
Tracked by #5111

ERC-998 Composable Non-Fungible Token Standard #998

mattlockyer opened this issue Apr 14, 2018 · 63 comments

Comments

@mattlockyer
Copy link

mattlockyer commented Apr 14, 2018

title: ERC-998 Composable Non-Fungible Token Standard
authors: Matt Lockyer mattdlockyer@gmail.com; Nick Mudge nick@perfectabstractions.com; Jordan Schalm jordan.schalm@gmail.com
status: Draft
type: Standards Track
category: ERC
created: 2018-04-15
requires: 721, 20

Update

The latest code is in the EIP and reference implementation

EIP
eip-998.md

Reference Implementation
https://github.com/mattlockyer/composables-998

Reading
Original Medium post:
Introducing Crypto Composables

Crypto Composables - Use Cases and Applications

Abstract

A standard extension for any non-fungible token to own another non-fungible ERC-721 or fungible ERC-20 tokens. Transferring the token composition means transferring the entire hierarchy of items. For example, a cryptokitty may own a scratching post and a feeding dish; the dish may contain some amount of fungible “chow” tokens. If I sell the cryptokitty, I sell all of the belongings of the cryptokitty.

Specification
After several discussions with the community we settled on providing these 4 extensions to a standard ERC-721 NFT in order to support composable functionality:

ERC998ERC721 top-down composable tokens that receive, hold and transfer ERC721 tokens
ERC998ERC20 top-down composable tokens that receive, hold and transfer ERC20 tokens
ERC998ERC721 bottom-up composable tokens that attach themselves to other ERC721 tokens.
ERC998ERC20 bottom-up composable tokens that attach themselves to ERC721 tokens.

For more details on the reference implementation, visit the EIP.

Summary

I’ve presented a new standard for composable non-fungible tokens, ERC-998, that allows for the ownership of ERC-998, ERC-721 and ERC-20 tokens. This allows standard assets to be composed into complex compositions and traded using a single transfer. The additional functionality that can be added to a non-fungible token based on the presence of specific child tokens is open for exploration. I believe that standard interfaces for non-fungible and fungible assets to be composed and act upon one another is an exciting new area for the Ethereum community to explore and I look forward to seeing the work.

LATEST: EIP with reference implementation code

eip-998.md

@mattlockyer mattlockyer changed the title ERC-1001 Composable Non-Fungible Token Standard ERC-998 Composable Non-Fungible Token Standard Apr 14, 2018
@mattlockyer
Copy link
Author

It was pointed out to me by @jeffwalsh that adding an ERC-20 token required it's own method for incrementing the balance rather than replacing it.

He's also implemented an example using a basic ERC-721 in this fiddle. Many thanks Jeff.

@Physes
Copy link

Physes commented Apr 15, 2018

You might want to check out Delegated Non-Fungible Tokens: #994 which already sets out the architecture of parent/child NFTs. Whereas a single NFT is considered a "deed", a tree of NFTs could be considered a "zone".

@mryellow
Copy link

mryellow commented Apr 16, 2018

sets out the architecture of parent/child NFTs

// maps child delegate token to parent ID
mapping(uint256 => uint256) private sub;

sub[_delegate] == _tokenId, sub[newTokenId] = _tokenId

Not too different from the proposed mapping:

// which asset owns which other assets at which address
mapping(uint256 => mapping(address => uint256)) children;

However not dealing with the same contract with parents and children being simply related key to key as is the case with #994.

sub[newTokenId] = _tokenId as described works as it's always the child having a single parent. Where children[_tokenID][_childContract] = _index looks to fail as it will over-write the previous child token entry when a new child is added.

Could it be isChild[_tokenId][_childContract][_childId] = true is appropriate mapping instead?

mapping(uint256 => mapping(address => mapping(uint256 => bool))

@mryellow
Copy link

mryellow commented Apr 16, 2018

This was an edit, but then grew so I'll post again:

I guess for fungible you want it to be an integer.

mapping(uint256 => mapping(address => mapping(uint256 => uint256))

Where > 0 == true for NFTs. i.e. setting it to 1 is sufficient to be recognised as a child.

Alternatively the whole thing on it's head so that one child has one parent as is the case with #994.

parents[_childContract][_childId] = _tokentId with fungibles using index 0 for _childId

edit: Although that then breaks when you're trying to figure out where a fungible is held.

@mattlockyer
Copy link
Author

mattlockyer commented Apr 16, 2018

I see your point about the child tokenId being overwritten, that's a pain.

I had several approaches to this and I knew this wasn't perfect. You are right that a child contract will only be able to have one ERC-721 child. Darn.

A strict requirement of this proposal will be NOT requiring any changes to children that are already ERC-721 or ERC-20, meaning they won't have to upgrade to be a child of a composite.

Another approach was to keep the mapping:

mapping(uint256 => mapping(address => uint256));

And upgrade to a pseudo unique address for ERC-721 children:

// generate a unique address for ERC-721 child tokens
// from _childContract and _childTokenId
address childToken = address(keccak256(_childContract, _childTokenId));
// everything successful and you want to add the child token
// use the unique address
children[_tokenId][childToken] = _childTokenId;

Note: this is actually how the EVM generates smart contract addresses, using the hash of address + nonce as a bytes20 address type.

In this way we have a pseudo unique address for every child token.

We will have to adopt your bookkeeping method 0 means no child and 1 means child exists.

I really wanted to reduce the need for a deep mapping as in your proposed solution, although my unique address generation pushes a bit more computation and gas fees onto the miners and users.

This would have to be calculated to transfer a child token as well, like so:

address childToken = address(keccak256(_childContract, _childTokenId));
// other conditions
require(children[_tokenId][childToken] == 1); //0 is false and 1 is true for non-fungible tokens
// transfer child here if other conditions check out
children[_tokenId][childToken] = 0;

Thoughts on this @mryellow?

@mryellow
Copy link

address childToken = address(keccak256(_childContract, _childTokenId));

Interesting, so it's keccak256(_childContract) or just straight _childContract address in the case of a fungible?

I was thinking a lot of the issues go away when dealing with non-fungible or fungibles independently. It's always this bit where you have to start thinking about using "tokenId" index 0 or returning "tokenId" 1 as "true", which all ends up feeling more untidy then was the intention.

Perhaps the neatest solution is to not attempt to deal with them in the same variable, but storing differently once you know what type you're dealing with. Not sure how best to represent anything like that semantically though. Could add a boolean for fungible to any methods where tokenId/value appears to select between them, though that's not exactly neat either.

@mattlockyer
Copy link
Author

mattlockyer commented Apr 16, 2018

I think you avoid it when dealing with fungibles. So in the fungible functions, simply use the fungible contract address. There's no need for extra computation in the fungibles functions.

Could be possible to go with a completely new mapping for non-fungibles and deal with them seperately.

mapping(uint256 => mapping(address => bool)) childTokens; // NFTs
mapping(uint256 => mapping(address => uint256)) childBalance; // FTs

And still use the pseudo address construction for dealing with NFTs to ensure a unique address per NFT.

@mryellow
Copy link

pseudo address construction

Another thought on this concept, ERC: Ethereum Claims Registry #780 features some prior work which might be informative:

The key parameter is intentionally very generic. The ERC does not impose any restrictions saying that certain values for key should only be used for certain things. Rather this is something that the community is free to experiment with. If there is some emerging standard a new EIP can be created for that. There are also a lot of already existing naming conventions for claims that are in use today and coming up with our own standard might limit the ability for these to be used.

Having a key with a construction recipe for fungibles and non-fungibles which is consistent might be something worth looking at. Perhaps stepping away from address type here is nice or opens up more possibilities.

bytes32 key = bytes32(keccak256(_childContract, _childTokenId));

However would this add anything or is the key always built the same way?

Would anyone ever want to use the key for anything else other than an "address" for a token?

@mattlockyer
Copy link
Author

My feeling on this, and why I refer to is at "pseudo address creation" is because it is analogous to how smart contract addresses are generated given the nonce of the smart contract.

So here we are creating an address for every NFT based on the contract address and tokenId of the NFT. This is exactly the same as if every NFT were deployed as a new contract from the main contract.

@vernon99
Copy link

This is a great start, thanks. Like the idea of pseudo-addresses for child tokens. Have a bunch of thoughts. Here're three major points.

  1. Mixed token types in one mapping.

Could be possible to go with a completely new mapping for non-fungibles and deal with them seperately.

As you said, I think this is a preferred solution. I don't think it's a good idea to have one mapping for two different standards. It adds complexity (and fragility), adds unexpected behaviors, makes it harder for the wallets to process two different types. Wallets actually don't know the type beforehand - how would they know what token is that without guessing that through presence of the methods, etc? To me that's the biggest problem that I don't see how to resolve without splitting. Token type needs to be defined very explicitly.

So fungiblePossessions and nonfungiblePossessions may be more appropriate, with comments explicitly saying what standards to they support (ERC20+ERC677 and ERC721 respectively).

  1. Currently it requires two transactions to pass ownership. This is very old-school :) ERC721 supports safeTransferFrom with onERC721Received callback (that needs to be implemented by this standard). ERC20 unfortunately doesn't and ERC223 is not backwards compatible with ERC20. But there's a nice ERC677 addition to ERC20 that keeps it backward compatible while adding transferAndCall and tokenFallback pair similarly to described for 721 above. All the key exchanges already support that and this token should also support the callbacks. This is very critical to avoid two transactions per transfer, a bane of many early tokens.

  2. There has to be a way to enumerate all the children. Otherwise you have the wallets problem again, now for list of possessions. ERC721 actually implementa it, see ERC721Enumerable (which should really not be optional). The way it works is we maintain both the hashing for retrieval/checks (to see if it holds this particular token) but also an array to be able to get a token by index. Then you just iterate till you hit 0. That also gives you numberOfChildren that is missing right now completely from the spec. To avoid loops in the smart contract we will need to smartly update the array and keep indices. See how Decentraland did it in the storage and then in enumerable interface.

Also, more like a suggestion. "children" naming sounds a bit off for ERC20. It's more like stuff that belongs to this token. Maybe possessions would be better, or something like that. Nit.

@mg6maciej
Copy link
Contributor

Hello everybody and thanks @mattlockyer for creating this ERC. I was thinking about something like this for over a month, happy to see it brought to attention of a broader community.

My initial idea was very similar to this, that you embed such functionality as an extension to ERC721, and your ERC998 contract would be an owner of some ERC721 tokens (from the perspective of owned token) while keeping information that the true owner is a token this contract itself keeps track of.

I see some problems with such approach. The most important one being that if you have a chain of tokens from different ERC998 contracts, as a user you will have hard time understanding what contract you need to call to transfer a CryptoKitty that is owned by a DogERC998 token that is owned by PersonERC998 token that you own. I might be wrong here, but my intuition tells me you would have to call PersonERC998 and pass it 2 arrays of all contracts and tokens that are owners (parent, grandparent, ...) of the token to be able to retrieve that poor little Kitty.

Currently I'm thinking this is a perfect example where global / master registry could be used. I started drafting it (code is here) and would like to ask all of you if you also think this might be a good idea. I also agree with all the points @vernon99 mentioned and already partially incorporated them into the codebase.
The biggest advantage of using registry is that it will support CryptoKitty being owned by another CryptoKitty.

@mattlockyer
Copy link
Author

Thank you everyone for the comments. I have been grinding through an implementation with tests so we have a basis from which to work on finding the most optimal solutions together.

@vernon99 I have address the first 2 concerns for nonfungible "possessions". I like this name.

mapping(uint256 => mapping(address => bool)) nonfungiblePossessions;
  mapping(uint256 => mapping(address => uint256)) fungiblePossessions;
  
  function bytesToUint(bytes b) internal pure returns (uint256 result) {
    result = 0;
    for (uint256 i = 0; i < b.length; i++) {
      uint256 c = uint256(b[i]);
      if (c >= 48 && c <= 57) {
        result = result * 10 + (c - 48);
      }
    }
  }
  
  function _nonfungibleAddress(
    address _childContract, uint256 _childTokenId
  ) internal pure returns (address) {
    return address(keccak256(_childContract, _childTokenId));
  }
  
  /**************************************
  * ERC-721 Non-Fungible Possessions
  **************************************/
  
  //adding nonfungible possessions
  //receives _data which determines which NFT composable of this contract the possession will belong to
  function onERC721Received(address _from, uint256 _tokenId, bytes _data) public returns(bytes4) {
    //convert _data bytes to uint256, assuming tokens were passed in as string data
    // i.e. tokenId = 5 would be "5" coming from web3 or another contract
    uint256 id = bytesToUint(_data);
    nonfungiblePossessions[id][_nonfungibleAddress(msg.sender, _tokenId)] = true;
    return ERC721_RECEIVED;
  }
  
  //transfer the ERC-721
  function transferChild(
    address _to,
    uint256 _tokenId,
    address _childContract,
    uint256 _childTokenId
  ) public {
    // require ownership of parent token &&
    // check parent token owns the child token
    // use the 'pseudo address' for the specific child tokenId
    address childToken = _nonfungibleAddress(_childContract, _childTokenId);
    require(_owns(msg.sender, _tokenId));
    require(nonfungiblePossessions[_tokenId][childToken] == true);
    require(
      _childContract.call(
        // if true, transfer the child token
        // not a delegate call, the child token is owned by this contract
        bytes4(keccak256("safeTransferFrom(address,address,uint256)")),
        this, _to, _childTokenId
      )
    );
    // remove the parent token's ownership of the child token
    nonfungiblePossessions[_tokenId][childToken] = false;
  }

This code is working now, it's here if you want to run it yourself.

I have put ERC-20 on hold for a bit, seems relatively trivial once ERC-721 is working. @vernon99 thanks again for the comments on where to look for that implementation.

@mg6maciej thanks for dropping in here. I think the global registry is a fantastic solution to a lot of the needs of developers out there.

I do foresee some issues with functionality, specific implementations that restrict the composability of assets for example. For all basic compositions though, I think it's quite a good approach!

@mattlockyer
Copy link
Author

I've made some updates to the comment at the top to reflect the dramatic changes to the originally proposed code.

Thank you @vernon99 @mg6maciej for the feedback. It motivated me to push forward and get a more comprehensive and elegant solution up.

@mg6maciej
Copy link
Contributor

I do foresee some issues with functionality, specific implementations that restrict the composability of assets for example.

Yes, there are things that need to be taken care of in 998. I can think of these cases:

  • once possession is attached to 998, it may want to not be transferable; this can be implemented overriding ERC721::transferFrom to always revert and making sure 998 always calls this function even if that meant to transfer 721 token from itself to itself when changing which 998 owns it.
  • possession may want to be attached only to certain kinds of 998s. This could be handled with some kind of callback (e.g. onAttachedTo(address erc998, uint tokenId)) to extended 721 that is aware of 998 and allowing attachment if function does not exist on 721.
  • 998 may want to own only certain kinds of possessions. This is the easiest to implement with local 998, but the global registry could call another callback (e.g. onAttachedChild(address erc721, uint tokenId)) and also accept if function is not there.

@mg6maciej
Copy link
Contributor

Another thing worth discussing is what kind of events these transfers will fire.

  • Xxxxx(address, address, uint256, address, uint256) // sending NFT from address to NFT
  • Xxxxx(address, uint256, address, address, uint256) // sending NFT from NFT to address
  • Xxxxx(address, uint256, address, uint256, address, uint256) // sending NFT from NFT to another NFT
  • Yyyyy(address, address, uint256, address, uint256) // sending fungible from address to NFT
  • Yyyyy(address, uint256, address, address, uint256) // sending fungible from NFT to address
  • Yyyyy(address, uint256, address, uint256, address, uint256) // sending fungible from NFT to another NFT

@mattlockyer
Copy link
Author

mattlockyer commented May 1, 2018

Another thing worth discussing is what kind of events these transfers will fire.

Absolutely.

I think this is a great start.

@mg6maciej What are the thoughts on NFTP as a title for both the non-fungible token possessions and FTP for fungible token possessions?

@mg6maciej
Copy link
Contributor

mg6maciej commented May 2, 2018

I'm not that interested in choosing names for anything for now. I think it's too early. I'd be more than happy to change names in my implementation at some point.

I've noticed a potential issue with different implementations of ERC721 tho. There are a couple of ways to interact with them.

  • When taking approved token in possession to assign it as child of another one you can call
    • transferFrom(owner, this, tokenId) for current ERC721 standard and CryptoKitties/Bots
    • takeOwnership(tokenId) for some other tokens, e.g. EthMoji
  • When returning a possesion to some address
    • transferFrom(this, addr, tokenId) for current ERC721 standard (with OpenZeppelin implementation can't call approve on yourself)
    • approve(this, tokenId) + transferFrom(this, addr, tokenId) for CryptoKitties/Bots
    • transfer(addr, tokenId) for CryptoKitties/Bots and some other tokens

If you just use transferFrom, you will basically lock all old ERC721 tokens as children.

@7flash
Copy link

7flash commented May 22, 2018

It looks that ERC-998 is perfect fit for intellectual property tokens. For example, parent contract could reflect the ownership of education course of author. Children tokens could reflect the copies for students. If there is any issues?

@mattlockyer
Copy link
Author

mattlockyer commented May 23, 2018

@mg6maciej I'm going to be adding the callbacks and events soon to the implementation

I know you're but syncing on the composables instances + registry will be crucial, let's touch base about this next week.

If anyone has any feedback on how to interop a global composable NFT registry with individual instances of Composable 998 (721 extended) please feel free to chime in!

Also there is a Discord for general #buidl chatter around NFTs here: https://discord.gg/3TtqP2C

@crowdedconscious
Copy link

Very interesting work Matt! Following your progress closely - keep it up.

@jordanschalm
Copy link
Contributor

jordanschalm commented Jun 22, 2018

Hi everyone! Myself, @flockonus and a few other folks from the CryptoKitties team had the opportunity to meet @mattlockyer a couple days ago to discuss designs for composable NFTs. We primarily discussed a different design than what is currently in this ERC, which I’ll summarize here to get feedback on from all of you fine NFT enthusiasts!

Current Design Issues

Ownership state is split between child/parent

When a token is owned by a composable “parent” NFT, the complete ownership state is split between the child contract and the parent contract. The child contract (which may not know about ERC998) state says that the token is owned by the parent contract address. The parent contract specifies which NFT within the contract actually owns the child token. Similarly, sometimes methods related to ownership are executed from the child contract and other times they are executed from the parent contract.
With the bottom-up approach, all ownership state and associated methods would be implemented in the child.

Implementation Complexity

Basically, the current design requires implementors to maintain a lot of state! Parent contracts would need to track who owns their tokens, which child tokens are owned by the parent contract’s tokens, and reimplement/wrap transfers and the like for these child tokens. With a bottom-up approach, all ownership state and methods would be compartmentalized in the contract responsible for the given token.

Ownership terminology overloading

Ownership in Ethereum is currently strongly defined as a mapping from Ethereum addresses to tokens of some kind. Current contracts rely on this relationship for ownership operations (you transfer an ERC20 token to a native Ethereum address). Since being backwards-compatible with this existing notion of ownership is not possible for composable tokens, we suggested being explicit about the semantic difference between the two notions of ownership by naming them differently. We suggested maintaining the own verb for ERC20/ERC721-style ownership, and using something else (maybe link or attach?) for when an NFT “owns” some other token.

Bottom-up Design Proposal

The current proposal describes ERC721 tokens with logic added so they can own child tokens. The bottom-up design inverts this, instead proposing an extension to ERC721 and ERC20/223 that enables them to be linked/attached to any ERC721 token, in addition to being owned by an Ethereum address (as is the case now).

Similarly to the current proposal, the ownership hierarchy for a particular NFT can be thought of as a tree, with an Ethereum address at the root, NFTs in the middle, and either NFTs or fungible tokens at the leaves.
image

Rationale

In addition to the benefits in the next section, the rationale behind this proposal comes from our experience with CryptoKitties. After releasing CryptoKitties, many developers built apps, tools, and tokens that extend the CryptoKitties ecosystem. It doesn’t make much sense for a CryptoKitty to own EOS tokens, but it does make sense (and is super cool!) for a CryptoKitty to own a provably unique cat-hat token (see KittyHats). In general, we see value in NFTs being extended by contextual tokens with semantic meaning, not NFTs being able to own arbitrary tokens. We think the bottom-up approach best fits this ecosystem-extension view of composable tokens.

Benefits

  • ownership logic is compartmentalized in the child contract, where the ownership logic already is
  • Since ownership logic and state is maintained in one place, implementation complexity is much lower
  • any NFTs (including already deployed NFTs) can be parents and “own” child tokens

Drawbacks

  • generic on-chain enumerability of children is not possible. However, off-chain enumerability is possible using events, and on-chain enumerability is possible for specific child types on a per-contract basis.

@mryellow
Copy link

We suggested maintaining the own verb for ERC20/ERC721-style ownership, and using something else (maybe link or attach?) for when an NFT “owns” some other token.

Wonder if there are issues similar to "approval" where some "attachment" might be orphaned by a chance in "ownership" at the child level.

Does a cat continue to be attached to a hat after the hat's ownership has changed?

Who can detach the hat?

In general, we see value in NFTs being extended by contextual tokens with semantic meaning, not NFTs being able to own arbitrary tokens.

I'm rather interested by the possibilities when it comes to arbitrary standard tokens rather than specifically crafted tokens setup to be children. Semantics can be re-imagined in new contexts.

i.e. Turn the power on for your city in a dApp game by adding POWR tokens, or covering your enemy in GOO, those tokens contributing value which is then contested and exchanged as part of gameplay. In a non-game contexts this might mean bonded curves and curation market type associations where parent token value is a function of children.

This "composable" approach may be entirely unsuited for such tasks in the end, the proper approach is probably a lot more custodian and based around "deposits", but worth mentioning a use-case for the parent-child ownership style even if I'm not seeing the cleanest way to lay it out.

The current proposal describes ERC721 tokens with logic added so they can own child tokens

Perhaps the best approach is to separate everything. ERC721's go on being themselves, ERC20's do their thing. Then another contract can associate them, perhaps by taking custody and wrapping into a new combined entity.

@mudgen
Copy link
Contributor

mudgen commented Jun 22, 2018

I have been looking into the bottom-up approach. Some comments:

Using the bottom-up approach it is possible for new composable-aware ERC721 parent contracts to have onchain enumeration for any token that is transferred to it. This is done by using the safeTransferFrom and the onERC721Received functions to pass into the parent contract the child tokenId. And when the child contract transfers itself out of the parent it should make a call to the parent contract to remove itself.

I really like the idea of doing enumeration off-chain because I don't like the expense and complexity of the bookkeeping on-chain.

I like the idea of using events to track child tokens of parents. A cache of this info (child tokens) could be included in the JSON file returned by the tokenURI for each parent.

There is another issue which is buy/trade/sales verification of state. When selling a parent token the state of the parent token (all the children) can be checked using on-chain enumeration to verify it is all that is expected by the buyer. This verification can be done in the same transaction that the transfer is done. Without this verification it is possible for someone to remove children right before a transfer call and the buyer does not get what he/she paid for.

So my question is this: How to securely do state verification of a parent ERC721 token when selling it if there is no on-chain enumeration?

Perhaps the state verification can be done onchain by passing in an array of child contracts and child token Ids to check for after the transfer function is called. I think that would work.

@mryellow
Copy link

mryellow commented Jun 22, 2018

Related work, haven't dug into it but have seen this mentioned in relation to "composables".

We describe a specification for a new primitive that facilitates the lowcost, trustless creation, and exchange of a {Set}, a collateralized basket of ERC20 tokens on the Ethereum Blockchain. {Set}s serve as an abstraction for end users who want to think about higher-level token concepts without
dealing with the details of specific tokens. {Set}s are a superset of the ERC20 token standard with issue and redeem functionality, allowing for the atomic swap of the {Set}s and their underlying tokens. As an investment, {Set}s are similar to index funds (e.g. S&P 500, DJIA) and exchange traded funds (ETFs) in traditional financial services, allowing users to easily get exposure to a multitude of tokens. As {Set}s are ERC20 tokens and composable, it is possible for one token to represent a limitless number of other tokens.

https://setprotocol.com/pdf/set_protocol_whitepaper.pdf

edit:

https://github.com/SetProtocol/set-protocol-contracts/blob/52260e68651a75adac7e5f1c4add96d8b9c48986/contracts/core/SetToken.sol#L121-L122

They create an ERC20 which has a pre-defined set of addresses and quantities associated with it.

@flockonus
Copy link

flockonus commented Jun 25, 2018

Does a cat continue to be attached to a hat after the hat's ownership has changed?
Who can detach the hat?

@mryellow I believe that's the intention, the hat would keep on belonging to the same cat. Who can detach the item is the current owner of the cat.

So my question is this: How to securely do state verification of a parent ERC721 token when selling it if there is no on-chain enumeration?
Perhaps the state verification can be done onchain by passing in an array of child contracts and child token Ids to check for after the transfer function is called. I think that would work.

@mudgen I think so too! If the item in case that is for purchase, has attached items that are important for the buyer then it would make sense that the purchase order would pass an array of address+ids to verify the items are indeed attached to the NFT.

It would work with signed transactions (similar to 0x style) orders as well, if the seller at the time of the sales creation also bundle the promise of the items in the order.

@mg6maciej
Copy link
Contributor

mg6maciej commented Jun 25, 2018

@flockonus @mudgen #1180 proposes a generic solution for validation when selling NFTs.

Using the bottom-up approach it is possible for new composable-aware ERC721 parent contracts to have onchain enumeration for any token that is transferred to it. This is done by using the safeTransferFrom and the onERC721Received functions to pass into the parent contract the child tokenId. And when the child contract transfers itself out of the parent it should make a call to the parent contract to remove itself.

@mudgen I think your assumptions are wrong here. If I understand this bottom-up concept correctly, child token is never directly transferred to its parent, i.e. childContract.ownerOf(someTokenId) != parentContract is true, unless someone just sent a token to a contract using transferFrom and basically burned it.

@jordanschalm @flockonus Are you going to join our next composable call on NFTy Magicians discord? I would like to discuss this concept with you in depth and believe we should schedule a call sooner than planned. Also @jordanschalm I asked you some questions there, but if you want I can move it to GitHub.

@mudgen
Copy link
Contributor

mudgen commented Jan 13, 2020

@viltiki I totally agree with you!

@nichanank
Copy link

@viltiki I'm also interested in this development. my project's NFTs currently adhere to both ERC998ERC721 and ER998ERC20 but afaik wouldn't interoperate as-is with Enjin metaverse unfortunately.

@tclairet
Copy link

Hi, we developed a lightweight version working with the EIP1155 and the latest Open Zeppelin contracts. It would be very cool if you have time check it out. The repo is available here https://github.com/rocksideio/ERC998-ERC1155-TopDown.

All feedbacks are welcome.

@mudgen
Copy link
Contributor

mudgen commented Oct 21, 2020

@tclairet That's great. I'm doing something similar with Aavegotchi.

@jamesmorgan
Copy link

Hi 998 people! We are also building out a 1155 flavour of this with a parent 721 which can hold 1155 tokens. However for us the token itself is the owner of the children, so move the parent 721, you also get ownership of the child 1155 tokens.

I have looked at @tclairet repo which looks great, thanks for that its really good to see that.

I have noticed that the method signatures for 998 topdown 721 are not really applicable for topdown 721 -> 1155 token.

Is there a common understand of what a common interface for a 721 -> 1155 top down interface would look like? Or even a more standardised 721 -> 1155 EIP somewhere?

@mudgen
Copy link
Contributor

mudgen commented Oct 29, 2020

@jamesmorgan I modeled the 721 -> 1155 top down interface functions from 998. The function signatures can be similar to what ERC998 uses.

Like this:

/// @notice Transfer tokens from owner address to a token
/// @param _from The owner address
/// @param _id ID of the token
/// @param _toContract The ERC721 contract of the receiving token
/// @param _toTokenId The receiving token
/// @param _value The amount of tokens to transfer
function transferToParent(
    address _from,
    address _toContract,
    uint256 _toTokenId,
    uint256 _id,
    uint256 _value
) external;

/// @notice Transfer token from a token to an address
/// @param _fromContract The address of the owning contract
/// @param _fromTokenId The owning token
/// @param _to The address the token is transferred to
/// @param _id ID of the token
/// @param _value The amount of tokens to transfer
function transferFromParent(
    address _fromContract,
    uint256 _fromTokenId,
    address _to,
    uint256 _id,
    uint256 _value
) external;

/// @notice Transfer a token from a token to another token
/// @param _fromContract The address of the owning contract
/// @param _fromTokenId The owning token
/// @param _toContract The ERC721 contract of the receiving token
/// @param _toTokenId The receiving token
/// @param _id ID of the token
/// @param _value The amount tokens to transfer
function transferAsChild(
    address _fromContract,
    uint256 _fromTokenId,
    address _toContract,
    uint256 _toTokenId,
    uint256 _id,
    uint256 _value
) external;

@jamesmorgan remember there is EIP-2535 Diamond Standard if you start hitting the 24K max contract size limit. But it is also good for organizing a lot of functionality you want to keep at the same contract address. And for transparent upgrades.

@tak1827
Copy link

tak1827 commented Jan 3, 2021

I'd like to use this token as our service POC.
we expect to issue parent and child ERC721 from a same ComposableTopDown contract.
But, I realized that it's impossible because onERC721Received is external .

(Solidity version upgraded ERC998ERC721TopDown implementation is here: ERC998ERC721TopDown-solidity-v6)

Steps

First, mint 2 ERC721 token from a same contract.

composable_contract.mint(wallet_address) => tokenID_1
composable_contract.mint(wallet_address) => tokenID_2 

Second, safeTransfer 2nd token to a same ComposableTopDown contract.

composable_contract.safeTransferFrom(wallet_address, composable_contract_address, tokenID_2,  tokenID_1_bytes) => fail

Don't you expect this usage?

@rnbrady
Copy link

rnbrady commented Apr 1, 2021

Hi @mattlockyer, nice work on this.

So here we are creating an address for every NFT based on the contract address and tokenId of the NFT. This is exactly the same as if every NFT were deployed as a new contract from the main contract.

Why not just take that approach? It seems desirable to make NFTs first class citizens with addresses. They can then own and be owned by other contracts or tokens. They could also be implemented as minimal proxy contracts that point back to logic in the main contract, minimising gas and storage costs.

On a related not, another capability that I think would improve composability and usability is separation of ownership and control/possession. Any ideas on that gratefully received.

@Swader
Copy link
Contributor

Swader commented Jul 5, 2021

Just to chime in here, we have developed nested NFTs as part of RMRK and our set of art legos, and will take this EIP and its comprehensive discussion into consideration when porting our logic over to EVM. For a breakdown, please see Dawn of the Art Legos. We are also thinking hard how to slap royalties onto this, but that's a separate problem.

@mudgen
Copy link
Contributor

mudgen commented Jul 5, 2021

Just to chime in here, we have developed nested NFTs as part of RMRK and our set of art legos, and will take this EIP and its comprehensive discussion into consideration when porting our logic over to EVM. For a breakdown, please see Dawn of the Art Legos. We are also thinking hard how to slap royalties onto this, but that's a separate problem.

That's great. If your contract bytecode exceeds the max contract size you might want to consider using EIP-2535 Diamonds. I understand the challenge of implementing a lot of functionality into an ERC721 contract.

@lukasz-glen
Copy link

Hi.
Any progress with ERC998ERC1155TopDown? I was going to implement it and encountered this discussion. I see some people thought about it for a while. This would be great to have a standard. So, are there any plans to extend EIP-998 with new interfaces?
I also suggest to change naming convention. Repeating the term child can be misleading, it can be unclear what type of child it refers to. Names like safeTransferERC1155() are more transparent.

@lukasz-glen
Copy link

@mudgen
I have trouble with ERC165 interface id for ERC998ERC721TopDown. The spec claims it to be 0x1efdf36a. But type(IERC998ERC721TopDown).interfaceId returns 0xcde244d9. I even calculated it manually

bytes4(keccak256('rootOwnerOf(uint256)')) ^
bytes4(keccak256('rootOwnerOfChild(address,uint256)')) ^
bytes4(keccak256('ownerOfChild(address,uint256)')) ^
bytes4(keccak256('onERC721Received(address,address,uint256,bytes)')) ^
bytes4(keccak256('transferChild(uint256,address,address,uint256)')) ^
bytes4(keccak256('safeTransferChild(uint256,address,address,uint256)')) ^
bytes4(keccak256('safeTransferChild(uint256,address,address,uint256,bytes)')) ^
bytes4(keccak256('transferChildToParent(uint256,address,uint256,address,uint256,bytes)')) ^
bytes4(keccak256('getChild(address,uint256,address,uint256)'))

Still 0xcde244d9. Could you look at it?

MrDeadCe11 pushed a commit to MrDeadCe11/ERC998TopDownComposable that referenced this issue Sep 17, 2021
…e NFT.

ERC998 for Solidity ^0.8.2:
info here:  ethereum/EIPs#998

Most of the functionality is there.

feel free to use.
@brabanod
Copy link

Are there any plans to further improve this EIP and update to the review status? Why has it been on draft status for so long?

@mudgen
Copy link
Contributor

mudgen commented Jan 13, 2022

@mudgen I have trouble with ERC165 interface id for ERC998ERC721TopDown. The spec claims it to be 0x1efdf36a. But type(IERC998ERC721TopDown).interfaceId returns 0xcde244d9. I even calculated it manually

bytes4(keccak256('rootOwnerOf(uint256)')) ^
bytes4(keccak256('rootOwnerOfChild(address,uint256)')) ^
bytes4(keccak256('ownerOfChild(address,uint256)')) ^
bytes4(keccak256('onERC721Received(address,address,uint256,bytes)')) ^
bytes4(keccak256('transferChild(uint256,address,address,uint256)')) ^
bytes4(keccak256('safeTransferChild(uint256,address,address,uint256)')) ^
bytes4(keccak256('safeTransferChild(uint256,address,address,uint256,bytes)')) ^
bytes4(keccak256('transferChildToParent(uint256,address,uint256,address,uint256,bytes)')) ^
bytes4(keccak256('getChild(address,uint256,address,uint256)'))

Still 0xcde244d9. Could you look at it?

I verified this and you are correct. I fixed it in the standard. Thank you very much for pointing this out. Sorry for how long it took to get updated.

@mudgen
Copy link
Contributor

mudgen commented Jan 13, 2022

@columbbus

Are there any plans to further improve this EIP and update to the review status? Why has it been on draft status for so long?

I don't have plans. It's been draft status for so long because nobody is improving it, like updating the code to use a recent version of Solidity. And probably nobody is improving it because there isn't enough incentive vs other things.

@brabanod
Copy link

@mudgen That is very unfortunate, but I actually can't imagine that there is no need for such functionality for NFTs. Are there any other standards that try to accomplish the same, i. e. NFTs owning other NFTs that have become established? Or are there any other alternatives to achieve something similar to this ERC?

I know there is the ERC 994 but it seems that this is even less regarded than this ERC.

@mudgen
Copy link
Contributor

mudgen commented Jan 13, 2022

@columbbus I commonly run into projects that need, want or have implemented NFT composability (NFTs owning or controlling other NFTs). They implemented ERC998 or some modified version of it. ERC998 still works today and projects use it.

A fully updated and finalized standard for NFT composability would be useful and helpful but is not required to implement the functionality.

My most recently implementation is Aavegotchi, which uses the ideas of ERC998 to make ERC721s (Aavegotchis) own ERC1155 tokens (wearables and other items). Here: https://github.com/aavegotchi/aavegotchi-contracts

@MicahZoltu
Copy link
Contributor

I'm closing this issue for organizational purposes, but feel free to continue using it for discussion on EIP-998 for now. At some point before EIP-998 moves to review, the discussion link should be updated to point at an Ethereum Magicians thread.

@heypran
Copy link

heypran commented Aug 18, 2022

Just to be sure, we are not dropping this standard right?

@robotocryptogame
Copy link

I don't have plans. It's been draft status for so long because nobody is improving it, like updating the code to use a recent version of Solidity. And probably nobody is improving it because there isn't enough incentive vs other things.

This is a pretty important standard for industrial and gamming applications and I'm wondering why not keep improving it.

In industriall applications for example if you create an NFT for car parts like the ENGINE or the Wheels. and the NFT of the car is an 998 grouping all the 721's of individual parts.

I'm also in charge of a project that is implementing a game character that is going to have different NFT for different uses. And is really important for us to have this kind of functionality.

As a last question is possible to do this kind of work with other standard?. I have reviewed and have found nothing so far.

@robotocryptogame
Copy link

Same question and need.

I'm closing this issue for organizational purposes, but feel free to continue using it for discussion on EIP-998 for now. At some point before EIP-998 moves to review, the discussion link should be updated to point at an Ethereum Magicians thread.

I have the same question also for you guys. I feel this standard is more important than it seems.

@xinbenlv
Copy link
Contributor

xinbenlv commented Jan 17, 2023

I am happy to help drive the process from draft to final if existing authors want and there is interest of adoptions

@robotocryptogame
Copy link

aft to final if existing authors want and there is interest of adoptions

@xinbenlv I'm defently interested and our project. How can we help you?:

@mattlockyer @mudgen @jordanschalm

@steven2308
Copy link
Contributor

For anyone interested on NFTs owning other NFTs, or equipping them, there are already 2 ERCs in final status which achieve such functionality:

  • ERC-6059: Parent-Governed Nestable Non-Fungible Tokens
  • ERC-6220: Composable NFTs utilizing Equippable Parts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests