From dc20405a2b1e5eb327d024ed40b21126fff81219 Mon Sep 17 00:00:00 2001 From: Sammy Date: Wed, 30 Oct 2024 02:53:44 +0800 Subject: [PATCH 1/2] feat(interchain-token-service): add message type SetChainConfig (#669) --- .../interchain-token-service/src/contract.rs | 10 +++- .../src/contract/execute.rs | 30 +++++++++--- contracts/interchain-token-service/src/msg.rs | 8 +++ .../interchain-token-service/src/state.rs | 34 +++++++++++-- .../interchain-token-service/tests/execute.rs | 49 ++++++++++++++++++- .../tests/utils/execute.rs | 19 +++++++ packages/axelar-wasm-std/src/nonempty/uint.rs | 1 + 7 files changed, 137 insertions(+), 14 deletions(-) diff --git a/contracts/interchain-token-service/src/contract.rs b/contracts/interchain-token-service/src/contract.rs index 503ea1539..ee536d5ec 100644 --- a/contracts/interchain-token-service/src/contract.rs +++ b/contracts/interchain-token-service/src/contract.rs @@ -29,8 +29,8 @@ pub enum Error { RegisterItsContract, #[error("failed to deregsiter an its edge contract")] DeregisterItsContract, - #[error("too many coins attached. Execute accepts zero or one coins")] - TooManyCoins, + #[error("failed to set chain config")] + SetChainConfig, #[error("failed to query its address")] QueryItsContract, #[error("failed to query all its addresses")] @@ -99,6 +99,12 @@ pub fn execute( execute::deregister_its_contract(deps, chain) .change_context(Error::DeregisterItsContract) } + ExecuteMsg::SetChainConfig { + chain, + max_uint, + max_target_decimals, + } => execute::set_chain_config(deps, chain, max_uint, max_target_decimals) + .change_context(Error::SetChainConfig), }? .then(Ok) } diff --git a/contracts/interchain-token-service/src/contract/execute.rs b/contracts/interchain-token-service/src/contract/execute.rs index 11f3e0558..0112882ce 100644 --- a/contracts/interchain-token-service/src/contract/execute.rs +++ b/contracts/interchain-token-service/src/contract/execute.rs @@ -1,4 +1,4 @@ -use axelar_wasm_std::IntoContractError; +use axelar_wasm_std::{nonempty, FnExt, IntoContractError}; use cosmwasm_std::{DepsMut, HexBinary, QuerierWrapper, Response, Storage}; use error_stack::{bail, ensure, report, Result, ResultExt}; use router_api::{Address, ChainName, ChainNameRaw, CrossChainId}; @@ -21,12 +21,12 @@ pub enum Error { FailedItsContractRegistration(ChainNameRaw), #[error("failed to deregister its contract for chain {0}")] FailedItsContractDeregistration(ChainNameRaw), - #[error("failed to execute message")] - FailedExecuteMessage, - #[error("failed to query nexus")] - NexusQueryError, - #[error("storage error")] - StorageError, + #[error("chain config for {0} already set")] + ChainConfigAlreadySet(ChainNameRaw), + #[error("invalid chain max uint")] + LoadChainConfig(ChainNameRaw), + #[error("failed to save chain config for chain {0}")] + SaveChainConfig(ChainNameRaw), } /// Executes an incoming ITS message. @@ -132,3 +132,19 @@ pub fn deregister_its_contract(deps: DepsMut, chain: ChainNameRaw) -> Result Result { + match state::may_load_chain_config(deps.storage, &chain) + .change_context_lazy(|| Error::LoadChainConfig(chain.clone()))? + { + Some(_) => bail!(Error::ChainConfigAlreadySet(chain)), + None => state::save_chain_config(deps.storage, &chain, max_uint, max_target_decimals) + .change_context_lazy(|| Error::SaveChainConfig(chain))? + .then(|_| Ok(Response::new())), + } +} diff --git a/contracts/interchain-token-service/src/msg.rs b/contracts/interchain-token-service/src/msg.rs index c9ff32b55..8b570c1c1 100644 --- a/contracts/interchain-token-service/src/msg.rs +++ b/contracts/interchain-token-service/src/msg.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; +use axelar_wasm_std::nonempty; use axelarnet_gateway::AxelarExecutableMsg; use cosmwasm_schema::{cw_serde, QueryResponses}; use msgs_derive::EnsurePermissions; @@ -33,6 +34,13 @@ pub enum ExecuteMsg { /// The admin is allowed to remove the ITS address of a chain for emergencies. #[permission(Elevated)] DeregisterItsContract { chain: ChainNameRaw }, + /// Set the chain configuration for a chain. + #[permission(Governance)] + SetChainConfig { + chain: ChainNameRaw, + max_uint: nonempty::Uint256, // The maximum uint value that is supported by the chain's token standard + max_target_decimals: u8, // The maximum number of decimals that is preserved when deploying a token to anothe chain where smaller uint values are used + }, } #[cw_serde] diff --git a/contracts/interchain-token-service/src/state.rs b/contracts/interchain-token-service/src/state.rs index c977ad5fa..806e7a01c 100644 --- a/contracts/interchain-token-service/src/state.rs +++ b/contracts/interchain-token-service/src/state.rs @@ -10,14 +10,10 @@ use router_api::{Address, ChainNameRaw}; pub enum Error { #[error(transparent)] Std(#[from] StdError), - #[error("ITS contract got into an invalid state, its config is missing")] - MissingConfig, #[error("its address for chain {0} not found")] ItsContractNotFound(ChainNameRaw), #[error("its address for chain {0} already registered")] ItsContractAlreadyRegistered(ChainNameRaw), - #[error("gateway token already registered {0}")] - GatewayTokenAlreadyRegistered(nonempty::String), } #[cw_serde] @@ -25,8 +21,15 @@ pub struct Config { pub axelarnet_gateway: Addr, } +#[cw_serde] +pub struct ChainConfig { + max_uint: nonempty::Uint256, + max_target_decimals: u8, +} + const CONFIG: Item = Item::new("config"); const ITS_CONTRACTS: Map<&ChainNameRaw, Address> = Map::new("its_contracts"); +const CHAIN_CONFIGS: Map<&ChainNameRaw, ChainConfig> = Map::new("chain_configs"); pub fn load_config(storage: &dyn Storage) -> Config { CONFIG @@ -38,6 +41,29 @@ pub fn save_config(storage: &mut dyn Storage, config: &Config) -> Result<(), Err Ok(CONFIG.save(storage, config)?) } +pub fn may_load_chain_config( + storage: &dyn Storage, + chain: &ChainNameRaw, +) -> Result, Error> { + Ok(CHAIN_CONFIGS.may_load(storage, chain)?) +} + +pub fn save_chain_config( + storage: &mut dyn Storage, + chain: &ChainNameRaw, + max_uint: nonempty::Uint256, + max_target_decimals: u8, +) -> Result<(), Error> { + Ok(CHAIN_CONFIGS.save( + storage, + chain, + &ChainConfig { + max_uint, + max_target_decimals, + }, + )?) +} + pub fn may_load_its_contract( storage: &dyn Storage, chain: &ChainNameRaw, diff --git a/contracts/interchain-token-service/tests/execute.rs b/contracts/interchain-token-service/tests/execute.rs index 368d60d39..dd82596d3 100644 --- a/contracts/interchain-token-service/tests/execute.rs +++ b/contracts/interchain-token-service/tests/execute.rs @@ -1,9 +1,11 @@ +use std::str::FromStr; + use assert_ok::assert_ok; use axelar_wasm_std::response::inspect_response_msg; use axelar_wasm_std::{assert_err_contains, permission_control}; use axelarnet_gateway::msg::ExecuteMsg as AxelarnetGatewayExecuteMsg; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; -use cosmwasm_std::HexBinary; +use cosmwasm_std::{HexBinary, Uint256}; use interchain_token_service::contract::{self, ExecuteError}; use interchain_token_service::events::Event; use interchain_token_service::msg::ExecuteMsg; @@ -314,3 +316,48 @@ fn execute_message_when_invalid_message_type_fails() { ); assert_err_contains!(result, ExecuteError, ExecuteError::InvalidMessageType); } + +#[test] +fn set_chain_config_should_succeed() { + let chain = "ethereum".parse().unwrap(); + let max_uint = Uint256::from_str("120000000000000000000000000") + .unwrap() + .try_into() + .unwrap(); + let decimals = 18; + + let mut deps = mock_dependencies(); + utils::instantiate_contract(deps.as_mut()).unwrap(); + + assert_ok!(utils::set_chain_config( + deps.as_mut(), + chain, + max_uint, + decimals + )); +} + +#[test] +fn set_chain_config_should_fail_if_chain_config_is_already_set() { + let chain: ChainNameRaw = "ethereum".parse().unwrap(); + let max_uint = Uint256::from_str("120000000000000000000000000") + .unwrap() + .try_into() + .unwrap(); + let decimals = 18; + + let mut deps = mock_dependencies(); + utils::instantiate_contract(deps.as_mut()).unwrap(); + + assert_ok!(utils::set_chain_config( + deps.as_mut(), + chain.clone(), + max_uint, + decimals + )); + assert_err_contains!( + utils::set_chain_config(deps.as_mut(), chain, max_uint, decimals), + ExecuteError, + ExecuteError::ChainConfigAlreadySet(_) + ) +} diff --git a/contracts/interchain-token-service/tests/utils/execute.rs b/contracts/interchain-token-service/tests/utils/execute.rs index 8dda5fbd5..77d1ba538 100644 --- a/contracts/interchain-token-service/tests/utils/execute.rs +++ b/contracts/interchain-token-service/tests/utils/execute.rs @@ -4,6 +4,7 @@ use axelar_core_std::nexus; use axelar_core_std::nexus::query::IsChainRegisteredResponse; use axelar_core_std::query::AxelarQueryMsg; use axelar_wasm_std::error::ContractError; +use axelar_wasm_std::nonempty; use cosmwasm_std::testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage}; use cosmwasm_std::{ from_json, to_json_binary, Addr, DepsMut, HexBinary, MemoryStorage, OwnedDeps, Response, @@ -58,6 +59,24 @@ pub fn deregister_its_contract( ) } +pub fn set_chain_config( + deps: DepsMut, + chain: ChainNameRaw, + max_uint: nonempty::Uint256, + max_target_decimals: u8, +) -> Result { + contract::execute( + deps, + mock_env(), + mock_info(params::GOVERNANCE, &[]), + ExecuteMsg::SetChainConfig { + chain, + max_uint, + max_target_decimals, + }, + ) +} + pub fn make_deps() -> OwnedDeps> { let addr = Addr::unchecked(params::GATEWAY); let mut deps = OwnedDeps { diff --git a/packages/axelar-wasm-std/src/nonempty/uint.rs b/packages/axelar-wasm-std/src/nonempty/uint.rs index 861fedaba..db52cb7d9 100644 --- a/packages/axelar-wasm-std/src/nonempty/uint.rs +++ b/packages/axelar-wasm-std/src/nonempty/uint.rs @@ -53,6 +53,7 @@ impl fmt::Display for Uint64 { // TODO: consider using macro for these types #[cw_serde] #[derive(Copy, PartialOrd, Eq, IntoInner)] +#[serde(try_from = "cosmwasm_std::Uint256")] pub struct Uint256(cosmwasm_std::Uint256); impl TryFrom for Uint256 { From a6952bbbee1d03159da2421561ede1a43bdd1c03 Mon Sep 17 00:00:00 2001 From: CJ Cobb <46455409+cjcobb23@users.noreply.github.com> Date: Wed, 30 Oct 2024 11:51:35 -0400 Subject: [PATCH 2/2] feat(minor-its-hub): add support for global freeze (#668) --- .../interchain-token-service/src/contract.rs | 14 +- .../src/contract/execute.rs | 139 +++++++++++++++++- .../interchain-token-service/src/events.rs | 4 + contracts/interchain-token-service/src/msg.rs | 6 + .../interchain-token-service/tests/execute.rs | 78 +++++++++- 5 files changed, 237 insertions(+), 4 deletions(-) diff --git a/contracts/interchain-token-service/src/contract.rs b/contracts/interchain-token-service/src/contract.rs index ee536d5ec..6424d8084 100644 --- a/contracts/interchain-token-service/src/contract.rs +++ b/contracts/interchain-token-service/src/contract.rs @@ -1,7 +1,7 @@ use std::fmt::Debug; use axelar_wasm_std::error::ContractError; -use axelar_wasm_std::{address, permission_control, FnExt, IntoContractError}; +use axelar_wasm_std::{address, killswitch, permission_control, FnExt, IntoContractError}; use axelarnet_gateway::AxelarExecutableMsg; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; @@ -35,6 +35,10 @@ pub enum Error { QueryItsContract, #[error("failed to query all its addresses")] QueryAllItsContracts, + #[error("failed to disable execution")] + DisableExecution, + #[error("failed to enable execution")] + EnableExecution, } #[cfg_attr(not(feature = "library"), entry_point)] @@ -70,6 +74,8 @@ pub fn instantiate( state::save_its_contract(deps.storage, chain, address)?; } + killswitch::init(deps.storage, killswitch::State::Disengaged)?; + Ok(Response::new().add_events( msg.its_contracts .into_iter() @@ -99,6 +105,12 @@ pub fn execute( execute::deregister_its_contract(deps, chain) .change_context(Error::DeregisterItsContract) } + ExecuteMsg::DisableExecution => { + execute::disable_execution(deps).change_context(Error::DisableExecution) + } + ExecuteMsg::EnableExecution => { + execute::enable_execution(deps).change_context(Error::EnableExecution) + } ExecuteMsg::SetChainConfig { chain, max_uint, diff --git a/contracts/interchain-token-service/src/contract/execute.rs b/contracts/interchain-token-service/src/contract/execute.rs index 0112882ce..f554ff3bd 100644 --- a/contracts/interchain-token-service/src/contract/execute.rs +++ b/contracts/interchain-token-service/src/contract/execute.rs @@ -1,4 +1,4 @@ -use axelar_wasm_std::{nonempty, FnExt, IntoContractError}; +use axelar_wasm_std::{killswitch, nonempty, FnExt, IntoContractError}; use cosmwasm_std::{DepsMut, HexBinary, QuerierWrapper, Response, Storage}; use error_stack::{bail, ensure, report, Result, ResultExt}; use router_api::{Address, ChainName, ChainNameRaw, CrossChainId}; @@ -21,12 +21,18 @@ pub enum Error { FailedItsContractRegistration(ChainNameRaw), #[error("failed to deregister its contract for chain {0}")] FailedItsContractDeregistration(ChainNameRaw), + #[error("failed to execute message")] + FailedExecuteMessage, + #[error("execution is currently disabled")] + ExecutionDisabled, #[error("chain config for {0} already set")] ChainConfigAlreadySet(ChainNameRaw), - #[error("invalid chain max uint")] + #[error("failed to load chain config for chain {0}")] LoadChainConfig(ChainNameRaw), #[error("failed to save chain config for chain {0}")] SaveChainConfig(ChainNameRaw), + #[error("state error")] + State, } /// Executes an incoming ITS message. @@ -40,6 +46,10 @@ pub fn execute_message( source_address: Address, payload: HexBinary, ) -> Result { + ensure!( + killswitch::is_contract_active(deps.storage), + Error::ExecutionDisabled + ); ensure_its_source_address(deps.storage, &cc_id.source_chain, &source_address)?; match HubMessage::abi_decode(&payload).change_context(Error::InvalidPayload)? { @@ -133,6 +143,14 @@ pub fn deregister_its_contract(deps: DepsMut, chain: ChainNameRaw) -> Result Result { + killswitch::engage(deps.storage, Event::ExecutionDisabled).change_context(Error::State) +} + +pub fn enable_execution(deps: DepsMut) -> Result { + killswitch::disengage(deps.storage, Event::ExecutionEnabled).change_context(Error::State) +} + pub fn set_chain_config( deps: DepsMut, chain: ChainNameRaw, @@ -148,3 +166,120 @@ pub fn set_chain_config( .then(|_| Ok(Response::new())), } } + +#[cfg(test)] +mod tests { + use assert_ok::assert_ok; + use axelar_wasm_std::msg_id::HexTxHashAndEventIndex; + use axelar_wasm_std::{assert_err_contains, killswitch, nonempty, permission_control}; + use cosmwasm_std::testing::{mock_dependencies, MockApi, MockQuerier}; + use cosmwasm_std::{Addr, HexBinary, MemoryStorage, OwnedDeps, Uint256}; + use router_api::{ChainNameRaw, CrossChainId}; + + use super::disable_execution; + use crate::contract::execute::{ + enable_execution, execute_message, register_its_contract, Error, + }; + use crate::state::{self, Config}; + use crate::{HubMessage, Message}; + + const SOLANA: &str = "solana"; + const ETHEREUM: &str = "ethereum"; + + const ITS_ADDRESS: &str = "68d30f47F19c07bCCEf4Ac7FAE2Dc12FCa3e0dC9"; + + const ADMIN: &str = "admin"; + const GOVERNANCE: &str = "governance"; + const AXELARNET_GATEWAY: &str = "axelarnet-gateway"; + + fn its_address() -> nonempty::HexBinary { + HexBinary::from_hex(ITS_ADDRESS) + .unwrap() + .try_into() + .unwrap() + } + + #[test] + fn should_be_able_to_disable_and_enable_execution() { + let mut deps = mock_dependencies(); + init(&mut deps); + + assert_ok!(disable_execution(deps.as_mut())); + + let msg = HubMessage::SendToHub { + destination_chain: ChainNameRaw::try_from(SOLANA).unwrap(), + message: Message::InterchainTransfer { + token_id: [7u8; 32].into(), + source_address: its_address(), + destination_address: its_address(), + amount: Uint256::one().try_into().unwrap(), + data: None, + }, + }; + let res = execute_message( + deps.as_mut(), + CrossChainId { + source_chain: ChainNameRaw::try_from(SOLANA).unwrap(), + message_id: HexTxHashAndEventIndex::new([1u8; 32], 0u32) + .to_string() + .try_into() + .unwrap(), + }, + ITS_ADDRESS.to_string().try_into().unwrap(), + msg.clone().abi_encode(), + ); + assert_err_contains!(res, Error, Error::ExecutionDisabled); + + assert_ok!(enable_execution(deps.as_mut())); + + assert_ok!(execute_message( + deps.as_mut(), + CrossChainId { + source_chain: ChainNameRaw::try_from(SOLANA).unwrap(), + message_id: HexTxHashAndEventIndex::new([1u8; 32], 0u32) + .to_string() + .try_into() + .unwrap(), + }, + ITS_ADDRESS.to_string().try_into().unwrap(), + msg.abi_encode(), + )); + } + + fn init(deps: &mut OwnedDeps) { + assert_ok!(permission_control::set_admin( + deps.as_mut().storage, + &Addr::unchecked(ADMIN) + )); + assert_ok!(permission_control::set_governance( + deps.as_mut().storage, + &Addr::unchecked(GOVERNANCE) + )); + + assert_ok!(state::save_config( + deps.as_mut().storage, + &Config { + axelarnet_gateway: Addr::unchecked(AXELARNET_GATEWAY), + }, + )); + + assert_ok!(killswitch::init( + deps.as_mut().storage, + killswitch::State::Disengaged + )); + let amplifier_chain = ChainNameRaw::try_from(SOLANA).unwrap(); + let core_chain = ChainNameRaw::try_from(ETHEREUM).unwrap(); + + assert_ok!(register_its_contract( + deps.as_mut(), + core_chain.clone(), + ITS_ADDRESS.to_string().try_into().unwrap(), + )); + + assert_ok!(register_its_contract( + deps.as_mut(), + amplifier_chain.clone(), + ITS_ADDRESS.to_string().try_into().unwrap(), + )); + } +} diff --git a/contracts/interchain-token-service/src/events.rs b/contracts/interchain-token-service/src/events.rs index db7ce05ca..6d98c9597 100644 --- a/contracts/interchain-token-service/src/events.rs +++ b/contracts/interchain-token-service/src/events.rs @@ -16,6 +16,8 @@ pub enum Event { ItsContractDeregistered { chain: ChainNameRaw, }, + ExecutionDisabled, + ExecutionEnabled, } impl From for cosmwasm_std::Event { @@ -35,6 +37,8 @@ impl From for cosmwasm_std::Event { cosmwasm_std::Event::new("its_contract_deregistered") .add_attribute("chain", chain.to_string()) } + Event::ExecutionDisabled => cosmwasm_std::Event::new("execution_disabled"), + Event::ExecutionEnabled => cosmwasm_std::Event::new("execution_enabled"), } } } diff --git a/contracts/interchain-token-service/src/msg.rs b/contracts/interchain-token-service/src/msg.rs index 8b570c1c1..30216ebf3 100644 --- a/contracts/interchain-token-service/src/msg.rs +++ b/contracts/interchain-token-service/src/msg.rs @@ -34,6 +34,12 @@ pub enum ExecuteMsg { /// The admin is allowed to remove the ITS address of a chain for emergencies. #[permission(Elevated)] DeregisterItsContract { chain: ChainNameRaw }, + + #[permission(Elevated)] + DisableExecution, + + #[permission(Elevated)] + EnableExecution, /// Set the chain configuration for a chain. #[permission(Governance)] SetChainConfig { diff --git a/contracts/interchain-token-service/tests/execute.rs b/contracts/interchain-token-service/tests/execute.rs index dd82596d3..e0524f8ea 100644 --- a/contracts/interchain-token-service/tests/execute.rs +++ b/contracts/interchain-token-service/tests/execute.rs @@ -11,7 +11,7 @@ use interchain_token_service::events::Event; use interchain_token_service::msg::ExecuteMsg; use interchain_token_service::{HubMessage, Message, TokenId, TokenManagerType}; use router_api::{Address, ChainName, ChainNameRaw, CrossChainId}; -use utils::{make_deps, TestMessage}; +use utils::{make_deps, params, TestMessage}; mod utils; @@ -317,6 +317,82 @@ fn execute_message_when_invalid_message_type_fails() { assert_err_contains!(result, ExecuteError, ExecuteError::InvalidMessageType); } +#[test] +fn disable_execution_when_not_admin_fails() { + let mut deps = mock_dependencies(); + utils::instantiate_contract(deps.as_mut()).unwrap(); + + let result = contract::execute( + deps.as_mut(), + mock_env(), + mock_info("not-admin", &[]), + ExecuteMsg::DisableExecution, + ); + assert_err_contains!( + result, + permission_control::Error, + permission_control::Error::PermissionDenied { .. } + ); +} + +#[test] +fn enable_execution_when_not_admin_fails() { + let mut deps = mock_dependencies(); + utils::instantiate_contract(deps.as_mut()).unwrap(); + + let result = contract::execute( + deps.as_mut(), + mock_env(), + mock_info("not-admin", &[]), + ExecuteMsg::EnableExecution, + ); + assert_err_contains!( + result, + permission_control::Error, + permission_control::Error::PermissionDenied { .. } + ); +} + +#[test] +fn admin_or_governance_can_enable_execution() { + let mut deps = mock_dependencies(); + utils::instantiate_contract(deps.as_mut()).unwrap(); + + assert_ok!(contract::execute( + deps.as_mut(), + mock_env(), + mock_info(params::ADMIN, &[]), + ExecuteMsg::EnableExecution + )); + + assert_ok!(contract::execute( + deps.as_mut(), + mock_env(), + mock_info(params::GOVERNANCE, &[]), + ExecuteMsg::EnableExecution + )); +} + +#[test] +fn admin_or_governance_can_disable_execution() { + let mut deps = mock_dependencies(); + utils::instantiate_contract(deps.as_mut()).unwrap(); + + assert_ok!(contract::execute( + deps.as_mut(), + mock_env(), + mock_info(params::ADMIN, &[]), + ExecuteMsg::DisableExecution + )); + + assert_ok!(contract::execute( + deps.as_mut(), + mock_env(), + mock_info(params::GOVERNANCE, &[]), + ExecuteMsg::DisableExecution + )); +} + #[test] fn set_chain_config_should_succeed() { let chain = "ethereum".parse().unwrap();