Updating Contracts
Learn how to upgrade NEAR smart contracts safely, including programmatic updates, migration strategies, and best practices for contract versioning.
Learn how to update NEAR smart contracts, both through tools like NEAR CLI and programmatically. Understand the implications of state migration when changing contract logic.
NEAR accounts separate their logic (contract's code) from their state (storage), allowing the code to be changed.
Contract's can be updated in two ways:
- Through tools such as NEAR CLI or the NEAR API (if you hold the account's full access key).
- Programmatically, by implementing a method that takes the new code and deploys it.
Updating Through Tools
Simply re-deploy another contract using your preferred tool, for example, using NEAR CLI:
# (optional) If you don't have an account, create one
near create-account <account-id> --useFaucet
# Deploy the contract
near deploy <account-id> <wasm-file>
Programmatic Update
A smart contract can also update itself by implementing a method that:
- Takes the new wasm contract as input
- Creates a Promise to deploy it on itself
pub fn update_contract(&self) -> Promise {
// Check the caller is authorized to update the code
assert!(
env::predecessor_account_id() == self.manager,
"Only the manager can update the code"
);
// Receive the code directly from the input to avoid the
// GAS overhead of deserializing parameters
let code = env::input().expect("Error: No input").to_vec();
// Deploy the contract on self
Promise::new(env::current_account_id())
.deploy_contract(code)
.function_call(
"migrate".to_string(),
NO_ARGS,
NearToken::from_near(0),
CALL_GAS,
)
.as_return()
}
How to Invoke Such Method?
[tab] CLI
# Call the update_contract method near contract call-function as-transaction <contract-account> update_contract file-args </path/to/wasm.wasm> prepaid-gas '300.0 Tgas' attached-deposit '0 NEAR' sign-as <manager-account> network-config testnet sign-with-keychain send
[!TIP]
DAO Factories
This is how DAO factories update their contracts
Migrating the State
Since the account's logic (smart contract) is separated from the account's state (storage), the account's state persists when re-deploying a contract.
Because of this, adding methods or modifying existing ones will yield no problems.
However, deploying a contract that modifies or removes structures stored in
the state will raise an error: Cannot deserialize the contract state, in which
case you can choose to:
- Use a different account
- Rollback to the previous contract code
- Add a method to migrate the contract's state
The Migration Method
If you have no option but to migrate the state, then you need to implement a method that:
- Reads the current state of the contract
- Applies different functions to transform it into the new state
- Returns the new state
[!TIP]
DAO Update
This is how DAOs update themselves
Example: Guest Book Migration
Imagine you have a Guest Book where you store messages, and the users can pay
for such messages to be "premium". You keep track of the messages and payments
using two near_sdk::store::Vector collections in the following state:
const MESSAGES_PREFIX: &[u8] = b"m";
const PAYMENTS_PREFIX: &[u8] = b"p";
#[near(serializers=[json, borsh])]
#[derive(Clone)]
pub struct PostedMessage {
pub premium: bool,
pub sender: AccountId,
pub text: String,
}
#[near(contract_state)]
pub struct GuestBook {
messages: Vector<PostedMessage>,
payments: Vector<NearToken>,
}
Update Contract
At some point you realize that you could keep track of the payments inside of
the PostedMessage itself, so you change the contract to:
const MESSAGES_PREFIX: &[u8] = b"m";
#[near(serializers=[json, borsh])]
#[derive(Clone)]
pub struct PostedMessage {
pub payment: NearToken,
pub premium: bool,
pub sender: AccountId,
pub text: String,
}
#[near(contract_state)]
pub struct GuestBook {
messages: Vector<PostedMessage>,
}
Incompatible States
If you deploy the update into an initialized account the contract will fail to deserialize the account's state, because:
- There is an extra
paymentsvector saved in the state (from the previous contract) - The stored
PostedMessagesare missing thepaymentfield (as in the previous contract)
Migrating the State
To fix the problem, you need to implement a method that goes through the old
state, removes the payments vector and adds the information to the
PostedMessages:
#[near(serializers=[borsh])]
pub struct OldPostedMessage {
pub premium: bool,
pub sender: AccountId,
pub text: String,
}
#[near(serializers=[borsh])]
pub struct OldState {
messages: Vector<OldPostedMessage>,
payments: Vector<NearToken>,
}
#[near]
impl GuestBook {
#[private]
#[init(ignore_state)]
pub fn migrate() -> Self {
// retrieve the current state from the contract
let mut old_state: OldState = env::state_read().expect("failed");
// new messages vector to hold the migrated messages
let mut new_messages: Vector<PostedMessage> = Vector::new(MESSAGES_PREFIX);
// iterate through the state migrating it to the new version
for (idx, posted) in old_state.messages.iter().enumerate() {
// get the payment and remove it from the old state payments vector so it won't be left in the new state
let payment = old_state
.payments
.get(idx as u32)
.expect("failed to get payment")
.clone();
// push the new message to the new messages vector
new_messages.push(PostedMessage {
payment,
premium: posted.premium,
sender: posted.sender.clone(),
text: posted.text.clone(),
})
}
// remove the payments from the old state
old_state.payments.clear();
// return the new state
Self {
messages: new_messages,
}
}
}
Notice that migrate is actually an initialization method that ignores the existing state (#[init(ignore_state)]), thus being able to execute and rewrite the state.
[accordion] Why we should remove old structures from the state?
To understand why we should remove old structures from the state let's take a look to how the data is stored.
For example, if the old version of the contract stores two messages with payments according methods
get_messagesandget_paymentswill return the following results:[accordion] get_messags result
INFO --- Result ------------------------- | [ | { | "premium": false, | "sender": "test-ac-1719933221123-3.testnet", | "text": "Hello" | }, | { | "premium": false, | "sender": "test-ac-1719933221123-3.testnet", | "text": "Hello" | } | ] | ------------------------------------[accordion] get_payments result
INFO --- Result ------------------------- | [ | "10000000000000000000000", | "10000000000000000000000" | ] | ------------------------------------But if we take a look at the storage as text using following command, we will see that each payment is stored under its own key started with
p\prefix.near contract view-storage <CONTRACT_ID> all as-text network-config testnet now[accordion] Storage as text result
INFO Contract state (values): | key: STATE | value: \x02\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00m\x02\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00p | -------------------------------- | key: m\x00\x00\x00\x00\x00\x00\x00\x00 | value: \x00\x1f\x00\x00\x00test-ac-1719933221123-3.testnet\x05\x00\x00\x00Hello | -------------------------------- | key: m\x01\x00\x00\x00\x00\x00\x00\x00 | value: \x00\x1f\x00\x00\x00test-ac-1719933221123-3.testnet\x05\x00\x00\x00Hello | -------------------------------- | key: p\x00\x00\x00\x00\x00\x00\x00\x00 | value: \x00\x00@\xb2\xba\xc9\xe0\x19\x1e\x02\x00\x00\x00\x00\x00\x00 | -------------------------------- | key: p\x01\x00\x00\x00\x00\x00\x00\x00 | value: \x00\x00@\xb2\xba\xc9\xe0\x19\x1e\x02\x00\x00\x00\x00\x00\x00 | --------------------------------That means that while migrating the state to a new version we need not only change the messages structure, but also remove all payments related keys from the state. Otherwise, the old keys will simply stay behind being orphan, still occupying space.
To remove them in
migratemethod, we callclear()method on payments vector in mutableold_statestruct. This method removes all elements from the collection.
[!TIP]
You can follow a migration step by step in the official migration example
State versioning
State versioning lets old and new representations coexist. Instead of rewriting every stored value during one migration, store an enum whose variants represent each supported version:
#[derive(BorshDeserialize, BorshSerialize)]
pub enum VersionedPostedMessage {
V1(PostedMessageV1),
V2(PostedMessageV2),
}
When the contract reads an older variant, it can convert it to the latest representation before applying new logic. This makes future changes easier, but it also means the contract must continue understanding every version that can remain in storage.
See the state-versioning example for a complete implementation.
Locking a contract account
You can prevent external actors from upgrading a contract by removing every full-access key from its account. Once the keys are removed, nobody can sign transactions in the account's name to deploy new code or transfer its balance.
First, list the account's keys and identify every full-access key:
near list-keys <contract-account>
Then remove each full-access key:
near delete-key <contract-account> '<public-key>'
[!WARNING]
Removing all full-access keys cannot be undone through an externally signed transaction. Confirm that you have the intended upgrade and recovery design before locking the account.
A locked contract can still upgrade itself if its current code exposes an authorized programmatic update method. If the contract must be permanently immutable, do not include such a method.
If the account uses a global contract by account ID, removing its keys does not lock its code. The owner of the global contract can deploy a new version at any time, and the locked account then runs the new code. To lock the code, deploy it to the account directly, or use a global contract by code hash.
Testing Upgrades
The migration example includes tests for basic migrations, versioned state, and manager-authorized updates. From the repository root, run:
cargo install cargo-near --version 0.21.1 --locked --no-default-features
cargo test --workspace --locked
Each test builds the base and updated contracts from source, deploys them to a local near-sandbox node, and sends transactions using near-api. The assertions check that messages, senders, premium flags, and payments survive the upgrade.