Skip to main content

Marketplace Development Journey

· 18 min read
Jason Tulp
Blockchain Technology Specialist - Protocol

Overview

During the design of the native Marketplace Pallet on The Root Network, we overcame many technical challenges and made design decisions that ultimately shaped the Marketplace Pallet you see today. The following article outlines some of these decisions and hopefully, by the end of reading it, you will have a deeper understanding of how the Marketplace, NFT, SFT and related pallets fit into The Root Network runtime. We cover our design processes and share a glimpse of the journey we take as protocol developers when converting features from ideas to reality. If you are interested in reading more, delve into the open-source code here.

Substrate + Pallets

To fully understand the functioning of our Marketplace Pallet, it's helpful to have some background knowledge of Substrate. Essentially, Substrate is a Blockchain framework upon which The Root Network is built. At its core, Substrate is a collection of independent “pallets” each designed to fulfil a specific purpose. The idea is that, if you aim to build a blockchain using Substrate, you can utilize the default pallets provided, which offer basic functionality without requiring extensive custom code. Each pallet is designed to be modular, meaning you may use one but not another for your runtime. While this modular approach is advantageous, Substrate doesn’t provide pallets for every function. And with The Root Network aiming to stay ahead of the game, we have to design and maintain many custom pallets ourselves.

A pallet consists of three main aspects: storage, extrinsics, and events.

  • Storage is all of the data stored within a pallet. For example, an account's ownership of a non-fungible token (NFT) is stored on-chain.
  • Extrinsics are functions that can be called by a signed account to perform actions on-chain, for example, transfer ownership of an NFT.
  • Events are information logs displayed to everyone viewing the chain, indicating that an extrinsic was executed. For example, NFT x was transferred from account y to account z.

At the time of writing, The Root Network has a total of 23 custom pallets that we’ve designed and implemented from scratch for our custom runtime. We will focus on three key pallets in this document: Marketplace, NFT, and SFT.

Non Fungible Origins

Now that you understand more about pallets, and how they serve as the fundamental building blocks of The Root Network, let’s delve deeper into one of those custom pallets: the NFT Pallet. As you may know, Non-Fungible Tokens (NFT) are unique, digital assets that reside securely on the blockchain. On Ethereum, NFTs are a token_id contained within ERC-721 Smart Contracts (or similar), each containing unique data per contract. ERC-721 serves as a token standard used to provide some cohesion between these NFT contracts.

There's a significant difference between Ethereum and The Root Network regarding NFTs. Ethereum “technically” doesn’t have any conceptual knowledge of what an NFT actually is. Instead, it just allows people to write and deploy contracts that provide the behaviour of what we know as an NFT. These contracts enable actions such as minting, transferring, burning and other expected NFT operations. However, it’s the contract itself that defines this logic, rather than the network. In contrast, The Root Network aims to provide a more standardized level of NFT logic. This is achieved by defining the logic of an NFT at the protocol level and providing common interfaces that can be easily utilized by anyone to use and interact with this logic. This means, to deploy a collection on The Root Network you don’t need to write a single line of code, just call create_collection on the NFT Pallet and you can be the proud owner of an NFT collection at a fraction of the cost.

We talk about how the NFT Pallet holds the “logic” of an NFT within itself, but what does this actually mean? It acts as a portal with a set of rules to interact with a set of data, common across all NFT collections. This data will be different between collections, however each collection will behave identically. For example, to determine ownership of a token, each collection has a token_ownership map which is a list of serial numbers within the collection owned by a particular address.

Protocol-level logic greatly benefits the standard user, however, it comes with its own set of challenges. How do you design a system for everyone? Designing pallets, like the NFT Pallet or the Marketplace Pallet, requires the designer to see into the future. It’s nearly impossible to know what use cases will come along and in which way the limitations will be stretched. However, one of the biggest advantages of housing the logic in the runtime (as opposed to a Solidity contract) is that it can be expanded upon, given it’s designed in such a way that allows for expandability.

The History of the Marketplace

In the first iteration of the Marketplace Pallet on The Root Network, the entirety of the Marketplace Pallet’s functionality was encapsulated within the NFT Pallet. This meant there was only one pallet responsible for tasks such as minting, transferring, buying, and selling NFTs. While this approach initially worked well, with SFTs on the horizon and the ever-growing complexity of the marketplace component, a change was necessary. One pallet was simply too stretched to adequately contain both the NFT and marketplace functionality. Therefore, it was logical to split the pallet into two distinct components. One would be primarily focused on NFT-related tasks, including minting, creating collections, transferring, and burning. Another (the newly appointed Marketplace Pallet) would focus on functionalities such as buying, selling, auctions and offers.

This sounds great in theory, but dividing a pallet in half comes with many complexities. The biggest and riskiest part is transferring the storage from the NFT Pallet to the new Marketplace Pallet and continuing operations from where we left off. This process is called Runtime Storage Migrations, and I like to think of it as replacing the tire of a moving car. Or in this case, splitting the tire into two pieces and installing an entirely new wheel.

Storage migrations are a particularly sensitive aspect of updating the runtime as each migration runs the risk of corrupting data. If this happens, the best case scenario is that people lose their NFTs and any value associated with them, the worst case scenario is that The Root Network becomes bricked and completely inaccessible. Despite implementing numerous procedures to test migrations against copies of the chain’s state before operating, it’s still a part of this job that causes the heart to skip a few beats when we perform the upgrade.

Another consideration when implementing significant changes like this is how it impacts any external teams building on the network. Imagine developing a frontend for a marketplace, only to find the pallet you were interacting with has split its functionality and moved to a different pallet. This is why communication regarding our changes and their potential impact on any external teams affected is crucial.

The Rise of the Semi-Fungible Token

Now you have an understanding of the structure of Substrate and some of the complications of how we got to this point. The Marketplace and NFT Pallets are now two separate components, working together to provide a service to those who use them. But what if we introduced a new token standard?

Enter SFTs. Designed as a native The Root Network solution to the ERC-1155 token standard on Ethereum, SFTs entered The Root Network to completely shake up the relationship between NFTs and the Marketplace. The reason for SFT support is to create a token standard with strong gamification purposes, think in-game items or partially unique collectibles. To understand the differences between NFTs and SFTs it’s best to look at an example.

For NFTs, each token_id is represented by a collection_id and a serial_number as follows: NFT_token_id = (1124, 4)

This represents token 4 from collection 1124. It is impossible to have two NFTs with the same token_id, however, you can have multiple unique tokens within the same collection

i.e.

(1124, 0)
(1124, 1)
(1124, 100)

The above tokens are all from the same collection however, they are unique due to their unique serial_numbers.

For SFTs, this is similar in that each token is represented by (collection_id, serial_number) but it is possible to own more than one of the same token, or multiple people can own a copy of the same token_id.

For example,

Bob owns 5 of (2534, 5)
Jason owns 100 of (2534, 6)

This seemingly simple difference means that an entirely new data structure must be designed to accommodate SFT ownership.

That’s where the SFT Pallet comes into play. Although fundamentally it seems very similar to the NFT Pallet, internally the moving pieces move in a very different way.

One Marketplace for All Tokens

The Root Network is growing rapidly, new pallets left and right supporting all types of token standards. SFTs, NFTs and Assets are increasing and more collections are being bridged/minted on The Root Network. This is an exciting time, but we are missing a piece to this complicated puzzle, SFT marketplace support. Why own an SFT if you can’t trade it? This is where our work before becomes extremely valuable, by separating the Marketplace code from the NFT Pallet, we open up the possibility to expand the Marketplace Pallet to support not only NFTs but also SFTs.

This is great in theory, however it is not as simple as it sounds. Due to the differences in the way NFTs and SFTs are stored on-chain, a different interface is required to sell these tokens. Currently selling an NFT looks like this:

pub fn sell_nft(
origin: OriginFor<T>,
collection_id: CollectionUuid,
serial_numbers: Vec<SerialNumber>,
buyer: Option<T::AccountId>,
payment_asset: AssetId,
fixed_price: Balance,
duration: Option<T::BlockNumber>,
marketplace_id: Option<MarketplaceId>,
)

You can see from the above interface that we can specify a collection and then specify multiple serial numbers within that collection, these are the NFTs that will be included within the marketplace listing. So you could sell tokens (1124, 5) and (1124, 6) in one listing.

But for SFTs, we may want to sell a quantity of a certain token, e.g. 5 x (1124, 5) so the interface would look something like this:

pub fn sell_sft(
origin: OriginFor<T>,
collection_id: CollectionUuid,
serial_numbers: Vec<(SerialNumber, Balance)>,
...
)

Separating the sell extrinsic into two parts, one for SFT and one for NFT would work in theory, however, it is a messy approach that doesn’t allow for any expansion in the future. Due to this, we ended up changing the interface to be generic across multiple types of tokens, either SFT or NFT, with room to grow in the future. The interface for that looks like this:

pub fn sell(
origin: OriginFor<T>,
tokens: ListingTokens<T>,
buyer: Option<T::AccountId>,
payment_asset: AssetId,
fixed_price: Balance,
duration: Option<T::BlockNumber>,
marketplace_id: Option<MarketplaceId>,
)

Where listing tokens is an enum that looks like this:

/// The type of tokens included in a marketplace listing, used to specify the type of listing
pub enum ListingTokens {
Nft(NftListing),
Sft(SftListing),
}

// A group of NFT serial numbers from the same collection
pub struct NftListing {
pub collection_id: CollectionUuid,
pub serial_numbers: Vec<SerialNumber>,
}

// A group of SFT serial numbers and balances from the same collection
pub struct SftListing {
pub collection_id: CollectionUuid,
pub serial_numbers: Vec<(SerialNumber, Balance)>,
}

That way, we can create one interface to rule them all! This also allows us to do some clever tricks where we can implement some common functionality over ListingTokens that can be uniquely handled for each case. i.e. if we want to lock the tokens (an important step when creating a listing to prevent the owner from transferring the tokens while the listing is active) we can do this:

/// Locks a group of tokens before listing for sale
/// Throws an error if owner does not own all tokens
pub fn lock_tokens(&self, owner: &T::AccountId, listing_id: ListingId) -> DispatchResult {
match self {
ListingTokens::Nft(nfts) =>
for serial_number in nfts.serial_numbers.iter() {
let token_id = (nfts.collection_id, *serial_number);
T::NFTExt::set_token_lock(
token_id,
TokenLockReason::Listed(listing_id),
*owner,
)?;
},
ListingTokens::Sft(sfts) =>
for (serial_number, balance) in sfts.serial_numbers.iter() {
let token_id = (sfts.collection_id, *serial_number);
T::SFTExt::reserve_balance(token_id, *balance, owner)?;
},
}
Ok(())
}

Then we just need to call this by using listing_tokens.lock_tokens(…) You can see that we end up with quite a clean, robust interface that not only allows us to have one extrinsic for both NFTs and SFTs but keeps the internal code clean and expandable within the Marketplace Pallet.

This system also allows us to expand upon the marketplace in the future and introduce new token standards without breaking the existing interface. We simply need to expand the enum to include the new token standard and implement its required behavioural patterns.

Dividing the Fungible Pie

Due to the value of NFTs and SFTs, we need a way to manage and share royalties. Let’s look back at Ethereum for an example, on OpenSea when you sell an NFT, you don’t receive 100% of the sale price. A small percentage of all sales within a collection are automatically transferred from the buyer to the creator of the collection, as well as a smaller cut to OpenSea themselves.

On The Root Network, we call this a RoyaltiesSchedule. The creator of any SFT or NFT collection can list up to 6 accounts and their percentage of royalties when creating the collection. Then anytime anybody sells one of those tokens, the network automatically distributes the royalties to the correct parties.

It doesn’t end there though, a marketplace can register itself as a marketplace on the network, and provide its unique MarketplaceId to add its cut to the royalties schedule of tokens sold on its site.

On top of that, 0.5% of every sale is automatically sent to our network's “FeePot” address. This forms part of the unique tokenomics of The Root Network and feeds the rewards pool for those securing the network through the vortex-distribution pallet.

And finally, the majority share will be sent to the seller of the token and the transfer will be initiated. There is no real limit to the royalties set by the marketplace and the collection owner. Theoretically, they could take 90% of all tokens, but who would want to buy a token where the owner takes 90%?

It’s worth noting, a collection owner can change their royalties entitlement at any time (unless they revoke ownership of the collection). However, a marketplace cannot change their royalties entitlement once registered.

Limitations and Restrictions

The unfortunate reality of designing any new pallet is that there will never be a perfect solution to accommodate everyone's needs and requirements. The perfect solution that appears in our dreams might be clouded by technical limitations or time constraints. Although we always have a core set of requirements that must be met, certain features may not make the final cut and we may have to introduce additional constraints to deliver the solution within the time frame.

For the Marketplace Pallet, a limitation that has existed throughout its lifetime is that a marketplace listing can only contain tokens from one collection. You may sell multiple tokens from the same collection in one listing, however, more than one collection has been intentionally restricted. The reason for this is due to the way royalties are paid out at a sales close. Just like on popular marketplaces such as OpenSea, The Root Network Marketplace Pallet supports sale royalties that are paid out to the collection owner at the time of sale. i.e. 5% of the sale price will be sent to the collection owner. The collection owner has the ability to specify different royalties on a collection-wide basis. This means that if you sell 5 NFTs or SFTs from one collection for 100 ROOT, 5 ROOT would go to the collection owner.

Now imagine the listing contains NFTs from multiple different collections. It would be easy to split the royalties if each collection owner took the same amount of royalties (i.e. 10% of the sale price) in this scenario we could easily distribute that 10% amongst the individual collections. e.g. if tokens from 5 collections were included in the bundle, each collection would receive 2%.

However, this will not work as we may have one collection with 10%, one with 15%, one with 5% etc.

If we were to estimate the royalty percentage based purely on the total sell price, there would be situations where some collection owners do not receive the royalties they deserve for the bundle sale. (i.e. if the value of their token made up 90% of the bundle price but they only receive 30% of the royalties).

Our preferred way to distribute royalties amongst the different collections would be to distribute based on the perceived value of each item within the bundle, this is not possible to do node side however, as we have no way to retrieve the value of an NFT besides what the user inputs as the sale price.

One way to achieve this would be to input the value of each token within the bundle when listing that token for sale, rather than one flat fee for the whole bundle. Then we can internally calculate the total bundle price from each price, distributing the royalties accordingly. This first step can become complicated and will be the responsibility of the marketplace to allow a user to enter these values.

With ERC-721 collections, such as FLUF World, the value of the FLUF NFT is determined by each individual trait in the tokens metadata. On OpenSea the floor price for each trait is displayed which allows users to easily see roughly what their FLUF is worth at that time.

The marketplace could implement a similar system to ensure their users are selling the items in the bundle at the appropriate price while still giving the collection owners their fair share of royalties.

Interesting Problems With Interesting Solutions

Sometimes when designing features you encounter problems that are not immediately obvious. One of these problems has to do with optimization, causing a butterfly effect of problems. In Substrate, you can store any type of data, strings, integers, custom structs etc. For NFT collections we store royalties for a collection which contains a list of entitlements. i.e. you can setup your collection to payout a percentage of each sale to multiple accounts automatically. Due to the nature of the blockchain, we can’t store infinitely large lists of data so we use a storage type called a BoundedVec for arrays. This is essentially a list of items with an upper bound to length. For royalties, the upper bound is set quite low (6 different accounts max). This works well, however, this doesn’t take into account a strange edge case within the Marketplace Pallet that adds two items to this list, potentially sending it over the upper limit of the bound, and causing the listing to fail.

Say a collection is created with the upper limit of 6 total entitlements. When we create a listing, there is the possibility the marketplace also collects royalties (marketplace royalties). We also have a network fee of 0.5% which is used in the same context. Meaning, when a listing is created, both the marketplace royalties and network fees get appended to the entitlements of the collection which will push it past the upper bound of the BoundedVec type. If this happens the listing will fail, rendering all NFTs within this collection unsellable.

Obviously, this is not ideal as it is unclear to the owner of the NFT why their token failed to sell which leads to a poor user experience.

The solution was relatively simple for this example, we increased the bound of the storage item within Substrate to 8, then when creating the collection or updating the royalties, we simply checked that it was less than the original bound of 6. That way there will always be room for the network fee and marketplace royalties when the token is listed for sale.

What’s Next for NFTs, SFTs, and the Marketplace Pallet?

Although we’ve covered the current state of the three pallets, their journey in the exciting world of The Root Network runtime is far from complete. Much like every moving part in The Root Network, requirements change, code can be optimized and features can be developed to extend the existing framework.

For SFTs that comes in the form of supporting bridging to and from Ethereum (much like it’s NFT big brother). For the Marketplace Pallet, the future is even more exciting and includes a robust system for both NFTs and SFTs (currently only NFTs are supported) and the potential for buying and selling bundles of both NFTs and SFTs from different collections.

Although it’s impossible to see the future, we can do our best to design the pallets in a way that allows for easy expansion when needed.

Learn about more features and custom pallets The Root Network has to offer here. To stay up-to-date with developments and join our growing community, follow us on X and join our Discord.