Updating Contracts
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:
- Short
- Full
# (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>
# (optional) If you don't have an account, create one
near account create-account sponsor-by-faucet-service somrnd.testnet autogenerate-new-keypair save-to-keychain network-config testnet create
# Deploy the contract
near contract deploy <accountId> use-file <route_to_wasm> without-init-call network-config testnet sign-with-keychain send
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
- 🦀 Rust
Loading...
How to Invoke Such Method?
- Near CLI (short)
- Near CLI (full)
- 🌐 JavaScript
# Load the contract's raw bytes
CONTRACT_BYTES=`cat ./path/to/wasm.wasm | base64`
# Call the update_contract method
near call <contract-account> update_contract "$CONTRACT_BYTES" --base64 --accountId <manager-account> --gas 300000000000000
# 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
// Load the contract's raw bytes
const code = fs.readFileSync("./path/to/wasm.wasm");
// Call the update_contract method
await wallet.callMethod({
contractId: guestBook,
method: "update_contract",
args: code,
gas: "300000000000000",
});
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
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 the following state:
- 🌐 Javascript
- 🦀 Rust
Loading...
Loading...
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:
- 🌐 Javascript
- 🦀 Rust
Loading...
Loading...
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
payments
vector saved in the state (from the previous contract) - The stored
PostedMessages
are missing thepayment
field (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
:
- 🌐 Javascript
- 🦀 Rust
Loading...
Loading...
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.
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_messages
and get_payments
will return the following results:
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"
| }
| ]
| ------------------------------------
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
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 migrate
method, we call clear()
method on payments vector in mutable old_state
struct. This method removes all elements from the collection.
You can follow a migration step by step in the
official migration example
Javascript migration example testfile can be found on here:
test-basic-updates.ava.js,
run by this command: pnpm run test:basic-update
in examples directory.