Migration

La migración se refiere al proceso de actualización o sustitución del código de un contrato inteligente existente y, en algunos casos, a la modificación de los datos de estado.

CosmWasm ha hecho de la migración de contratos una experiencia fluida. Durante la instanciación del contrato, hay un campo admin opcional que se puede establecer. Si no se especifica este campo, el contrato se convierte en inmutable. Sin embargo, si se asigna a una cuenta externa o a un contrato de gobierno, esa cuenta puede iniciar la migración. El administrador también puede transferir la propiedad a otra cuenta o hacer que el contrato sea completamente inmutable después de algún tiempo estableciendo el campo admin en un valor vacío.

Aquí es donde entra en juego la especificación CW2. Especifica un Singleton especial que contiene el nombre del contrato y la información de la versión que deben almacenar todos los contratos compatibles con CW2 durante la instanciación. Cuando se invoca la función de migración, el contrato recién creado puede acceder a esta información para determinar si es compatible con la migración del contrato anterior.

La Especificación CW2 proporciona una función set_contract_version que debe utilizarse al instanciar un nuevo contrato a través de la función instantiate para almacenar la información de versionado del contrato. Para el contrato que migra, es importante utilizar la función set_contract_version como parte de la lógica de migración dentro de la función migrate(...), en lugar de la función instantiate, para actualizar el versionado del contrato durante el proceso de migración:

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)?;
}

Para el contrato en migración, además de utilizar set_contract_version, también puede utilizar la función get_contract_version para determinar la versión anterior del contrato. Es importante asegurarse, por ejemplo, de que la actualización sólo se realiza si la versión que se está actualizando es posterior a la versión original del contrato:

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())
}

Tanto el método set_contract_version como el método get_contract_version operan sobre una estructura de datos Item de cw_storage_plus, que gestiona esta información:

#[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,
}

Así, un ejemplo serializado puede ser el siguiente:

{
    "contract": "crates.io:cw20-base",
    "version": "v0.1.0"
}

Establecer un contrato para las migraciones

Realizar una migración de contrato implica tres pasos. En primer lugar, escriba una versión más reciente del contrato que desea actualizar. En segundo lugar, almacene el nuevo código en la cadena, pero no lo instancie. En tercer lugar, utilice una transacción MsgMigrateContract dedicada para dirigir el contrato antiguo hacia el nuevo código, y la migración se habrá completado. Vea Ejecutar una transacción de migración más abajo para saber cómo ejecutar una transacción de migración usando archwayd, Archway Developer CLI o arch3.js.

Durante el proceso de migración se ejecuta la función migrar definida en el nuevo contrato y no la función migrar del código antiguo, por lo que es necesario que el código del nuevo contrato tenga una función migrar definida y correctamente exportada como 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> {
}

La función migrate ofrece la posibilidad de realizar cualquier cambio granular deseado en el Estado, de forma similar a una migración de base de datos.

Si la función migrate devuelve un error, la transacción se abortará, todos los cambios de Estado se revertirán y la migración no se llevará a cabo.

A continuación se presentan diversas variantes de migración para dar una idea de algunos casos de uso habituales.

Migración básica de contratos

Esta migración puede ser la más frecuente. Se limita a sustituir el código de un contrato. Sin embargo, no se implementan comprobaciones de seguridad, ya que no se realiza ninguna comprobación cw2::set_contract_version.

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())
}

Migración por versión de código y nombre de contrato

Esta migración es un ejemplo más completo. La función migrar garantiza:

  • Migrar desde el mismo tipo de contrato verificando su nombre.

  • La actualización a partir de una versión anterior del contrato verificando su versión

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())
}

Migrar usando comparación semver

Esta migración utiliza Semver en lugar de una comparación String.

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())
}

Este ejemplo utiliza Semver para ayudar con la comprobación de versiones. También necesitaría añadir la dependencia semver a sus dependencias de carga:

[dependencies]
semver = "1"

También puede implementar errores personalizados Semver:

#[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())
    }
}

Uso de migrate para actualizar un estado que de otro modo sería inmutable

Este ejemplo muestra cómo puede utilizarse una migración para actualizar un valor que normalmente es inmutable. Esta característica permite cambiar el valor sólo durante una migración si es necesario.

#[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())
}

En el ejemplo anterior, nuestro MigrateMsg tiene un campo verificador que contiene el nuevo valor para el campo verificador de nuestro contrato ubicado en el Estado. Siempre que su contrato no exponga también un UpdateState o algo como UpdateVerifier ExecuteMsgs, entonces este proporciona el único método para cambiar el valor del verificador.

Utilizar las migraciones para "quemar" un contrato

Las migraciones también pueden utilizarse para abandonar un contrato antiguo y borrar su estado. Esto tiene varias aplicaciones, pero si lo necesita, puede encontrar un ejemplo aquí:

#[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))
}

En el ejemplo anterior, el estado se elimina completamente durante la migración. Además, todo el saldo del contrato se envía a una dirección de pago designada especificada en el MigrationMsg. Como resultado, todos los fondos son drenados y todo el estado es eliminado, quemando efectivamente el contrato.

Crear un contrato mutable

En CosmWasm, un contrato mutable se refiere a un contrato inteligente que permite la modificación de su estado y código después de su despliegue. Por defecto, los contratos CosmWasm son inmutables, lo que significa que su estado no se puede cambiar una vez que se despliegan en la blockchain. Sin embargo, si el contrato es instanciado con una dirección de administrador, esa cuenta puede ejecutar migraciones para actualizar el código y el estado del contrato.

Crear un contrato mutable a través de archway developer CLI

Archway Developer CLI es nuestra herramienta recomendada para interactuar con tus contratos inteligentes ya que simplifica muchas complejidades asociadas a otros métodos. Para crear un contrato mutable, necesitas establecer una dirección admin durante el proceso de instanciación del contrato.

archway instantiate --admin-address [value]  -a [arg value]

Sustituya [valor] por la dirección que servirá de dirección de administración del contrato. Sólo esta dirección puede ejecutar migraciones de contrato. Si el contrato requiere argumentos, sustituya [arg value] por un objeto JSON que represente los argumentos, por ejemplo '{"count": 0}'.

Crear un contrato mutable mediante archwayd

Cuando todo lo demás falla, archwayd permite un acceso de bajo nivel a los diversos comandos que pueden ejecutarse contra la 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)')

El [code_id] debe corresponder al código ID del contrato almacenado que quieres instanciar. [json_encoded_init_args] es un objeto json que contiene los argumentos que necesita la función de instanciación. El [label_text] es un nombre legible por humanos para el contrato. [admin_address] es la dirección que actuará como administrador del contrato. El valor [coins] son los tokens que se enviarán al contrato durante la instanciación y el valor [wallet] puede ser el nombre del monedero o la dirección de la cuenta que firmará la transacción.

Crear un contrato mutable mediante arch3.js

Este es un ejemplo básico de cómo podrías instanciar un contrato con una dirección admin usando arch3.js. Crea un nuevo proyecto npm e instala las siguientes dependencias:

  • npm install --save @archwayhq/arch3.js

  • npm install --save dotenv

También tendrá que crear un archivo .env en la raíz de la carpeta de su proyecto y añadir lo siguiente:

MNEMONIC="enter mnemonic here"

El siguiente código Javascript puede almacenarse en un archivo index.js y ejecutarse ejecutando 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();

Ejecutar una operación de migración

En esta sección, demostraremos el proceso de realizar una migración usando la CLI de Archway Developer, archwayd, y arch3.js.

Ejecutar la migración a través de 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)')

Deberá sustituir [direccion_contrato] por la dirección del contrato que desea actualizar. El [new_code_id] debe corresponder al ID de código del nuevo contrato almacenado que sustituirá al contrato antiguo. [json_encoded_migration_args] es un objeto json que contiene los argumentos que requiere tu función de instanciación. El valor [wallet] puede ser el nombre del monedero o la dirección de la cuenta que firmará la transacción.

Ejecutar la migración mediante arch3.js

Este es un ejemplo básico de cómo podrías migrar un contrato usando arch3.js. Crea un nuevo proyecto npm e instala las siguientes dependencias:

  • npm install --save @archwayhq/arch3.js

  • npm install --save dotenv

También tendrá que crear un archivo .env en la raíz de la carpeta de su proyecto y añadir lo siguiente:

MNEMONIC="enter mnemonic here"

El siguiente código Javascript puede almacenarse en un archivo index.js y ejecutarse ejecutando 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();

Hacer un contrato inmutable

En algunos casos, puede ser necesario hacer que un contrato sea inmutable. El concepto de inmutabilidad en el contexto de los contratos inteligentes se refiere a la imposibilidad de modificar o alterar el código o el estado del contrato una vez que se ha desplegado en una red blockchain.

Hay varias situaciones en las que hacer que un contrato sea inmutable puede ser beneficioso. Una de ellas es cuando se trata de funciones o procesos críticos que no deben ser susceptibles de modificaciones no autorizadas. Al hacer el contrato inmutable, te aseguras de que la lógica del contrato permanece intacta y sin cambios, proporcionando un mayor nivel de seguridad y confianza.

Para hacer que un contrato sea inmutable, es esencial establecer la dirección admin en un valor vacío, impidiendo así la ejecución de cualquier migración de contrato.

Hacer inmutable el contrato mediante 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)')

Deberá sustituir [contract_address] por la dirección del contrato que desea actualizar. Establezca el valor [new_admin_address] en una cadena vacía.

Hacer inmutable un contrato mediante arch3.js

Este es un ejemplo básico de cómo podrías actualizar la dirección admin de un contrato usando arch3.js. Crea un nuevo proyecto npm e instala las siguientes dependencias:

  • npm install --save @archwayhq/arch3.js

  • npm install --save dotenv

También tendrá que crear un archivo .env en la raíz de la carpeta de su proyecto y añadir lo siguiente:

MNEMONIC="enter mnemonic here"

El siguiente código Javascript puede almacenarse en un archivo index.js y ejecutarse ejecutando 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();

Last updated