NEAR allows you to update deployed smart contracts while preserving their state. This enables you to fix bugs, add features, and improve contracts after deployment.
Contract updates are powerful but require careful planning. Improper updates can corrupt state or break functionality.
Overview
Updating a contract involves:
Deploying new contract code to an existing account
Migrating state if the data structure changed
Testing thoroughly before and after
GitHub Repository View the complete contract update example
Simple update (no state changes)
If your contract structure doesn’t change, updating is straightforward:
Deploy the update
near contract deploy < contract-accoun t > .testnet \
use-file ./target/near/contract.wasm \
without-init-call \
network-config testnet
Using without-init-call preserves existing state. The new code can access all previously stored data.
Verify the update
Test that existing functionality still works and new features are available: # Test existing function
near contract call-function as-transaction < contrac t > .testnet \
existing_method json-args '{}' \
prepaid-gas '30 TeraGas' \
attached-deposit '0 NEAR' \
network-config testnet
# Test new function
near contract call-function as-transaction < contrac t > .testnet \
new_method json-args '{}' \
prepaid-gas '30 TeraGas' \
attached-deposit '0 NEAR' \
network-config testnet
Update with state migration
When your contract structure changes, you need to migrate state:
Example scenario
Old Contract
New Contract
#[near(contract_state)]
pub struct OldContract {
owner : AccountId ,
counter : u64 ,
}
#[near(contract_state)]
pub struct NewContract {
owner : AccountId ,
counter : u128 , // Changed from u64
created_at : u64 , // New field
}
Migration method
Implement a migration function:
use near_sdk :: {near, env};
#[near(contract_state)]
pub struct OldContract {
owner : AccountId ,
counter : u64 ,
}
#[near(contract_state)]
pub struct NewContract {
owner : AccountId ,
counter : u128 ,
created_at : u64 ,
}
#[near]
impl NewContract {
#[init(ignore_state)]
#[private]
pub fn migrate () -> Self {
// Read old state
let old : OldContract = env :: state_read ()
. expect ( "Failed to read old state" );
// Return new state with migrated data
Self {
owner : old . owner,
counter : old . counter as u128 ,
created_at : env :: block_timestamp (),
}
}
}
import { NearBindgen , near , initialize } from 'near-sdk-js' ;
@ NearBindgen ({ requireInit: false })
class NewContract {
owner : string ;
counter : bigint ;
created_at : bigint ;
@ initialize ({ privateFunction: true })
migrate () {
// Read old state
const state = JSON . parse ( near . storageRead ( "STATE" ));
// Migrate data
this . owner = state . owner ;
this . counter = BigInt ( state . counter );
this . created_at = near . blockTimestamp ();
}
}
Executing migration
Deploy the new contract code
near contract deploy < contrac t > .testnet \
use-file ./target/near/contract.wasm \
without-init-call \
network-config testnet
Call the migration method
near contract call-function as-transaction < contrac t > .testnet \
migrate json-args '{}' \
prepaid-gas '300 TeraGas' \
attached-deposit '0 NEAR' \
network-config testnet
Mark migration methods as #[private] so only the contract account can call them.
Verify the migration
Check that state was migrated correctly: near contract call-function as-read-only < contrac t > .testnet \
get_counter json-args '{}' \
network-config testnet
Migration patterns
Use Option<T> for new fields or provide defaults: #[near(contract_state)]
pub struct Contract {
owner : AccountId ,
new_field : Option < String >, // Can be None for old state
}
impl Default for Contract {
fn default () -> Self {
Self {
owner : env :: current_account_id (),
new_field : None ,
}
}
}
Simply don’t include removed fields in the new struct. Old data is ignored: // Old contract had 'deprecated_field', new one doesn't
#[near(contract_state)]
pub struct NewContract {
owner : AccountId ,
// deprecated_field removed
}
Read old state, convert types, write new state: #[init(ignore_state)]
#[private]
pub fn migrate () -> Self {
#[near(serializers = [borsh])]
struct OldContract {
counter : u64 ,
}
let old : OldContract = env :: state_read () . unwrap ();
Self {
counter : old . counter as u128 ,
}
}
For complex changes, implement multi-step migrations: #[near]
impl Contract {
#[private]
pub fn migrate_step_1 ( & mut self ) {
// First migration step
}
#[private]
pub fn migrate_step_2 ( & mut self ) {
// Second migration step
}
}
Best practices
Test thoroughly Always test migrations on testnet with production-like data before updating mainnet
Version your contracts Track contract versions in state to enable conditional migrations
Make migrations private Only the contract should be able to trigger migrations
Plan for rollback Keep old contract code available in case you need to redeploy it
Testing migrations
#[cfg(test)]
mod tests {
use super ::* ;
use near_sdk :: test_utils :: VMContextBuilder ;
use near_sdk :: testing_env;
#[test]
fn test_migration () {
let context = VMContextBuilder :: new () . build ();
testing_env! ( context );
// Create old contract state
let old = OldContract {
owner : "alice.near" . parse () . unwrap (),
counter : 42 ,
};
env :: state_write ( & old );
// Perform migration
let new = NewContract :: migrate ();
// Verify migration
assert_eq! ( new . owner, old . owner);
assert_eq! ( new . counter, 42 u128 );
assert! ( new . created_at > 0 );
}
}
import { Worker } from 'near-workspaces' ;
test ( 'migration preserves data' , async () => {
const worker = await Worker . init ();
const root = worker . rootAccount ;
// Deploy old contract
const contract = await root . devDeploy ( 'old_contract.wasm' );
await contract . call ( 'init' , { owner: root . accountId });
await contract . call ( 'increment' , {});
// Get old counter value
const oldCounter = await contract . view ( 'get_counter' , {});
// Deploy new contract
await contract . deploy ( 'new_contract.wasm' );
// Run migration
await contract . call ( 'migrate' , {});
// Verify data preserved
const newCounter = await contract . view ( 'get_counter' , {});
expect ( newCounter ). toBe ( oldCounter );
});
Versioning contracts
Track versions to enable smart migrations:
#[near(contract_state)]
pub struct Contract {
version : u32 ,
// other fields...
}
#[near]
impl Contract {
#[init(ignore_state)]
#[private]
pub fn migrate () -> Self {
let version : u32 = env :: storage_read ( b"version" )
. and_then ( | v | v . try_into () . ok ())
. unwrap_or ( 0 );
match version {
0 => Self :: migrate_v0_to_v1 (),
1 => Self :: migrate_v1_to_v2 (),
_ => env :: panic_str ( "Unknown version" ),
}
}
}
Next steps
Contract security Secure your contract updates
Testing guide Comprehensive testing strategies
Lock contracts Make contracts immutable when ready
Factory pattern Deploy versioned contracts with factories