Skip to main content

Command Palette

Search for a command to run...

Sui Smart Contract Upgrades: From Version Control to Migration

Updated
6 min read

Smart contract upgrades are a critical aspect of blockchain development, allowing developers to fix bugs, add features, and improve functionality without losing existing state or forcing users to migrate to entirely new contracts. Sui's approach to upgrades is particularly elegant, offering built-in upgrade capabilities that maintain backward compatibility while enabling seamless evolution of your applications.

In this comprehensive guide, we'll walk through implementing upgradeable smart contracts on Sui using the Move programming language, covering everything from initial design to production deployment.

Understanding Sui's Upgrade Architecture

Sui's upgrade system operates on three key principles:

  1. UpgradeCap Authority: Every published package automatically gets an UpgradeCap object that controls upgrade permissions

  2. Layout Compatibility: New versions must maintain compatibility with existing object structures

  3. Migration Functions: Developers implement functions to update existing objects to work with new code

Unlike other blockchains where you might deploy entirely separate contracts, Sui upgrades create linked versions of the same logical contract, preserving object relationships and user experience.

Building an Upgradeable Token Contract

Let's build a practical example: a token contract that starts with basic admin functionality and evolves to support more administrators through upgrades.

Version 1: Foundation with Versioning

Our initial contract establishes the foundation with built-in version tracking:

module token::token { 
    use std::ascii;
    use std::option;
    use sui::coin::{Self, Coin, TreasuryCap, CoinMetadata};
    use sui::url;
    use sui::object::{Self, UID};
    use sui::tx_context::{Self, TxContext};
    use sui::transfer;
    use sui::vec_set::{Self, VecSet};

    // Version constants - critical for upgrade tracking
    const CURRENT_VERSION: u64 = 1;
    const MAX_ADMINS_V1: u32 = 2;
    const MAX_ADMINS_V2: u32 = 4;  // Prepared for future upgrade

    // Error codes
    const E_NOT_OWNER_ADMIN: u64 = 1;
    const E_WRONG_VERSION: u64 = 2;
    const E_ALREADY_MIGRATED: u64 = 3;

    // One-Time Witness for token creation
    public struct TOKEN has drop {}

    // Versioned shared objects
    public struct AdminRegistry has key {
        id: UID,
        version: u64,                    // Version tracking
        owner_address: address,
        admin_address: VecSet<address>,
        max_admins: u32,                 // Upgradeable limit
    }

    public struct Treasury has key {
        id: UID,
        version: u64,                    // Version tracking
        treasury_wal_address: address 
    }

    // Owned objects don't need versioning
    public struct OwnerCap has key, store {
        id: UID,
    }

    // Witness pattern for access control
    public struct Witness has drop {}

The key design decisions here are:

  • Version fields in all shared objects for upgrade tracking

  • Forward-looking constants (MAX_ADMINS_V2) for planned upgrades

  • Consistent error handling with defined error codes

  • Separation of concerns between owned and shared objects

Initialization with Upgrade Awareness

fun init(witness: TOKEN, ctx: &mut TxContext) {
    let icon_url = ascii::string(b"https://example.com/token-icon.png");

    let (mut treasury_cap, metadata) = coin::create_currency<TOKEN>(
        witness, 
        9, 
        b"TOKEN", 
        b"Upgradeable Token", 
        b"A token contract demonstrating Sui upgrades", 
        option::some(url::new_unsafe(icon_url)), 
        ctx
    );

    let sender = tx_context::sender(ctx);
    let treas_address = @0xee571f26d4a51d32601e318dbaacd7f1250ed20915582ae0037d8b02e562fe78;

    // Initialize all shared objects with version 1
    let treasury = Treasury {
        id: object::new(ctx),
        version: CURRENT_VERSION,
        treasury_wal_address: treas_address
    };

    let owner_cap = OwnerCap {
        id: object::new(ctx),
    };

    let admin_registry = AdminRegistry { 
        id: object::new(ctx),
        version: CURRENT_VERSION,
        owner_address: sender,
        admin_address: vec_set::empty<address>(),
        max_admins: MAX_ADMINS_V1,       // Start with 2 admin limit
    };

    // Standard token setup
    coin::mint_and_transfer(&mut treasury_cap, TOKEN_SUPPLY, treas_address, ctx);
    transfer::public_transfer(treasury_cap, sender);
    transfer::public_freeze_object(metadata);

    // Object distribution
    transfer::share_object(treasury);
    transfer::public_transfer(owner_cap, sender);
    transfer::share_object(admin_registry);
}

Migration Function: The Upgrade Bridge

The migration function is where the magic happens - it's responsible for updating existing objects to work with new code

entry fun migrate(
    admin_registry: &mut AdminRegistry,
    treasury: &mut Treasury, 
    ctx: &TxContext
) {
    let sender = tx_context::sender(ctx);

    // Security: Only owner can migrate
    assert!(admin_registry.owner_address == sender, E_NOT_OWNER_ADMIN);

    // Version control: Prevent duplicate migrations
    assert!(admin_registry.version == 1, E_ALREADY_MIGRATED);

    // Apply upgrades to affected objects
    admin_registry.version = 2;
    admin_registry.max_admins = MAX_ADMINS_V2;  // 2 → 4 admins

    // Update other shared objects to maintain version consistency
    treasury.version = 2;
}

This migration specifically:

  • Validates authorization - only the original owner can perform migrations

  • Checks version state - prevents accidental double-migrations

  • Updates business logic - increases the admin limit from 2 to 4

  • Maintains consistency - updates all shared objects to the same version

The Upgrade Process in Practice

Let's walk through the complete upgrade workflow:

Step 1: Deploy Version 1

sui client publish --gas-budget 100000000

This creates your initial contract with:

  • AdminRegistry (version 1, max_admins = 2)

  • Treasury (version 1)

  • UpgradeCap (automatically created by Sui)

Step 2: Test Version 1 Functionality

# Add first admin (should work)
sui client call --package <V1_PACKAGE_ID> --module token --function called_by_two_entity \
  --args <ADMIN_REGISTRY_ID> <ADMIN_ADDRESS_1> --gas-budget 10000000

# Add second admin (should work) 
sui client call --package <V1_PACKAGE_ID> --module token --function called_by_two_entity \
  --args <ADMIN_REGISTRY_ID> <ADMIN_ADDRESS_2> --gas-budget 10000000

# Try third admin (should fail - exceeds limit)
sui client call --package <V1_PACKAGE_ID> --module token --function called_by_two_entity \
  --args <ADMIN_REGISTRY_ID> <ADMIN_ADDRESS_3> --gas-budget 10000000

Step 3: Prepare Version 2

Update your contract code:

const CURRENT_VERSION: u64 = 2;  // Increment version
// Keep all other code the same for this upgrade

Step 4: Execute the Upgrade

sui client upgrade --upgrade-capability <UPGRADE_CAP_ID> --gas-budget 100000000

This command:

  • Validates the new code is compatible with existing objects

  • Publishes the new package version

  • Updates the UpgradeCap to track the new version

  • Returns a new package ID

Step 5: Run Migration

sui client call --package <V2_PACKAGE_ID> --module token --function migrate \
  --args <ADMIN_REGISTRY_ID> <TREASURY_ID> --gas-budget 10000000

Step 6: Verify Upgrade Success

# This should now work (admin #3)
sui client call --package <V2_PACKAGE_ID> --module token --function called_by_two_entity \
  --args <ADMIN_REGISTRY_ID> <ADMIN_ADDRESS_3> --gas-budget 10000000

# And admin #4
sui client call --package <V2_PACKAGE_ID> --module token --function called_by_two_entity \
  --args <ADMIN_REGISTRY_ID> <ADMIN_ADDRESS_4> --gas-budget 10000000

# Try admin #5 (should fail - new limit is 4)
sui client call --package <V2_PACKAGE_ID> --module token --function called_by_two_entity \
  --args <ADMIN_REGISTRY_ID> <ADMIN_ADDRESS_5> --gas-budget 10000000

Understanding Package Coexistence

One of the most interesting aspects of Sui upgrades is that both old and new package versions continue to exist and can interact with the same objects:

# Both of these work immediately after upgrade (before migration)
sui client call --package <V1_PACKAGE_ID> --module token --function called_by_two_entity
sui client call --package <V2_PACKAGE_ID> --module token --function called_by_two_entity

However, after migration, version checks in your functions may cause V1 calls to fail:

// This check will cause V1 calls to fail after migration
assert!(admin_registry.version == 2, E_WRONG_VERSION);

Conclusion

Sui's upgrade system provides a powerful foundation for evolving smart contracts while maintaining user experience and data integrity. The key to successful upgrades lies in:

  1. Planning ahead with version tracking from day one

  2. Understanding the distinction between owned and shared objects

  3. Implementing robust migration functions that handle state transitions safely

  4. Testing thoroughly before production deployment

  5. Following security best practices throughout the upgrade lifecycle

The example we've built demonstrates these principles in action, showing how a simple admin limit change requires careful consideration of state management, access control, and version compatibility.

Resources