Upgrading Packages
Sui smart contracts are immutable package objects consisting of a collection of Move modules. Because the packages are immutable, transactions can safely access smart contracts without full consensus (fastpath transactions). If someone could change these packages, they would become shared objects, which would require full consensus before completing a transaction.
The inability to change package objects, however, becomes a problem when considering the iterative nature of code development. Builders require the ability to update their code and pull changes from other developers while still being able to reap the benefits of fastpath transactions. Fortunately, the Sui network provides a method of upgrading your packages while still retaining their immutable properties.
Upgrade considerations
There are some details of the process that you should consider before upgrading your packages.
For example, module initializers do not re-run with package upgrades. When you publish your initial package, Move runs the init
function you define for the package once (and only once) at the time of the publish event. Any init
functions you might include in subsequent versions of your package are ignored.
As alluded to previously, all packages on the Sui network are immutable. Because of this fact, you cannot delete old packages from the chain. As a result, there is nothing that prevents other packages from accessing the methods and types defined in the old versions of your upgraded packages. By default, users can choose to keep using the old version of a package, as well. As a package developer, you must be aware of and account for this possibility.
For example, you might define an increment
function in your original package:
public fun increment(c: &mut Counter) {
c.value = c.value + 1;
}
Then, your package upgrade might add an emit event to the increment
function:
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 });
}
}
If there is a mix of callers for both the old and upgraded increment
function, then the process fails because the old function is not aware of the Progress
event.
Similar to mismatched function definitions, you might also run into issues maintaining dynamic fields that need to remain in sync with a struct's original fields. To address these issues, you can introduce a new type as part of the upgrade and port users over to it, breaking backwards compatibility. For example, if you're using owned objects to demonstrate proof, like proof of ownership, and you develop a new version of your package to address problematic code, you can introduce a new type in the upgraded package. You can then add a function to your package that trades old objects for new ones. Because your logic only recognizes objects with the new type, you effectively force users to update.
Another example of having users update to the latest package is when you have a bookkeeping shared object in your package that you discover has flawed logic so is not functioning as expected. As in the previous example, you want users to use only the object defined in the upgraded package with the correct logic, so you add a new type and migration function to your package upgrade. This process requires a couple of transactions, one for the upgrade and another that you call from the upgraded package to set up the new shared object that replaces the existing one. To protect the setup function, you would need to create an AdminCap
object or similar as part of your package to make sure you, as the package owner, are the only authorized initiator of that function. Perhaps even more useful, you might include a flag in the shared object that allows you, as the package owner, to toggle the enabled state of that shared object. You can add a check for the enabled state to prevent access to that object from the on-chain public while you perform the migration. Of course, you would probably create this flag only if you expected to perform this migration at some point in the future, not because you're intentionally developing objects with flawed logic.
Versioned shared objects
When you create packages that involve shared objects, you need to think about upgrades and versioning from the start given that all prior versions of a package still exist on-chain. A useful pattern is to introduce versioning to the shared object and using a version check to guard access to functions in the package. This enables you to limit access to the shared object to only the latest version of a package.
Considering the earlier counter
example, which might have started life as follows:
module example::counter {
use sui::object::{Self, UID};
use sui::transfer;
use sui::tx_context::TxContext;
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 this package can limit access of the shared object to the latest version of the package, you need to:
- Track the current version of the module in a constant,
VERSION
. - Track the current version of the shared object,
Counter
, in a newversion
field. - Introduce an
AdminCap
to protect privileged calls, and associate theCounter
with itsAdminCap
with a new field (you might already have a similar type for shared object administration, in which case you can re-use that). This cap is used to protect calls to migrate the shared object from version to version. - Guard the entry of all functions that access the shared object with a check that its
version
matches the packageVERSION
.
An upgrade-aware counter
module that incorporates all these ideas looks as follows:
module example::counter {
use sui::object::{Self, ID, UID};
use sui::transfer;
use sui::tx_context::{Self, TxContext};
// 1. Track the current version of the module
const VERSION: u64 = 1;
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,
}
struct AdminCap has key {
id: UID,
}
/// Not the right admin for this counter
const ENotAdmin: u64 = 0;
/// 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;
}
}
To upgrade a module using this pattern requires making two extra changes, on top of any implementation changes your upgrade requires:
- Bump the
VERSION
of the package. - Introduce a
migrate
function to upgrade the shared object.
The following module is an upgraded counter
that emits Progress
events as originally discussed, but also provides tools for an admin (AdminCap
holder) to prevent accesses to the counter from older package versions:
module example::counter {
use sui::event;
use sui::object::{Self, ID, UID};
use sui::transfer;
use sui::tx_context::{Self, TxContext};
// 1. Bump the `VERSION` of the package.
const VERSION: u64 = 2;
struct Counter has key {
id: UID,
version: u64,
admin: ID,
value: u64,
}
struct AdminCap has key {
id: UID,
}
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 to this version of the package requires performing the package upgrade, and calling the migrate
function in a follow-up transaction. Note that the migrate
function:
- Is an
entry
function and notpublic
. This allows it to be entirely changed (including changing its signature or removing it entirely) in later upgrades. - Accepts an
AdminCap
and checks that its ID matches the ID of the counter being migrated, making it a privileged operation. - Includes a sanity check that the version of the module is actually an upgrade for the object. This helps catch errors such as failing to bump the module version before upgrading.
After a successful upgrade, calls to increment
on the previous version of the package aborts on the version check, while calls on the later version should succeed.
Extensions
This pattern forms the basis for upgradeable packages involving shared objects, but you can extend it in a number of ways, depending on your package's needs:
- The version constraints can be made more expressive:
- Rather than using a single
u64
, versions could be specified as aString
, or a pair of upper and lower bounds. - You can control access to specific functions or sets of functions by adding and removing marker types as dynamic fields on the shared object.
- Rather than using a single
- The
migrate
function could be made more sophisticated (modifying other fields in the shared object, adding/removing dynamic fields, migrating multiple shared objects simultaneously). - You can implement large migrations that need to run over multiple transactions in a three phase set-up:
- Disable general access to the shared object by setting its version to a sentinel value (e.g.
U64_MAX
), using anAdminCap
-guarded call. - Run the migration over the course of multiple transactions (e.g. if a large volume of objects need to be moved, it is best to do this in batches, to avoid hitting transaction limits).
- Set the version of the shared object back to a usable value.
- Disable general access to the shared object by setting its version to a sentinel value (e.g.
Upgrade requirements
To upgrade a package, your package must satisfy the following requirements:
- You must have an
UpgradeTicket
for the package you want to upgrade. The Sui network issuesUpgradeCap
s when you publish a package, then you can issueUpgradeTicket
s as the owner of thatUpgradeCap
. The Sui Client CLI handles this requirement automatically. - Your changes must be layout-compatible with the previous version.
- Existing
public
function signatures must remain the same. - Existing struct layouts (including struct abilities) must remain the same.
- You can add new structs and functions.
- You can remove generic type constraints from existing functions (public or otherwise).
- You can change function implementations.
- You can change non-
public
function signatures, includingfriend
andentry
function signatures.
- Existing
If you have a package with a dependency, and that dependency is upgraded, your package does not automatically depend on the newer version. You must explicitly upgrade your own package to point to the new dependency.
Upgrading
Use the sui client upgrade
command to upgrade packages that meet the previous requirements, providing values for the following flags:
Beginning with the Sui v1.24.1
release, the --gas-budget
flag is no longer required for CLI commands.
--gas-budget
: The maximum number of gas units that can be expended before the network cancels the transaction.--cap
: The ID of theUpgradeCap
. You receive this ID as a return from the publish command.
Developers upgrading packages using Move code have access to types and functions to define custom upgrade policies. For example, a Move developer might want to disallow upgrading a package, regardless of the current package owner. The make_immutable
function is available to them to create this behavior. More advanced policies using available types like UpgradeTicket
and Upgrade Receipt
are also possible. For an example, see this custom upgrade policy on GitHub.
When you use the Sui Client CLI, the upgrade
command handles generating the upgrade digest, authorizing the upgrade with the UpgradeCap
to get an UpgradeTicket
, and updating the UpgradeCap
with the UpgradeReceipt
after a successful upgrade. To learn more about these processes, see the Move documentation for the package module.
Example
You develop a package named sui_package
. Its manifest looks like the following:
[package]
name = "sui_package"
version = "0.0.0"
[addresses]
sui_package = "0x0"
When your package is ready, you publish it:
sui client publish --gas-budget <GAS-BUDGET-AMOUNT>
And receive the response:
INCLUDING DEPENDENCY Sui
INCLUDING DEPENDENCY MoveStdlib
BUILDING MyFirstPackage
Successfully verified dependencies on-chain against source.
----- Transaction Digest ----
2bn3EtHvbVY4bM1887MvFiGWnqq1YZ2RKmbeK7TrRbLL
----- Transaction Data ----
Transaction Signature: [Signature(Ed25519SuiSignature(Ed25519SuiSignature([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(SuiPureValue { 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::sui::SUI>"),
"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("sui_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>::sui_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::sui::SUI"),
"amount": String("-9328480"),
},
]
The result includes an Object changes section with two pieces of information you need for upgrading, an UpgradeCap
ID and your package ID.
You can identify the different objects using the Object.objectType
value in the response. The UpgradeCap
entry has a value of String("0x2::package::UpgradeCap")
and the objectType
for the package reads String("<PACKAGE-ID>::sui_package::<MODULE-NAME>")
To make sure your other packages can use this package as a dependency, you must update the Move.toml
manifest file for your package to include this information.
- Automated Addresses
- Manual Addresses
Add an entry for the package in the [addresses]
section that points to the value of the on-chain ID:
[addresses]
sui_package = "<ORIGINAL-PACKAGE-ID>"
Update the alias address and add a new published-at
entry in the [package]
section, both pointing to the value of the on-chain ID:
[package]
name = "sui_package"
version = "0.0.0"
published-at = "<ORIGINAL-PACKAGE-ID>"
[addresses]
sui_package = "<ORIGINAL-PACKAGE-ID>"
After a while, you decide to upgrade your sui_package
to include some requested features. Before running the upgrade
command, you need to edit the manifest again.
- Automated Addresses
- Manual Addresses
In the [addresses]
section, you update the sui_package
address value to 0x0
again so the validator issues a new address for the upgrade package.
[addresses]
sui_package = "0x0"
In the [addresses]
section, you update the sui_package
address value to 0x0
again so the validator issues a new address for the upgrade package. You can leave the published-at
value the same, because it is only read by the toolchain when publishing a dependent package. The saved manifest now resembles the following:
[package]
name = "sui_package"
version = "0.0.1"
published-at = "<ORIGINAL-PACKAGE-ID>"
[addresses]
sui_package = "0x0"
With the new manifest and code in place, it's time to use the sui client upgrade
command to upgrade your package. Pass the UpgradeCap
ID (the <UPGRADE-CAP-ID>
value from the example) to the --upgrade-capability
flag.
sui client upgrade --gas-budget <GAS-BUDGET-AMOUNT> --upgrade-capability <UPGRADE-CAP-ID>
The console alerts you if the new package doesn't satisfy requirements, otherwise the compiler publishes the upgraded package to the network and returns its result:
INCLUDING DEPENDENCY Sui
INCLUDING DEPENDENCY MoveStdlib
BUILDING MyFirstPackage
Successfully verified dependencies on-chain against source.
----- Transaction Digest ----
HZdnGWE2VoqDWwBhoBwe17tDFn7uYgfBpK5nk75Rmh5z
----- Transaction Data ----
Transaction Signature: [Signature(Ed25519SuiSignature(Ed25519SuiSignature([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(SuiPureValue { value_type: Some(U8), value: 0 }), Pure(SuiPureValue { 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::sui::SUI>"),
"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::sui::SUI"),
"amount": String("-6350420"),
},
]
The result provides a new ID for the upgraded package.
- Automated Addresses
- Manual Addresses
After upgrading, return the original sui_package
ID value in the [addresses]
section:
[addresses]
sui_package = "<ORIGINAL-PACKAGE-ID>"
So that packages that depend on your sui_package
know where to find the on-chain bytecode for verification, edit your manifest again. Provide the upgraded package ID for the published-at
value and return the original sui_package
ID value in the [addresses]
section:
[package]
name = "sui_package"
version = "0.0.1"
published-at = "<UPGRADED-PACKAGE-ID>"
[addresses]
sui_package = "<ORIGINAL-PACKAGE-ID>"
Note the published-at
value changes with every upgrade and needs to be updated after every upgrade.
The ID for the sui_package
in the [addresses]
section always points to the original package ID after upgrading. You must always change that value back to 0x0
, however, before running the upgrade
command so the validator knows to create a new ID for the upgrade.