Skip to main content

Stardust Move Models

Exchanges and dApp Devs Only

The migration documentation describes the processes needed to claim and migrate output types manually; However, for the average user, this knowledge is not needed and is abstracted in the wallet web application (dashboard). The specific migration knowledge described here is unnecessary for people using a regular wallet.

This document describes what move models the Stardust UTXOs are migrated to in the Move-based ledger in IOTA Rebased. First, the Stardust Move Package is discussed that emulates the legacy UTXO models in object-based move.

Summary

Stardust Output (UTXO)Move Container or Migrated ObjectEncapsulated Move Assets Inside the Container
Basic Output with IOTA onlyCoin<IOTA>ready to be used, no need to extract any assets
Basic Output with IOTA and native tokensCoin<IOTA> and one or more Coin<NATIVE_TOKEN>ready to be used, no need to extract any assets
Basic Output with unlock conditions or featuresBasicOutput<IOTA>Balance<IOTA>, Bag of Balance<NATIVE_TOKEN> key-ed by "NATIVE_TOKEN"
Alias OutputAliasOutput<IOTABalance<IOTA>, Bag of Balance<NATIVE_TOKEN> key-ed by "NATIVE_TOKEN", Alias object
Nft OutputNftOutput<IOTABalance<IOTA>, Bag of Balance<NATIVE_TOKEN> key-ed by "NATIVE_TOKEN", Nft object
Foundry OutputMigrated to a Move Package with template and CoinManager. Base tokens of the output are sent to the controlling alias as Coin<IOTA> objects.-
Vested Rewards From the Stardust UpgradeTimelock<Balance<IOTA>> objects.-

Design Principles

  • The migration to IOTA Rebased aims to move away from the Stardust-based ledger concepts and transform assets into their pure object-based Move representation.
  • Wherever possible, the automatic migration transforms assets into ready-to-be-used pure move objects. However, for more complicated assets (NFTs, Aliases, funds locked behind timelock or spending conditions) a user-initiated claiming flow has to be carried out on-chain. After completing the claiming flow, users should see move assets that semantically resemble the former Stardust assets in their wallet.
  • Stardust Assets on the legacy ledger level are encapsulated in Outputs. An asset is semantically a valuable resource, while an output is merely a container with spending rules that holds the valuable resource.
  • The move models aim to mimic this hierarchy between assets and their containers in the genesis move ledger state, that is, to ensure that spending conditions still hold. Once the containers are unlocked and destroyed during a user-initiated claiming/migration transaction, the encapsulated assets will be extracted and sent to the user address without further spending restrictions.
  • We intentionally do not include constructors in move for such containers, as we want to discourage their use after the migration. They will only be created in the genesis ledger state via the migration script.

The Stardust Move Package

The move counterparts of the stardust models are implemented as part of the IOTA Move Framework and are shipped with the genesis of the network.

  • The move package is called stardust and is located in the iota_framework crate.
  • The on-chain identifier for the stardust package is 0x107a.
  • The following modules are present in the package:
    • basic_output: Defines how a basic output container is modeled.
    • alias_output: Defines how an alias output container is modeled.
    • alias: Defines how an alias asset is modeled.
    • nft_output: Defines how an NFT output container is modeled.
    • nft: Defines how an NFT asset is modeled.
    • address_unlock_condition: Defines how an address unlock condition is modeled.
    • expiration_unlock_condition: Defines how an expiration unlock condition is modeled.
    • timelock_unlock_condition: Defines how a timelock unlock condition is modeled.
    • irc27: Defines how an IRC-27 metadata is modeled.
    • utilities: Contains utility functions for the stardust package, primarily to help with extracting assets from containers.
  • There is no dedicated module for foundries and native tokens, since they are converted to move-native tokens along with the base tokens (IOTA). These models are already part of the iota-framework.

There is intentionally no constructor for any of the object types defined in the Stardust package, since they can only originate from the migrated legacy stardust ledger state. The only way to create these objects is via the migration script.

Asset Representation

The following sections describe how the different asset types in Stardust are represented in the Move ledger.

You may find more information on the original Stardust ledger and asset types in TIP-18. We consider the following Stardust asset types for the migration:

  • Base token (IOTA).
  • User defined native tokens.
  • Non-Fungible Tokens (NFTs).
  • Aliases.

Base Token (IOTA)

IOTA token is represented in UTXOs as simply the amount field of an output. In move this base token is represented instead as a balance struct, parameterized with the type of the token:

  • Balance<IOTA>: Represents a balance of the IOTA token.
  • The token types IOTA is defined as part of the IOTA Move Framework. The fully qualified type for them is:

Balances are simply structs in move, therefore they need to be put into an object that users can own. The IOTA Move Framework defines Coin as the primary object type to use by users to hold tokens.

info

Coin<IOTA> objects are also the primary gas objects in the move ledger that can pay for transaction fees.

/// A coin of type `T` worth `value`. Transferable and storable
public struct Coin<phantom T> has key, store {
/// Unique identifier of the coin object, the ObjectID
id: UID,
/// The balance of the coin
balance: Balance<T>
}

Majority of the funds on the IOTA network are held in basic outputs, without any special unlock conditions, as this is the most common way a wallet stores IOTA tokens. To make the migration as seamless as possible, all Basic outputs from the Stardust ledger that:

will be migrated to a Coin<IOTA> object. The Coin object will be owned by the address in the Address Unlock Condition of the Stardust output.

info

Such Coin objects don't have to be claimed by the users in IOTA Rebased, they will be sitting in user's wallets, ready to be used.

warning

IOTA amounts from Stardust that are locked behind unlock conditions (timelock, expiration, etc.), or reside in other types of outputs, will be migrated into the containers and have to be extracted from there during the claiming process.

Native Tokens

Native tokens in Stardust are user-defined tokens that were minted via Foundries. The foundry controls the supply of the native token and can mint new tokens. A user owned native token balance can be stored on any of the Stardust output types.

In Move, there are established ways for these operations, in particular using a Coin to hold balances of tokens, as well as the CoinManager (that wraps the TreasuryCap) to control the supply of the token through minting and burning.

A native token and its foundry from the Stardust ledger therefore translate to the following in the move ledger:

  • A published move package that defines the currency type (one-time witness) for the native token. The template that is used for every native token package can be found in the IOTA Genesis Builder. The parameters of the template are filled with the IRC-30 metadata of the native token from Stardust.
  • The CoinManager shared object that gives access to the coin metadata and supply on-chain.
  • The CoinManagerTreasuryCap object that controls the supply of the native token.
  • Balance<TOKEN-SYMBOL> values that are either stored inside Coin<TOKEN-SYMBOL> object or inside the stardust containers.
warning

Foundry outputs are migrated to move packages and coin manager objects. The base token funds present in the foundry output will be sent to the address of the controlling alias as a Coin<IOTA> object.

Aliases

Aliases were primarily developed in Stardust to support IOTA Smart Contracts (ISC) that are layer 2 chains anchoring into the L1 protocol. Aliases represent ledger accounts for such chains, however their use has been extended for Decentralized Digital Identifiers (DID) and also as foundry controllers.

The Stardust Move Package preserves the Alias concept and models it as a separate object in the move ledger: Alias. Applications that use aliases (ISC, DID, token issuer) can implement their own move models where the Alias asset migrated from Stardust is required to initialize the new object.

The unique AliasID of the Stardust asset is persisted as the ObjectID of the migrated Alias object. All assets and outputs that were owned by the Alias Address in Stardust are now owned by the Alias object in move. There is a special receiving functionality in the new move framework that allows one to receive assets on the an ObjectID that is an AliasID. The following functions let an Alias receive other Stardust containers:

Non-Fungible Tokens (NFTs)

NFTs are migrated very similarly to aliases. The NFT object in move is called Nft. The unique NftID of the Stardust asset is persisted as the ObjectID of the migrated Nft object. All assets and outputs that were owned by the Nft Address in Stardust are now owned by the Nft object in move. There is a special receiving functionality in the new move framework that allows one to receive assets on the an ObjectID that is an NftID. The following functions let an Nft receive other Stardust containers:

Unlock Conditions

There are 3 different types of containers in the Stardust package. They hold assets and ensure that spending/unlock conditions that were defined in the legacy IOTA Stardust Network are respected when assets are extracted from them. Once that happens, they are destroyed. The containers are:

On move level, all of them are defined with only a key type ability, which means that they only support custom transfer rules. The only way to interact with the containers is to call extract_assets() move function on them, which will return the assets they hold and destroy the container.

During the extract_assets() call, the legacy unlock conditions are checked, that is, the claiming transaction will fail if they are not respected. This is required to not change semantically the rules that were defined in the legacy network.

Address Unlock Condition

The most common unlock condition (which is mandatory for most outputs) in legacy Stardust is the Address Unlock Condition. It simply locks the output behind an address, therefore, in move it is represented as a simple address ownership of the object.

  • All migrated objects, except the ones with Expiration Unlock Condition, become owned objects in the move ledger.
  • The address that owns the object is the address that was in the Address Unlock Condition of the Stardust output.
  • Move addresses are 32 bytes, which is the same length as the legacy addresses in Stardust without the type byte.
  • Addresses and Keys describe in more detail how to convert the legacy addresses to move addresses.

Since address ownership of the objects is native to the system, there is no need for extra move code to represent it.

Storage Deposit Return Unlock Condition

The Storage Deposit Return Unlock Condition is a special Stardust feature that lets the sender of an output claim back the storage deposit that was paid for the output. Such a feature is not needed in IOTA Rebased anymore due to the revised storage deposit model where the deposit is directly paid in transactions.

However, there are outputs in Stardust with such unlock conditions that are still locked behind them. The migration script therefore makes sure that the storage deposit amount is returned to the original sender as a Coin<IOTA> object during the claiming process.

The move model StorageDepositReturnUnlockCondition is added to the containers during the migration process where applicable. The unlock() function is called when assets are extracted from the container, which will return the storage deposit to the sender.

Timelock Unlock Condition

A stardust timelock locks funds until a certain time has passed. It is modeled in move via the TimelockUnlockCondition that is added to the containers during the migration process where applicable. The unlock() function is called when assets are extracted from the container, which checks if the timelock has expired and if so, unlocks the container.

info

There is a special group of timelocked outputs in the IOTA Mainnet that originate from the Stardust Upgrade of the network that happened in October 2023. These outputs represent vested rewards that are unlocked over time. The migration script treats these special outputs differently and migrates them to a separate object type that is not part of the Stardust package: Timelock<Balance<IOTA>>.

  • Timelock objects can be staked to validators of the network to earn rewards. The functionality is provided by the timelock::timelocked_staking module.
  • The timelock package and it's utilities can be used by anyone to create their own timelocked assets.

Expiration Unlock Condition

The Expiration Unlock Condition in Stardust allows one to create a conditional transfer: if the recipient doesn't claim the output within a certain time, ownership is transferred back to the sender.

In move, the expiration unlock condition is modeled via the ExpirationUnlockCondition. The unlock() function is called when assets are extracted from the container, which checks if the expiration time has passed and if so, transfers the ownership back to the sender.

info

Since the conditional ownership can only be modeled in on-chain move code, outputs from Stardust with expiration unlock conditions are migrated to Shared Objects. As opposed to owned objects, shared objects can be touched by anyone, however the move level code ensures that only the rightful owner can claim the assets.

Deprecated Stardust Unlock Conditions

The following unlock conditions from Stardust are not supported in the move ledger and are therefore not migrated:

Output Features

Stardust UTXOs introduced output features which attach additional information to outputs. Due to the versatility of the move ledger, such explicit features are not needed since any developer can come up with their own features.

Therefore, the features currently present on Stardust UTXOs will be migrated into the move containers, but they have no effect on the claiming process whatsoever. They are preserved as additional information and getters are exposed such that developers can access them if needed, but they will be dropped during the claiming process.

The only exception are the features of an NFT/Alias output, which are preserved as the fields of the NFT/Alias object in move, as they might have semantic meaning for the application that uses them.

Containers

Containers lock assets behind unlock conditions and ensure that the spending rules defined in the legacy Stardust network.

BasicOutput Move Model

As mentioned above, all basic outputs from the legacy ledger state that simply lock simply base tokens (IOTA) behind an address unlock condition will be migrated to Coin<IOTA> objects. Funds however that are locked in basic outputs behind other spending conditions (timelock, expiration, etc.) or have special features (sender, metadata, etc.) will be migrated to BasicOutput containers.

    /// A basic output that has unlock conditions/features.
/// - basic outputs with expiration unlock condition must be a shared object, since that's the only
/// way to handle the two possible addresses that can unlock the output.
/// - notice that there is no `store` ability and there is no custom transfer function:
/// - you can call `extract_assets` on a BasicOutput,
/// - or you can call `receive` in other models to receive a `BasicOutput` on an ObjectID address.
public struct BasicOutput<phantom T> has key {
/// Hash of the `outputId` that was migrated.
id: UID,

/// The amount of coins held by the output.
balance: Balance<T>,

/// The `Bag` holds native tokens, key-ed by the stringified type of the asset.
/// Example: key: "0xabcded::doge::DOGE", value: Balance<0xabcded::doge::DOGE>.
native_tokens: Bag,

/// The storage deposit return unlock condition.
storage_deposit_return_uc: Option<StorageDepositReturnUnlockCondition>,
/// The timelock unlock condition.
timelock_uc: Option<TimelockUnlockCondition>,
/// The expiration unlock condition.
expiration_uc: Option<ExpirationUnlockCondition>,

// Possible features, they have no effect and only here to hold data until the object is deleted.

/// The metadata feature.
metadata: Option<vector<u8>>,
/// The tag feature.
tag: Option<vector<u8>>,
/// The sender feature.
sender: Option<address>
}
  • T is a type parameter that is either IOTA.
  • A Bag is a heterogeneous map-like collection of values. The native_tokens bag stores Balance<TOKEN-TYPE> structs under the stringified type of the token.
  • The unlock conditions are all defined as Options since not all outputs have them. Should they not exist, their None value means they are discarded during the claiming process.
info

The ObjectID of the migrated Stardust output becomes the blake2b hash of the outputId of the Stardust output. If one knows the outputId of the Stardust output that was migrated, they can calculate the ObjectID of the migrated object and query it from the node API/indexer directly.

The indexer API can also be queried for all objects owned by an address.

There are two ways to interact with BasicOutputs in move:

  1. Call the extract_assets() function on the object, which checks the unlock conditions, returns the assets stored in them and deletes the container. The function signature of the extract_assets() function is:
public fun extract_assets<T>(output: BasicOutput<T>, ctx: &mut TxContext) : (Balance<T>, Bag) { ... }

that shows that a base token balance and a Bag is returned. It is then the job of the caller to create coins out of the base token balance and from the native token balances extracted from the Bag. The Claiming Stardust Assets guide explains in detail what is the recommended way to deal with these results.

  1. Receive them on an ObjectID address. The Receiving Objects guide explains how to receive assets on an object. In the Stardust models the following two functions are used to receive BasicOutputs that were locked in the legacy network to Alias or Nft addresses:

    Once a BasicOutput was received on an ObjectID address, the extract_assets() function can be called on it to claim it.

AliasOutput Move Model

The AliasOutput container contains the following assets:

  • Base token balance (IOTA) of the migrated Stardust output.
  • A Bag that holds native tokens of the migrated Stardust output.
  • The Alias object that represents the alias in the move ledger. While this latter asset is not part of the container as a field, it is added to it as a dynamic object field during the migration process. This has the added benefit that the previous AliasID of the Stardust output is preserved as the ObjectID of the Alias object, and can be directly queried from the node API/indexer. During the claiming process, the extract_assets() function automatically loads the Alias, removes it as a dynamic field from the container and returns it.
    /// Owned Object controlled by the Governor Address.
public struct AliasOutput<phantom T> has key {
/// This is a "random" UID, not the AliasID from Stardust.
id: UID,

/// The amount of coins held by the output.
balance: Balance<T>,

/// The `Bag` holds native tokens, key-ed by the stringified type of the asset.
/// Example: key: "0xabcded::soon::SOON", value: Balance<0xabcded::soon::SOON>.
native_tokens: Bag,
}
  • T is a type parameter that is either IOTA.
  • The AliasOutput object is owned by the governor of the alias in Stardust.

Alias Model

The actual Alias asset that can be extracted from the AliasOutput container looks the following:

    /// The persisted Alias object from Stardust, without tokens and assets.
/// Outputs owned the AliasID/Address in Stardust will be sent to this object and
/// have to be received via this object once extracted from `AliasOutput`.
public struct Alias has key, store {
/// The ID of the Alias = hash of the Output ID that created the Alias Output in Stardust.
/// This is the AliasID from Stardust.
id: UID,

/// The last State Controller address assigned before the migration.
legacy_state_controller: address,
/// A counter increased by 1 every time the alias was state transitioned.
state_index: u32,
/// State metadata that can be used to store additional information.
state_metadata: Option<vector<u8>>,

/// The sender feature.
sender: Option<address>,
/// The metadata feature.
metadata: Option<vector<u8>>,

/// The immutable issuer feature.
immutable_issuer: Option<address>,
/// The immutable metadata feature.
immutable_metadata: Option<vector<u8>>,
}

The extract_assets() function of the AliasOutput container looks the following:

public fun extract_assets<T>(mut output: AliasOutput<T>): (Balance<T>, Bag, Alias) { ... }
  • The function returns the base token balance, the Bag of native tokens and the Alias object.
  • The caller has to manage the creation of Coins from the base token balance and the native tokens in the Bag.
  • The caller has to decide what address to send the Alias objects to.
  • The AliasOutput container is destroyed in the process.

NftOutput Move Model

The NftOutput container contains the:

  • Base token balance (IOTA) of the migrated Stardust output.
  • A Bag that holds native tokens of the migrated Stardust output.
  • The Nft object that represents the NFT in the move ledger. While this latter asset is not part of the container as a field, it is added to it as a dynamic object field during the migration process. This has the added benefit that the previous NftID of the Stardust output is preserved as the ObjectID of the Nft object, and can be directly queried from the node API/indexer. During the claiming process, the extract_assets() function automatically loads the Nft, removes it as a dynamic field from the container and returns it.
    /// The Stardust NFT output representation.
public struct NftOutput<phantom T> has key {
/// This is a "random" UID, not the NFTID from Stardust.
id: UID,

/// The amount of coins held by the output.
balance: Balance<T>,

/// The `Bag` holds native tokens, key-ed by the stringified type of the asset.
/// Example: key: "0xabcded::soon::SOON", value: Balance<0xabcded::soon::SOON>.
native_tokens: Bag,

/// The storage deposit return unlock condition.
storage_deposit_return_uc: Option<StorageDepositReturnUnlockCondition>,
/// The timelock unlock condition.
timelock_uc: Option<TimelockUnlockCondition>,
/// The expiration unlock condition.
expiration_uc: Option<ExpirationUnlockCondition>,
}
  • T is a type parameter that is either IOTA.
  • The unlock conditions are optional based on content of the migrated Stardust output. When present however, they are checked during the claiming process.
  • The NftOutput object is owned by the address that was in the Address Unlock Condition of the Stardust output, unless the ExpirationUnlockCondition is present. In this case, the NftOutput object is a shared object.
  • The NftOutput object is destroyed during the claiming process.

Nft Model

The actual Non-Fungible Token is represented in move as the following:

    /// The Stardust NFT representation.
public struct Nft has key, store {
/// The Nft's ID is nested from Stardust, therefore id is the NftID from Stardust.
id: UID,

/// The sender feature holds the last sender address assigned before the migration and
/// is not supported by the protocol after it.
legacy_sender: Option<address>,
/// The metadata feature.
metadata: Option<vector<u8>>,
/// The tag feature.
tag: Option<vector<u8>>,

/// The immutable issuer feature.
immutable_issuer: Option<address>,
/// The immutable metadata feature.
immutable_metadata: Irc27Metadata,
}
  • Notice that Nft has both the key and store type abilities, meaning it can be freely transferred by the owner.
  • The NftID is preserved as the ObjectID of the Nft object in move.
  • This Nft object owns all other containers or Coins that were owned by the Nft Address in Stardust. One can receive them via the following functions:
  • The immutable metadata field has been standardized as Irc27Metadata, which is a move struct that holds the metadata of the NFT. It is parsed from the Stardust output metadata during the migration process. If the metadata in Stardust does not follow the IRC-27 standard, or fields are missing, the migration script uses default values.

The extract_assets() function of the NftOutput container looks the following:

public fun extract_assets<T>(output: nft_output::NftOutput<T>, ctx: &mut tx_context::TxContext): (balance::Balance<T>, bag::Bag, nft::Nft) { ... }
  • The function returns the base token balance, the Bag of native tokens and the Nft object.
  • The caller has to manage the creation of Coins from the base token balance and the native tokens in the Bag.
  • The caller has to decide what address to send the Nft object to.
  • The NftOutput container is destroyed in the process.