Beitrag
Teile dein Wissen.
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
Antworten
2By 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
- Use a proxy module that holds canonical data (resources) and delegates logic calls to an implementation module address/version.
- Store an explicit
version
andmigration_state
resource to coordinate safe migrations. - Provide
migrate_vN_to_vN+1
functions in the new implementation that the proxy calls; make migration functions idempotent and proof-checked viaspec
invariants. - Use capability checks so only a governance/admin address can upgrade or run migrations.
- 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 inimpl_v2
should asserttotal_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.
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 thatsum(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 preservestotal
across versions. - On Sui, replace account storage with objects; you can keep versioned object types and migrate object-by-object.
Weißt du die Antwort?
Bitte melde dich an und teile sie.
Move is an executable bytecode language used to implement custom transactions and smart contracts.
Verdiene deinen Anteil an 1000 Sui
Sammle Reputationspunkte und erhalte Belohnungen für deine Hilfe beim Wachstum der Sui-Community.

- ... SUIMatthardy+2095
- ... SUIacher+1666
- ... SUIjakodelarin+1092
- ... SUIChubbycheeks +1081
- ... SUITucker+1047
- ... SUIKurosakisui+1034
- ... SUIzerus+890