Migration Process
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 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
CoinManagerandCoinManagerTreasuryCapobject, 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 the0x0address. - 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):
Alias Outputs
Alias Outputs are migrated to two Move objects:
AliasOutputobject, containing theBalance<IOTA>and aBagof native tokens in a static field.Aliasobject that is owned byAliasOutputin a dynamic object field.
Other noteworthy points:
- The
AliasOutputis owned by the address in the Stardust output'sGovernor Address Unlock Condition. There is no concept of a state controller on the Move side, and so theState Controlleraddress from Stardust is functionally discarded, although it can be accessed in theAliasobject. - The
AliasOutputobject has a freshly generatedUIDwhile theAliashas itsUIDset to theAlias IDof the Stardust output. If theAlias IDwas zeroed in the Stardust output, it is computed according to the protocol rules from TIP-18 and then set. Hence, noAliasobject in Move has a zeroedUID. - 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 ofTreasuryCaps 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:
NftOutputobject, containing theBalance<IOTA>and aBagof native tokens in a static field.Nftobject that is owned byNftOutputin a dynamic object field.
Other noteworthy points:
- The
NftOutputis owned by the address in the Stardust output'sAddress Unlock Conditionor if anExpiration Unlock Conditionis present, by either of the two addresses in that unlock condition. - The
NftOutputobject has a freshly generatedUIDwhile theNfthas itsUIDset to theNft IDof the Stardust output. If theNft IDwas zeroed in the Stardust output, it is computed according to the protocol rules from TIP-18 and then set. Hence, noNftobject in Move has a zeroedUID. - The
NftMove object contains anIrc27Metadataobject. 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.