Contract Upgrading
Upgrading a Dubhe DApp is a safe, structured process. The Framework tracks every package version on-chain inside DappStorage and enforces that only the latest package version is allowed to write data.
Key files involved
| File | Role | Edit? |
|---|---|---|
sources/codegen/genesis.move | run() (first deploy) + migrate() (each upgrade) | No — auto-managed |
sources/scripts/migrate.move | Holds the ON_CHAIN_VERSION constant and generated migrate_to_vN functions | Auto-updated by tooling |
sources/scripts/deploy_hook.move | One-time initialization at first deploy | Only on first deploy |
Version tracking
migrate.move holds a single constant that represents the current on-chain version:
// sources/scripts/migrate.move
module mygame::migrate {
const ON_CHAIN_VERSION: u32 = 1;
public fun on_chain_version(): u32 {
ON_CHAIN_VERSION
}
}The Framework stores this version in DappStorage. When migrate_to_vN is called after publishing, it invokes dapp_system::upgrade_dapp, which registers the new package ID and advances DappStorage.version automatically.
Upgrade types
| Upgrade type | Command | What the tooling does |
|---|---|---|
| Bug fix — no schema change, old clients stay compatible | dubhe upgrade | Publishes only; no migration call |
| Schema migration — new resources added | dubhe upgrade | Auto-detects pending resources, generates and calls migrate_to_vN |
| Breaking logic change — must invalidate old clients | dubhe upgrade --bump-version | Forces migrate_to_vN generation even without new resources |
Never use
sui client upgradedirectly.dubhe upgradehandles the full lifecycle includingmigrate_to_vNgeneration and the on-chain migration call.
Upgrade workflow
Step 1 — Update dubhe.config.ts (when adding resources)
Add new resource definitions. New resources trigger automatic schema migration:
resources: {
level: 'u32', // existing
health: 'u64', // existing
guild: 'String', // NEW — added in this upgrade
}Step 2 — Regenerate code (when config changed)
dubhe generateNew resource modules appear in sources/codegen/resources/. genesis.move migrate() is updated automatically.
Step 3 — Run the upgrade
# Schema migration (new resources detected automatically):
dubhe upgrade --network testnet
# Breaking logic change (no new resources, but must block old clients):
dubhe upgrade --network testnet --bump-versiondubhe upgrade performs in order:
sui move buildwith--dump-bytecode-as-base64package::authorize_upgrade+upgrade+package::commit_upgradein one PTB- (if migration needed) generates and calls
migrate_to_vN(dapp_hub, dapp_storage)— the generated function:- reads the new package ID via
dapp_key::package_id() - reads the new version via
migrate::on_chain_version() - calls
dapp_system::upgrade_dapp→ advancesDappStorage.versionand registers the new package ID - calls
genesis::migratefor any custom data migration logic
- reads the new package ID via
Pre-upgrade lint check: Before any on-chain transaction,
dubhe upgradescans everypublic entry funinsources/systems/for missingensure_latest_versionguards. If any are found, an interactive confirmation prompt is shown. Typeyto proceed.
Step 4 — Verify
# Check Published.toml for the new version
cat src/<name>/Published.toml
# Check latest.json for resource list and package ID
cat src/<name>/.history/sui_testnet/latest.jsonBlocking old package versions
Add ensure_latest_version at the top of every user-facing entry function to block
calls from clients running against an outdated package:
module mygame::player_system;
use dubhe::dapp_service::{DappStorage, UserStorage};
use dubhe::dapp_system;
use mygame::dapp_key::DappKey;
use mygame::migrate;
public entry fun level_up(
dapp_storage: &DappStorage,
user_storage: &mut UserStorage,
ctx: &mut TxContext,
) {
// Aborts if the caller is not from the latest package version.
dapp_system::ensure_latest_version<DappKey>(dapp_storage, migrate::on_chain_version());
// ... game logic
}ensure_latest_version compares the ON_CHAIN_VERSION constant compiled into the
calling package against DappStorage.version. After a --bump-version or schema
migration upgrade, old packages carry a lower constant and abort immediately.
deploy_hook vs genesis::migrate
deploy_hook::run | genesis::migrate | |
|---|---|---|
| When called | First deploy only (genesis::run) | Every upgrade (migrate_to_vN) |
| Purpose | Initialize global state, set initial config | Custom data migration between versions |
| Idempotent | Protected by framework (second call is no-op) | Should be written idempotently |
| You edit it? | Yes — put your initialization logic here | Yes — put cross-version data transforms here |
genesis::migrate is intentionally empty by default. Fill it in when an upgrade
requires migrating existing user data (e.g. copying values from an old resource to a
new one).
What happens to existing data?
- Existing records are never deleted by an upgrade. Old data in
UserStorageobjects remains readable. - New resource tables are empty after
migrate_to_vN— new resources start with no data unless you populate them ingenesis::migrate. - Old system functions are blocked via
ensure_latest_versiononceDappStorage.versionhas advanced. - Adding a field to an existing resource creates a new BCS layout; old encoded data cannot be decoded by the new struct. Use separate resource names rather than changing field order in an existing resource.
Deployment artifacts
| File | Contents | Updated by |
|---|---|---|
Published.toml | version, publishedAt, originalId, chainId | publishHandler / upgradeHandler |
.history/sui_<network>/latest.json | version, packageId, dappHubId, dappStorageId, resources, enums | saveContractData |
Move.lock | [env.<network>] with published IDs | Sui CLI during build/publish |
sources/scripts/migrate.move | ON_CHAIN_VERSION, migrate_to_vN functions | appendMigrateFunction (auto on schema migration or --bump-version) |
Multiple sequential upgrades
You can upgrade a DApp multiple times. Each upgrade:
- Increments the version by exactly 1 (
oldVersion + 1). - Appends a new
migrate_to_vNtomigrate.move(if migration needed). - Records the new package ID in
DappStorage.package_ids(append-only). - Updates
DappStorage.versionto the new version number.
The originalId field in Published.toml always refers to the first published package ID and never changes across upgrades.
Rollback
Sui does not support on-chain package rollback. Once a package is upgraded:
- The old package ID remains on-chain permanently (immutable).
- The
UpgradeCapnow points to the new version. - Old package callers are blocked by
ensure_latest_versiononceDappStorage.versionhas advanced.
To effectively “roll back”, publish a new upgrade that reverts the code changes.
ON_CHAIN_VERSION must still increment (it cannot decrease).