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

[RFC] NFT standard philosophy, requirements, and some implementation … #4887

Closed
wants to merge 1 commit into from

Conversation

sblackshear
Copy link
Collaborator

@sblackshear sblackshear commented Sep 29, 2022

…ideas

Philosophy and requirements are in README.md, the implementation ideas are in sources.

…ideas

Philosophy and requirements are in README.md, the implementation ideas are in `sources`
Copy link
Contributor

@randall-Mysten randall-Mysten left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copyedit pass


### Individual NFT’s

An NFT is a Sui object (i.e., a struct value whose declared type has the `key` ability, which in turn means the struct value has a field named `id` holding a globally unique ID). All NFT’s are Sui objects, but not every Sui object is an NFT.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all NFTs are Sui objects

but wouldn't it be only NFTs on the Sui network/blockchain that are Sui objects vs all NFTs?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the Sui network:
Token is Object.
NFTs are Objects.
Make a general trading market for Objects, including token defi and NFT defi.
Object is valuable because Storage Fund supports the bottom price.


An NFT collection is a Move struct type with the `key` ability. Every NFT collection has a distinct Move struct type, but not every Move struct type corresponds to an NFT collection.

Collection metadata is a singleton object of type `sui::collection::Collection<T>` created via the [module initializer](https://examples.sui.io/basics/init-function.html) of the module that defines the NFT type `T`. Initializing a collection gives the creator a `MintCap<T>` granting permission to mint NFT's of the given type, and a `RoyaltyCap<T>` granting permission to mint `RoyaltyReceipt<T>`'s (which will be required upon sale--more on this below).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might be more clear to use that grants vs granting permission to

and (which will be required)
which is required

upon sale = to sell the collection?


### Transfers

Every Sui object whose declared type has the `store` ability can be freely transferred via the polymorphic `transfer::transfer` API in the Sui Framework, or the special `TransferObject` transaction type. Objects without the `store` ability may be freely transferrable, transferrable with restrictions, or non-transferrable—the implementer of the module declaring the type can decide.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sui objects with the store ability (from the declared type) are transferrable using the polymorphic transfer::transfer API in the Sui Framework, or the special TransferObject transaction type. Objects without the store ability can be transferrable, transferrable with restrictions, or non-transferrable as determined by the module implementer that declares the type.

By freely I think you mean "without restriction" but in the context of blockchain some might read it as "without fees"


Beyond the basics, we broadly view NFT standards in two separate parts: display and commerce.

- Display standards give programmers the power to tell clients (wallets, explorers, marketplaces, …) how to display and organize NFT’s. Clients have a “default” scheme for displaying Sui objects (show a JSON representation of all field values); these standards allow programmers to make this representation more visually appealing and end-user friendly.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NFTs for plural


Every Sui object whose declared type has the `store` ability can be freely transferred via the polymorphic `transfer::transfer` API in the Sui Framework, or the special `TransferObject` transaction type. Objects without the `store` ability may be freely transferrable, transferrable with restrictions, or non-transferrable—the implementer of the module declaring the type can decide.

Beyond the basics, we broadly view NFT standards in two separate parts: display and commerce.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might make this Display and Commerce so it is easier to see the distinction in the following section.

| name | std::string::String, std::ascii::String | Name of the NFT. Shown at the top of the NFT view |
| description | std::string::String, std::ascii::String | Description of the NFT. Shown in the NFT properties view. |
| url | sui::url::Url, sui::url::UrlCommitment, vector<sui::url::Url>, vector<sui::url::UrlCommitmen>t> | URL containing the content for an NFT, or a vector containing several such URLs. If the URL contains an image, it will be displayed on the left of the NFT view |
- A set of types with special display interpretation on the client side. When *any* Sui object (wrapped or non-wrapped) has a field of the given type that does not match one of the rules in the previous table, these rules will be applied.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rules apply


| Move type | Description |
| --- | --- |
| std::string::String, std::ascii::String | Displayed as a UTF8-encoded string for std::string::String or a vector<u8> if the underlying bytes are not valid ASCII., and an ASCII-encoded string for std::ascii::String. Displayed as a vector<u8> |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the period used in this one? ASCII., ?

| sui::url::Url, sui::url::UrlCommitment | Displayed as a clickable hyperlink |
| sui::object::ID,
sui::object::UID | Displayed in hex with a leading 0x (e.g., 0xabc..), with a clickable hyperlink to the object view page in the Sui explorer |
| std::option::Option<T> | Displayed as None if the Option does not contain a value, and Some(_) with display rules applied to the contents if the Option contains a value. |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Option does not (extra space)

Why is it not Option here?


## Philosophy

The aim of this design is to gives Sui programmers maximum flexibility—they don’t have to use special wrapper types (e.g., `NFT<T>`) to implement NFT’s with visual elements. They can simply define ordinary Sui objects that use the special field and type names, and clients will understand them.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to give


## Status

- The standards above are supported by several (perhaps all?) Sui wallets and the explorer
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sui Explorer?

total_supply: u64,
ctx: &mut TxContext,
): (Collection<T,M>, MintCap<T>, RoyaltyCap<T>) {
abort(0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: I would suggest making witness a one-time one. Otherwise it's too easy to cheat on the system.


/// Proof that the given NFT is one of the limited `total_supply` NFT's in `Collection`
struct CollectionProof has store {
collection_id: ID
Copy link
Contributor

@damirka damirka Sep 30, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be cool to have an identifier in collection as well (eg 50/100). Some people like myself prefer to hunt for magic numbers in edition.


### Collections

An NFT collection is a Move struct type with the `key` ability. Every NFT collection has a distinct Move struct type, but not every Move struct type corresponds to an NFT collection.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • We like the idea that NFTs of a given NFT Collection have their own type T which is expressed in their Collection type Collection<T> , T being a witness type representing the NFT type
  • Our understanding is that this is useful for human readability as well as to facilitate clients distinguish which collection a given NFT belongs to. Do we miss any other benefits?
  • The result of this is that each NFT creator will have to deploy its own module. This is fine and can be abstracted by an SDK, nevertheless these modules should be as light as possible (if we assume 1KB per deployed module 50,000 collections would roughly equate to 50 MB, that’s fine)
  • We believe this module deployed by the NFT creators should serve solely as a type exporter and should not contain any custom logic. Instead, the custom logic would be offloaded to another layer of modules that do not need to be deployed every time there is a new collection

Looking into the example implementation we see the following types:

Collection: Collection<SuimarineNft>
NFT: SuimarineNft

Where SuimarineNft would be the object type NFTs of a given NFT Collection. We had in mind the following:

Collection: Collection<SuimarineNft>
NFT: Nft<SuimarineNft>

This would allow us to leverage the base layer module and its type Nft as a type unifier that can be used across the full spectrum of liquidity layer modules. Moreover, we are currently in the process of merging a newly improved design of our nft-protocol and believe this Single Witness Pattern could be implemented on top of its base contracts. In this new design there is a base module NFT and we implement custom NFT types on top of it (i.e. Unique NFTs, Collectibles, Composable NFTs, Tickets, etc.). We could therefore have the following three layered approach:

BaseLayerNftCustomLayerNftSingleWitness

If we wanted to build a collection of NFT collectibles we could have:
NftCollectiblesSuimarine

Or if we wanted it to be a unique nft collection instead:
NftUniqueNftSuimarine

}
}

public fun buy(policy: &mut RoyaltyPolicy<ExampleNFT>, payment: &mut Coin<SUI>, ctx: &mut TxContext): RoyaltyReceipt<ExampleNFT> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the README and this interface. We thought about how it would complement our approach to an NFT orderbook and other trading facilitators. Let us begin the discussion by firstly stating what challenges do we forsee with the proposal as of now.

Over the course of the next few days, we will deliver sample implementations as a meeting point between what we see as the way to go about the NFT orderbook and your proposed solution, as it has some ideas we fancy.


  1. In trading smart contracts (TSC) we cannot call the foo_nft::buy(policy, payment, ctx): RoyaltyReceipt<FooNft> of a particular type, (FooNftor ExampleNFT or T) unless we redeploy a dedicated TSC for each collection kind. Our thinking when it comes to the liq.lay. is to have a suite of a few contracts which expose logic that's used for all collections, rather than deploying a new contract for each collection.

1a. It's easier to discover new orderbooks if you can query events for just one smart contract.

1b. With one contract you can have client "trust" - they only have to validate what a handful of TSCs (OB, auction, ...) do what they claim rather than checking it per each collection's own copy redeploy.


An NFT collection is a Move struct type with the `key` ability. Every NFT collection has a distinct Move struct type, but not every Move struct type corresponds to an NFT collection.

Collection metadata is a singleton object of type `sui::collection::Collection<T>` created via the [module initializer](https://examples.sui.io/basics/init-function.html) of the module that defines the NFT type `T`. Initializing a collection gives the creator a `MintCap<T>` granting permission to mint NFT's of the given type, and a `RoyaltyCap<T>` granting permission to mint `RoyaltyReceipt<T>`'s (which will be required upon sale--more on this below).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • We have given much thought to this. Our current perspective is that if we force all collections to have a Limited Supply via MintCap<T> we could risk leaving out some domain-specific use cases.

  • One use case could be a gaming item that can be minted on demand by the Gaming Creators. If the game wants unlimited supply and does not care about keeping track of the amount of objects minted in the collection object, we can take the most advantage out of parallelisation because we don’t require each mint transaction to mutate the supply in the Collection/MintCap object.

Copy link
Collaborator Author

@sblackshear sblackshear Oct 6, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Definitely agree with the desire to allow unlimited supply as an option.
  • The convention I had in mind for unlimited supply is to set the collection supply to U64_MAX. No one can feasibly mint that many NFT's, and client logic can be taught to render U64_MAX as "unlimited".
  • The prototype code does not require touching the Collection object to mint, for exactly the reasons you mention (want to max out parallelization). I don't think the requirement to touch a MintCap to mint is a barrier to parallelization--if you don't want a single MintCap to be a point of contention, you can always split it into as many sub MintCap's as desired.


/// Proof that the royalty policy for collection `T` has been satisfied.
/// Needed to complete a sale of an NFT from a Collection<T>`
struct RoyaltyReceipt<phantom T> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. The mechanism for collecting royalties with RoyaltyReceipt type: we believe that this should be left up to the TSCs (trading smart contracts.)

2a. The foo_nft::buy function is fool-able in the sense that a TSC could provide the least valuable input possible. I.e. there is no way to trust the marketplace about correct inputs into the function.

2b. Therefore it seems fit to change the purpose of RoyaltyReceipt to "force the trading contracts to consider correct royalty implementation.", rather than making it inconvenient to bypass royalties.


/// The royalty policy. This is a shared object that allows a buyer to complete a sale by
/// complying with it.
/// In all likelihood, there would be libraries for common policies that NFT creators can
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. We appreciate the advantage of the proposed RoyaltyReceipt in terms of a single definition of collection's RoyaltyPolicy. One defines a policy once and henceforth all TSC refer to that implementation. However, aside from having a new OB contract (or auction, etc) per collection, we struggle to see how to implement it due to point 1 (https://github.com/MystenLabs/sui/pull/4887/files#r984856303). We will further prompt how we could enable arbitrary royalty strategies better.

3a. Custom royalty policies, such as the one mentioned in the example, will require an incompatible foo_nft::buy function signatures

/// Requiring `royalty` ensures that the caller has paid the required royalty for this collection
/// before completing the purchase.
/// This invalidates all other `TransferCap`'s by increasing safe.transfer_cap_version
public fun buy_nft<T>(cap: TransferCap, royalty: RoyaltyReceipt<T>, id: ID, safe: &mut Safe<T>): T {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. The RoyaltyReceipt type only works if the TSCs integrate with the Safe type.

4a. Correctness. If the Safe type is not integrated in the method which performs the trade, it's up to the client (who has to batch txs) to prevent a race condition where two users could obtain TransferCap at the same time. Unless the safe::buy_nft is called atomically with the trade (and fails tx if the NFT was already sold), then both users paid protocol fees but only one claimed NFT. We're not convinced this subtle problem should be left on the clients. Therefore, TSCs should integrate with Safe.

4b. Enforcement. By taking enforcement out of trading contracts and having it done via Safe, honest trading contracts are limited to always work with Safe because otherwise any client could just sell their NFT as an object.

4c. By working with Safe, clients must know up front which exact Safe instance to provide into the entry method. That is not a problem for slowly trading TSCs. Clients can retry. However, frequent trades may render the OB become unusable.
The client has to fetch the OB state to know what's the lowest ask, because that determines what Safe to include in a tx to create a new bid.
The client then has to send the tx. If lowest ask changed, the tx fails. The client has to retry.
The client is interested in any collection's NFT, yet it observes failures due to abstraction leak.

4d. The OB cannot give some sort of receipt and let the user claim the NFT later via its own endpoint, because of the race condition mentioned above.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To elaborate on 4c: to lessen the frequency of collisions the client could provide an array of Safe objects, e.g. for the cheapest N NFTs. However, if someone happens to offer their NFT for the lowest price after the client observes the state of the OB, their tx would still fail.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An idea on how to facilitate the OB or any fast trading where collisions are a problem: we can have either ExclusiveTransferCap or a property exclusive: bool on TransferCap. Then for trading contracts such as OB we would enforce that an NFT is listed only once, while on other marketplaces such as auctions or bidding contracts we can resort to multi-listing.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness. If the Safe type is not integrated in the method which performs the trade, it's up to the client (who has to batch txs) to prevent a race condition where two users could obtain TransferCap at the same time. Unless the safe::buy_nft is called atomically with the trade (and fails tx if the NFT was already sold), then both users paid protocol fees but only one claimed NFT. We're not convinced this subtle problem should be left on the clients. Therefore, TSCs should integrate with Safe.

Unfortunately, I think this problem is fundamental if we want to support (a) listing on multiple marketplaces simultaneously and/or (b) revocation of TransferCap's (and I think both of these are important--e.g., ERC721 allows this).

Note that the "loser" of the race will only lose the gas fees for the aborted tx, not the cost of the NFT + commission + royalty--what will happen is that Safe::buy will abort with an invalid capability error (and thus no payments or other effects happen). This is unfortunate, but this same sort of thing happens on Eth, and (as mentioned above) seems like a fundamental consequence of sharing the permission to transfer an NFT.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's my understanding too: contracts have to integrate with the Safe to make the "loss" tolerable, ie. only the gas fee. Safe::buy must be always called in the same tx as where the trade happens.

Therefore when a trading contract exposes its public entry fun for buying an NFT, it must accept a Safe object. This leads to the issues mentioned in 4c.

A compromise we're offering here is to enable TransferCap to be optionally exclusive. Therefore OB or other contracts where exclusivity matters can use the pattern of giving a user some sort of receipt redeemable for the NFT later (e.g. when the corresponding Safe ID is known.) Yet other trading contracts such as auctions where one always knows which Safe to include could benefit from the multi-listing feature.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I think the "UniqueTransferCap" (which should also be non-revocable) idea makes sense. This is definitely a useful abstraction.

- A single, heterogenous `NFTSafe` is probably the simplest/most convenient, but one could also imagine `NFTSafe<T>` that partitions by type. This draft PR goes for the latter.
- A reasonable implementation of the `NFTSafe` idea will rely heavily on the forthcoming “dynamic child object access” feature: https://github.com/MystenLabs/sui/issues/4203.

# Display

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We were wondering what the vision is when it comes to wallets organizing NFTs by their business-level domains (i.e. Art, Profile Pictures, Gaming Assets, etc.).

The way we are currently approaching this is by having a tags field in the collection object where collection can push tags such as art, pfp with the idea of having wallets reading these tags and displaying them in the associated NFT tab. That way a an NFT collection can tag itself as a art and simultaneously as a gaming_asset collection if it fits both purposes.

Any thoughts on this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I think this makes a lot of sense!


/// Grants the permission to mint `RoyaltyReceipt`'s for `T`.
/// Receipts are required when paying for NFT's
struct RoyaltyCap<phantom T> has key, store {
Copy link
Contributor

@porkbrain porkbrain Oct 2, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at this with a fresh perspective: perhaps what we could do is to have a method in T which can create a new RoyaltyCap at will by the admin. Then, the admin - after they verified that a trading facilitator contract adheres to royalties - can give this object to that contract.

This means RoyaltyCap effectively acts as a whitelist.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Therefore one could either go via the T::buy_nft royalty collection fn or be trusted by the collection admin by getting RoyaltyCap from them.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is an interesting idea. The RoyaltyReceipt pattern is my least favorite part of this current proposal--I think the current setup creates too much friction for flexibility that is likely not needed. Some iteration on other techniques for low-level royalty enforcement would definitely be useful.


/// Gives the holder permission to transfer the nft with id `nft_id` out of
/// the safe with id `safe_id`. Can only be used once.
struct TransferCap has key, store {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be useful to extend this with collection_id so that in trading contracts we don't have to ask for both Safe<T> and TransferCap to verify that the NFT is of the right collection, we can only ask for TransferCap and then assert that the ID is what we expect it to be.

Copy link
Contributor

@damirka damirka Oct 2, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might get it wrong here. But if we don't include both into the same tx, race conditions (when listed on multiple marketplaces) become possible.

Copy link
Contributor

@porkbrain porkbrain Oct 2, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's correct for selling. However, for listing an NFT, the Safe might not be necessary.

}

public fun buy<T>(
royalty: RoyaltyReceipt<T>, coin: &mut Coin<SUI>, id: ID, safe: &mut Safe<T>, marketplace: &mut Marketplace
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reckon the trading contracts should be the ones enforcing royalties, otherwise, as a client, I would write my own interaction with this method in which I fool the buy_nft fn to get a RoyaltyReceipt for cheap.

In other cases, I might not know the price I will pay upfront. If royalty is a percentage cut, and I buy something from an orderbook, the state can change since I sent the request.


/// A shared object for storing NFT's of type `T`, owned by the holder of a unique `OwnerCap`.
/// Permissions to allow others to list NFT's can be granted via TransferCap's and BorrowCap's
struct Safe<phantom T> has key {
Copy link
Contributor

@porkbrain porkbrain Oct 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An idea how to support custom royalty policies and make them enforceable via this object:

  1. When a buyer pays for an NFT, the trading contract such as OB transfers the Coin<FT> they should receive to be owned by an object UncollectedRoyalty has store. (Better name TBD)
  2. The UncollectedRoyalty is stored in the Safe object.
  3. The user must call some function in T reference by Safe<T> which knows how to execute the royalty policy. Only then can they access the Coin<FT>.

To recapitulate: the trading algorithms know nothing of royalties, they just make the Coin<FT> object child of some UncollectedRoyalty object. The user can access the Coin<FT> via T::collect_royalty fn which unwraps the Coin<FT>, takes royalty, and gives the rest to the NFT seller.

Copy link
Contributor

@porkbrain porkbrain Oct 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can wrap the marketplace/wallet cut into this object too to incentivize them to perform the T::collect_royalty function (which can be called permission-lessly.) The marketplace/wallet could either: (i) include the call via a batched tx, (ii) have automation that does this as a scheduled job.

That way we take away the responsibility for sending that tx from the user.

- NFT’s that are listed for sale should still have an owner, and should be usable for tokengating, games, etc.
- However, it is important that listed NFT’s cannot be mutated by the owner (or if it can, this fact + the risks should be very clear to the buyer)—e.g., the owner of a `Hero` listed for sale at a high price should not be able to remove items from the `Hero`'s inventory just before a sale happens.
- An NFT should be able to be listed on multiple marketplaces at the same time. Note that the natural way of implementing marketplace listing (make the NFT object a child of a shared object marketplace) does not support this.
- **Low-level royalty enforcement**. We believe royalties are the raison d'être of NFT’s from the creator perspective—this is a feature physical or web2 digital creations cannot easily have.
Copy link
Contributor

@porkbrain porkbrain Oct 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps a useful angle to divide this problem under is to consider the following scenarios separately:

  1. Both client and marketplace want to pay royalties
  2. Marketplace wants to avoid royalties but the client does not
  3. Client wants to avoid royalties but the marketplace does not
  4. Both client and marketplace are disingenuous and want to avoid royalties

Case 4. is fundamentally lost, there will always exist a way for them to settle trades.

Case 1. is an ideal world scenario where we wouldn't have to build anything to perform royalty enforcement.

Case 3. is simple to solve if we let the marketplace tell the Safe: "this is how much has been paid for the NFT, now go and do your thing with the appropriate FooNft::pay_royalty function." We implemented this today in the OB to see how it would work.

Case 4. is harder to enforce. If most popular wallets want to enforce royalties, what mechanism can we provide them to validate that the marketplace has indeed paid the expected amount, otherwise fail the tx?
If we use the approach of RoyaltyReceipt then we must somehow in the marketplace validate that this is indeed the expected amount of royalty paid, otherwise scenario 2. is not covered. Additionally, a client might not always know the exact price they will pay upfront (e.g. contracts where slippage is appropriate such as order book.)

@porkbrain
Copy link
Contributor

On today's roundtable, we will present an alternative approach to royalty enforcement which we call strong enforcement and it has whitelisting at its core.

To take a strong stance and support creators, we deem it necessary to implement a logical transfer of ownership for NFTs. In Sui terms - an NFT only has the key ability. Here we are presented with a crucial dichotomy: (i) an NFT wrapper type analogical of the Coin object or (ii) a collection’s own type.

We are strongly opposed to the latter because it breaks composability. Packages cannot access the functions which facilitate the transfers, therefore they would have to work around them via some intermediary state. Whenever possible, we don’t want to rely on the implementation of the client (with batched txs) to guarantee an atomic transfer.

The proposed NFT wrapper contains a metadata property (generic of a collection type) and a property with the address of the owner which we shall refer to as the logical owner for the rest of this document to disambiguate it from the owner of the Sui object.

We create means for whitelisting trading contracts. Only whitelisted contracts can perform NFT transfers between two addresses. There is a shared object which can be controlled by creators or the creators can delegate the maintenance of that object to some emerged DAO or some organization akin to an HTTPs certificate authority. The advantage of whitelists is that these schemas are all enabled and don't have to be decided from day 1.

The logical transfer of ownership in the NFT wrapper contract is facilitated by the following public functions:

transfer<C>(whitelist: &Whitelist<C>, source: &UID, nft: Nft<C>, target: address)

  • Will assert that the &UID is contained (as ID) in the whitelist. Therefore, one could whitelist an orderbook. Interesting whitelists to explore further are package IDs to avoid generic via witness patterns.
  • Creators can also whitelist contracts that facilitate p2p transfers via some off-chain validation. They act as an oracle.

transfer_to_object<C, T>(nft: Nft<C>, target: &mut T)

  • We allow transfer to an arbitrary object for an NFT. It does not change the logical owner.

transfer_to_owner<C>(nft: Nft<C>)

  • Transfers the NFT to its logical owner. Can be used for example to claw back the NFT from a shared object.

The concept of Safe was introduced in previous conversations. To quickly recapitulate, its purpose is to enable listing in multiple marketplaces, borrowing, and royalty enforcement. A user can create a TransferCap object and give it to e.g. an auction house. Such a trading contract can then use the TransferCap to tell Safe to transfer ownership of the NFT to the buyer.
The funds paid for the NFT are wrapped in an object defined by the standard. To unwrap these funds, one must validate the call with a witness pattern. The witness used to unwrap these funds is declared in the specific collection contract, e.g. foo.move for the purposes of this discussion. The wrapped funds are transferred as children objects to a shared object declared by foo.move: RoyaltyCollector. Therefore, if an auction wants to perform a trade, it must accept the collector in the entry arguments. (Is there a possibility to deduplicate responsibility and have the whitelist serve as the collector?)

To be more concrete, the Safe contract exposes the following public function:

trade_nft<C, R>(whitelist: &Whitelist<C>, source: &UID, cap: TransferCap, buyer: address, royalty_collector: &mut R)

The collector remembers the last payment, and therefore Safe can check that some royalty has been transferred. This in conjunction with the whitelisting concept guarantees that marketplaces remain honest.

There’s an entry function in the foo.move contract which facilitates the royalty collection. We design it this way to enable custom royalty enforcement while most of the logic resides in the standard’s contracts and is therefore composable.

The RoyaltyCollector must be defined in the same module as the collection logic due to constraints on accessing child objects imposed by Sui. The wrapped coins are unwrapped, deducted royalties, and the rest is sent to the stated beneficiaries. The beneficiaries here are the NFT seller and optionally also e.g. wallets if a commission logic is implemented by the auction contract.

- A single, heterogenous `NFTSafe` is probably the simplest/most convenient, but one could also imagine `NFTSafe<T>` that partitions by type. This draft PR goes for the latter.
- A reasonable implementation of the `NFTSafe` idea will rely heavily on the forthcoming “dynamic child object access” feature: https://github.com/MystenLabs/sui/issues/4203.

# Display
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For display standards, we discussed the idea of creating some trait-like behaviour. Relevant for the Sui NS project was the expiry of an NFT.

If an object has been recognized as some NFT, wallets can apply an Expires trait optionally. They do this by looking for metadata.expiry date (unix or ISO 8601?) or - if missing - then metadata.is_expired bool flag.
Any NFT can have expiry logic. By having this as a trait rather than on e.g. only domain name NFTs, we have a more composable standard.

@gguoss
Copy link

gguoss commented Nov 11, 2022

…ideas

Philosophy and requirements are in README.md, the implementation ideas are in sources.

Should we come up with the simplest NFT standard first? Because there are a variety of product development requirements in the Sui network, everyone needs a standard to develop.
And now there is no perfect standard, can we come up with the simplest and backward compatible standard first, so that the developer community can use it first. This standard can be changed at will, at least until the Sui mainnet is launched.

As for the launch of the Sui network, this NFT standard should not be achieved overnight. It needs to face various development needs in the future and define a new NFT standard with forward compatibility.

@icodezjb
Copy link
Contributor

The object itself is very good as an NFT abstraction.
Of course, the NFT standard will speed up the development of the Sui Ecosystem.
But too specific NFT standard lack flexibility, which will limit the development of Sui Ecosystem.
If the standard is not good, it will block NFT creativity.
aptos-token is a negative example.

I think Sui should notice that NFT standards exist, but not the only option.
Standard-NFT and Custom-NFT (origin object) are equally important for Sui,
and appropriately encourage the community to get creative on Custom-NFT.

Announcing the existence of Custom-NFT is not only to reserve corresponding interfaces
for applications such as NFT wallets and trading markets, but also to protect the possible ideas of NFT.

Because once the NFT wallet and trading market do not support Custom-NFT, there will be no entry and trading market for Custom-NFT, and even the best Custom-NFT ideas will be stifled in the bud.

Erc721 is the application first, then the community promotes, and finally becomes the NFT standard, which is bottom-up.

Best wishes to Sui network.

@damirka
Copy link
Contributor

damirka commented Jun 28, 2024

I believe this one has served its goal well! Closing!

@damirka damirka closed this Jun 28, 2024
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

Successfully merging this pull request may close these issues.

7 participants