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
CoinManager
andCoinManagerTreasuryCap
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 the0x0
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 Address
es, 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:
AliasOutput
object, containing theBalance<IOTA>
and aBag
of native tokens in a static field.Alias
object that is owned byAliasOutput
in a dynamic object field.
Other noteworthy points:
- The
AliasOutput
is 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 Controller
address from Stardust is functionally discarded, although it can be accessed in theAlias
object. - The
AliasOutput
object has a freshly generatedUID
while theAlias
has itsUID
set to theAlias ID
of the Stardust output. If theAlias ID
was zeroed in the Stardust output, it is computed according to the protocol rules from TIP-18 and then set. Hence, noAlias
object 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 ofTreasuryCap
s 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 theBalance<IOTA>
and aBag
of native tokens in a static field.Nft
object that is owned byNftOutput
in a dynamic object field.
Other noteworthy points:
- The
NftOutput
is owned by the address in the Stardust output'sAddress Unlock Condition
or if anExpiration Unlock Condition
is present, by either of the two addresses in that unlock condition. - The
NftOutput
object has a freshly generatedUID
while theNft
has itsUID
set to theNft ID
of the Stardust output. If theNft ID
was zeroed in the Stardust output, it is computed according to the protocol rules from TIP-18 and then set. Hence, noNft
object in Move has a zeroedUID
. - The
Nft
Move object contains anIrc27Metadata
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.