Closed MarvinJanssen closed 1 year ago
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
andrecipient
in transfer event withfrom
andto
?
Re:
I like to stick with
sender
andrecipient
personally, as we see this terminology elsewhere too.
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))
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?
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
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.
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.
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.
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.
@fiftyeightandeight thanks for the explanation. Once (if) this SIP settles, I will update the reference repository with a list of interesting implementations like yours.
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.
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).
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?
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:
Was also going to point out the unguarded trait reference but @LNow beat me to it 😁.
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
@MarvinJanssen have you though about using both NFT and FT at the same time?
NFT could be tied directly to type
and would represent ownership of particular `type.
And FT would represent amount.
Transferring X
amount of Y
type would result in:
X
amount of FT to recipientY
to contractThat way user will see on the transaction confirmation screen that X
units will be transferred out of user wallet, NFT Y
will be transferred to contract and eventually returned.
I assume your last sentence should read:
burning NFT if user transferred all or transferring NFT back to [contract]
Yes I thought of a few different ways of including an NFT—even introducing a more complex NFT type definition with a tuple. It would be really useful to be able to do post conditions but I couldn't come up with one that didn't have downsides or would clash in one situation or another. Can you share a bit more on what you think it could look like?
No. NFT is transferred back to user.
Main logic is the same as in your examples. FT represents amount, balance per type is stored in map. The only difference is that users receives unique NFT that represents ownership of one specific token type. And this NFT is not transferable.
For example user would have one NFT for gold, one for sword, one for helmet, one for viking helmet etc. ID of each NFT is unique. My sword NFT will have different ID then your sword NFT.
At first I thought that this idea is stupid and doesn't bring any value to what you proposed. But at some point in the future wallets might be able to display basic NFT metadata. And when that happen users will see exactly whether what they are transferring is exactly what they wanted to transfer or not.
In this PoC NFT is always returned back to sender, so it can be treated as mark that you have or had tokens of a specific type.
(define-constant CONTRACT_ADDRESS (as-contract tx-sender))
(define-fungible-token sft-value)
(define-non-fungible-token sft-token uint)
(define-constant ERR_INSUFFICIENT_BALANCE (err u1000))
(define-constant ERR_UNAUTHORIZED (err u1001))
(define-data-var lastNftId uint u0)
(define-map TypeSupply
uint ;; TypeId
uint ;; Supply
)
(define-map OwnerTypeToToken
{ owner: principal, typeId: uint }
{ tokenId: uint, balance: uint }
)
(define-read-only (get-balance (typeId uint) (who principal))
(ok (get-balance-uint typeId who))
)
(define-read-only (get-balance-uint (typeId uint) (who principal))
(get balance (default-to {balance: u0} (map-get? OwnerTypeToToken { owner: who, typeId: typeId})))
)
(define-read-only (get-overall-balance (who principal))
(ok (ft-get-balance sft-value who))
)
(define-read-only (get-total-supply (typeId uint))
(ok (get-total-supply-uint typeId))
)
(define-read-only (get-total-supply-uint (typeId uint))
(default-to u0 (map-get? TypeSupply typeId))
)
(define-read-only (get-overall-supply)
(ok (ft-get-supply sft-value))
)
(define-read-only (get-decimals (typeId uint))
(ok u0)
)
(define-read-only (get-token-uri (typeId uint))
(ok none)
)
(define-public (transfer (typeId uint) (amount uint) (sender principal) (recipient principal))
(let
(
(tokenOwnedBySender (unwrap! (map-get? OwnerTypeToToken { owner: sender, typeId: typeId}) ERR_INSUFFICIENT_BALANCE))
(tokenOwnedByRecipient (map-get? OwnerTypeToToken { owner: recipient, typeId: typeId}))
)
(asserts! (is-eq tx-sender sender) ERR_UNAUTHORIZED)
(asserts! (>= (get balance tokenOwnedBySender) amount) ERR_INSUFFICIENT_BALANCE)
;; transfer NFT to contract
(try! (nft-transfer? sft-token (get tokenId tokenOwnedBySender) sender CONTRACT_ADDRESS))
;; transfer FT to recipient
(try! (ft-transfer? sft-value amount sender recipient))
;; update recipient balance and mint NFT if necessary
(match tokenOwnedByRecipient token
(map-set OwnerTypeToToken
{ owner: recipient, typeId: typeId }
{ tokenId: (get tokenId token), balance: (+ (get balance token) amount) }
)
(let
((newNftId (+ (var-get lastNftId) u1)))
(try! (nft-mint? sft-token newNftId recipient))
(map-set OwnerTypeToToken
{ owner: recipient, typeId: typeId }
{ tokenId: newNftId, balance: amount }
)
(var-set lastNftId newNftId)
)
)
;; update owner balance
(map-set OwnerTypeToToken
{ owner: sender, typeId: typeId }
{ tokenId: (get tokenId tokenOwnedBySender), balance: (- (get balance tokenOwnedBySender) amount) }
)
(try! (as-contract (nft-transfer? sft-token (get tokenId tokenOwnedBySender) CONTRACT_ADDRESS sender)))
(ok true)
)
)
(define-public (transfer-memo (typeId uint) (amount uint) (sender principal) (recipient principal) (memo (buff 34)))
(begin
(try! (transfer typeId amount sender recipient))
(print memo)
(ok true)
)
)
(define-public (mint (typeId uint) (amount uint) (recipient principal))
(let
(
(tokenOwnedByRecipient (map-get? OwnerTypeToToken { owner: recipient, typeId: typeId}))
(totalSupply (get-total-supply-uint typeId))
)
;; not guarded by anything special, as this is only a PoC
;; update balance and mint NFT if necessary
(match tokenOwnedByRecipient token
(map-set OwnerTypeToToken
{ owner: recipient, typeId: typeId }
{ tokenId: (get tokenId token), balance: (+ (get balance token) amount) }
)
(let
((newNftId (+ (var-get lastNftId) u1)))
(try! (nft-mint? sft-token newNftId recipient))
(map-set OwnerTypeToToken
{ owner: recipient, typeId: typeId }
{ tokenId: newNftId, balance: amount }
)
(var-set lastNftId newNftId)
)
)
;; mint requested amount
(try! (ft-mint? sft-value amount recipient))
(map-set TypeSupply typeId (+ totalSupply amount))
(ok true)
)
)
Right, and how would you know the internal token ID so you can construct your post condition? Read it first via a read-only call? Not insurmountable but seems inconvenient.
Are these roundtrip NFTs fine for post conditions? I assume so but never tried.
We could also define the NFT as this, perhaps:
(define-non-fungible-token sft-token {token-id: uint, owner: principal})
Then it is easier to determine what the token ID will be for post conditions. Every transfer would require the user to send that NFT to the contract which it will later get back. Or it could a burn-and-mint in one go, instead of a transfer. Also never tried how post conditions respond to that.
Could the fractionalisation be done another way - e.g. using Judes nftree to fractionalise a single NFT file - in a similar way to how dropbox builds a merkle tree from blocks of a single file in order to make the download more efficient. Each chunk/block is already backed by an FT (tickets) equating to the value of that chunk w.r.t. the whole?
I think this in similar way to the Myceanas project which tokenised high value artworks to try to democratise artwork ownership?
We could also define the NFT as this, perhaps:
(define-non-fungible-token sft-token {token-id: uint, owner: principal})
Yes, good point. As this NFT is not a normal NFT that you can trade with other user it doesn't have to be compliant with SIP-009, therefore custom/complex ID should not be a big deal. And it eliminates extra read-only call from the workflow. :+1:
With regards to post-conditions - they will work perfectly fine.
@LNow would it be that simple? https://github.com/MarvinJanssen/stx-semi-fungible-token/commit/309a9a9fb6be96543aec3ca36d5ccb0c6a32648e
I always naturally assumed something like this is meaningless because the final state doesn't change and that is what post conditions check. Nonetheless, I never tried it out. We could deploy something on testnet and play with it. (Side-note: the explorer really needs post condition support.)
Yes it is that simple.
I always naturally assumed something like this is meaningless because the final state doesn't change and that is what post conditions check.
Post conditions are very weird and they don't work exactly as they are described in the documentation.
For burn+mint in one TX the only valid post condition is single DoesNotOwn
that covers burn operation.
example contract
For transfer+transfer in one TX (like in my PoC) - you need 2 DoesNotOwn
post conditions (one for user and one for contract). example contract
Explorer, Wallet and documentation needs proper post-condition support.
@LNow (& others) thoughts?
https://github.com/MarvinJanssen/stx-semi-fungible-token/pull/3
Will update the SIP doc in this PR in a bit.
@MarvinJanssen looks ok. To increase security it would be also good to change how wallet displays NFT post conditions. Right now NFT id is not displayed on TX confirmation screen. So every time you do something with NFT you can be tricked by UI to transfer/swap different (more expensive) token.
Thanks for sending this in @MarvinJanssen! Left feedback.
Did a second pass. I unresolved a couple comments because it helps me remember which ones need to be addressed before advancing this to Accepted status. Hope you don't mind ;)
I unresolved a couple comments because it helps me remember which ones need to be addressed before advancing this to Accepted status. Hope you don't mind ;)
Sure, just makes it a little harder for me to figure out which ones I had already addressed but not yet published. 😁
Happy to have been working on this since moving to the Clarity Lab.
Updated per discussions and comments above @jcnelson @friedger @radicleart @LNow.
sip013-semi-fungible-token-trait
and a new optional sip013-send-many-trait
.SIP-013-001.tar.gz
, which will contain the reference repository once we move into the accepted stage.Looks good to me. Feel free to transition this to Accepted status :)
Great, done!
I do have a few more open points / questions:
In the current version, Post Condition coverage sounds more advisory than mandatory. I am not sure if I prefer making the mechanisms in the reference implementation mandatory; specifically (a) the fungible token transfer to cover the amount, and (b) the burn-and-mint to cover the token ID. It would be good to get some more input and weigh the pros and cons. Also to add there is no good way to enforce these mechanisms. Traits obviously do not cover them and Clarinet would need something like https://github.com/hirosystems/clarinet/issues/268 to make such deep analysis possible.
Suggestions on good Activation requirements that is clearly falsifiable. To me the standard seems "obvious" but is "N number of SIP013 token contract deployed by block height H" sufficient?
@friedger @LNow what do you think regarding the above questions? Should we just move ahead with whatever feels right? Let's get this standard over the finish line.
1) Instead of enforcing post conditions, I would define a behavioural specification between the functions, like the balance function must return the same amount until transfer, transfer-memo was called. Tokens can be implemented with many native assets in various ways, therefore a specification for post conditions would not be too restrictive.
2) My concern would be that, currently, there is not a lot of demand for semi fungible tokens. Therefore, activation is not a question of number of deployed contracts. You could add that no alternative SIP draft was published after the deployment of a compliant contract and after so many blocks.
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?
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.
Sounds good.
ALEX already have one implementation and Clarity Lab has another on testnet so N should be minimum 3.
Sounds good.
ALEX already have one implementation and Clarity Lab has another on testnet so N should be minimum 3.
Yes, our implementation is expected to go live later this month.
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.
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.
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.
@MarvinJanssen adding activation criteria is still outstanding.
@radicleart updated Bitcoin block heights for activation and submitted.
@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
Thanks for adding the Activation
section! Are you ready for a review from the technical CAB?
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
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.
@kantai Can you take a look at this SIP when you get a free moment? Thanks!
Pinging @kantai, is it good to move forward?
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. :)
Pinging @kantai, is it good to move forward?
Hi @MarvinJanssen on the weekly SIP call https://github.com/stacksgov/sips/issues/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.
Let's move to ratified!
It appears that all activation criteria have been met, so can we move straight to Ratified
? @friedger @obycode?
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 usingdefine-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 withdefine-non-fungible-token
can be used.Options I am considering:
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:
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.