suideveloper
All questions
Packages

What is an UpgradeCap, and what upgrade policies exist?

November 13, 2026

An UpgradeCap (0x2::package::UpgradeCap) is the object Sui creates when you publish a package. Whoever owns it can upgrade the package. Its policy decides how much an upgrade may change. There are four levels: compatible (the default), additive, dependency-only and immutable. A policy can only become stricter. Making a package immutable destroys the cap, and nobody can upgrade the package after that.

The object

Move
public struct UpgradeCap has key, store {
    id: UID,
    package: ID,   // latest version of the package
    version: u64,  // number of upgrades applied
    policy: u8,    // what kind of upgrades are allowed
}

It has key and store, so you can transfer it, hold it in a multisig address, or wrap it in another object that enforces your own rules. sui client publish sends it to the publishing address and records its ID in Published.toml under upgrade-capability.

The policies

Policy u8 value What an upgrade may do
Compatible 0 Change function bodies, add modules, structs and functions, change non-public signatures. Existing public signatures and struct layouts stay fixed.
Additive 128 Add new functions and types, or change dependencies. Existing functions can't change.
Dependency-only 192 Only change the package's dependencies.
Immutable (no cap) Nothing. The cap is deleted.

The framework exposes these values as package::compatible_policy(), package::additive_policy() and package::dep_only_policy(). In TypeScript, the UpgradePolicy enum exported from @mysten/sui/transactions has COMPATIBLE, ADDITIVE and DEP_ONLY.

Changing the policy

Three entry functions in 0x2::package take the cap:

  • only_additive_upgrades(cap: &mut UpgradeCap)
  • only_dep_upgrades(cap: &mut UpgradeCap)
  • make_immutable(cap: UpgradeCap), which consumes and deletes the cap

From the CLI:

Terminal
sui client call \
  --package 0x2 \
  --module package \
  --function only_additive_upgrades \
  --args 0x<UPGRADE_CAP_ID>

Because a policy can only get stricter, calling only_additive_upgrades on a cap that is already dependency-only aborts.

To set a policy at deploy time, so the package never exists under the default policy, publish and restrict in one PTB:

Terminal
sui client ptb \
  --move-call sui::tx_context::sender \
  --assign sender \
  --publish "." \
  --assign upgrade_cap \
  --move-call sui::package::only_additive_upgrades upgrade_cap \
  --transfer-objects "[upgrade_cap]" sender

With sui client ptb you have to transfer (or destroy) the UpgradeCap yourself, otherwise the transaction fails with an unused value error.

Tickets, receipts and custom policies

Every upgrade goes through the cap inside one transaction:

  1. authorize_upgrade(&mut cap, policy, digest) returns an UpgradeTicket. The requested policy must be at least as strict as the cap's, and only one ticket can be outstanding at a time.
  2. The Upgrade command consumes the ticket, checks the bytecode against digest and the policy, and returns an UpgradeReceipt.
  3. commit_upgrade(&mut cap, receipt) records the new package ID and increments version.

Tickets and receipts have no abilities, so they can't be stored or dropped. A custom upgrade policy is any module that holds the cap and only calls authorize_upgrade when its own conditions hold: a timelock, a vote, an allow-list, or a digest approved in advance. sui move build --dump-bytecode-as-base64 prints the digest of your build so a policy can check it.

Advice

  • For a production package, don't leave the cap in a single hot key. Use a multisig address or a custom policy.
  • Only call make_immutable if you're sure the package will never need a fix. It can't be reversed.
  • Losing the cap (for example, sending it to an address nobody controls) makes the package effectively immutable.

For the upgrade process itself, see how package upgrades work.

Sources

More on packages