Skip to main content

Asset Tokenization

Asset tokenization refers to the process of representing real-world assets, such as real estate, art, commodities, stocks, or other valuable assets, as digital tokens on the blockchain network. This involves converting the ownership or rights of an asset into digital tokens, which are then recorded and managed on the blockchain.

High-level overview

The concept is to divide high-value assets into smaller, more affordable units, representing ownership or a fraction of the asset.

This strategy enables wider participation from investors who might want to mitigate risk by investing in a portion of a digital asset rather than being the sole owner, thereby expanding accessibility to a broader range of investors.

This pattern is similar to the ERC1155 multi-token standard with additional functionality. This makes it a suitable choice for Solidity based use cases that one might want to implement on IOTA.

  • Asset creation

    Each asset is fractionalized into a total supply, with each fraction represented as either a non-fungible token (NFT) or fungible token (FT) type collectible. This ensures that each individual fraction maintains a balance equal to or greater than one, and when combined, all fractions collectively reach the total supply of the asset.

    Besides the total supply, each asset is defined by various other fields such as name, description, and more. These fields collectively form the metadata for the asset, and they remain consistent across all fractions of the asset.

  • NFTs vs FTs distinction

    Each time a tokenized asset is minted, there's a possibility for it to be created with new metadata. If new metadata is incorporated, the tokenized asset is deemed unique, transforming it into an NFT. In this case, its balance is limited to one, signifying that only a single instance of this asset exists.

    If there's no additional metadata, the tokenized asset is categorized as an FT, allowing its balance to exceed one, enabling multiple identical instances of the asset to exist.

    FTs possess the capability to merge (join) among themselves or be split when the balance is greater than one. This functionality allows for the aggregation or division of units of the token, offering flexibility in handling varying quantities as needed.

    As previously mentioned, all the collectibles of tokenized assets, whether NFTs or FTs, when combined, can amount to the maximum total supply of the asset.

  • Burnability

    When you create the asset, you can define whether the fractions of the asset are eligible for removal or destruction from circulation. The process of removing or destroying assets is called burning.

    If a tokenized asset is burnable, then burning a fraction causes the circulating supply to decrease by the balance of the burnt item. The total supply, however, remains constant, allowing you to mint the burned fractions again if needed, thus maintaining the predetermined total supply of the asset.

Move packages

As with all smart contracts on Iota, Move provides the logic that powers asset tokenization.

asset_tokenization package

info

The asset_tokenization reference implementation uses the Kiosk standard to ensure that tokenized assets operate within their defined policy. Use the implementation as presented to have marketable tokenized assets that support rules like royalties, commissions, and so on.

If using Kiosk is not a requirement, then you can exclude the unlock module and some of the proxy's methods related to transfer policies.

Select a module to view its details:

The tokenized_asset module operates in a manner similar to the coin library.

When it receives a new one-time witness type, it creates a unique representation of a fractional asset. This module employs similar implementations to some methods found in the Coin module. It encompasses functionalities pertinent to asset tokenization, including new asset creation, minting, splitting, joining, and burning.

Structs

  • AssetCap

Generate an AssetCap for each new asset represented as a fractional NFT. In most scenarios, you should create it as an owned object, which you can then transfer to the platform's administrator for access-restricted method invocation.

public struct AssetCap<phantom T> {
id: UID,
// the current supply in circulation
supply: Supply<T>,
// the total max supply allowed to exist at any time
total_supply: u64,
// Determines if the asset can be burned or not
burnable: bool
}
  • AssetMetadata

The AssetMetadata struct defines the metadata representing the entire asset to fractionalize. This should be a shared object.

public struct AssetMetadata<phantom T> has key, store {
id: UID,
/// Name of the asset
name: String,
// the total max supply allowed to exist at any time
total_supply: u64,
/// Symbol for the asset
symbol: ascii::String,
/// Description of the asset
description: String,
/// URL for the asset logo
icon_url: Option<Url>
}
  • TokenizedAsset

The TokenizedAsset is minted with a specified balance that is less than or equal to the remaining supply. If the VecMap of an asset is populated with values, indicating multiple unique entries, it is considered an NFT. Conversely, if the VecMap of an asset is not populated, indicating an absence of individual entries, it is considered an FT.

public struct TokenizedAsset<phantom T> has key, store {
id: UID,
/// The balance of the tokenized asset
balance: Balance<T>,
/// If the VecMap is populated, it is considered an NFT, else the asset is considered an FT.
metadata: VecMap<String, String>,
/// URL for the asset image (optional)
image_url: Option<Url>,
}
  • PlatformCap

The PlatformCap refers to the capability issued to the individual who deploys the contract. This capability grants specific permissions or authority related to the platform's functionalities, allowing the deployer certain controlled actions or access rights within the deployed contract.

/// Capability that is issued to the one deploying the contract
public struct PlatformCap has key, store { id: UID }

Functions

  • init

This function creates a PlatformCap and sends it to the sender.

fun init(ctx: &mut TxContext) {}
  • new_asset

This function holds the responsibility of creating a fresh representation of an asset, defining its crucial attributes. Upon execution, it returns two distinct objects: the AssetCap and AssetMetadata. These objects encapsulate the necessary information and characteristics defining the asset within the system.

public fun new_asset<T: drop>(
witness: T,
total_supply: u64,
symbol: ascii::String,
name: String,
description: String,
icon_url: Option<Url>,
burnable: bool,
ctx: &mut TxContext
): (AssetCap<T>, AssetMetadata<T>) {}
  • mint

The function performs the minting of a tokenized asset. If new metadata is introduced during this process, the resulting tokenized asset is considered unique, resulting in the creation of an NFT with a balance set to 1. Alternatively, if no new metadata is added, the tokenized asset is classified as an FT, permitting its balance to surpass 1, as specified by a provided argument. Upon execution, the function returns the tokenized asset object.

public fun mint<T>(
cap: &mut AssetCap<T>,
keys: vector<String>,
values: vector<String>,
value: u64,
ctx: &mut TxContext
): TokenizedAsset<T> {}
  • split

This function is provided with a tokenized asset of the FT type and a balance greater than 1, along with a value less than the object's balance, and performs a split operation on the tokenized asset. The operation divides the existing tokenized asset into two separate tokenized assets. The newly created tokenized asset has a balance equal to the given value, while the balance of the provided object is reduced by the specified value. Upon completion, the function returns the newly created tokenized asset. This function does not accept or operate on tokenized assets of the NFT type.

public fun split<T>(
self: &mut TokenizedAsset<T>,
split_amount: u64,
ctx: &mut TxContext
): TokenizedAsset<T> {}
  • join

This function is given two tokenized assets of the FT type and executes a merge operation on the tokenized assets. The operation involves increasing the balance of the first tokenized asset by the balance of the second one. Subsequently, the second tokenized asset is burned or removed from circulation. After the process concludes, the function returns the ID of the burned tokenized asset.

This function does not accept or operate on tokenized assets of the NFT type.

public fun join<T>(
self: &mut TokenizedAsset<T>,
other: TokenizedAsset<T>
): ID {}
  • burn

This function requires the assetCap as a parameter, thereby restricting its invocation solely to the platform admin. Additionally, it accepts a tokenized asset that is burned as part of its operation. Upon burning the provided tokenized asset, the circulating supply decreases by the balance of the burnt item. It necessitates a tokenized asset that is burnable.

public fun burn<T>(
cap: &mut AssetCap<T>,
tokenized_asset: TokenizedAsset<T>
)
  • total_supply

This function retrieves and returns the value representing the total supply of the asset.

public fun total_supply<T>(cap: &AssetCap<T>): u64 {}
  • supply

This function retrieves and returns the value representing the current circulating supply of the asset.

public fun supply<T>(cap: &AssetCap<T>): u64 {}
  • value

This function takes a tokenized asset as input and retrieves its associated balance value.

public fun value<T>(tokenized_asset: &TokenizedAsset<T>): u64 {}
  • create_vec_map_from_arrays

This internal helper function populates a VecMap<String, String>. It assists in the process of filling or setting key-value pairs within the VecMap data structure.

fun create_vec_map_from_arrays(
keys: vector<String>,
values: vector<String>
): VecMap<String, String> {}

template package

The template package allows for seamless asset creation. It leverages the above explained asset_tokenization package.

To represent a new asset as a fractional asset, modify this module to <template>::<TEMPLATE>, with the <template> (in capitals) being the OTW of this new asset.

This module calls the asset_tokenization::tokenized_asset::new_asset(...) method, which facilitates the declaration of new fields for the asset:

  • witness: The OTW NEW_ASSET
  • total_supply: The total supply allowed to exist at any time
  • symbol: The symbol for the asset
  • name: The name of the asset
  • description: The description of the asset
  • icon_url: The URL for the asset logo (optional)
  • burnable: Boolean that defines if the asset can be burned by an admin

The template package also contains a genesis type of module that includes a OTW so that the sender can claim the publisher.

Publish and mint tokenized sequence diagram

Join sequence diagram

The following sequence diagram presenting how the join flow would take place. The following flow assumes that:

  • Tokenized assets X and Y have already been minted by the creator of their type.
  • Tokenized assets X and Y are already placed and locked inside the user's kiosk.
  • Everything is executed in the same programmable transaction block (PTB).

Burn sequence diagram

The following sequence diagram shows the burn flow and assumes that:

  • Tokenized asset has already been minted by the creator of its type.
  • Tokenized asset is already placed and locked inside the user's Kiosk.
  • Everything is executed in the same PTB.

Variations

The packages and modules provided demonstrate how you could implement asset tokenization for your project. Your particular use case probably necessitates altering the contract for convenience or to introduce new features.

Example convenience alteration

Instead of implementing the unlock functionality in multiple steps inside of a PTB, it would also be possible to create a method that performs the purchase, borrowing, unlocking and joining of an asset all on one function. This is how that would look like for the joining operation:

public fun kiosk_join<T>(
kiosk: &mut Kiosk,
kiosk_cap: &KioskOwnerCap,
protected_tp: &ProtectedTP<TokenizedAsset<T>>,
ta1_id: ID,
ta2_id: ID,
ctx: &mut TxContext
) {

kiosk::list<TokenizedAsset<T>>(kiosk, kiosk_cap, ta2_id, 0);
let (ta1, promise_ta1) = kiosk::borrow_val(kiosk, kiosk_cap, ta1_id);
let coin = coin::zero<IOTA>(ctx);
let (ta2, request) = kiosk::purchase(kiosk, ta2_id, coin);

let tp_ref = proxy::transfer_policy(protected_tp);
let (_item, _paid, _from) = transfer_policy::confirm_request(
tp_ref,
request
);

tokenized_asset::join(&mut ta1, ta2);

kiosk::return_val(kiosk, ta1, promise_ta1);
}

Example alteration for use case

caution

The following example splits (effectively replacing) the AssetCap<T> into two new objects: the Treasury<T> and the AdminCap<T>. The access to methods defined in the original package, should now be carefully re-designed as this change can introduce unwanted effects. This required re-design is not entirely contained in this example and only some methods are changed for demonstration purposes (or as a thorough exercise).

Assume you want to allow the users to also burn assets, not only admins. This still needs to be an authorized operation but it would allow the flexibility of consuming tokenized assets for a use case specific purpose (for example, burning all of the collectibles you've gathered to combine them). To achieve this, the admin can mint tickets that contain the ID of the asset they are allowed to burn. To support this functionality you must redesign the smart contract and separate the admin from the asset's treasury of each asset, which now holds only supply related information. Sample changes that need to happen follow:

Structs

Create a ticket that has only the key ability so that the receiver cannot trade it.

public struct BurnTicket<phantom T> has key {
id: UID,
tokenized_asset_id: ID // the tokenized asset that this ticket gives access to burn
}

The struct that now only holds treasury related information (results from splitting the AssetCap, meaning it's no longer part of this design) is created as a shared object. Change functions like mint to also take as input both the Treasury object and the AdminCap object.

public struct Treasury<phantom T> has key, store {
id: UID,
supply: Supply<T>,
total_supply: u64,
}

The other half of the AssetCap functionality which retains the admin capability and the configuration of burnability is an owned object sent to the creator of type <T>.

public struct AdminCap<phantom T> has key, store {
id: UID,
burnable: bool
}

Method Signatures

The AdminCap here acts both as an admin capability and a type insurance. Encoding the information of both the asset type that is allowed to be deleted with this ticket. This function should assert that the asset T is burnable and return a BurnTicket<T>.

public fun mint_burn_ticket<T>(
cap: &AdminCap<T>,
tokenized_asset_id: ID,
ctx: &mut TxContext
): BurnTicket

Burning on the user side requires for them to access the shared Treasury object. This function burns the tokenized asset and decreases the supply.

public fun burn_with_ticket<T>(
treasury: &mut Treasury<T>,
self: TokenizedAsset<T>,
ticket: BurnTicket<T>
)