mijoco.btc

Aug 215 min read

Semi-Fungible Tokens: Practical Guide to SFTs on the Stacks Blockchain

A new standard (SIP-013) for Semi Fungible Tokens has been accepted.

Source: Clarity Innovation Lab, Clarity Standards

Following up on our recent post on a proposed standard for Semi-Fungible Tokens on the Stacks Network. The proposal, currently under review by the Stacks Improvement Process as SIP-013, suggests a standard for Semi-Fungible Tokens. SFTs are important because they extend and make more efficient the uses of NFTs on the Stacks Blockchain.

This post reports on our practical experience of working with SFTs and the Clarity Smart Contract programming language and provides tips for handling Post Conditions. Post Conditions are a unique feature of Clarity which protect users from poor or malicious smart contracts by placing constraints on the contract preventing it from transferring any user assets not explicitly listed in the post conditions.

The SIP-013 contract discussed below is based on this Clarity contract - with the main data structures defined as;

(define-fungible-token edition-token u100000)
(define-non-fungible-token artwork-token {token-id: uint, owner: principal})
(define-map token-balances {token-id: uint, owner: principal} uint)
(define-map token-supplies uint uint)

Note that edition-token represents fractional ownership of collection of all artwork-token NFTs, each individual NFT being capped at 100 edition tokens.

Post Conditions

The key to making post conditions work for SFTs has been using a mint and burn technique for destroying and then re-creating the assets when a transfer occurs.

For example (reference to this transaction) say Alice owns 20 shares of NFT 18 and transfers 5 to Bob then we will see contract events;

The point to note is that the senders total balance (20 editions tokens) is first burnt. Their ultimate balance (15 edition tokens) is then minted back to them with the other 5 token being minted to the new owner.

NB the NaN on the Artwork Token events indicates the Stacks Explorer does not yet understand how to display tuple keys for NFTs - this is a bug that’s been reported and will be fixed soon.

Scenario 1: Minting SFTs

This example is from a simple SIP-013 (SFT) contract which mints fractionalised artwork NFTs with a cap on the fungible tokens per NFT of 100 Edition (Fungible) Tokens - ownership of 100 edition tokens represents 100% ownership of the NFT.

Using the mapping from our earlier post;

(owner-address, token id) :---> int (quantity) 

The admin mint operation in the contract does not require any post conditions as none of the users assets are transferred by the contract - this method is linked to an off chain payment process. The user purchased 30% of this SFT via a card payment.

Stacks API: Token History

The token history Stacks API call requires a value that is the serialised NFT key - unlike most SIP-009 NFT contracts the SIP-013 contract defines the NFT key as a tuple of the tokenId and the owning address;

const getSerialisedNftTuple = function (data) {
  const tupCV = tupleCV({
    'token-id': uintCV(data.tokenId),
    owner: standardPrincipalCV(data.owner)
  })
  return `0x${serializeCV(tupCV).toString('hex')}`
}

Note that the API call is unable to return the balance (Amount in the picture below) as this is a property of a map in the contract.

Scenario 2: Full Transfer

Full transfer of the NFT requires two post conditions - one for the NFT and one for the FT transfer. The post conditions code looks as follows;

const getSerialisedNftTuple = function (data) {
  const tupCV = tupleCV({
    'token-id': uintCV(data.nftIndex),
    owner: standardPrincipalCV(data.owner)
  })
  return tupCV
}

const getGFTMintPostConds = function (data) {
  const postConditionAddress = data.owner
  const postConditionCode = FungibleConditionCode.Equal
  const postConditionAmount = new BigNum(data.amount)
  const fungibleAssetInfo = createAssetInfo(data.contractAddress, data.contractName, 'edition-token')

  const standardFungiblePostCondition = makeStandardFungiblePostCondition(
    postConditionAddress,
    postConditionCode,
    postConditionAmount,
    fungibleAssetInfo
  )

  const nonFungibleAssetInfo = createAssetInfo(
    data.contractAddress,
    data.contractName,
    (data.assetName) ? data.assetName : data.contractName.split('-')[0]
  )
  const standardNonFungiblePostConditionNotOwns = makeStandardNonFungiblePostCondition(
    data.owner,
    NonFungibleConditionCode.DoesNotOwn,
    nonFungibleAssetInfo,
    getSerialisedNftTuple(data)
  )

  const postConds = []
  if (data.amount >= data.balance) {
    postConds.push(standardNonFungiblePostConditionNotOwns)
  } else {
    postConds.push(standardNonFungiblePostConditionNotOwns)
  }
  postConds.push(standardFungiblePostCondition)
  return postConds
}

Scenario 3: Partial Transfer

Example of a partial transfer shows the the post conditions are actually the same as for the full transfer. This may be surprising given that the NFT is not transferred in this scenario but only some of the balance of edition tokens. Some further evidence for this (thanks to @Lnow) can be found here.

Post conditions for NFT transfers are determined by burn events rather than by mint events.

Summary

The ability for Stacks Clarity Smart Contracts to display Post Conditions on NFT / FT transfer events is something that sets it apart from Ethereum EVM based programming languages. Its a property that will play a central role in using Semi Fungibles for exploring many real world use cases.

Clarity Smart Contract on the Stacks Blockchain provide users the ability to control how a contract interacts with their digital property. They help prevent accidental and malicious loss of property. Alongside the fact that Stacks transactions settle on Bitcoin, Clarity post conditions will play a central role in the evolution of Web3 and the User Owned Internet.

Share this story