Stardust Move Models
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 Object | Encapsulated Move Assets Inside the Container |
---|---|---|
Basic Output with IOTA only | Coin<IOTA> | ready to be used, no need to extract any assets |
Basic Output with IOTA and native tokens | Coin<IOTA> and one or more Coin<NATIVE_TOKEN> | ready to be used, no need to extract any assets |
Basic Output with unlock conditions or features | BasicOutput<IOTA> | Balance<IOTA> , Bag of Balance<NATIVE_TOKEN> key-ed by "NATIVE_TOKEN" |
Alias Output | AliasOutput<IOTA | Balance<IOTA> , Bag of Balance<NATIVE_TOKEN> key-ed by "NATIVE_TOKEN" , Alias object |
Nft Output | NftOutput<IOTA | Balance<IOTA> , Bag of Balance<NATIVE_TOKEN> key-ed by "NATIVE_TOKEN" , Nft object |
Foundry Output | Migrated 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 Upgrade | Timelock<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 theiota_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:0x2::iota::IOTA
for IOTA.
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.
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:
- contain IOTA tokens only (no native tokens),
- have only an
Address Unlock Condition
and no other unlock conditions, - have no output features
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.
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.
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 insideCoin<TOKEN-SYMBOL>
object or inside the stardust containers.
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:
BasicOutput
: Represents a basic output container.AliasOutput
: Represents an alias output container.NftOutput
: Represents an NFT output container.
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.
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.
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:
- State Controller Address Unlock Condition.
- Governor Address Unlock Condition: Used in Stardust exclusively for aliases. The move ledger simplifies aliases and gives ownership to the governor address of the alias, as the governor has the right to replace any state controller.
- Immutable Alias Address Unlock Condition: With the transition to move token models, the concept of a foundry is deprecated, hence this unlock condition is obsolete.
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 eitherIOTA
.- A
Bag
is a heterogeneous map-like collection of values. Thenative_tokens
bag storesBalance<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, theirNone
value means they are discarded during the claiming process.
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:
- 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 theextract_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.
-
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 receiveBasicOutputs
that were locked in the legacy network toAlias
orNft
addresses:Once a
BasicOutput
was received on anObjectID
address, theextract_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 adynamic object field
during the migration process. This has the added benefit that the previousAliasID
of the Stardust output is preserved as theObjectID
of theAlias
object, and can be directly queried from the node API/indexer. During the claiming process, theextract_assets()
function automatically loads theAlias
, 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 eitherIOTA
.- 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>>,
}
- Notice that
Alias
has both thekey
andstore
type abilities, meaning it can be freely transferred by the owner. - The
AliasId
is preserved as theObjectID
of theAlias
object in move. - This
Alias
object owns all other containers orCoins
that were owned by theAlias Address
in Stardust. One can receive them via the following functions:
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 theAlias
object. - The caller has to manage the creation of
Coins
from the base token balance and the native tokens in theBag
. - 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 adynamic object field
during the migration process. This has the added benefit that the previousNftID
of the Stardust output is preserved as theObjectID
of theNft
object, and can be directly queried from the node API/indexer. During the claiming process, theextract_assets()
function automatically loads theNft
, 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 eitherIOTA
.- 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 theAddress Unlock Condition
of the Stardust output, unless theExpirationUnlockCondition
is present. In this case, theNftOutput
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 thekey
andstore
type abilities, meaning it can be freely transferred by the owner. - The
NftID
is preserved as theObjectID
of theNft
object in move. - This
Nft
object owns all other containers orCoins
that were owned by theNft 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 theNft
object. - The caller has to manage the creation of
Coins
from the base token balance and the native tokens in theBag
. - The caller has to decide what address to send the
Nft
object to. - The
NftOutput
container is destroyed in the process.