- Docs
- Smart Contracts
- Migration
Migration
Migration refers to the process of updating or replacing the code of an existing smart contract, and in some cases, the modification of state data.
CosmWasm has made contract migration a seamless experience. During contract instantiation, there's an optional admin field that can be set. If this field is not specified, the contract becomes immutable. However, if it's assigned to an external account or governance contract, that account can initiate the migration. The admin can also transfer ownership to another account or make the contract completely immutable after some time by setting the admin field to an empty value.
This is where the CW2 specification comes into play. It specifies a special Singleton that contains the contract's name and version information to be stored by all CW2-compliant contracts during instantiation. When the migration function is invoked, the newly created contract can access this information to determine if it is compatible with migrating from the previous contract.
The CW2 Specification provides a set_contract_version function that should be utilized when instantiating a new contract through the instantiate function to store the contract's versioning information. For the migrating contract, it is important to use the set_contract_version function as part of the migration logic within the migrate(...) function, rather than the instantiate function, to update the contract versioning during the migration process:
const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME");const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");#[cfg_attr(not(feature = "library"), entry_point)]pub fn instantiate(deps: DepsMut, env: Env, info: MessageInfo, msg: InstantiateMsg) -> Response { // Use CW2 to set the contract version, this is needed for migrations set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;}
For the migrating contract, in addition to using set_contract_version, you can also utilize the get_contract_version function to determine the previous version of the contract. It is important to ensure, for example, that the update is only performed if the version being upgraded is later than the original contract version:
use semver::Version;// Migrate contract if version is lower than current version#[entry_point]pub fn migrate(deps: DepsMut, _env: Env, _msg: Empty) -> Result<Response, ContractError> { let version: Version = CONTRACT_VERSION.parse()?; let storage_version: Version = get_contract_version(deps.storage)?.version.parse()?; if storage_version < version { set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; // If state structure changed in any contract version in the way migration is needed, it // should occur here } Ok(Response::new())}
Both the set_contract_version and get_contract_version methods operate on an Item data structure from cw_storage_plus, which manages this information:
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]pub struct ContractVersion { /// contract is a globally unique identifier for the contract. /// it should build off standard namespacing for whichever language it is in, /// and prefix it with the registry we use. /// For rust we prefix with `crates.io:`, to give us eg. `crates.io:cw20-base` pub contract: String, /// version is any string that this implementation knows. It may be simple counter "1", "2". /// or semantic version on release tags "v0.7.0", or some custom feature flag list. /// the only code that needs to understand the version parsing is code that knows how to /// migrate from the given contract (and is tied to it's implementation somehow) pub version: String,}
Thus, a serialized example may appear as follows:
{ "contract": "crates.io:cw20-base", "version": "v0.1.0"}
Setting up a contract for migrations
Performing a contract migration involves three steps. Firstly, write a newer version of the contract you wish to update. Secondly, store the new code on chain, but don't instantiate it. Thirdly, utilize a dedicated MsgMigrateContract transaction to point the old contract towards the new code, and the migration is complete. See Execute a migrate transaction below on how to execute a migration transaction using either archwayd, Archway Developer CLI or arch3.js.
During the migration process, the migrate function defined in the new contract is executed and not the migrate function from the old code and therefore it is necessary for the new contract code to have a migrate function defined and properly exported as an entry_point:
use cosmwasm_std::{ Empty, Env, DepsMut, Response,};use crate::error::ContractError;use cosmwasm_std::entry_point;#[cfg_attr(not(feature = "library"), entry_point)]pub fn migrate(deps: DepsMut, _env: Env, _msg: Empty) -> Result<Response, ContractError> {}
The migrate function provides the ability to make any desired granular changes to the State, similar to a database migration.
If the migrate function returns an error, the transaction will abort, all State changes will be reverted, and the migration will not be carried out.
Below are several migration variations to give an idea of some common use cases.
Basic contract migration
This migration might be the most frequently encountered. It merely replaces the code of a contract. However, safety checks are not implemented as there is no cw2::set_contract_version check being done.
use cosmwasm_std::{ Env, DepsMut, Response };use crate::error::ContractError;use crate::msg::{ MigrateMsg };use cosmwasm_std::entry_point;const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME");const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");#[entry_point]pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result<Response, ContractError> { // No state migrations performed, just returned a Response Ok(Response::default())}
Migration by code version and contract name
This migration is a more comprehensive example. The migrate function ensures:
- Migrating from the same type of contract by verifying its name
- Upgrading from an older version of the contract by checking its version
const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME");const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");#[entry_point]pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result<Response, ContractError> { let ver = cw2::get_contract_version(deps.storage)?; // ensure we are migrating from a compatible contract if ver.contract != CONTRACT_NAME { return Err(StdError::generic_err("Can only upgrade from same contract type").into()); } // note: it's better to do a proper semver comparison, but a string comparison *usually* works #[allow(clippy::cmp_owned)] if ver.version >= CONTRACT_VERSION { return Err(StdError::generic_err("Cannot upgrade from a newer contract version").into()); } // set the new version cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; // do any required state migrations... Ok(Response::default())}
Migrate using semver comparison
This migration uses Semver instead of a String comparison.
const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME");const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");#[entry_point]pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result<Response, ContractError> { let version: Version = CONTRACT_VERSION.parse()?; let storage_version: Version = get_contract_version(deps.storage)?.version.parse()?; if storage_version < version { set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; // If state structure changed in any contract version in the way migration is needed, it // should occur here } Ok(Response::default())}
This example uses Semver to help with version checks. You would also need to add the semver dependency to your cargo dependencies:
[dependencies]semver = "1"
You can also implement Semver custom errors:
#[derive(Error, Debug, PartialEq)]pub enum ContractError { #[error("Semver parsing error: {0}")] SemVer(String),}impl From<semver::Error> for ContractError { fn from(err: semver::Error) -> Self { Self::SemVer(err.to_string()) }}
Using migrate to update otherwise immutable state
This example demonstrates how a migration can be used to update a value that is typically immutable. This feature allows for changing the value only during a migration if necessary.
#[entry_point]pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result<Response, HackError> { let data = deps .storage .get(CONFIG_KEY) .ok_or_else(|| StdError::not_found("State"))?; let mut config: State = from_slice(&data)?; config.verifier = deps.api.addr_validate(&msg.verifier)?; deps.storage.set(CONFIG_KEY, &to_vec(&config)?); Ok(Response::default())}
In the above example, our MigrateMsg has a verifier field which contains the new value for our contract's verifier field located in the State. Provided your contract does not also expose an UpdateState or something like UpdateVerifier ExecuteMsgs, then this provides the only method to change the verifier value.
Using migrations to 'burn' a contract
Migrations can also be used to abandon an old contract and erase its state. This has various applications, but if needed, an example can be found here:
#[entry_point]pub fn migrate(deps: DepsMut, env: Env, msg: MigrateMsg) -> StdResult<Response> { // delete all state let keys: Vec<_> = deps .storage .range(None, None, Order::Ascending) .map(|(k, _)| k) .collect(); let count = keys.len(); for k in keys { deps.storage.remove(&k); } // get balance and send all to recipient let balance = deps.querier.query_all_balances(env.contract.address)?; let send = BankMsg::Send { to_address: msg.payout.clone(), amount: balance, }; let data_msg = format!("burnt {} keys", count).into_bytes(); Ok(Response::new() .add_message(send) .add_attribute("action", "burn") .add_attribute("payout", msg.payout) .set_data(data_msg))}
In the above example, the state is completely deleted during the migration. Also, all the contract's balance is sent to a designated payout address specified in the MigrationMsg. As a result, all funds are drained and all state is removed, effectively burning the contract.
Create a mutable contract
In CosmWasm, a mutable contract refers to a smart contract that allows for the modification of its state and code after deployment. By default, CosmWasm contracts are immutable, meaning that their state cannot be changed once they are deployed on the blockchain. However, if the contract is instantiated with an admin address, that account can execute migrations to update the contract's code and state.
Create a mutable contract via archwayd
When all else fails archwayd allows for low level access to the various commands that can be executed against the blockchain.
archwayd tx wasm instantiate [code_id] [json_encoded_init_args] --label [label_text] --admin [admin_address] --amount [coins] --from [wallet] --chain-id "archway-1" --node "https://rpc.mainnet.archway.io:443" --broadcast-mode sync --output json -y --gas auto --gas-adjustment 1.4 --gas-prices $(archwayd q rewards estimate-fees 1 --node 'https://rpc.mainnet.archway.io:443' --output json | jq -r '.gas_unit_price | (.amount + .denom)')
The [code_id] should correspond to the code ID of the stored contract that you want to instantiate. [json_encoded_init_args] is a json object that contains the arguments your instantiation function requires. The [label_text] is a human-readable name for the contract. [admin_address] is the address that will act as an admin for the contract. The [coins] value is the tokens to send to the contract during instantiation and the [wallet] value can be either the name of the wallet or the account address that will sign the transaction.
Create a mutable contract via arch3.js
This is a basic example of how you could instantiate a contract with an admin address using arch3.js. Create a new npm project and install the folloiwng dependencies:
- npm install --save @archwayhq/arch3.js
- npm install --save dotenv
You will also need to create a .env file in the root of your project folder and add the following:
MNEMONIC="enter mnemonic here"
The following Javascript code can be stored in a index.js file and executed by running node index.js.
import { SigningArchwayClient } from '@archwayhq/arch3.js';import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing";import dotenv from "dotenv";dotenv.config();async function main() { const network = { chainId: 'archway-1', endpoint: 'https://rpc.mainnet.archway.io', prefix: 'archway', }; // Get wallet and accounts from mnemonic const mnemonic = process.env.MNEMONIC; const wallet = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, { prefix: network.prefix }); const accounts = await wallet.getAccounts(); const accountAddress = accounts[0].address; const signingClient = await SigningArchwayClient.connectWithSigner(network.endpoint, wallet); // Instantiate a contract const codeId = 1; // Update with your stored contract code id // Add the message values required const msg = { }; const instantiateOptions = { memo: "Instantiating a new contract", admin: accountAddress // This sets the admin address to the address of the signer }; const contractLabel = "my-instance-label"; const instantiateResult = await signingClient.instantiate( accountAddress, codeId, msg, contractLabel, 'auto', instantiateOptions ); if (instantiateResult.code !== undefined && instantiateResult.code !== 0) { console.log("Instantiation failed:", instantiateResult.log || instantiateResult.rawLog); } else { console.log("Instantiation successful:", instantiateResult.transactionHash); }}main();
Execute a migrate transaction
In this section, we will demonstrate the process of performing a migration using the Archway Developer CLI, archwayd, and arch3.js.
Execute migration via archwayd
archwayd tx wasm migrate [contract_address] [new_code_id] [json_encoded_migration_args] --from [wallet] --chain-id "archway-1" --node "https://rpc.mainnet.archway.io:443" --broadcast-mode sync --output json -y --gas auto --gas-adjustment 1.4 --gas-prices $(archwayd q rewards estimate-fees 1 --node 'https://rpc.mainnet.archway.io:443' --output json | jq -r '.gas_unit_price | (.amount + .denom)')
You will need to substitute the [contract_address] with the address of the contract you want to update. The [new_code_id] should correspond to the code ID of the newly stored contract that will replace the old contract. [json_encoded_migration_args] is a json object that contains the arguments your instantiation function requires. The [wallet] value can be either the name of the wallet or the account address that will sign the transaction.
Execute migration via arch3.js
This is a basic example of how you could migrate a contract using arch3.js. Create a new npm project and install the folloiwng dependencies:
- npm install --save @archwayhq/arch3.js
- npm install --save dotenv
You will also need to create a .env file in the root of your project folder and add the following:
MNEMONIC="enter mnemonic here"
The following Javascript code can be stored in a index.js file and executed by running node index.js.
import { SigningArchwayClient } from '@archwayhq/arch3.js';import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing";import dotenv from "dotenv";dotenv.config();async function main() { const network = { chainId: 'archway-1', endpoint: 'https://rpc.mainnet.archway.io', prefix: 'archway', }; // Get wallet and accounts from mnemonic const mnemonic = process.env.MNEMONIC; const wallet = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, { prefix: network.prefix }); const accounts = await wallet.getAccounts(); const accountAddress = accounts[0].address; const signingClient = await SigningArchwayClient.connectWithSigner(network.endpoint, wallet); const codeId = 1; // Update with your stored contract code id const contractAddress = ""; // Enter contract address const memo = "Migrating contract"; // Add the message values required const migrateMsg = { }; const instantiateResult = await signingClient.migrate( accountAddress, contractAddress, codeId, migrateMsg, 'auto', memo ); if (result.code !== undefined && result.code !== 0) { console.log("Migration failed:", result.log || result.rawLog); } else { console.log("Migration successful:", result.transactionHash); }}main();
Make a contract immutable
In certain scenarios, it may be necessary to render a contract immutable. The concept of immutability in the context of smart contracts refers to the inability to modify or alter the contract's code or state once it has been deployed to a blockchain network.
There are several situations where making a contract immutable can be beneficial. One such scenario is when dealing with critical functions or processes that should not be susceptible to unauthorized modifications. By making the contract immutable, you ensure that the contract's logic remains intact and unchanged, providing a higher level of security and trust.
In order to make a contract immutabile, it is essential to set the admin address to an empty value, thereby preventing the execution of any contract migrations.
Make contract immutable via archwayd
archwayd tx wasm set-contract-admin [contract_address] [new_admin_address] --from [wallet] --chain-id "archway-1" --node "https://rpc.mainnet.archway.io:443" --broadcast-mode sync --output json -y --gas auto --gas-adjustment 1.4 --gas-prices $(archwayd q rewards estimate-fees 1 --node 'https://rpc.mainnet.archway.io:443' --output json | jq -r '.gas_unit_price | (.amount + .denom)')
You will need to substitute the [contract_address] with the address of the contract you want to update. Set the [new_admin_address] value to an empty string.
Make contract immutable via arch3.js
This is a basic example of how you could update a contract's admin address using arch3.js. Create a new npm project and install the folloiwng dependencies:
- npm install --save @archwayhq/arch3.js
- npm install --save dotenv
You will also need to create a .env file in the root of your project folder and add the following:
MNEMONIC="enter mnemonic here"
The following Javascript code can be stored in a index.js file and executed by running node index.js.
import { SigningArchwayClient } from '@archwayhq/arch3.js';import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing";import dotenv from "dotenv";dotenv.config();async function main() { const network = { chainId: 'archway-1', endpoint: 'https://rpc.mainnet.archway.io', prefix: 'archway', }; // Get wallet and accounts from mnemonic const mnemonic = process.env.MNEMONIC; const wallet = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, { prefix: network.prefix }); const accounts = await wallet.getAccounts(); const accountAddress = accounts[0].address; const signingClient = await SigningArchwayClient.connectWithSigner(network.endpoint, wallet); const contractAddress = ""; // Enter contract address const memo = "Clear admin"; const instantiateResult = await signingClient.clearAdmin( accountAddress, contractAddress, 'auto', memo ); if (result.code !== undefined && result.code !== 0) { console.log("Migration failed:", result.log || result.rawLog); } else { console.log("Migration successful:", result.transactionHash); }}main();