-
Notifications
You must be signed in to change notification settings - Fork 11.3k
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
Conversation
sui_programmability/examples/nft_standard/sources/collection.move
Outdated
Show resolved
Hide resolved
…ideas Philosophy and requirements are in README.md, the implementation ideas are in `sources`
90189df
to
a49613a
Compare
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.
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. |
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.
all NFTs are Sui objects
but wouldn't it be only NFTs on the Sui network/blockchain that are Sui objects vs all NFTs?
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.
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). |
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.
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. |
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.
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. |
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.
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. |
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 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. |
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.
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> | |
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.
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. | |
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.
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. |
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.
to give
|
||
## Status | ||
|
||
- The standards above are supported by several (perhaps all?) Sui wallets and the explorer |
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.
Sui Explorer?
total_supply: u64, | ||
ctx: &mut TxContext, | ||
): (Collection<T,M>, MintCap<T>, RoyaltyCap<T>) { | ||
abort(0) |
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.
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 |
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.
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. |
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.
- We like the idea that NFTs of a given NFT Collection have their own type
T
which is expressed in their Collection typeCollection<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:
BaseLayerNft
← CustomLayerNft
← SingleWitness
If we wanted to build a collection of NFT collectibles we could have:
Nft
← Collectibles
← Suimarine
Or if we wanted it to be a unique nft collection instead:
Nft
← UniqueNft
← Suimarine
} | ||
} | ||
|
||
public fun buy(policy: &mut RoyaltyPolicy<ExampleNFT>, payment: &mut Coin<SUI>, ctx: &mut TxContext): RoyaltyReceipt<ExampleNFT> { |
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 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.
- In trading smart contracts (TSC) we cannot call the
foo_nft::buy(policy, payment, ctx): RoyaltyReceipt<FooNft>
of a particular type, (FooNft
orExampleNFT
orT
) 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). |
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.
-
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.
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.
- 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 renderU64_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 aMintCap
to mint is a barrier to parallelization--if you don't want a singleMintCap
to be a point of contention, you can always split it into as many subMintCap
'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> { |
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.
- 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 |
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.
- We appreciate the advantage of the proposed
RoyaltyReceipt
in terms of a single definition of collection'sRoyaltyPolicy
. 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 { |
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.
- The
RoyaltyReceipt
type only works if the TSCs integrate with theSafe
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 |
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.
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?
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.
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 { |
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.
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.
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.
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.
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 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 { |
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.
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.
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 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.
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.
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 |
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 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 { |
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.
An idea how to support custom royalty policies and make them enforceable via this object:
- 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 objectUncollectedRoyalty has store
. (Better name TBD) - The
UncollectedRoyalty
is stored in theSafe
object. - The user must call some function in
T
reference bySafe<T>
which knows how to execute the royalty policy. Only then can they access theCoin<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.
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.
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. |
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.
Perhaps a useful angle to divide this problem under is to consider the following scenarios separately:
- Both client and marketplace want to pay royalties
- Marketplace wants to avoid royalties but the client does not
- Client wants to avoid royalties but the marketplace does not
- 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.)
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 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:
The concept of To be more concrete, the
The collector remembers the last payment, and therefore 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 |
- 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 |
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.
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.
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. 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. |
The I think Sui should notice that NFT standards exist, but not the only option. Announcing the existence of Custom-NFT is not only to reserve corresponding interfaces 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. |
I believe this one has served its goal well! Closing! |
…ideas
Philosophy and requirements are in README.md, the implementation ideas are in
sources
.