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

SIP-013 Semi-Fungible Token standard #42

Merged
merged 10 commits into from
Dec 6, 2022

Conversation

MarvinJanssen
Copy link
Collaborator

This SIP proposes a standard for semi-fungible tokens. Semi-Fungible Tokens, or SFTs, are digital assets that sit between fungible and non-fungible tokens. Fungible tokens are directly interchangeable, can be received, sent, and divided. Non-fungible tokens each have a unique identifier that distinguishes them from each other. Semi-fungible tokens have both an identifier and an amount.

The standard is still in draft stage but a PR is the best way to request comments and feedback. Copying some text from my reference repo for brevity.

Uses

Semi-fungible tokens can be very useful in many different settings. Here are some examples:

Art

Art initiatives can use them to group an entire project into a single contract and mint multiple series or collections in a single go. A single artwork can have multiple editions that can all be expressed by the same identifier. Artists can also use them to easily create a track-record of their work over time. Curation requires tracking a single contract instead of a new one per project.

Games

Games that have on-chain economies can leverage their flexibility to express their full in-game inventory in a single contract. For example, they may express their in-game currency with one token ID and a commodity with another. In-game item supplies can be managed in a more straightforward way and the game developers can introduce new item classes in a transparent manner.

SFTs and post conditions

Post conditions are tricky because it is impossible to make assertions based on custom print events as of Stacks 2.0. Still, native events can be utilised to safeguard SFT actions in different ways. The reference SFT implementation defines a fungible token using define-fungible-token to allow for post conditions asserting the amount of tokens transferred. It enables the user to state "I will transfer exactly 50 semi-fungible tokens of contract A". I am still exploring options in which the user can also assert the type of token. There are definitely ways in which an NFT defined with define-non-fungible-token can be used.

Options I am considering:

  • Mint and burn a native NFT with the provided token ID on every SFT action.
  • Define a native NFT with a more complex token identifier and mint these to the contract when an event takes place. The challenge is creating something that is unique whilst still making it easy enough to create assertions for. For example:
    (define-non-fungible-token sft-events {token-id: uint, amount: uint, sender: principal, recipient: principal, nonce: uint})
    
    (define-private (sft-event (token-id uint) (amount uint) (sender principal) (recipient principal))
    	(nft-mint? sft-events {token-id: token-id, amount: amount, sender: sender, recipient: recipient, nonce: (next-event-nonce)} (as-contract tx-sender))
    )

The second option makes it so we can also possibly do away with custom print events. A downside is that it creates an ever-growing NFT collection on the contract itself.

@MarvinJanssen
Copy link
Collaborator Author

Adding for visibility:

@LNow suggested shorter keys for the print events:

Also probably we should keep these events as small as possible, so maybe we should replace sender and recipient in transfer event with from and to?

Re:

I like to stick with sender and recipient personally, as we see this terminology elsewhere too.

Copy link
Contributor

@friedger friedger left a comment

Choose a reason for hiding this comment

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

I like it.


| Event name | Tuple structure | Description |
|----------------------|-------------------------------------------------------------------------------------------------|--------------------------------------|
| `sft_transfer` | `{type: "sft_transfer", token-id: uint, amount: uint, sender: principal, recipient: principal}` | Emitted when tokens are transferred. |
Copy link
Contributor

Choose a reason for hiding this comment

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

it would be nice if event types can be recognized easily from the hex representation (e.g. in a data base). It allows to filter by event type. As the keys of a tuple are ordered in alphabetic order it could make sense to use a as the type key.

Copy link

Choose a reason for hiding this comment

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

How about EVENT or just E? As long as there are no other keys written using capitals it should appear as first one.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Since one of the selling points Clarity is legibility, I would vote EVENT over just E. Then again, I am a stickler for code style and I do not use capitals anywhere. 😁 Event type recognition on a lower level would be pretty nice.

Copy link
Contributor

Choose a reason for hiding this comment

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

maybe action


### Bulk transfers with memos

`(transfer-many-memo ((transfers (list 200 {token-id: uint, amount: uint, sender: principal, recipient: principal, memo: (buff 34)}))) (response bool uint))`
Copy link
Contributor

Choose a reason for hiding this comment

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

why 200?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The send-many tool and some other bulk transfer functions have a 200 limit so I naturally adopted it. Do you have an alternative number in mind?

Copy link
Contributor

Choose a reason for hiding this comment

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

No, but let's add the tx costs for a basic implementation (with Stacks 2.05+ cost functions)

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 took a "worst case" scenario with a list of 200 items, all with a 34 byte buffer and calculated the costs with clarinet 0.20.0. It looks like we exhaust read_count:

+----------------------+-----------+------------+
|                      | Consumed  | Limit      |
+----------------------+-----------+------------+
| Runtime              | 117399567 | 5000000000 |
+----------------------+-----------+------------+
| Read count           | 12003     | 7750       |
+----------------------+-----------+------------+
| Read length (bytes)  | 873618    | 100000000  |
+----------------------+-----------+------------+
| Write count          | 8000      | 7750       |
+----------------------+-----------+------------+
| Write length (bytes) | 870000    | 15000000   |
+----------------------+-----------+------------+

So (7750-3)/60 puts us at a maximum list length of 129.1166666667, let us say 128 to be safe.

I'll do the same for the send-many function without memo and see how far I get.

Copy link
Contributor

Choose a reason for hiding this comment

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

A transaction should not take more than 80% of the block limit, otherwise it will be rejected, I think.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Source on this? If so, then ((7750*0.8)-3)/60 = 103.2833333333 or simply 100 to be safe.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Also, we could stick with a larger number to future-proof the SIP. I imagine costs will be lowered again at some point in the future. What do you think?

Copy link
Contributor

@radicleart radicleart left a comment

Choose a reason for hiding this comment

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

at line 88 get-token-uri. as opposed to to string-ascii in 009? - seems a shame to diverge unless there a good reason? Urls dont support non ascii characters so specifying ascii-string is unambiguous as to whether you need to escape non ascii characters in the url.

@MarvinJanssen
Copy link
Collaborator Author

That's right. Perhaps using UTF8 could save some space in comparison to punycode. What do you think?

Here's SIP009:

(get-token-uri (uint) (response (optional (string-ascii 256)) uint))

And here's SIP010:

(get-token-uri () (response (optional (string-utf8 256)) uint))

@radicleart
Copy link
Contributor

I see and yes utf8 is neater than using punycode but would there need to be utf8 characters in the url or is this for future proofing?

sips/sip-013/sip-013-semi-fungible-token-standard.md Outdated Show resolved Hide resolved
sips/sip-013/sip-013-semi-fungible-token-standard.md Outdated Show resolved Hide resolved
sips/sip-013/sip-013-semi-fungible-token-standard.md Outdated Show resolved Hide resolved
sips/sip-013/sip-013-semi-fungible-token-standard.md Outdated Show resolved Hide resolved
sips/sip-013/sip-013-semi-fungible-token-standard.md Outdated Show resolved Hide resolved

### Bulk transfers with memos

`(transfer-many-memo ((transfers (list 200 {token-id: uint, amount: uint, sender: principal, recipient: principal, memo: (buff 34)}))) (response bool uint))`
Copy link
Contributor

Choose a reason for hiding this comment

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

No, but let's add the tx costs for a basic implementation (with Stacks 2.05+ cost functions)


| Event name | Tuple structure | Description |
|----------------------|-------------------------------------------------------------------------------------------------|--------------------------------------|
| `sft_transfer` | `{type: "sft_transfer", token-id: uint, amount: uint, sender: principal, recipient: principal}` | Emitted when tokens are transferred. |
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe action

sips/sip-013/sip-013-semi-fungible-token-standard.md Outdated Show resolved Hide resolved
@saad-s
Copy link

saad-s commented Dec 9, 2021

Here I created a game contract as a reference implementation, loosely based on Clash of Clans using this standard. I really like the idea of SFTs and the ease this provides in managing multi token apps

@MarvinJanssen
Copy link
Collaborator Author

Updated the SIP013 document based on feedback.

@fiftyeightandeight would appreciate your feedback since your project is heavily leveraging SIP013. Did you run into any issues or limitations? Also note that the trait has changed a little; namely, string-ascii for get-token-uri and a new limit of 100 instead of 200 for the send-many functions.

@MarvinJanssen MarvinJanssen marked this pull request as ready for review December 23, 2021 12:23
@fiftyeightandeight
Copy link

We have a number of contracts implementing SIP013 (an example of which is here).
It helps maintain our code base compact (by aggregating similar contracts into a single contract), which is immensely helpful.
The proposal currently implements transfer (with/out memo) slightly different from SIP010, which may be debated (I am in favour of the approach SIP013 is taking).
It also has, necessarily perhaps, some compatibility issues with SIP010 (for example, get-balance vs. get-overall-balance), but I suppose this is more to do with wallet support, rather than the design of the proposal.
It would be interesting if Clarity would ever support polymorphism, but I assume this is not going to happen.

@MarvinJanssen
Copy link
Collaborator Author

Thanks for the quick response @fiftyeightandeight, immensely insightful. Would you mind giving a high level explanation of how ALEX utilises this SIP and what the upsides are in a few sentences? I think it is a nice implementation that warrants some background for other curious developers.

@fiftyeightandeight
Copy link

Hi @MarvinJanssen , we at ALEX offer fixed-rate / fixed-term lending/borrowing. The rate of such lending/borrowing is determined collectively and dynamically by the ALEX community (by buying/selling the underlying contract).
The term of such lending/borrowing is also determined collectively by the ALEX community, but is fixed at the time of such contract being created. For example, we may create a lending contract which expires on Jan 1, 2022 (i.e. fixed term), whose lending rate to expiry (i.e. fixed rate to expiry at a given point of time) is determined dynamically by the community.

From the Clarity contract perspective, this means you have a number of substantially similar contracts (e.g. lending contract) with slightly different configuration (i.e. expiry).

Before we adopted this SIP, we represented each of these contracts as a standalone contract, essentially copying and pasting an older contract, changing its name and updating it with a new expiry. You can see here what we had before we adopted this SIP. It is, put it mildly, not great.

After we adopted this SIP, we can aggregate all similar contracts (for example, yield-usda-xxxx.clar) into a single contract (for example, yield-usda.clar) without losing information about the expiry (in our case, token-id == expiry given its uniqueness). This immediately makes the code base much more compact while vastly reducing the margin of error. You can see how we now handle yield-token here.

@MarvinJanssen
Copy link
Collaborator Author

@fiftyeightandeight thanks for the explanation. Once (if) this SIP settles, I will update the reference repository with a list of interesting implementations like yours.

@Zk2u
Copy link

Zk2u commented Jan 31, 2022

This is super cool. I'm working on Fractal - an NFT fractionalisation protocol; blog here, testnet v1.5 here. Could we make some changes to this standard around memos? I don't think we need two functions for with/without memo. Wasn't aware of this proposal before working on this, so v2.0 of Fractal would support this standard.

@LNow
Copy link

LNow commented Jan 31, 2022

Just like you can transfer STX without providing memo, you should be able to do so with both FT and NFT, therefore I think separate functions should stay.

On a side note - @0xAsteria you have a huge security hole in your contract (unguarded trait + as-contract).

@Zk2u
Copy link

Zk2u commented Jan 31, 2022

you have a huge security hole in your contract (unguarded trait + as-contract).

haha funny you pointed this out, I noticed it this morning when reworking a few things for 2.0. haven't worked out a fix though. any ideas there?

@MarvinJanssen
Copy link
Collaborator Author

As @LNow said (also, welcome back!), I also think it should stay as separate functions. Effectively, most token transfers do not include a memo and the naming also mirrors the upcoming Stacks 2.1 stx-transfer-memo? complement to stx-transfer?. However, if you have a compelling case we have not considered then feel free to share it.

Fractional NFTs are basically a type of SFT. I created two (untested) example contracts on how it can be done with SIP013:

  1. Wrapping SIP009, where the owner of the original NFT can split it into fractions that can only be recombined by owning all fractions. The original NFT is held by the SFT contract in the mean time.

https://github.com/MarvinJanssen/stx-semi-fungible-token/blob/main/contracts/examples/fractional-sip009-sft.clar

  1. A SIP013 SFT contract that takes the SIP009 out of the equation. If an NFT creator foresees that owners might in the future decide to split their NFTs, might as well have that functionality built-in. A SFT token-id/supply combination is effectively an NFT if the supply is 1. Thus, SFTs are initially minted with a supply of 1 in this case, and whoever owns the total supply of a particular token ID can (re)fractionalise it to any supply of his or her choosing.

https://github.com/MarvinJanssen/stx-semi-fungible-token/blob/main/contracts/examples/fractional-nft.clar

Was also going to point out the unguarded trait reference but @LNow beat me to it 😁.

@LNow
Copy link

LNow commented Feb 1, 2022

you have a huge security hole in your contract (unguarded trait + as-contract).

haha funny you pointed this out, I noticed it this morning when reworking a few things for 2.0. haven't worked out a fix though. any ideas there?

Whitelisting or sandboxing.
Whitelisting is easy to implement and used by most (if not all) NFT marketplaces build on Stacks.
Sandboxing is more complicated (only tiny bit) and requires multiple contracts to be deployed, but at the end of the day it allows to build permission less systems.
https://github.com/LNow/clarity-notes/blob/main/security/as-contract.md

@314159265359879
Copy link
Contributor

314159265359879 commented May 17, 2022

You could add that no alternative SIP draft was published after the deployment of a compliant contract and after so many blocks.

The risk here is that we pick a timeout that's too low, and then we're stuck with it. What if we did a combination?

* There are N contracts by block B that implement this trait
* There are no revisions to this SIP's trait after C blocks

We pick N, B, and C accordingly, where C is significantly bigger than B.

Also, I think N should be at least 1. I think that if no one implements this trait -- not even the SIP authors -- then there's no reason to have the SIP at all.

I like this idea of Jude and Mike suggestion for N, minimum of 3.

Little history of SIP010 and SIP009
Block C should be a couple months in the future in my view. I see SIP010 (FT trait) was flagged as activation in progress on March 12th 2021, and the last revisions were made May 29th (changing the March 12th trait for the third/last time). Activation threshold (just N contracts deployed with trait) was met August 31st 2021.
For SIP009 (NFT trait), activation was on March 17th 2021, last changes made on March 19th and it was activated on May 20th 2021.

Allowing 2-3 months for revisions, is that reasonable? In my view SIP010 (and SIP009) were special because they were made in a very hectic time when Stacks 2.0 was just launched and a lot was happening, this also lead to some senior developers noticing inconsistencies later in the process. Perhaps that is less likely to happen as we improve the SIP process.
Suggestions/discussion for block B and block C:

Block B, (six months) 25920 BTC blocks after activation, or should this be closer to C? generally before or after C?
Block C, three months after activation.

@jcnelson
Copy link
Contributor

jcnelson commented Jun 8, 2022

How are you all feeling about this SIP? Is it ready for the technical CAB to give it a formal review? This would mean that after sign-off, no substantial changes would be made without a withdraw/resubmit.

@MarvinJanssen
Copy link
Collaborator Author

I'm happy to move this forward. I think we can keep the post condition stuff as-is. However, we need to add activation criteria before we put it up for review. I'll sleep on it and submit the update tomorrow.

@radicleart
Copy link
Contributor

@MarvinJanssen adding activation criteria is still outstanding.

@MarvinJanssen
Copy link
Collaborator Author

@radicleart updated Bitcoin block heights for activation and submitted.

@rafaelcr
Copy link
Contributor

@MarvinJanssen is there a way for a caller to get the last token ID similar to SIP-009's last-token-id? If not, could it be added to this SIP? If I'm not mistaken, get-overall-supply would not give the caller the number of token classes but rather the sum of all token class supplies, right?

This would be very useful for token metadata indexing services so they could iterate through all the available token classes for a SIP contract

@jcnelson
Copy link
Contributor

Thanks for adding the Activation section! Are you ready for a review from the technical CAB?

@radicleart
Copy link
Contributor

This would be very useful for token metadata indexing services so they could iterate through all the available token classes for a SIP contract

@rafaelcr having last token count in SIP 13 standard doesn't make sense in the way it did for the SIP 009 standard. Individual SIP 13s represent a mapping from {token-id, owner} --> amount as opposed to the SIP 009 {token-id} --> owner mapping.

Instead of using the last token count concept you can try reading from the contracts mint events which are available via the stacks api

@MarvinJanssen
Copy link
Collaborator Author

Thanks for adding the Activation section! Are you ready for a review from the technical CAB?

Yes it is ready for review @jcnelson. Missed this one.

@jcnelson
Copy link
Contributor

@kantai Can you take a look at this SIP when you get a free moment? Thanks!

@MarvinJanssen
Copy link
Collaborator Author

Pinging @kantai, is it good to move forward?

@Hero-Gamer
Copy link
Contributor

Hero-Gamer commented Oct 26, 2022

Pinging @kantai, is it good to move forward?

Hi @MarvinJanssen on the weekly SIP call #17 was said that Aaron is out of action until January afaiu.
On that call, Brice @obycode kindly volunteered to review this for you guys while Aaron is away. :)

@obycode
Copy link
Contributor

obycode commented Oct 28, 2022

Pinging @kantai, is it good to move forward?

Hi @MarvinJanssen on the weekly SIP call #17 was said that Aaron is out of action until January afaiu.
On that call, Brice @obycode kindly volunteered to review this for you guys while Aaron is away. :)

Looks good to me. It seems Aaron's concerns were addressed.

@friedger
Copy link
Contributor

Let's move to ratified!

@MarvinJanssen
Copy link
Collaborator Author

It appears that all activation criteria have been met, so can we move straight to Ratified? @friedger @obycode?

@jcnelson
Copy link
Contributor

Yup!

@jcnelson
Copy link
Contributor

Please switch the status to Ratified and add both myself and @obycode's contact information to the Sign-off section, and I'll merge it. Great job everyone!

@MarvinJanssen
Copy link
Collaborator Author

@jcnelson done!

@jcnelson jcnelson merged commit 0101c52 into stacksgov:main Dec 6, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Accepted SIP is in Accepted status
Projects
None yet
Development

Successfully merging this pull request may close these issues.