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

FileRoleEdit?
sources/codegen/genesis.moverun() (first deploy) + migrate() (each upgrade)No — auto-managed
sources/scripts/migrate.moveHolds the ON_CHAIN_VERSION constant and generated migrate_to_vN functionsAuto-updated by tooling
sources/scripts/deploy_hook.moveOne-time initialization at first deployOnly 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 typeCommandWhat the tooling does
Bug fix — no schema change, old clients stay compatibledubhe upgradePublishes only; no migration call
Schema migration — new resources addeddubhe upgradeAuto-detects pending resources, generates and calls migrate_to_vN
Breaking logic change — must invalidate old clientsdubhe upgrade --bump-versionForces migrate_to_vN generation even without new resources

Never use sui client upgrade directly. dubhe upgrade handles the full lifecycle including migrate_to_vN generation 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 generate

New 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-version

dubhe upgrade performs in order:

  1. sui move build with --dump-bytecode-as-base64
  2. package::authorize_upgrade + upgrade + package::commit_upgrade in one PTB
  3. (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 → advances DappStorage.version and registers the new package ID
    • calls genesis::migrate for any custom data migration logic

Pre-upgrade lint check: Before any on-chain transaction, dubhe upgrade scans every public entry fun in sources/systems/ for missing ensure_latest_version guards. If any are found, an interactive confirmation prompt is shown. Type y to 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.json

Blocking 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::rungenesis::migrate
When calledFirst deploy only (genesis::run)Every upgrade (migrate_to_vN)
PurposeInitialize global state, set initial configCustom data migration between versions
IdempotentProtected by framework (second call is no-op)Should be written idempotently
You edit it?Yes — put your initialization logic hereYes — 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 UserStorage objects remains readable.
  • New resource tables are empty after migrate_to_vN — new resources start with no data unless you populate them in genesis::migrate.
  • Old system functions are blocked via ensure_latest_version once DappStorage.version has 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

FileContentsUpdated by
Published.tomlversion, publishedAt, originalId, chainIdpublishHandler / upgradeHandler
.history/sui_<network>/latest.jsonversion, packageId, dappHubId, dappStorageId, resources, enumssaveContractData
Move.lock[env.<network>] with published IDsSui CLI during build/publish
sources/scripts/migrate.moveON_CHAIN_VERSION, migrate_to_vN functionsappendMigrateFunction (auto on schema migration or --bump-version)

Multiple sequential upgrades

You can upgrade a DApp multiple times. Each upgrade:

  1. Increments the version by exactly 1 (oldVersion + 1).
  2. Appends a new migrate_to_vN to migrate.move (if migration needed).
  3. Records the new package ID in DappStorage.package_ids (append-only).
  4. Updates DappStorage.version to 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 UpgradeCap now points to the new version.
  • Old package callers are blocked by ensure_latest_version once DappStorage.version has advanced.

To effectively “roll back”, publish a new upgrade that reverts the code changes. ON_CHAIN_VERSION must still increment (it cannot decrease).