Skip to main content

Managing Package Upgrades

In IOTA, smart contracts are represented as immutable package objects containing Move modules. This immutability ensures that transactions can be executed quickly without requiring full consensus using fast path transactions. If you could simply change these packages, they would become shared objects

However, this feature could be a limitation when developers need to iteratively update their code. Fortunately, the IOTA network allows you to upgrade packages while retaining their immutable characteristics.

Key Considerations for Upgrading Packages

Before proceeding with a package upgrade, you should consider several factors to ensure a smooth transition:

Module Initializers

Module initializers (init functions) do not re-run during package upgrades. The init function is executed only once when the package is initially published. Subsequent upgrades will ignore any new init functions.

Package Immutability

Since packages on IOTA are immutable, you cannot remove old versions from the chain. Users can continue to interact with older versions of your package, and as a developer, you need to account for this possibility.

Handling Function Changes During Upgrades

Consider an example where your original package includes an increment function:

public fun increment(c: &mut Counter) {
c.value = c.value + 1;
}

In a later upgrade, you add an event emission to this function:

public struct Progress has copy, drop {
reached: u64
}

public fun increment(c: &mut Counter) {
c.value = c.value + 1;

if (c.value % 100 == 0) {
event::emit(Progress { reached: c.value });
}
}

When both old and new versions of the increment function are called simultaneously, the process may fail because the old function is not aware of the Progress event added in the upgraded version.

Addressing Compatibility Issues

Automated Address Management

IOTA supports Automated Address Management, with which addresses will automatically be updated in the Move.lock file.

When upgrading packages, you might encounter issues where dynamic fields need to stay synchronized with a struct’s original fields. These issues can be mitigated by introducing a new type as part of the upgrade, effectively breaking backward compatibility and compelling users to update.

Example: Upgrading with New Types

Suppose you're using owned objects to prove ownership, and you release a new version of your package to fix some code issues. You can introduce a new type in the upgraded package and create a function that swaps old objects for the new ones. Since the logic in your package will only recognize the new type, users will be required to update to the latest version.

Example: Correcting Flawed Logic in Shared Objects

Consider a scenario where your package includes a shared object for bookkeeping, but you discover a flaw in its logic. To ensure that users only interact with the corrected version, you can add a new type and a migration function to your package upgrade. This process typically involves two transactions: one for the upgrade and another to set up the new shared object, which replaces the existing one.

To secure the setup function, you should create an AdminCap object or a similar mechanism to ensure that only you, the package owner, can initiate it. Additionally, you could implement a flag in the shared object that allows you to toggle its enabled state. This flag would prevent public access to the object during migration, providing a safeguard while changes are being made. It’s advisable to create such a flag if you anticipate needing to perform migrations in the future, rather than relying on intentionally flawed logic to need it.

Managing Versioned Shared Objects

Automated Address Management

IOTA supports Automated Address Management, with which addresses will automatically be updated in the Move.lock file.

When dealing with shared objects within your packages, versioning and upgrade considerations become crucial as all prior versions of a package still exist on-chain. You can limit access to only the latest version of your package by introducing version tracking within your shared objects which can effectively safeguard your shared objects from being accessed from old versions of your package.

Here’s how you might implement a versioned counter module:

module example::counter {

public struct Counter has key {
id: UID,
value: u64,
}

fun init(ctx: &mut TxContext) {
transfer::share_object(Counter {
id: object::new(ctx),
value: 0,
})
}

public fun increment(c: &mut Counter) {
c.value = c.value + 1;
}
}

To ensure that upgrades to your package can limit access to the shared object , making sure it only interacts with the latest version, follow these steps:

1. Track the Current Module Version

Define a constant, VERSION, in your module to represent the current version of the package.

2. Track the Shared Object Version

Add a version field to your shared object (e.g., Counter) to keep track of its version.

3. Introduce an AdminCap for Privileged Operations

Create an AdminCap to safeguard privileged actions, such as migrating the shared object. Link the Counter object with its AdminCap using a new field. If your package already has a similar type for shared object administration, you can reuse it.

4. Guard Function Access

Ensure that all functions accessing the shared object verify that the object’s version matches the module’s VERSION. This check prevents outdated versions from interacting with the latest package.

An upgrade-aware counter module that incorporates all these ideas looks as follows:

module example::counter {

// 1. Track the current version of the module
const VERSION: u64 = 1;

public struct Counter has key {
id: UID,
// 2. Track the current version of the shared object
version: u64,
// 3. Associate the `Counter` with its `AdminCap`
admin: ID,
value: u64,
}

public struct AdminCap has key {
id: UID,
}

/// Calling functions from the wrong package version
const EWrongVersion: u64 = 1;

fun init(ctx: &mut TxContext) {
let admin = AdminCap {
id: object::new(ctx),
};

transfer::share_object(Counter {
id: object::new(ctx),
version: VERSION,
admin: object::id(&admin),
value: 0,
});

transfer::transfer(admin, tx_context::sender(ctx));
}

public fun increment(c: &mut Counter) {
// 4. Guard the entry of all functions that access the shared object
// with a version check.
assert!(c.version == VERSION, EWrongVersion);
c.value = c.value + 1;
}
}
  • The VERSION constant ensures that you have a reference to the current version of the package.
  • The version field in Counter keeps the version information within the shared object itself, allowing functions to check against it.
  • The AdminCap provides a mechanism to control and protect privileged operations, ensuring only authorized upgrades or migrations occur.
  • The version check in functions guards each function to ensure it operates only on objects that match the current version, preventing older or incompatible objects from interacting with the latest package version.

To perform an upgrade, you need to:

  1. Update the VERSION constant.
  2. Implement a migrate function that updates the shared object's version.

Here’s an example of an upgraded counter module that not only emits Progress events, but also includes tools for an AdminCap holder to manage access and prevent interactions with older package versions:

module example::counter {
use iota::event;

// 1. Bump the `VERSION` of the package.
const VERSION: u64 = 2;

public struct Counter has key {
id: UID,
version: u64,
admin: ID,
value: u64,
}

public struct AdminCap has key {
id: UID,
}

public struct Progress has copy, drop {
reached: u64,
}

/// Not the right admin for this counter
const ENotAdmin: u64 = 0;

/// Migration is not an upgrade
const ENotUpgrade: u64 = 1;

/// Calling functions from the wrong package version
const EWrongVersion: u64 = 2;

fun init(ctx: &mut TxContext) {
let admin = AdminCap {
id: object::new(ctx),
};

transfer::share_object(Counter {
id: object::new(ctx),
version: VERSION,
admin: object::id(&admin),
value: 0,
});

transfer::transfer(admin, tx_context::sender(ctx));
}

public fun increment(c: &mut Counter) {
assert!(c.version == VERSION, EWrongVersion);
c.value = c.value + 1;

if (c.value % 100 == 0) {
event::emit(Progress { reached: c.value })
}
}

// 2. Introduce a migrate function
entry fun migrate(c: &mut Counter, a: &AdminCap) {
assert!(c.admin == object::id(a), ENotAdmin);
assert!(c.version < VERSION, ENotUpgrade);
c.version = VERSION;
}
}

Upgrading Packages

To upgrade to the latest version of your package, follow these steps:

  1. Perform the Package Upgrade: Start by upgrading the package itself.
  2. Call the migrate Function: After the upgrade, execute the migrate function in a separate transaction. This function is crucial for the migration process.

The migrate Function

The migrate function plays a pivotal role in package upgrades:

Entry Function

It's designed as an entry function, not public, allowing for future changes or removal without restrictions.

Privileged Operation

The function requires an AdminCap and verifies that its ID matches the counter's ID being migrated.

Version Check

A sanity check ensures the module version is a true upgrade, preventing errors like missing version bumps.

Once the upgrade is complete, any attempts to call increment on the previous package version will fail due to the version check, while calls on the updated version will proceed successfully.

Extending the Migration Pattern

This migration pattern serves as a foundation for upgradeable packages with shared objects. You can adapt it further depending on your package's specific needs:

Enhanced Version Constraints

Instead of a single u64, consider using a String or a combination of upper and lower bounds to specify versions.

Dynamic Access Control

Manage access to specific functions by adding or removing marker types as dynamic fields on the shared object.

Sophisticated Migrations

Modify other fields, adjust dynamic fields, or handle multiple shared objects during the migration process.

Large-Scale Migrations

For migrations requiring multiple transactions, consider a three-phase approach:

  1. Disable access to the shared object by setting its version to a sentinel value, guarded by an AdminCap.
  2. Execute the migration in batches over multiple transactions to avoid hitting transaction limits.
  3. Reset the shared object's version to a usable value.

Upgrade Requirements

Before you begin the upgrade, ensure your package meets these requirements:

  • Possess an UpgradeTicket: You'll need an UpgradeTicket for the package you're upgrading. The IOTA network provides UpgradeCaps when you publish a package, and you can issue UpgradeTickets as the owner of that UpgradeCap. The IOTA Client CLI automates this process.
  • Maintain Layout Compatibility: Your changes must be compatible with the previous version's layout:
    • Keep existing public function signatures unchanged.
    • Maintain the same struct layouts, including struct abilities.
    • You can add new structs and functions.
    • You can remove generic type constraints from existing functions.
    • Function implementations can be changed.
    • Non-public function signatures, including friend and entry functions, can be modified.
info

If your package has a dependency that is upgraded, your package will not automatically rely on the new version. You'll need to upgrade your package explicitly to reference the updated dependency.

Upgrading

Use the iota client upgrade command to perform the package upgrade. Provide values for the following flags:

  • --gas-budget: Specifies the maximum gas units allowed before the transaction is canceled.
  • --cap: The ID of the UpgradeCap, which you receive as a return value from the publish command.

Advanced Upgrade Policies

Developers working with Move can define custom upgrade policies using available types and functions. For instance, you might want to prevent upgrades, regardless of the package owner. The make_immutable function allows you to enforce this behavior. For more complex policies, you can leverage types like UpgradeTicket and UpgradeReceipt.

CLI-Driven Upgrade Process

When you use the IOTA Client CLI, the upgrade command simplifies the package upgrade process. It generates the upgrade digest, authorizes the upgrade with the UpgradeCap to obtain an UpgradeTicket, and updates the UpgradeCap with the UpgradeReceipt after a successful upgrade.

Example: Upgrading an iota_package

Automated Address Management

IOTA supports Automated Address Management, with which addresses will automatically be updated in the Move.lock file.

Let's walk through an example of developing and upgrading a package named iota_package. Initially, your manifest might look like this:

[package]
name = "iota_package"
version = "0.0.0"
edition = "2024.beta"

[addresses]
iota_package = "0x0"

Publishing Your Package

Once your package is ready, you can publish it using the following command:

iota client publish

If successful, you'll receive a response similar to this:

INCLUDING DEPENDENCY Iota
INCLUDING DEPENDENCY MoveStdlib
BUILDING MyFirstPackage
Successfully verified dependencies on-chain against source.
----- Transaction Digest ----
2bn3EtHvbVY4bM1887MvFiGWnqq1YZ2RKmbeK7TrRbLL
----- Transaction Data ----
Transaction Signature: [Signature(Ed25519IotaSignature(Ed25519IotaSignature([0, 156, 133, 71, 156, 44, 204, 30, 31, 250, 204, 247, 60, 212, 249, 61, 112, 249, 148, 180, 83, 207, 236, 58, 99, 134, 5, 174, 115, 226, 41, 139, 192, 1, 183, 133, 38, 73, 254, 205, 190, 54, 210, 112, 144, 204, 137, 3, 8, 30, 165, 147, 120, 199, 227, 119, 53, 208, 28, 101, 34, 239, 102, 210, 1, 103, 111, 108, 165, 156, 100, 95, 13, 236, 27, 13, 127, 150, 50, 47, 155, 217, 27, 164, 61, 245, 254, 81, 182, 121, 231, 58, 150, 214, 46, 27, 222])))]
Transaction Kind : Programmable
Inputs: [Pure(IotaPureValue { value_type: Some(Address), value: "<PUBLISHER-ID>" })]
Commands: [
Publish(_,,0x00000000000000000000000000000000000000000000000000000000000000010x0000000000000000000000000000000000000000000000000000000000000002),
TransferObjects([Result(0)],Input(0)),
]

Sender: <PUBLISHER-ID>
Gas Payment: Object ID:, version: 0x6, digest: HLAcq3SFPZm4xvcPryXk5MjA718xGVnTYCdtWbFsaJpe
Gas Owner: <PUBLISHER-ID>
Gas Price: 1
Gas Budget: <GAS-BUDGET-AMOUNT>

----- Transaction Effects ----
Status : Success
Created Objects:
- ID: <ORIGINAL-PACKAGE-ID> , Owner: Immutable
- ID: <UPGRADE-CAP-ID> , Owner: Account Address ( <PUBLISHER-ID> )
- ID: <PUBLISHER-ID> , Owner: Account Address ( <PUBLISHER-ID> )
Mutated Objects:
- ID: <GAS-COIN-ID> , Owner: Account Address ( <PUBLISHER-ID> )

----- Events ----
Array []
----- Object changes ----
Array [
Object {
"type": String("mutated"),
"sender": String("<PUBLISHER-ID>"),
"owner": Object {
"AddressOwner": String("<PUBLISHER-ID>"),
},
"objectType": String("0x2::coin::Coin<0x2::iota::IOTA>"),
"objectId": String("<GAS-COIN-ID>"),
"version": Number(7),
"previousVersion": Number(6),
"digest": String("6R39f68p4tGqJWJTakKCyL8tz2w2XTvJ3Mu5nGwxadda"),
},
Object {
"type": String("published"),
"packageId": String("<ORIGINAL-PACKAGE-ID>"),
"version": Number(1),
"digest": String("FrBhLF2Rn4jP3SUsss7aXqwDDRtoKxgGbPm8eVkH7jrQ"),
"modules": Array [
String("iota_package"),
],
},
Object {
"type": String("created"),
"sender": String("<PUBLISHER-ID>"),
"owner": Object {
"AddressOwner": String("<PUBLISHER-ID>"),
},
"objectType": String("0x2::package::UpgradeCap"),
"objectId": String("<UPGRADE-CAP-ID>"),
"version": Number(7),
"digest": String("BoGQ63r27DFZDMC8p7YwRcDpToFpbZ9rG1R4o4uXkaUw"),
},
Object {
"type": String("created"),
"sender": String("<PUBLISHER-ID>"),
"owner": Object {
"AddressOwner": String("<PUBLISHER-ID>"),
},
"objectType": String("<ORIGINAL-PACKAGE-ID>::iota_package::<MODULE-NAME>"),
"objectId": String("<PACKAGE-ID>"),
"version": Number(7),
"digest": String("BC3KeuATKJozLNipbUz2GWzoDXbodXH4HLm59TxJSmVd"),
},
]
----- Balance changes ----
Array [
Object {
"owner": Object {
"AddressOwner": String("<PUBLISHER-ID>"),
},
"coinType": String("0x2::iota::IOTA"),
"amount": String("-9328480"),
},
]

The Object Changes Section

The response includes an Object changes section, which contains essential information for future upgrades, including the UpgradeCap ID and your package ID. You can identify these objects using the Object.objectType value:

  • UpgradeCap: Identified by String("0x2::package::UpgradeCap").
  • Package: Identified by String("<PACKAGE-ID>::iota_package::<MODULE-NAME>").

Preparing for an Upgrade

After some time, you may decide to enhance your iota_package by adding new features. Before executing the upgrade, you need to modify the package manifest:

  1. Update the Address: In the [addresses] section, change the iota_package address value to 0x0. This action prompts the validator to issue a new address for the upgraded package.

Your updated manifest should look like this:

[package]
name = "iota_package"
version = "0.0.1"
edition = "2024.beta"

[addresses]
iota_package = "0x0"

Executing the Upgrade

With the updated manifest and code prepared, you're ready to perform the upgrade. Use the following command, passing the UpgradeCap ID using the --upgrade-capability flag:

iota client upgrade --upgrade-capability <UPGRADE-CAP-ID>

Finalizing the Upgrade

The console will notify you if the new package fails to meet the upgrade requirements. If everything checks out, the compiler will publish the upgraded package to the network and return the result.

With these steps, your iota_package will be successfully upgraded and ready for use with its new features.

INCLUDING DEPENDENCY Iota
INCLUDING DEPENDENCY MoveStdlib
BUILDING MyFirstPackage
Successfully verified dependencies on-chain against source.
----- Transaction Digest ----
HZdnGWE2VoqDWwBhoBwe17tDFn7uYgfBpK5nk75Rmh5z
----- Transaction Data ----
Transaction Signature: [Signature(Ed25519IotaSignature(Ed25519IotaSignature([0, 108, 166, 235, 244, 238, 72, 232, 143, 49, 225, 180, 55, 63, 131, 155, 146, 126, 50, 158, 138, 213, 174, 71, 162, 222, 62, 198, 245, 219, 224, 171, 82, 43, 197, 56, 16, 252, 186, 83, 154, 109, 104, 90, 212, 236, 122, 78, 175, 173, 107, 9, 2, 10, 30, 74, 101, 138, 228, 251, 170, 39, 25, 242, 8, 103, 111, 108, 165, 156, 100, 95, 13, 236, 27, 13, 127, 150, 50, 47, 155, 217, 27, 164, 61, 245, 254, 81, 182, 121, 231, 58, 150, 214, 46, 27, 222])))]
Transaction Kind : Programmable
Inputs: [Object(ImmOrOwnedObject { object_id: <UPGRADE-CAP-ID>, version: SequenceNumber(9), digest: o#Bvy7R33o4ogLuyfzM76nmM1RqKnEALQrbd34CLWZhf5Y }), Pure(IotaPureValue { value_type: Some(U8), value: 0 }), Pure(IotaPureValue { value_type: Some(Vector(U8)), value: [202,122,179,32,64,155,14,236,160,5,75,17,159,202,125,114,234,36,182,41,159,84,56,222,99,121,250,82,206,19,212,5] })]
Commands: [
MoveCall(0x0000000000000000000000000000000000000000000000000000000000000002::package::authorize_upgrade(,Input(0),Input(1)Input(2))),
Upgrade(Result(0),,0x00000000000000000000000000000000000000000000000000000000000000010x0000000000000000000000000000000000000000000000000000000000000002, <ORIGINAL-PACKAGE-ID>, _)),
MoveCall(0x0000000000000000000000000000000000000000000000000000000000000002::package::commit_upgrade(,Input(0)Result(1))),
]

Sender: <PUBLISHER-ID>
Gas Payment: Object ID: <GAS-COIN-ID>, version: 0x9, digest: 84ZKQcZZLTCmyAoRp9QhDrxxZ7nzGtdoBw18UbNm26ad
Gas Owner: <PUBLISHER-ID>
Gas Price: 1
Gas Budget: <GAS-BUDGET-AMOUNT>

----- Transaction Effects ----
Status : Success
Created Objects:
- ID: <MODULE-ID> , Owner: Immutable
Mutated Objects:
- ID: <GAS-COIN-ID> , Owner: Account Address ( <PUBLISHER-ID> )
- ID: <UPGRADE-CAP-ID> , Owner: Account Address ( <PUBLISHER-ID> )

----- Events ----
Array []
----- Object changes ----
Array [
Object {
"type": String("mutated"),
"sender": String("<PUBLISHER-ID>"),
"owner": Object {
"AddressOwner": String("<PUBLISHER-ID>"),
},
"objectType": String("0x2::coin::Coin<0x2::iota::IOTA>"),
"objectId": String("<GAS-COIN-ID>"),
"version": Number(10),
"previousVersion": Number(9),
"digest": String("EvfMLHBDXFRUeMd7vgmAMaacnwZbGFHg8d7Kov3fTt9L"),
},
Object {
"type": String("mutated"),
"sender": String("<PUBLISHER-ID>"),
"owner": Object {
"AddressOwner": String("<PUBLISHER-ID>"),
},
"objectType": String("0x2::package::UpgradeCap"),
"objectId": String("<UPGRADE-CAP-ID>"),
"version": Number(10),
"previousVersion": Number(9),
"digest": String("FZ9AruCAnhjW8zrozUMgtsY79SggTiHr3suwZNe5eMnM"),
},
Object {
"type": String("published"),
"packageId": String("<UPGRADED-PACKAGE-ID>"),
"version": Number(2),
"digest": String("8RDsE6kFND2V2gxGiytwxa815mctwxNh7A8YqRS4AJME"),
"modules": Array [
String("<MODULE-NAME>"),
],
},
]
----- Balance changes ----
Array [
Object {
"owner": Object {
"AddressOwner": String("<PUBLISHER-ID>"),
},
"coinType": String("0x2::iota::IOTA"),
"amount": String("-6350420"),
},
]


After successfully upgrading your iota_package, the result will include a new ID for the upgraded package. Just like before, it's essential to update your manifest with this new ID. This ensures that any other packages depending on your iota_package can locate the correct on-chain bytecode for verification.

Updating the Manifest

To reflect the new package ID, in the [addresses] section, return the iota_package value to the original package ID.

Your updated manifest should look like this:

[package]
name = "iota_package"
version = "0.0.1"
edition = "2024.beta"

[addresses]
iota_package = "<ORIGINAL-PACKAGE-ID>"
  • The iota_package address in the [addresses] section should always point back to the original package ID after an upgrade.
  • Before running any subsequent upgrade commands, always set the iota_package address to 0x0 in the [addresses] section. This ensures the validator assigns a new ID for the upgrade.

Question 1/4

What ensures that transactions in IOTA can be executed quickly without requiring full consensus?