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
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:
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:
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]" senderWith 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:
authorize_upgrade(&mut cap, policy, digest)returns anUpgradeTicket. The requestedpolicymust be at least as strict as the cap's, and only one ticket can be outstanding at a time.- The
Upgradecommand consumes the ticket, checks the bytecode againstdigestand the policy, and returns anUpgradeReceipt. commit_upgrade(&mut cap, receipt)records the new package ID and incrementsversion.
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_immutableif 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
- Custom Upgrade Policies, Sui documentation
package.move, Sui framework source- Sui Client PTB CLI, Sui documentation
- Upgrading Packages, Sui documentation