Move.

帖子

分享您的知识。

Sato$hii.
Aug 29, 2025
专家问答

How can I design upgradable modules (logic upgrade)?

How can I design upgradable modules (logic upgrade) while preserving on-chain invariants and preventing honest-state corruption?

  • Move CLI
  • Move
  • Smart Contract
  • Move Script
0
2
分享
评论
.

答案

2
Kurosakisui.
Aug 29 2025, 21:29

By upgrading the change code but must preserve storage invariants (no data loss, no invariant breaks). Move modules are usually immutable after publishing; upgrades are typically implemented by proxy patterns, limited-versioned storage, or migration scripts that carefully transform on-chain state.

Strategy

  1. Use a proxy module that holds canonical data (resources) and delegates logic calls to an implementation module address/version.
  2. Store an explicit version and migration_state resource to coordinate safe migrations.
  3. Provide migrate_vN_to_vN+1 functions in the new implementation that the proxy calls; make migration functions idempotent and proof-checked via spec invariants.
  4. Use capability checks so only a governance/admin address can upgrade or run migrations.
  5. Write spec lemmas in the new module that prove the migration preserves invariants (e.g., balances sum, non-negativity).

Working-style code (proxy + implementation + migration)

// Proxy module at 0xA::proxy_token — storage is here and persistent across upgrades.
module 0xA::proxy_token {
    use std::signer;
    use std::vector;
    use std::option;

    // Admin who can upgrade / trigger migrations
    struct Admin has key { addr: address }

    // Persistent storage
    struct TokenStore has key {
        balances: vector<(address, u128)>, // simple illustrative mapping
        total_supply: u128,
    }

    // Which implementation to delegate to (module id + version marker)
    struct ImplRef has key {
        impl_addr: address, // address of implementation module
        version: u64,
    }

    public fun init(admin: &signer, impl_addr: address) {
        move_to(admin, Admin { addr: signer::address_of(admin) });
        move_to(admin, TokenStore { balances: vector::empty(), total_supply: 0u128 });
        move_to(admin, ImplRef { impl_addr, version: 1u64 });
    }

    /////////////////////////////////////
    // Delegation entrypoints (very thin)
    /////////////////////////////////////
    public fun mint(admin: &signer, to: address, amt: u128) acquires ImplRef, TokenStore {
        // authorization
        let impl = borrow_global<ImplRef>(signer::address_of(admin));
        // delegate: call implementation's mint function by using a known interface.
        // In Move you call by module path; proxy knows the impl address.
        // Example: call 0xB::impl_v1::mint_impl(...) — but proxy must import the impl module at compile time.
        // To support multiple impls you'd switch on impl.impl_addr and call the correct function.
        // Here we show the pattern for a single friend impl at 0xB.
        0xB::impl_v1::mint_impl(signer::address_of(admin), to, amt);
    }

    public fun set_impl(admin: &signer, new_impl_addr: address, new_version: u64) acquires ImplRef, Admin {
        let a = borrow_global<Admin>(signer::address_of(admin));
        assert!(a.addr == signer::address_of(admin), 1);
        let r = borrow_global_mut<ImplRef>(signer::address_of(admin));
        r.impl_addr = new_impl_addr;
        r.version = new_version;
    }

    /////////////////////////////////////
    // Storage access helpers (implementation calls these)
    /////////////////////////////////////
    public friend 0xB::impl_v1;
    public fun get_store(addr: address): &mut TokenStore acquires TokenStore {
        borrow_global_mut<TokenStore>(addr)
    }
}
// Implementation v1 (0xB::impl_v1) — contains logic and migration entrypoints
module 0xB::impl_v1 {
    use std::signer;
    use std::vector;

    // Mint implementation will directly update storage owned by proxy (friend).
    public fun mint_impl(proxy_admin: address, to: address, amt: u128) {
        // Here we assume proxy exposes friend accessors and the storage is stored under admin address
        let store = 0xA::proxy_token::get_store(proxy_admin);
        // very naive balance update for demo; use map in prod.
        vector::push_back(&mut store.balances, (to, amt));
        store.total_supply = store.total_supply + amt;
    }

    ////////////////////////////
    // Migration entrypoint for v1 -> v2
    ////////////////////////////
    // Suppose v2 changes representation: we need an idempotent migration function.
    public fun migrate_v1_to_v2(proxy_admin: address) {
        // 1) Read old store
        let store = 0xA::proxy_token::get_store(proxy_admin);
        // 2) Create any auxiliary storage needed by v2, copy state carefully.
        // For safety: make the migration idempotent by checking a migration marker in proxy storage
        // (not shown) or by designing migration to be a no-op if already applied.
        // Example check (pseudo):
        // if (!migrated) { perform migration; mark migrated; }
    }
}

How this preserves invariants

  • Storage never moves addresses — data stays under proxy_token::TokenStore so external references to that resource remain valid after upgrade.
  • New implementation provides migrate_* functions that are explicit and idempotent; the proxy admin triggers them and can verify results.
  • spec blocks in impl_v2 should assert total_supply equals sum(balances) before and after migration — this is how the Move Prover can be used to check migration correctness.

Practical recommendations

  • Keep storage layout stable where possible. If layout must change, migrate piecewise and leave a rollback or checkpoint.
  • Use small, well-tested migration scripts and unit tests (#[test]) that run init → prepopulate → migrate → assert invariants.
  • Limit upgrade privileges to a multisig/governance contract to reduce centralized risk.

2
评论
.
Michael Ace.
Aug 29 2025, 21:58

Instead of a single proxy that delegates all logic, keep versioned storage containers and deploy new implementation modules side-by-side. Each implementation reads from the canonical storage container but only if a migration gate (a small VersionGate resource) has advanced to permit that code-path. Migrations are performed by a controlled migrator that copies transforms from Vn storage to Vn+1 storage while leaving Vn intact until the migration completes and is audited. This gives:

  • Safer rollbacks (you can keep old storage while testing new storage).
  • Explicit migration checkpoints and auditability.
  • Minimized logic in proxy; implementations operate on stable, versioned data shapes.

How it defends invariants

  • Storage containers are typed per-version (StoreV1, StoreV2) — code never reads a layout it wasn't compiled against.
  • Migration functions are idempotent and gated by a MigrationMarker that records migration progress and can’t be advanced unless the migrator holds a capability.
  • The Move Prover can reason about each storage version independently; spec lemmas can assert that sum(balances_v1) == sum(balances_v2) after migration.

Working-style Move sketch

module 0xA::versioned_token {
    use std::signer;
    use std::vector;

    /// Admin who controls upgrades
    struct Admin has key { addr: address }

    /// Migration capability; linear resource only Admin has
    struct MigratorCap has key {}

    /// Version gate: which version is active. Stored under admin addr.
    struct VersionGate has key { active: u64, in_migration: bool }

    /// Storage for v1 layout
    struct StoreV1 has key {
        balances: vector<(address, u128)>,
        total: u128
    }

    /// Storage for v2 layout (example: switch to per-account meta)
    struct StoreV2 has key {
        // different layout: per-account records hashed in vectors for demo
        balances_v2: vector<(address, u128)>,
        total_v2: u128
    }

    public fun init(admin: &signer) {
        move_to(admin, Admin { addr: signer::address_of(admin) });
        move_to(admin, MigratorCap {});
        move_to(admin, VersionGate { active: 1u64, in_migration: false });
        move_to(admin, StoreV1 { balances: vector::empty(), total: 0u128 });
        // v2 storage not created until migration starts
    }

    ////////////////////////////////////////////////////////////
    // Implementations call these helpers; behavior chosen by version gate
    ////////////////////////////////////////////////////////////

    public fun balance_of(admin_addr: address, owner: address): u128 acquires VersionGate, StoreV1, StoreV2 {
        let gate = borrow_global<VersionGate>(admin_addr);
        if (gate.active == 1u64) {
            let s = borrow_global<StoreV1>(admin_addr);
            // naive lookup for demo
            let i = 0;
            while (i < vector::length(&s.balances)) {
                let pair = *vector::borrow(&s.balances, i);
                if (pair.0 == owner) { return pair.1; }
                i = i + 1;
            }
            0u128
        } else {
            let s2 = borrow_global<StoreV2>(admin_addr);
            // different lookup logic
            let i = 0;
            while (i < vector::length(&s2.balances_v2)) {
                let pair = *vector::borrow(&s2.balances_v2, i);
                if (pair.0 == owner) { return pair.1; }
                i = i + 1;
            }
            0u128
        }
    }

    ////////////////////////////////////////////////////////////
    // Migration flow: admin triggers `start_migration`, migrator executes idempotent steps,
    // then `finalize_migration` flips gate to new version
    ////////////////////////////////////////////////////////////

    public fun start_migration(admin: &signer) acquires VersionGate, MigratorCap {
        let g = borrow_global_mut<VersionGate>(signer::address_of(admin));
        assert!(!g.in_migration, 1);
        g.in_migration = true;
        // create v2 store if not exists
        if (!exists<StoreV2>(signer::address_of(admin))) {
            move_to(admin, StoreV2 { balances_v2: vector::empty(), total_v2: 0u128 });
        }
    }

    // Migrator runs this repeatedly to copy batches; function is idempotent per-account.
    public fun migrate_account_batch(admin: &signer, start_idx: u64, batch_size: u64) acquires StoreV1, StoreV2, VersionGate {
        let g = borrow_global<VersionGate>(signer::address_of(admin));
        assert!(g.in_migration, 2);

        let s1 = borrow_global_mut<StoreV1>(signer::address_of(admin));
        let s2 = borrow_global_mut<StoreV2>(signer::address_of(admin));

        let len = vector::length(&s1.balances) as u64;
        let mut i = start_idx;
        while (i < start_idx + batch_size && i < len) {
            let pair = *vector::borrow(&s1.balances, (i as u64));
            // Check if already migrated by searching s2; if present, skip (idempotent)
            if (!exists_in_vec(&s2.balances_v2, pair.0)) {
                vector::push_back(&mut s2.balances_v2, pair);
                s2.total_v2 = s2.total_v2 + pair.1;
            }
            i = i + 1;
        }
    }

    public fun finalize_migration(admin: &signer, new_version: u64) acquires VersionGate {
        let g = borrow_global_mut<VersionGate>(signer::address_of(admin));
        assert!(g.in_migration, 3);
        g.active = new_version;
        g.in_migration = false;
    }

    fun exists_in_vec(v: &vector<(address, u128)>, a: address): bool {
        let i = 0;
        while (i < vector::length(v)) {
            if ((*vector::borrow(v, i)).0 == a) { return true; }
            i = i + 1;
        }
        false
    }
}

Trade-offs & tips

  • Pros: safe, auditable migrations; easy rollback since old storage remains untouched until finalize.
  • Cons: requires extra storage and migration work (batching), but acceptable for large systems.
  • Use spec to prove migration preserves total across versions.
  • On Sui, replace account storage with objects; you can keep versioned object types and migrate object-by-object.

0
评论
.

你知道答案吗?

请登录并分享。

Move is an executable bytecode language used to implement custom transactions and smart contracts.

242帖子541答案
Sui.X.Peera.

赚取你的 1000 Sui 份额

获取声誉积分,并因帮助 Sui 社区成长而获得奖励。

奖励活动八月