-
Notifications
You must be signed in to change notification settings - Fork 3.7k
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
docs: ADR-043 Base NFT Module #9329
Conversation
@alexanderbez @fedekunze @marbar3778 @shahankhatch We updated this ADR and resolved all comments in #9284, could you review it again? |
Co-authored-by: Ryan Christoffersen <[email protected]>
Co-authored-by: Ryan Christoffersen <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good. Some items missing:
- How are NFTs stored?: imo we should remove
GetId
andGetOwner
and use them as primary and secondary index keys, respectively, to store the NFTs and NFT Ids. Otherwise, define how you can query all the NFTs by owner.
id (string) --> NFT (bytes) // primary index for NFTs, stored by ID
owner (address) --> []string // store the NFT IDs owned by an address
-
The decision of leaving minting, burning restrictions to upper-level modules that import the NFT module should be clearly stated.
-
The logic for transferring the ownership of an NFT to another address (no coins involved) should also be defined in this module since you are working already with
owners
.
func (k Keeper) TransferOwnership(nftID string, currentOwner, newOwner sdk.AccAddress) error
- It's not entirely clear how the
BaseNFT
is based on ERC721. Define how you handle thename
andsymbol
fields (eg: perhaps leave it to the collection module) and how you can perform a conversion from ERC712 -->BaseNFT
(i.e thex
ERC721 field maps to they
field in the CosmosNFT
).
Co-authored-by: Federico Kunze <[email protected]>
My intention to add these two methods is that func (k Keeper) SaveNFT(nft NFT) error{
if !validate(nft){
assert("invalid nft")
}
saveNFTById(nft.GetId(), nft)
// saveNFTByOwner(nft.GetOwner(), nft)
saveNFTByOwner(nft.GetOwner(), nft.GetId())
} instead of func (k Keeper) SaveNFT(nftId string, owner sdk.AccAddress, nft NFT) error{
if !validate(nft){
assert("invalid nft")
}
saveNFTById(nftId, nft)
// saveNFTByOwner(owner, nft)
saveNFTByOwner(nft.GetOwner(), nft.GetId())
} |
I guess it's fine as long as you document you are aware of the tradeoff of storing the extra bytes (id and owner) on the struct too Note, I wouldn't store the NFT twice under different keys, and instead would use: saveNFTByOwner(nft.GetOwner(), nft.GetId()) |
Actually, I have no particular preference for storing the extra redundant fields. Just follow the conventions in other modules(e.g. staking & gov). Do you have some suggestions?
Thanks for your note. |
@alexanderbez @aaronc would you have a moment to leave any last reviews on this ADR? @aaronc I know you mentioned in particular giving this module its own go.mod -- |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm concerned that this ADR doesn't actually specify much.
For instance how do any NFTs get minted or burned? It says metadata can be mutated but how? It says that also says that the logic for transferring NFTs is defined but what is the logic? What rules are or are not enforced?
Also, I don't think the protobuf API is very usable. Why do id
and owner
get packed inside an Any
if all NFTs must have them? The Any
just wraps polymorphic metadata but how does that even get set? And if there is another module actually managing the NFTs why doesn't that module manage metadata for its class of NFTs?
To move this forward, is some representative of this ADR able to discuss at the next architecture call on June 4th?
Thanks @jandrieu. So I've been meaning to comment in #9589, but as I understand it there is no known technical solution for IID's. It would be great if @colin-axner could comment, but based on my read of the situation, once IBC is involved we can't disambiguate essentially identical denoms/classes because of how a token was routed from one chain to another. IID's are an idea that I really like in theory, but without a technical solution it is a bit hard to use them in this case. |
Well, if we are storing class and ID combined then it sounds like we're doing the same thing as ICS20 in which case we wouldn't want class and ID as top-level elements in this design, or am I reading this wrong?
I don't think a denom prefix of
Can you say a little more about the problem you're seeing here @okwme? It may be a legimate concern and it may also be one that has straightforward solutions. In my mind I'm assuming that a) client software has ways of knowing what the denoms actually are - in the IBC software I've used this has been the case and b) that if a state machine needs to know this there is a light client proof that can provide metadata about the denom. i.e. in neither case we actually need to communicate that the token is in the transfer packet we just need to communicate the ownership information. In my mind, I'm treating this similarly to the URI case that we talked about @okwme. i.e. the URI isn't something that gets "transferred" - it is there on the host chain if any client needs it but it's not IBC transfer's job to transfer metadata. Now, a boolean for FT vs NFT could be an exception to this if I really understood the use case clearly. But, I do think we should justify why for this boolean we do include that in the IBC packet necessitating a completely separate ICS, whereas for metadata/URIs we can just use light client proofs.
I agree that it sounds like a similar amount of work and if this is the case we should just make a new ICS. |
@aaronc wrote:
I'm fascinated by this. Could you explain in more detail? IIDs are DIDs and DIDs are under wide development and use, including deployments that are expected to reach tens of millions of people this year alone. So, it isn't the DID part of IIDs that are the problem. The extensions to DID properties added by IIDs are neither controversial nor complicated. They are a straight-forward engineering task. To say there is "no known technical solution" means either you are seeing something I'm not aware of or I have failed to communicate IIDs effectively.
I would love to understand how this is a failure. The DID universe not only allows, it requires each namespace to define its own method-specific identifiers. The rest of the ecosystem treats those as opaque. A wallet need not understand anything about particular DID methods, it just needs a resolver that does. Best practice is to use your own resolver (or buy a resolver service from a trusted party), but the wallet just knows the ID and the method, and doesn't need anything else until it parses the DID Document returned from the resolution process. That's where the verification relationships describe how to interact securely with the asset and service definitions present where to interact with the asset. So how does an opaque identifier--whose purpose is literally to delineate potentially similar assets--how does that become ambiguous just because IBC? A DID is a DID is a DID, regardless of the chain its on. My expectation is that there are assumptions here, such as constructing identifiers as hashes of denoms and classes, which I believe are the disconnect. This is the disconnect I'd like to figure out. From your comment, I believe you are actually making my point, namely these two factors are in conflict:
Your conclusion is that IIDs don't work because #1 and #2 are incompatible. My conclusion is that #2 is the problem and that #1 is a requirement regardless of whether or not IIDs are adopted as a solution. If you're building your identifiers such that they are NOT globally unambiguous, I say you're doing it wrong. I'm not sure if we're talking past each other, but I think we're getting closer to why I believe IIDs are a critical feature for NFTs: if you can't remove the ambiguity (and IIDs do that) then you can't even know which NFT is involved in a given transaction. Settle on an approach that is compatible with IIDs and then you'll have globally unique identifiers that work. Use a different approach and you still need to figure out global uniqueness or you can't tell NFTs apart. There are other ways to solve that, but you need one as part of the solution. |
@jandrieu this discussion goes into some of the issues: tendermint/cns#16. Again I'd love if @colin-axner or maybe @AdityaSripal could comment. But as I understand it, with IBC it's a hard problem to prove that two tokens indeed come from the same chain and even if we do know that they're from the same chain, we can't easily make them fungible. I believe this is because IBC tokens are really vouchers for tokens on the origin chain, or in the case of multi-chain hops vouchers for vouchers for origin chain tokens, if that makes sense. Maybe there is a technical solution to all of this but I believe the complexity is that one chain will have different levels of trust for other chains. If tokens from chain Hope that helps, but again I'd love it if someone more knowledgeable than me can comment. |
Also I would say that maybe a resolver at the UI level can help and that would be great, but it would still have to deal with the challenges of multi-chain hops, fungibility, equality, etc. By the way maybe best if we move this to the IID discussion thread. |
This is at least one of the languaging issues. With DIDs, "resolving" is the process of taking a DID and retrieving or generating its current authoritative DID Document. The way that you find the public key for verifying a particular interaction on behalf of a given DID is by looking up that key in the DID Document that you retrieve by resolving the DID. This has nothing to do with making a human readable string as in this quote from the tendermint thread:
Perhaps more confusing to me is the attempt to assert that an NFT on any given chain is the same as the NFT on another chain. IMO, that's a non-starter. Each chain is its own global statespace. What is on one chain is fundamentally NOT on another chain. Chains are rivalrous just like geography. Your token can't be on two chains at once any more than your physical person can be in two different geographic locations at once. I know I'm veering into fundamental IBC questions, but I'm trying to understand the semantics of the interaction. This was one of the items we had discussed on the Interchain NFT calls and I thought I understood the answers we got from the Gravity Bridge folks. It's one thing to freeze an asset on one chain (asset A) and cryptographically tie it to the state of an asset on another chain (asset B). You could consider that the equivalent of moving the NFT from one chain to another... and one COULD construct that freeze to enable its defrosting if--and only if--Asset B is appropriately consolidated and frozen. In this way, you could argue there is a reasonable semantic for "moving" an NFT from one chain to another. However, in neither case is Asset A ever Asset B. Asset A will have available all of the chain mechanisms of Chain A, and Asset B will not. Full stop. You most likely won't be able to call smart contracts on Chain A from Chain B. You would probably move control back to Chain A to trigger the contract, meaning the smart contract on Chain A only ever acts on Asset A. It probably has no idea about Chain B or Asset B. For example, if I have a CryptoKitty that allows me to breed on Ethereum, there is no guarantee that that service even exists on Chain B and definitely no guarantee that Chain A would recognize any changes made on Chain B. So, when Chain B "breeds" that CryptoKitty, and sets the siring ID and the breeding lock, that siring ID and breeding lock are ONLY relevant on Chain B. Chain A doesn't even know about it. In other words, the asset on Chain B is fundamentally NOT the asset on Chain A. Ever. They may have mutual ownership and, in some cases, may recognize mutual encumbrances, but they aren't the same NFT. So, could you (or someone) walk me through how a CryptoKitty, identified uniquely by an Ethereum IID causes problems when "transferred" in some way to Cosmos, with its Cosmos IID. My understanding is Gravity Bridge would allow something like this, but I don't understand how it works. Neither the IIDs nor their DID Documents are the same. Something in the Chain A DID Document would likely memorialize the transfer to Chain B and hopefully Chain B has some pointer to Asset A, so these can be cryptographically linked. The details of how this works is TBD and hopefully part of token management enabled by a "Tokens 2.0" module. However, that feels like mostly straightforward engineering; the conceptual foundation is that with the right cryptographic semantics IIDs should absolutely be usable across chain, if only because DIDs as an architecture are chain agnostic. I'm sure this is a misconnect on the IBC model, because I'm not understanding how very basic statements hold up, like
Umm... if two tokens have the same IID method, they are on the same chain (barring multi-chain IID methods, but even this can be addressed because such methods provide a means to find the chain the asset is on). This is why I like IIDs. They can uniquely identify any asset on any chain. How does IBC undo this uniqueness? As for making an NFT fungible, the way I would to it is by locking the asset on Chain A with a cryptographic link to a fungible token on Chain B. That link is all you need (as long as its semantics are clear). What is important is not that the Asset A === Asset B, but that Asset B is a rigorous derivation of value from Asset A. This semantic makes sense to me, but it seems the IBC way is perhaps much more loose about these definitions. Or maybe its just that words are hard and everyone shaves the edges sometimes and I'm still absorbing the IBC tribal knowledge. FWIW, I think of this like how corporations work in the US (apologies to non-US folks, I'm not sure how this is handled elsewhere). Corporations only exist if they are registered with the Secretary of State of a specific state. However, to "do business" in many states, they must register as a foreign corporation with each state. So, a startup forms a Delaware corp then registers as a foreign corp in California. In this sense, the corporation has NOT moved its charter to California. It is not a California Corp. It's just filed some paperwork so the machinery of state can track it for taxes and compliance and lawsuits and such. IMO, IBC is like that registration of a foreign corp and does NOT move assets from chain to chain, it simply registers the foreign asset so that on-chain components can make sense of it. The other option of course would be to dissolve the Delaware corp and incorporate anew in California. However, again, the CA corp would NOT be the same legal entity as the Delaware corp, even if it owns 100% of the same assets and assumes 100% of the Delaware corps obligations. It's not a big deal if the initial NFT module uses something other than IIDs or DIDs, but it would be a big deal if the NFT architecture precludes the use of DIDs, so I'm trying to understand this argument that there is "no technical solution" for using IIDs with IBC-compatible NFTs. Could someone walk me through how IIDs for NFTs break when used for IBC interactions? Or even more "standard", how they would break if simple DIDs were used? |
The light client solution to the problem above is an entire new IBC transaction. I've discussed with @colin-axner and @cwgoes about the problem of IBC queries in the past and they suggested that it would be far easier and more straight forward to make a normal IBC transaction that requests information from another chain and is returned as a result rather than take state from that chain and try to verify it with light client proofs outside of the standard IBC flow. At the core of this I still stand behind my primary reason for separating the current bank module from the NFT module: fungible and non-fungible tokens serve different purposes and different users to a great enough extent that is worthwhile keeping them separate at the level of module state and IBC. |
I'd also like to suggest the IID / DID conversation be taken to the IID discussion thread. |
Hello! 👋 Sorry for the delay, I was hoping to post a more detailed response today, but it seems the discussion has jumped ahead I will only touch on IBC integration based on the current proposal From my understanding, ICS 20 is essentially a generic transfer protocol which makes the assumption we are only transferring fungibile tokens. Specifically, an object with a denom and an amount. I'm working under the impression an ICS 30 protocol would basically be a ICS20, but with the packet data using an With that said, lets look through some of the comments on ICS 20 to ensure we all have the same understanding.
This is correct with one small correction. ICS 20 does not specify
The origin denom is preserved as indicated by the full denomination transferred in the packet data. What needs to be traced back and proved is the origin chain
This is true, but it is still certainly possible. The important thing to note though, in most situations, usage of a different channel/port actually implies that the classes are not equivalent. The only time they would be equivalent is if the NFTs were transferred using the same underlying connection, but why would we have 2 channels doing the exact same thing? Usually each channel performing ICS 20 functions (or ICS 30) will have different security guarantees via usage of different connections/IBC clients. Every time a fungible token or eventually a non fungible token is transferred to another chain, it accrues baggage in the form of security guarantees. The virtual copy is now secured by a different validator set and that should certainly affect its uniqueness. It is not identical to the NFT escrowed on the origin chain because of this Regardless of the design of the NFT module, associating an object transferred across IBC with a channel/port is an unavoidable. Something good to note, associating an object with the channel/port it was transferred over does give it uniqueness since all channel/port ids are unique on a given chain. So while channel-5 on the hub and regen represent different things, they both represent something very concrete and uniquely identifiable (ie channel-5 is a connection to a chain with id "mychain" with this list of security guarantees and this validator set and this current consensus root at this latest height). Identifying the chain corresponding to channel-5 in a human readable fashion does require extra work, but it isn't a difficult solution, just tedious
Is this the primary question being debated? Once NFTs are transferred over IBC, what functionality is retained?
I agree with this. My conceptual model is that the
I'd favor just doing a copy/paste of ICS 20. Future improvements to the ICS 20 spec could make it a generic transfer protocol, but I think it'd be faster to implement to just copy past the spec and implementation and swap out the packet data to contain NFT information Without modifications to ICS 20, I don't see it being a good solution to the current discussion because I think we would want to transfer metadata with the NFT in the same way we transfer the amount of a FT. After going through most of the posts I think I am starting to understand the concerns at hand. ICS 20 offers a very important security guarantee that a counterparty chain cannot change the supply of the origin chain. So a malicious chain connected to the hub cannot increase or decrease the supply of atom. This is an important notion in relation to NFTs because it restricts management of metadata on the receiving chain I think most posts are working under the implicit assumption that ICS 30 would only transfer the NFT id, it would not transfer permissions to modify the metadata. This makes sense once you consider the issue of modifying metadata on a receiving chain and then trying to unescrow it on the original chain. This does add an extra layer of complexity UX wise since metadata would always need to be obtained from the origin chain. ICS 20 tracks most information of a token so tracing the full history is only needed when you want to know the chain id of the origin chain However, even if we don't allow metadata to be modified after an IBC transfer, I don't see why we cannot transfer the metadata with the NFT id. This would solve the issue of querying the origin chain. ICS 30 could simply specify that once an NFT is in escrow its metadata cannot be modified. Transferring the class information is probably too much since a single escrow of any nft in that class would need to freeze modifications of that class information. I think this is fine though. I guess the class information is kinda like knowing the original chain id Some might object here and say "but it'd be really nice to modify NFT metadata while it exists on a different chain", but this seems very complex to me. If we transfer over IBC, how do we know that the receiver is the same identity as the owner? The origin chain cannot do verification of an address corresponding to a counterparty chain. Given the complexity of modifying metadata once it has been transferred via IBC, I think it is an okay requirement to freeze modifications until the NFT is unescrowed. Future improvements could implement a new ICS to tackle this problem, but this seems to be far in the future Let me know if this clarifies things. I think the current design is clean. It is hard to comment on the design without a understanding of what ICS 30 would look like. Designing ICS 30 in the same approach as ICS 20 makes the most sense to me technically and from a product point of view. As I stated above, without significant complexity in an ICS 30 design, we cannot transfer the permissions to modify an NFT after an IBC transfer nor can we do modifications while it is escrowed. Thus transferring the metadata with the NFT seems like a reasonable approach and handles most UX concerns |
Thanks for the thorough rundown @colin-axner ! It's exciting to hear your thoughts about the viability of transferring metadata. I think it points out that we need to have much more discussion about that point, but that it should take place as part of a future ICS conversation and that there is no reason that it would require any changes to the current ADR or block development of the NFT module in its current trajectory. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @colin-axner. That helps me better understand the IBC situation and @okwme I am now comfortable with the rationale for ICS30 with this current approach. I'll come through later and add some more comments, but happy to signal my pre-approval.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there are definitely some things that need to be thought through with the proposed design of using ICS30 for metadata. Left a few comments regarding those. They can be dealt with in follow-up PRs as ICS30 evolves if that's preferable.
Also, generally the SDK team is wanting to move forward with the ADR 033 approach. Since that is not ready yet I do want to block this work on that, but it is something that should be added in the near future.
message Class { | ||
string id = 1; | ||
string name = 2; | ||
string symbol = 3; | ||
string description = 4; | ||
string uri = 5; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the idea of transferring metadata using ICS30, we probably want to think of the minimal on-chain required metadata here. description
for instance might be overkill.
```go | ||
type Keeper interface { | ||
NewClass(class Class) | ||
UpdateClass(class Class) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
UpdateClass(class Class) |
If class metadata is transferred using ICS30, I'm assuming it would effectively become immutable. i.e. once class metadata is on another chain once, it can no longer be mutated on the origin chain because that could cause inconsistencies. Maybe there are other ways to deal with this but immutability seems the simplest.
|
||
Mint(nft NFT,receiver sdk.AccAddress) // updates totalSupply | ||
Burn(classId string, nftId string) // updates totalSupply | ||
Update(nft NFT) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update(nft NFT) | |
Update(nft NFT) |
We need to be very careful about how this Update
method is used if ICS30 transfers metadata. Basically once an NFT is escrowed, metadata would be locked. If there is a module who's logic allows a minter (not owner) of an NFT to modify the metadata, that could result in an inconsistent state.
Done. #9589 (comment) |
@robert-zaremba ok to update your changes requested review? |
i believe the issues were addressed in the larger thread and didn't reply to last request to confirm
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is not clear how the interoperability is achieved between the new NFT modules. I guess you want to make the x/nft
to be a new bank module for NFTs. So all new NFT modules will need to hook into the x/nft
(similarly to what we have been discussing with x/bank).
Please describe in detail in this ADR how the interoperability is achieved. Maybe in a new section before Consequences?
|
||
### Forward Compatibility | ||
|
||
This specification conforms to the ERC-721 smart contract specification for NFT identifiers. Note that ERC-721 defines uniqueness based on (contract address, uint256 tokenId), and we conform to this implicitly because a single module is currently aimed to track NFT identifiers. Note: use of the (mutable) data field to determine uniqueness is not safe.s |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So maybe we should change the language and use:
This specification follows the ERC-721 [link needed!] smart contract standard....
Also, an ERC-721 implementer has more choices (he creates a new smart contract for every ERC-721 token), here he can't do that.
|
||
### Positive | ||
|
||
- NFT identifiers available on Cosmos Hub. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Without IID, the NFT identifier is not different from bank identifier and can be confused.
wow, get merged while writing a comments. Fine - we can iterate over it. I would still like to have a section in the ADR about interoperability and how it will work. |
sorry about that @robert-zaremba you can see it was merged moments before you wrote your comments. Anyway probably better to put interoperability conversation into another thread dedicated to it—maybe the ICS. |
No problem @okwme. The ADR is not finalized, so it's fine to have "checkpoints" and move forward. |
## Description Following on: + NFT IBC [dependency](#9329 (comment)) thread/issue + [interoperablity](#9329 (review)) with other NFT implementations + x/bank interoperability [discussion](#9065 (comment)) we need to be clear how the NFT token interoperability between modules and chains is achieved. --- ### Author Checklist *All items are required. Please add a note to the item if the item is not applicable and please add links to any relevant follow up issues.* I have... - [ ] included the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title - [ ] added `!` to the type prefix if API or client breaking change - [ ] targeted the correct branch (see [PR Targeting](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#pr-targeting)) - [ ] provided a link to the relevant issue or specification - [ ] followed the guidelines for [building modules](https://github.com/cosmos/cosmos-sdk/blob/master/docs/building-modules) - [ ] included the necessary unit and integration [tests](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#testing) - [ ] added a changelog entry to `CHANGELOG.md` - [ ] included comments for [documenting Go code](https://blog.golang.org/godoc) - [ ] updated the relevant documentation or specification - [ ] reviewed "Files changed" and left comments if necessary - [ ] confirmed all CI checks have passed ### Reviewers Checklist *All items are required. Please add a note if the item is not applicable and please add your handle next to the items reviewed if you only reviewed selected items.* I have... - [ ] confirmed the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title - [ ] confirmed `!` in the type prefix if API or client breaking change - [ ] confirmed all author checklist items have been addressed - [ ] reviewed state machine logic - [ ] reviewed API design and naming - [ ] reviewed documentation is accurate - [ ] reviewed tests and test coverage - [ ] manually tested (if applicable)
Description
replace: #9284
ref: #9174 #9065
This ADR defines a generic NFT module of
x/nft
which supports NFTs as a, which supports storing NFT by id and owner, and contains the data field asproto.Any
and containsBaseNFT
as the default implementation.proto.Any
to extend the usage scenarios of NFTs.Before we can merge this PR, please make sure that all the following items have been
checked off. If any of the checklist items are not applicable, please leave them but
write a little note why.
docs/
) or specification (x/<module>/spec/
)godoc
comments.Unreleased
section inCHANGELOG.md
Files changed
in the Github PR explorerCodecov Report
in the comment section below once CI passes