Skip to main content

Migration Process

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.

Edge Cases

This guide doesn't describe every possible edge case. For that, please check out the code, which is part of a subdirectory in the iota-genesis-builder crate. This document will give a high to mid-level overview of the migration process.

Design Principles

  • The migration aims to move away from the Stardust-based ledger concepts and transform assets into their pure object-based Move representation. After completing the migration 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 a container with spending rules that holds the valuable resource.
  • We aim to mimic this hierarchy between assets and their containers in the genesis Move ledger state, ensuring 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, and the Move containers will be destroyed.
  • 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.

Foundry Outputs & Native Tokens

Foundry Outputs in Stardust represent the capability to control the supply of user-defined Native Tokens. In Move, there are established ways for these operations, in particular using Coin and TreasuryCap. The two main goals of the foundry migration are to convert it to a CoinManager, a type introduced in the Move IOTA framework simplify working with the TreasuryCap, and to a Coin<IOTA> to hold the IOTA tokens of the foundry itself.

A unique coin type, Coin<XYZ>, is represented by the one-time witness XYZ defined in its own package. Therefore, a corresponding package must be created for each foundry defining the native token's one-time witness and initial minting operations. To that end, a move package template is populated with the native token's metadata, e.g., its symbol, circulating supply, and description, as well as any other metadata.

To collect this metadata, the foundry output's IRC-30 Metadata is extracted. In most cases, this is possible. However, for example, if the metadata does not follow the IRC-30 standard or the symbol of the token is not a valid Move identifier (e.g., containing UTF-8 characters), a random identifier for the token package is generated, and the metadata uses generic defaults. As much as possible, though, the migration attempts to do as little modification to the UTXO as possible. The package template is filled with the extracted data, compiled, and published.

The result of the foundry migration is the following:

  • A package representing the native token, particularly containing a one-time witness representing the unique type of the native token (to be used as Coin<package_id::token_name::NativeTokenOneTimeWitness>; abbreviated in the rest of this document).
  • A CoinManager and CoinManagerTreasuryCap object, the latter of which is owned by the address of the alias that owned the original foundry.
  • A minted coin (Coin<NativeTokenOneTimeWitness>) containing the entire circulating supply of the native tokens owned by the 0x0 address.
  • A gas coin (Coin<IOTA>) containing the migrated IOTA tokens of the foundry, owned by the address of the alias that owned the original foundry.

After this process, the minted coin sits on the 0x0 address with the entire minted supply. When other output types are migrated that contain a balance of this native token, that balance is split off of the minted coin into a new Coin object, which is then owned by the migrated output. If, by the end of this process, a non-zero balance remains on the minted coin, it is left at the zero address. This means they were burned in Stardust and, therefore, are effectively also burned on the Move ledger, as no one controls the 0x0 address.

In Move, the TreasuryCap type uses a u64 representation (within Balance) as its maximum supply. Since Stardust allows for u256 to be used, in some rare cases, the maximum or circulating supply may exceed the maximum value representable by u64 (MAX_U64_SUPPLY, which here refers to 2^64 - 2 for technical reasons). If the maximum supply exceeds it, it is truncated to MAX_U64_SUPPLY, which is fine since no more than MAX_U64_SUPPLY tokens were actually minted (the circulating supply). However, if the circulating supply exceeds MAX_U64_SUPPLY, simply truncating would cause problems in the migration, since there would not be enough minted supply to distribute. In this case, all migrated tokens are multiplied by MAX_U64_SUPPLY / stardust_circulating_supply, which means they are adjusted proportionally to fit within the maximum supply of MAX_U64_SUPPLY. This maintains the token ratio of the Native Token in Stardust with the TreasuryCap constraints. Note that in cases where the ratio is very small, this might result in a Native Token balance of 0.

Output Migration Design

Outputs that are not foundries are migrated using a common pattern. Their Move smart contract has a function called extract_assets, which returns all the migrated assets. Generally, these outputs are migrated to an object that contains the other associated assets in static or dynamic fields. Those assets can be the Coin<IOTA>, Coin<NativeTokenOneTimeWitness> or another object. In particular, Native Tokens are stored in a Bag where the token is behind a key of its own name (<package_id>::<token_name>::NativeTokenOneTimeWitness) and the stored value is of type Balance<NativeTokenOneTimeWitness>. If an output owns multiple native tokens, the bag contains multiple keys.

The extraction function enforces that any potentially present unlock conditions, like Timelock, Expiration or Storage Deposit Return are enforced. For instance, if a time-locked basic output's assets are attempted to be extracted, the transaction would fail, just like it would in Stardust.

Address Ownership is migrated directly by converting Ed25519 Address, Alias Address, or Nft Address to an IotaAddress without any modification. For Ed25519 Addresses, their original backing keypair can simply continue to be used to unlock objects in Move. The Alias and Nft address types represent object ownership. Those are effectively migrated as a transfer. For example, an Alias Output A owning a Basic Output B in Stardust is migrated as setting the owner of B to the address of A (the Alias ID), which is equivalent to the Alias Address in Stardust. This is the same as if B would have been transferred (using either of iota::transfer::{transfer,public_transfer}) to the address of A (the Alias ID). The migrated alias A can then receive B using the stardust::address_unlock_condition::unlock_alias_address_owned_basic function, which is essentially a wrapper around iota::transfer::receive. There are equivalent unlock functions for the other possible variants of object ownership.

Basic Outputs

Every Basic Output has an Address Unlock and some coin balance (u64). Depending on what other fields it has, different objects are created. The most common case is that any output without special unlock conditions (or an expired Timelock) is migrated to a Coin<IOTA> object which can be directly used as a gas object.

The migrated objects are owned by the address in the Stardust output's Address Unlock Condition, except when an Expiration Unlock Condition is present, in which case the object is a shared one.

A special case is vesting reward outputs, that is, those Basic Outputs whose OutputId begins with 0xb191c4bc825ac6983789e50545d5ef07a1d293a98ad974fc9498cb18 and whose Timelock is still locked at the time of migration. They are migrated to Timelock<Balance<IOTA>> and contain the label 00000000000000000000000000000000000000000000000000000000000010cf::stardust_upgrade_label::STARDUST_UPGRADE_LABEL by which they can be identified as vesting reward objects.

The full decision graph (without the vesting reward output case) is depicted here (with coin being IOTA):

Stardust on Move Migration GraphStardust on Move Migration Graph

Alias Outputs

Alias Outputs are migrated to two Move objects:

  • AliasOutput object, containing the Balance<IOTA> and a Bag of native tokens in a static field.
  • Alias object that is owned by AliasOutput in a dynamic object field.

Other noteworthy points:

  • The AliasOutput is owned by the address in the Stardust output's Governor Address Unlock Condition. There is no concept of a state controller on the Move side, and so the State Controller address from Stardust is functionally discarded, although it can be accessed in the Alias object.
  • The AliasOutput object has a freshly generated UID while the Alias has its UID set to the Alias ID of the Stardust output. If the Alias ID was zeroed in the Stardust output, it is computed according to the protocol rules from TIP-18 and then set. Hence, no Alias object in Move has a zeroed UID.
  • The Foundry Counter in Stardust is used to give foundries a unique ID. The Foundry ID is the concatenation of Address || Serial Number || Token Scheme Type. In Move the foundries are represented by unique packages that define the corresponding Coin Type (a one-time witness) of the Native Token. Because the foundry counter can no longer be enforced to be incremented when a new package is deployed, which defines a native token and is owned by that Alias, the Foundry Counter becomes meaningless. Hence, it is not migrated and has no equivalent field in Move. The same count can be determined (off-chain) by counting the number of TreasuryCaps the Alias owns. The Stardust constraint that foundries can only be owned by aliases is no longer enforced in the Move version.

NFT Outputs

Much like Alias Outputs, NFT Outputs are migrated to two Move objects:

  • NftOutput object, containing the Balance<IOTA> and a Bag of native tokens in a static field.
  • Nft object that is owned by NftOutput in a dynamic object field.

Other noteworthy points:

  • The NftOutput is owned by the address in the Stardust output's Address Unlock Condition or if an Expiration Unlock Condition is present, by either of the two addresses in that unlock condition.
  • The NftOutput object has a freshly generated UID while the Nft has its UID set to the Nft ID of the Stardust output. If the Nft ID was zeroed in the Stardust output, it is computed according to the protocol rules from TIP-18 and then set. Hence, no Nft object in Move has a zeroed UID.
  • The Nft Move object contains an Irc27Metadata object. It is extracted from the immutable metadata of the Stardust NFT, if possible. If the Stardust NFT does not have valid IRC-27 metadata, it is migrated on a best-effort basis.

You can examine the convert_immutable_metadata function in the migration code for more details.