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 terpd.
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.