Near Learn

Upgrades and state migrations

NEAR contract upgrades: redeploying keeps old state, layout changes need a migrate method, and full-access keys decide who can change your code.

Advanced6 min read3-question check

On NEAR, code and state live on the same account, but separately. Deploying new Wasm replaces the code and leaves the state exactly as it was — there is no proxy pattern to learn. That makes upgrades easy, and makes two mistakes easy too: changing the state layout without migrating it, and leaving upgrade power with whoever happens to hold a key.

The bug#

Vulnerable — v2 adds a field and is deployed over v1 state with no migration
Rust
// v1 (deployed, holds real data)
// pub struct Guestbook { owner: AccountId, messages: Vector<String> }

// v2: a new field in the root struct
#[near(contract_state)]
#[derive(PanicOnDefault)]
pub struct Guestbook {
    owner: AccountId,
    messages: Vector<String>,
    paused: bool, // BUG: the stored bytes don't contain this field
}

// deploy v2 -> every method that loads state now panics while
// deserializing it, including the owner's admin methods.

The fix#

Fixed — read the old layout, write the new one in a private migrate method
Rust
use near_sdk::store::Vector;
use near_sdk::{env, near, AccountId, PanicOnDefault};

// the v1 layout, kept only to decode the existing state
#[near(serializers = [borsh])]
pub struct OldGuestbook {
    owner: AccountId,
    messages: Vector<String>,
}

#[near(contract_state)]
#[derive(PanicOnDefault)]
pub struct Guestbook {
    owner: AccountId,
    messages: Vector<String>, // same storage prefix, so the data carries over
    paused: bool,
}

#[near]
impl Guestbook {
    #[private]             // only the contract account itself can run it
    #[init(ignore_state)]  // allowed to run even though state exists
    pub fn migrate() -> Self {
        let old: OldGuestbook = env::state_read()
            .unwrap_or_else(|| env::panic_str("failed to read old state"));
        Self { owner: old.owner, messages: old.messages, paused: false }
    }
}
Deploy and migrate in one transaction (near-cli-rs), so there is no window with broken state
Shell
near contract deploy guestbook.near \
  use-file ./target/near/guestbook.wasm \
  with-init-call migrate json-args '{}' \
  prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' \
  network-config mainnet sign-with-keychain send

Who can upgrade?#

SetupWho can change the codeUse when
Full-access key on the contract accountWhoever holds that keyEarly development only
DAO / multisig controls upgradesThe DAO or multisig, via proposals (e.g. it holds the key, or calls a guarded self-upgrade method)Production contracts holding user funds
No full-access keys, no upgrade method (locked)NobodyYou want immutability — test migrations first
Verifiability (NEP-330)Publishes source + build info via a contract_source_metadata viewAlways — lets users check deployed Wasm matches the source
Upgrade authority options

Check yourself

3 questions · progress saved in this browser

  1. 1.You redeploy a contract whose root struct gained a new field, without any migration. What happens?
  2. 2.Why is a migrate method marked #[init(ignore_state)] and #[private]?
  3. 3.A team says its contract is “immutable”, but the contract account still has a full-access key. Is that accurate?