diff --git a/Cargo.lock b/Cargo.lock index 91b7e1a96..cd773a872 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -855,6 +855,7 @@ dependencies = [ "regex", "report", "schemars", + "semver 1.0.23", "serde", "serde_json", "serde_with 3.11.0", @@ -6901,6 +6902,7 @@ dependencies = [ "rand", "report", "router-api", + "semver 1.0.23", "serde_json", "thiserror", ] diff --git a/Cargo.toml b/Cargo.toml index f8b73745e..94af7cf71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,6 +56,7 @@ rewards = { version = "^1.2.0", path = "contracts/rewards" } router = { version = "^1.1.0", path = "contracts/router" } router-api = { version = "^1.0.0", path = "packages/router-api" } schemars = "0.8.10" +semver = "1.0" serde = { version = "1.0.145", default-features = false, features = ["derive"] } serde_json = "1.0.89" service-registry = { version = "^1.1.0", path = "contracts/service-registry" } diff --git a/contracts/router/Cargo.toml b/contracts/router/Cargo.toml index c90fe931e..d514708ed 100644 --- a/contracts/router/Cargo.toml +++ b/contracts/router/Cargo.toml @@ -47,6 +47,7 @@ mockall = { workspace = true } msgs-derive = { workspace = true } report = { workspace = true } router-api = { workspace = true } +semver = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } diff --git a/contracts/router/src/contract.rs b/contracts/router/src/contract.rs index 98a8a0472..6139265fe 100644 --- a/contracts/router/src/contract.rs +++ b/contracts/router/src/contract.rs @@ -5,8 +5,9 @@ use cosmwasm_std::{ to_json_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Response, Storage, }; use router_api::error::Error; +use semver::{Version, VersionReq}; -use crate::contract::migrations::v1_0_1; +use crate::contract::migrations::v1_1_1; use crate::events::RouterInstantiated; use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; use crate::state; @@ -18,7 +19,6 @@ mod query; pub const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); -const BASE_VERSION: &str = "1.0.1"; #[cfg_attr(not(feature = "library"), entry_point)] pub fn migrate( @@ -26,10 +26,11 @@ pub fn migrate( _env: Env, msg: MigrateMsg, ) -> Result { - cw2::assert_contract_version(deps.storage, CONTRACT_NAME, BASE_VERSION)?; + let old_version = Version::parse(&cw2::get_contract_version(deps.storage)?.version)?; + let version_requirement = VersionReq::parse(">= 1.1.0, < 1.2.0")?; + assert!(version_requirement.matches(&old_version)); - let axelarnet_gateway = address::validate_cosmwasm_address(deps.api, &msg.axelarnet_gateway)?; - v1_0_1::migrate(deps.storage, axelarnet_gateway)?; + v1_1_1::migrate(deps.storage, msg.chains_to_remove)?; // this needs to be the last thing to do during migration, // because previous migration steps should check the old version diff --git a/contracts/router/src/contract/migrations/mod.rs b/contracts/router/src/contract/migrations/mod.rs index 45f45da41..6259bdf8f 100644 --- a/contracts/router/src/contract/migrations/mod.rs +++ b/contracts/router/src/contract/migrations/mod.rs @@ -1 +1 @@ -pub mod v1_0_1; +pub mod v1_1_1; diff --git a/contracts/router/src/contract/migrations/v1_0_1.rs b/contracts/router/src/contract/migrations/v1_0_1.rs deleted file mode 100644 index 0f06b160a..000000000 --- a/contracts/router/src/contract/migrations/v1_0_1.rs +++ /dev/null @@ -1,94 +0,0 @@ -use axelar_wasm_std::error::ContractError; -use cosmwasm_std::{Addr, Storage}; - -use crate::state; - -pub fn migrate(storage: &mut dyn Storage, axelarnet_gateway: Addr) -> Result<(), ContractError> { - // migrate config - state::save_config(storage, &state::Config { axelarnet_gateway }).map_err(Into::into) -} -#[cfg(test)] -mod test { - #![allow(deprecated)] - - use assert_ok::assert_ok; - use axelar_wasm_std::error::ContractError; - use cosmwasm_schema::cw_serde; - use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; - use cosmwasm_std::{Addr, DepsMut, Env, MessageInfo, Response}; - use cw_storage_plus::Item; - - use crate::contract::migrations::v1_0_1; - use crate::state; - - #[deprecated(since = "1.0.1", note = "only used during migration")] - #[cw_serde] - pub struct InstantiateMsg { - // admin controls freezing and unfreezing a chain - pub admin_address: String, - // governance votes on chains being added or upgraded - pub governance_address: String, - // the address of the nexus gateway - pub nexus_gateway: String, - } - - #[deprecated(since = "1.0.1", note = "only used during migration")] - #[cw_serde] - struct Config { - pub nexus_gateway: Addr, - } - - #[deprecated(since = "1.0.1", note = "only used during migration")] - const CONFIG: Item = Item::new("config"); - - #[test] - fn config_gets_migrated() { - let mut deps = mock_dependencies(); - instantiate_1_0_1_contract(deps.as_mut()).unwrap(); - - assert_ok!(CONFIG.load(deps.as_mut().storage)); - assert!(state::load_config(&deps.storage).is_err()); - - let axelarnet_gateway = Addr::unchecked("axelarnet-gateway"); - assert_ok!(v1_0_1::migrate( - deps.as_mut().storage, - axelarnet_gateway.clone() - )); - assert!(CONFIG.load(deps.as_mut().storage).is_err()); - - let config = assert_ok!(state::CONFIG.load(deps.as_mut().storage)); - assert_eq!(config.axelarnet_gateway, axelarnet_gateway); - } - - fn instantiate_1_0_1_contract(deps: DepsMut) -> Result { - let admin = "admin"; - let governance = "governance"; - let nexus_gateway = "nexus_gateway"; - - let msg = InstantiateMsg { - admin_address: admin.to_string(), - governance_address: governance.to_string(), - nexus_gateway: nexus_gateway.to_string(), - }; - - instantiate(deps, mock_env(), mock_info(admin, &[]), msg.clone()) - } - - #[deprecated(since = "1.0.1", note = "only used to test the migration")] - fn instantiate( - deps: DepsMut, - _env: Env, - _info: MessageInfo, - msg: InstantiateMsg, - ) -> Result { - let config = Config { - nexus_gateway: deps.api.addr_validate(&msg.nexus_gateway)?, - }; - - CONFIG - .save(deps.storage, &config) - .expect("must save the config"); - - Ok(Response::new()) - } -} diff --git a/contracts/router/src/contract/migrations/v1_1_1.rs b/contracts/router/src/contract/migrations/v1_1_1.rs new file mode 100644 index 000000000..73803293c --- /dev/null +++ b/contracts/router/src/contract/migrations/v1_1_1.rs @@ -0,0 +1,138 @@ +use axelar_wasm_std::flagset::FlagSet; +use axelar_wasm_std::msg_id::MessageIdFormat; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Storage}; +use cw_storage_plus::{Index, IndexList, IndexedMap, MultiIndex}; +use error_stack::{Result, ResultExt}; +use router_api::error::Error; +use router_api::{Gateway, GatewayDirection}; + +// the below types and functions are duplicated from the state module, except +// chain names are just stored as String instead of ChainName. This is so we +// can access chains with names that are no longer valid, and were stored +// when the checks on ChainName were less restrictive +#[cw_serde] +struct ChainEndpoint { + pub name: String, + pub gateway: Gateway, + pub frozen_status: FlagSet, + pub msg_id_format: MessageIdFormat, +} + +struct ChainEndpointIndexes<'a> { + pub gateway: GatewayIndex<'a>, +} + +struct GatewayIndex<'a>(MultiIndex<'a, Addr, ChainEndpoint, String>); + +impl<'a> GatewayIndex<'a> { + pub fn new( + idx_fn: fn(&[u8], &ChainEndpoint) -> Addr, + pk_namespace: &'a str, + idx_namespace: &'a str, + ) -> Self { + GatewayIndex(MultiIndex::new(idx_fn, pk_namespace, idx_namespace)) + } +} + +impl<'a> IndexList for ChainEndpointIndexes<'a> { + fn get_indexes(&'_ self) -> Box> + '_> { + let v: Vec<&dyn Index> = vec![&self.gateway.0]; + Box::new(v.into_iter()) + } +} + +const CHAINS_PKEY: &str = "chains"; +fn chain_endpoints_old<'a>() -> IndexedMap<'a, String, ChainEndpoint, ChainEndpointIndexes<'a>> { + return IndexedMap::new( + CHAINS_PKEY, + ChainEndpointIndexes { + gateway: GatewayIndex::new( + |_pk: &[u8], d: &ChainEndpoint| d.gateway.address.clone(), + CHAINS_PKEY, + "gateways", + ), + }, + ); +} + +pub fn migrate(storage: &mut dyn Storage, chains_to_remove: Vec) -> Result<(), Error> { + for chain in chains_to_remove { + chain_endpoints_old() + .remove(storage, chain) + .change_context(Error::StoreFailure)?; + } + Ok(()) +} +#[cfg(test)] +mod test { + #![allow(deprecated)] + + use assert_ok::assert_ok; + use axelar_wasm_std::msg_id::MessageIdFormat; + use cosmwasm_std::testing::mock_dependencies; + use cosmwasm_std::Addr; + use itertools::Itertools; + use router_api::{ChainName, Gateway, GatewayDirection}; + + use super::{chain_endpoints_old, migrate, ChainEndpoint}; + use crate::state::{self, chain_endpoints}; + + #[test] + fn can_delete_chains() { + let mut deps = mock_dependencies(); + let good_chain_names: Vec = ["ethereum", "avalanche"] + .into_iter() + .map(|name| ChainName::try_from(name).unwrap()) + .collect(); + for chain_name in &good_chain_names { + state::chain_endpoints() + .save( + deps.as_mut().storage, + chain_name.clone(), + &router_api::ChainEndpoint { + name: chain_name.clone(), + gateway: Gateway { + address: Addr::unchecked("gateway_address"), + }, + frozen_status: GatewayDirection::None.into(), + msg_id_format: MessageIdFormat::HexTxHashAndEventIndex, + }, + ) + .unwrap(); + } + + let bad_chain_name = "some really really long chain name that is not valid"; + chain_endpoints_old() + .save( + deps.as_mut().storage, + bad_chain_name.to_string(), + &ChainEndpoint { + name: bad_chain_name.to_string(), + gateway: Gateway { + address: Addr::unchecked("gateway_address"), + }, + frozen_status: GatewayDirection::None.into(), + msg_id_format: MessageIdFormat::HexTxHashAndEventIndex, + }, + ) + .unwrap(); + + assert_ok!(migrate( + deps.as_mut().storage, + vec![good_chain_names[0].to_string(), bad_chain_name.to_string()] + )); + + let chains: Vec = assert_ok!(chain_endpoints() + .range( + deps.as_mut().storage, + None, + None, + cosmwasm_std::Order::Ascending + ) + .map(|item| { item.map(|(_, endpoint)| endpoint.name) }) + .try_collect()); + + assert_eq!(chains, vec![good_chain_names[1].clone()]); + } +} diff --git a/contracts/router/src/msg.rs b/contracts/router/src/msg.rs index 6a66c0bfe..65f110661 100644 --- a/contracts/router/src/msg.rs +++ b/contracts/router/src/msg.rs @@ -12,7 +12,7 @@ pub struct InstantiateMsg { #[cw_serde] pub struct MigrateMsg { - pub axelarnet_gateway: String, + pub chains_to_remove: Vec, } // these messages are extracted into a separate package to avoid circular dependencies diff --git a/packages/axelar-wasm-std/Cargo.toml b/packages/axelar-wasm-std/Cargo.toml index 3472b6de8..8395706cf 100644 --- a/packages/axelar-wasm-std/Cargo.toml +++ b/packages/axelar-wasm-std/Cargo.toml @@ -43,6 +43,7 @@ num-traits = { workspace = true } regex = { version = "1.10.0", default-features = false, features = ["perf", "std"] } report = { workspace = true } schemars = "0.8.10" +semver = { workspace = true } serde = { version = "1.0.145", default-features = false, features = ["derive"] } serde_json = "1.0.89" serde_with = { version = "3.11.0", features = ["macros"] } diff --git a/packages/axelar-wasm-std/src/error.rs b/packages/axelar-wasm-std/src/error.rs index d08977ca8..7e161f893 100644 --- a/packages/axelar-wasm-std/src/error.rs +++ b/packages/axelar-wasm-std/src/error.rs @@ -39,6 +39,14 @@ impl From for ContractError { } } +impl From for ContractError { + fn from(err: semver::Error) -> Self { + ContractError { + report: report!(err).change_context(Error::Report), + } + } +} + impl From for ContractError { fn from(err: permission_control::Error) -> Self { ContractError {