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
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
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:
- Update the
VERSION
constant. - 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:
- Perform the Package Upgrade: Start by upgrading the package itself.
- Call the
migrate
Function: After the upgrade, execute themigrate
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:
- Disable access to the shared object by setting its version to a sentinel value, guarded by an
AdminCap
. - Execute the migration in batches over multiple transactions to avoid hitting transaction limits.
- 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 anUpgradeTicket
for the package you're upgrading. The IOTA network providesUpgradeCaps
when you publish a package, and you can issueUpgradeTickets
as the owner of thatUpgradeCap
. 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, includingfriend
andentry
functions, can be modified.
- Keep existing
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 theUpgradeCap
, 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
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:
- Update the Address: In the
[addresses]
section, change theiota_package
address value to0x0
. 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 to0x0
in the[addresses]
section. This ensures the validator assigns a new ID for the upgrade.