⚠️Blast API (blastapi.io) ends Oct 31. Migrate to Dwellir and skip Alchemy's expensive compute units.
Switch Today →
Skip to main content

Upgradability

Smart contract upgradability is a critical feature on Aptos that allows developers to fix bugs, add features, and improve performance while maintaining the same on-chain address. Move's upgrade system provides powerful flexibility with built-in safety checks to prevent breaking changes that could corrupt existing on-chain state or break dependent contracts.

Overview#

On Aptos, modules can be upgraded by publishing new versions to the same address. The blockchain enforces compatibility rules to ensure that upgrades don't break existing functionality or corrupt stored data. Upgrades can be configured with different policies ranging from fully immutable (no upgrades allowed) to arbitrary (any changes permitted), with compatibility checks as the recommended middle ground.

Upgrade Policies#

Aptos supports three upgrade policies that control what changes are permitted:

// In Move.toml or via CLI
[package]
name = "MyProject"
version = "1.0.0"
upgrade_policy = "compatible" // Options: immutable, compatible, arbitrary

Immutable: Once published, the module cannot be upgraded. This provides maximum security and predictability but eliminates flexibility to fix bugs or add features.

Compatible (Recommended): Upgrades must maintain backward compatibility. Public functions, struct layouts, and type signatures cannot change in breaking ways, but you can add new functions and internal improvements.

Arbitrary: Any changes are permitted including breaking changes to public APIs and struct layouts. Use with extreme caution as this can corrupt on-chain data.

Compatibility Rules#

When using the compatible upgrade policy, the following rules apply:

Allowed Changes#

  • Adding new functions (public, entry, or internal)
  • Adding new structs and types
  • Adding new fields to structs (with care)
  • Modifying internal function implementations
  • Improving gas efficiency
  • Adding new modules to the package

Prohibited Changes#

  • Removing public functions
  • Changing public function signatures (parameters or return types)
  • Removing or reordering existing struct fields
  • Changing struct abilities (key, store, copy, drop)
  • Removing or renaming modules
  • Changing friend declarations that break existing integrations

Upgrade Process#

# 1. Prepare the upgraded module
# Edit your Move code with compatible changes

# 2. Test the upgrade locally
aptos move test --package-dir .

# 3. Compile with upgrade check
aptos move compile --package-dir . --save-metadata

# 4. Deploy the upgrade
aptos move publish \
--package-dir . \
--named-addresses my_project=0xYOUR_ADDRESS \
--profile mainnet

# 5. Verify the upgrade
aptos move view \
--function-id 0xYOUR_ADDRESS::module::version \
--profile mainnet

Version Management#

Implement version tracking in your modules:

module 0x1::upgradeable_contract {
use std::signer;

const VERSION: u64 = 2; // Increment with each upgrade

struct Config has key {
version: u64,
admin: address,
// Other config fields
}

public entry fun initialize(account: &signer) {
move_to(account, Config {
version: VERSION,
admin: signer::address_of(account)
});
}

// Migration function for upgrades
public entry fun migrate(admin: &signer) acquires Config {
let config = borrow_global_mut<Config>(signer::address_of(admin));
assert!(config.version < VERSION, 1); // Already migrated

// Perform migration logic
if (config.version == 1) {
// Migrate from v1 to v2
config.version = VERSION;
};
}

#[view]
public fun get_version(addr: address): u64 acquires Config {
borrow_global<Config>(addr).version
}
}

Safe Struct Evolution#

When adding fields to structs, ensure backward compatibility:

// Version 1
struct UserProfile has key {
name: vector<u8>,
created_at: u64
}

// Version 2 - Safe addition with default handling
struct UserProfile has key {
name: vector<u8>,
created_at: u64,
avatar_url: Option<vector<u8>> // Use Option for new fields
}

// Migration helper
public fun migrate_profile(user: &signer) acquires UserProfile {
let profile = borrow_global_mut<UserProfile>(signer::address_of(user));
// Existing profiles automatically get None for new optional field
}

Real-World Use Cases#

  1. Bug Fixes: Quickly patch security vulnerabilities or logical errors in deployed contracts without changing addresses or disrupting users.

  2. Feature Additions: Add new functionality to existing protocols like new token types, additional governance mechanisms, or enhanced analytics.

  3. Performance Optimization: Upgrade contract implementations to use more efficient algorithms or data structures while maintaining the same external interface.

  4. Parameter Tuning: Adjust economic parameters, fee structures, or limits in DeFi protocols based on real-world usage and market conditions.

  5. Emergency Responses: Implement circuit breakers, pause functionality, or other emergency measures when security issues are discovered.

  6. Protocol Evolution: Gradually evolve complex protocols like DEXes or lending platforms by adding new features while maintaining backward compatibility with existing integrations.

Best Practices#

Always Use Compatible Mode: Unless you have specific requirements, use the compatible upgrade policy to balance flexibility with safety.

Version All Modules: Include version constants in all modules and expose them through view functions for transparency and debugging.

Test Upgrades Thoroughly: Test upgraded modules against existing on-chain state before deploying to mainnet. Use testnet to validate migration paths.

Implement Migration Functions: Provide explicit migration functions for users to upgrade their stored data when struct layouts change.

Document Breaking Changes: Even when using arbitrary mode, clearly document all changes and provide migration guides for users and integrators.

Use Multi-Sig for Upgrades: Require multiple signatures for upgrade transactions on production contracts to prevent unauthorized or accidental upgrades.

Gradual Rollout: For major upgrades, consider implementing feature flags that allow gradual activation after deployment.

Preserve Historical Versions: Keep records of all deployed versions with their code and deployment transactions for audit and recovery purposes.

Package Manager Pattern#

For complex upgrades, consider implementing a package manager:

module 0x1::package_manager {
struct PackageMetadata has key {
name: vector<u8>,
version: u64,
upgrade_number: u64,
upgrade_policy: u8,
source_digest: vector<u8>
}

public entry fun record_upgrade(
publisher: &signer,
version: u64,
digest: vector<u8>
) acquires PackageMetadata {
let addr = signer::address_of(publisher);
if (!exists<PackageMetadata>(addr)) {
move_to(publisher, PackageMetadata {
name: b"MyPackage",
version,
upgrade_number: 0,
upgrade_policy: 1, // compatible
source_digest: digest
});
} else {
let metadata = borrow_global_mut<PackageMetadata>(addr);
metadata.version = version;
metadata.upgrade_number = metadata.upgrade_number + 1;
metadata.source_digest = digest;
};
}
}