From 09747847aab0e4a1cd81c144a10147db579a78eb Mon Sep 17 00:00:00 2001 From: CJ Cobb Date: Tue, 29 Oct 2024 13:41:20 -0400 Subject: [PATCH] feat(minor-interchain-token-service): freeze/unfreeze * Add support for freezing and unfreezing specific chains --- .../interchain-token-service/src/contract.rs | 13 +- .../src/contract/execute.rs | 224 +++++++++++++++++- .../src/contract/query.rs | 28 ++- contracts/interchain-token-service/src/msg.rs | 6 + .../interchain-token-service/src/state.rs | 40 +++- .../interchain-token-service/tests/execute.rs | 90 ++++++- 6 files changed, 384 insertions(+), 17 deletions(-) diff --git a/contracts/interchain-token-service/src/contract.rs b/contracts/interchain-token-service/src/contract.rs index 503ea1539..e35436188 100644 --- a/contracts/interchain-token-service/src/contract.rs +++ b/contracts/interchain-token-service/src/contract.rs @@ -7,6 +7,7 @@ use axelarnet_gateway::AxelarExecutableMsg; use cosmwasm_std::entry_point; use cosmwasm_std::{Addr, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, Storage}; use error_stack::{Report, ResultExt}; +use execute::{freeze_chain, unfreeze_chain}; use crate::events::Event; use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; @@ -29,8 +30,10 @@ 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 freeze chain")] + FreezeChain, + #[error("failed to unfreeze chain")] + UnfreezeChain, #[error("failed to query its address")] QueryItsContract, #[error("failed to query all its addresses")] @@ -99,6 +102,12 @@ pub fn execute( execute::deregister_its_contract(deps, chain) .change_context(Error::DeregisterItsContract) } + ExecuteMsg::FreezeChain { chain } => { + freeze_chain(deps, chain).change_context(Error::FreezeChain) + } + ExecuteMsg::UnfreezeChain { chain } => { + unfreeze_chain(deps, chain).change_context(Error::UnfreezeChain) + } }? .then(Ok) } diff --git a/contracts/interchain-token-service/src/contract/execute.rs b/contracts/interchain-token-service/src/contract/execute.rs index 11f3e0558..ba8b1e14a 100644 --- a/contracts/interchain-token-service/src/contract/execute.rs +++ b/contracts/interchain-token-service/src/contract/execute.rs @@ -5,7 +5,7 @@ use router_api::{Address, ChainName, ChainNameRaw, CrossChainId}; use crate::events::Event; use crate::primitives::HubMessage; -use crate::state::{self, load_config, load_its_contract}; +use crate::state::{self, is_chain_frozen, load_config, load_its_contract}; #[derive(thiserror::Error, Debug, IntoContractError)] pub enum Error { @@ -27,6 +27,12 @@ pub enum Error { NexusQueryError, #[error("storage error")] StorageError, + #[error("failed to freeze chain")] + FailedToFreezeChain, + #[error("failed to unfreeze chain")] + FailedToUnfreezeChain, + #[error("chain {0} is frozen")] + ChainFrozen(ChainNameRaw), } /// Executes an incoming ITS message. @@ -41,12 +47,22 @@ pub fn execute_message( payload: HexBinary, ) -> Result { ensure_its_source_address(deps.storage, &cc_id.source_chain, &source_address)?; + ensure!( + !is_chain_frozen(deps.storage, &cc_id.source_chain) + .change_context(Error::FailedExecuteMessage)?, + Error::ChainFrozen(cc_id.source_chain) + ); match HubMessage::abi_decode(&payload).change_context(Error::InvalidPayload)? { HubMessage::SendToHub { destination_chain, message, } => { + ensure!( + !is_chain_frozen(deps.storage, &destination_chain) + .change_context(Error::FailedExecuteMessage)?, + Error::ChainFrozen(destination_chain) + ); let destination_address = load_its_contract(deps.storage, &destination_chain) .change_context_lazy(|| Error::UnknownChain(destination_chain.clone()))?; @@ -132,3 +148,209 @@ pub fn deregister_its_contract(deps: DepsMut, chain: ChainNameRaw) -> Result Result { + state::freeze_chain(deps.storage, &chain).change_context(Error::FailedToFreezeChain)?; + + Ok(Response::new()) +} + +pub fn unfreeze_chain(deps: DepsMut, chain: ChainNameRaw) -> Result { + state::unfreeze_chain(deps.storage, &chain); + + 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 crate::contract::execute::{ + execute_message, freeze_chain, register_its_contract, unfreeze_chain, Error, + }; + use crate::state::{self, Config}; + use crate::{HubMessage, Message}; + + const SOLANA: &str = "solana"; + const ETHEREUM: &str = "ethereum"; + const XRPL: &str = "xrpl"; + + 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 execution_should_fail_if_source_chain_is_frozen() { + let mut deps = mock_dependencies(); + init(&mut deps); + + let source_chain = ChainNameRaw::try_from(SOLANA).unwrap(); + let destination_chain = ChainNameRaw::try_from(ETHEREUM).unwrap(); + + assert_ok!(freeze_chain(deps.as_mut(), source_chain.clone())); + + let msg = HubMessage::SendToHub { + destination_chain, + 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: source_chain.clone(), + 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::ChainFrozen(..)); + + assert_ok!(unfreeze_chain(deps.as_mut(), source_chain.clone())); + assert_ok!(execute_message( + deps.as_mut(), + CrossChainId { + source_chain, + message_id: HexTxHashAndEventIndex::new([1u8; 32], 0u32) + .to_string() + .try_into() + .unwrap(), + }, + ITS_ADDRESS.to_string().try_into().unwrap(), + msg.clone().abi_encode(), + )); + } + + #[test] + fn execution_should_fail_if_destination_chain_is_frozen() { + let mut deps = mock_dependencies(); + init(&mut deps); + + let source_chain = ChainNameRaw::try_from(SOLANA).unwrap(); + let destination_chain = ChainNameRaw::try_from(ETHEREUM).unwrap(); + + assert_ok!(freeze_chain(deps.as_mut(), destination_chain.clone())); + + let msg = HubMessage::SendToHub { + destination_chain: destination_chain.clone(), + 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 cc_id = CrossChainId { + source_chain, + message_id: HexTxHashAndEventIndex::new([1u8; 32], 0u32) + .to_string() + .try_into() + .unwrap(), + }; + let res = execute_message( + deps.as_mut(), + cc_id.clone(), + ITS_ADDRESS.to_string().try_into().unwrap(), + msg.clone().abi_encode(), + ); + assert_err_contains!(res, Error, Error::ChainFrozen(..)); + assert_ok!(unfreeze_chain(deps.as_mut(), destination_chain)); + + assert_ok!(execute_message( + deps.as_mut(), + cc_id, + ITS_ADDRESS.to_string().try_into().unwrap(), + msg.clone().abi_encode(), + )); + } + + #[test] + fn frozen_chain_that_is_not_source_or_destination_should_not_affect_execution() { + let mut deps = mock_dependencies(); + init(&mut deps); + + let source_chain = ChainNameRaw::try_from(SOLANA).unwrap(); + let destination_chain = ChainNameRaw::try_from(ETHEREUM).unwrap(); + let other_chain = ChainNameRaw::try_from(XRPL).unwrap(); + + assert_ok!(freeze_chain(deps.as_mut(), other_chain.clone())); + + let msg = HubMessage::SendToHub { + destination_chain: destination_chain.clone(), + 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 cc_id = CrossChainId { + source_chain, + message_id: HexTxHashAndEventIndex::new([1u8; 32], 0u32) + .to_string() + .try_into() + .unwrap(), + }; + assert_ok!(execute_message( + deps.as_mut(), + cc_id.clone(), + ITS_ADDRESS.to_string().try_into().unwrap(), + msg.clone().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 + )); + + for chain_name in [SOLANA, ETHEREUM, XRPL] { + let chain = ChainNameRaw::try_from(chain_name).unwrap(); + assert_ok!(register_its_contract( + deps.as_mut(), + chain.clone(), + ITS_ADDRESS.to_string().try_into().unwrap(), + )); + } + } +} diff --git a/contracts/interchain-token-service/src/contract/query.rs b/contracts/interchain-token-service/src/contract/query.rs index f9aeeea0e..e35928735 100644 --- a/contracts/interchain-token-service/src/contract/query.rs +++ b/contracts/interchain-token-service/src/contract/query.rs @@ -1,14 +1,28 @@ +use axelar_wasm_std::IntoContractError; use cosmwasm_std::{to_json_binary, Binary, Deps}; +use error_stack::{Result, ResultExt}; use router_api::ChainNameRaw; -use crate::state; +use crate::state::{load_all_its_contracts, may_load_its_contract}; -pub fn its_contracts(deps: Deps, chain: ChainNameRaw) -> Result { - let contract_address = state::may_load_its_contract(deps.storage, &chain)?; - Ok(to_json_binary(&contract_address)?) +#[derive(thiserror::Error, Debug, IntoContractError)] +pub enum Error { + #[error("failed to load ITS contract")] + ItsContract, + #[error("failed to load all ITS contracts")] + AllItsContracts, + #[error("failed to serialize data to JSON")] + JsonSerializationError, } -pub fn all_its_contracts(deps: Deps) -> Result { - let contract_addresses = state::load_all_its_contracts(deps.storage)?; - Ok(to_json_binary(&contract_addresses)?) +pub fn its_contracts(deps: Deps, chain: ChainNameRaw) -> Result { + let contract_address = + may_load_its_contract(deps.storage, &chain).change_context(Error::ItsContract)?; + to_json_binary(&contract_address).change_context(Error::JsonSerializationError) +} + +pub fn all_its_contracts(deps: Deps) -> Result { + let contract_addresses = + load_all_its_contracts(deps.storage).change_context(Error::AllItsContracts)?; + to_json_binary(&contract_addresses).change_context(Error::JsonSerializationError) } diff --git a/contracts/interchain-token-service/src/msg.rs b/contracts/interchain-token-service/src/msg.rs index c9ff32b55..1612d2892 100644 --- a/contracts/interchain-token-service/src/msg.rs +++ b/contracts/interchain-token-service/src/msg.rs @@ -33,6 +33,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)] + FreezeChain { chain: ChainNameRaw }, + + #[permission(Elevated)] + UnfreezeChain { chain: ChainNameRaw }, } #[cw_serde] diff --git a/contracts/interchain-token-service/src/state.rs b/contracts/interchain-token-service/src/state.rs index c977ad5fa..415451aff 100644 --- a/contracts/interchain-token-service/src/state.rs +++ b/contracts/interchain-token-service/src/state.rs @@ -4,6 +4,7 @@ use axelar_wasm_std::{nonempty, IntoContractError}; use cosmwasm_schema::cw_serde; use cosmwasm_std::{ensure, Addr, StdError, Storage}; use cw_storage_plus::{Item, Map}; +use error_stack::{report, Result, ResultExt}; use router_api::{Address, ChainNameRaw}; #[derive(thiserror::Error, Debug, IntoContractError)] @@ -18,6 +19,8 @@ pub enum Error { ItsContractAlreadyRegistered(ChainNameRaw), #[error("gateway token already registered {0}")] GatewayTokenAlreadyRegistered(nonempty::String), + #[error("storage error")] + Storage, } #[cw_serde] @@ -27,6 +30,7 @@ pub struct Config { const CONFIG: Item = Item::new("config"); const ITS_CONTRACTS: Map<&ChainNameRaw, Address> = Map::new("its_contracts"); +const FROZEN_CHAINS: Map<&ChainNameRaw, ()> = Map::new("frozen_chains"); pub fn load_config(storage: &dyn Storage) -> Config { CONFIG @@ -35,18 +39,22 @@ pub fn load_config(storage: &dyn Storage) -> Config { } pub fn save_config(storage: &mut dyn Storage, config: &Config) -> Result<(), Error> { - Ok(CONFIG.save(storage, config)?) + CONFIG.save(storage, config).change_context(Error::Storage) } pub fn may_load_its_contract( storage: &dyn Storage, chain: &ChainNameRaw, ) -> Result, Error> { - Ok(ITS_CONTRACTS.may_load(storage, chain)?) + ITS_CONTRACTS + .may_load(storage, chain) + .change_context(Error::Storage) } pub fn load_its_contract(storage: &dyn Storage, chain: &ChainNameRaw) -> Result { - may_load_its_contract(storage, chain)?.ok_or_else(|| Error::ItsContractNotFound(chain.clone())) + may_load_its_contract(storage, chain) + .change_context(Error::Storage)? + .ok_or_else(|| report!(Error::ItsContractNotFound(chain.clone()))) } pub fn save_its_contract( @@ -59,7 +67,9 @@ pub fn save_its_contract( Error::ItsContractAlreadyRegistered(chain.clone()) ); - Ok(ITS_CONTRACTS.save(storage, chain, address)?) + ITS_CONTRACTS + .save(storage, chain, address) + .change_context(Error::Storage) } pub fn remove_its_contract(storage: &mut dyn Storage, chain: &ChainNameRaw) -> Result<(), Error> { @@ -76,9 +86,27 @@ pub fn remove_its_contract(storage: &mut dyn Storage, chain: &ChainNameRaw) -> R pub fn load_all_its_contracts( storage: &dyn Storage, ) -> Result, Error> { - Ok(ITS_CONTRACTS + ITS_CONTRACTS .range(storage, None, None, cosmwasm_std::Order::Ascending) - .collect::, _>>()?) + .map(|res| res.change_context(Error::Storage)) + .collect::, _>>() +} + +pub fn is_chain_frozen(storage: &dyn Storage, chain: &ChainNameRaw) -> Result { + FROZEN_CHAINS + .may_load(storage, chain) + .change_context(Error::Storage) + .map(|res| res.is_some()) +} + +pub fn freeze_chain(storage: &mut dyn Storage, chain: &ChainNameRaw) -> Result<(), Error> { + FROZEN_CHAINS + .save(storage, chain, &()) + .change_context(Error::Storage) +} + +pub fn unfreeze_chain(storage: &mut dyn Storage, chain: &ChainNameRaw) { + FROZEN_CHAINS.remove(storage, chain) } #[cfg(test)] diff --git a/contracts/interchain-token-service/tests/execute.rs b/contracts/interchain-token-service/tests/execute.rs index 368d60d39..ab43ae68f 100644 --- a/contracts/interchain-token-service/tests/execute.rs +++ b/contracts/interchain-token-service/tests/execute.rs @@ -9,7 +9,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; @@ -314,3 +314,91 @@ fn execute_message_when_invalid_message_type_fails() { ); assert_err_contains!(result, ExecuteError, ExecuteError::InvalidMessageType); } + +#[test] +fn freeze_chain_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::FreezeChain { + chain: ChainNameRaw::try_from("ethereum").unwrap(), + }, + ); + assert_err_contains!( + result, + permission_control::Error, + permission_control::Error::PermissionDenied { .. } + ); +} + +#[test] +fn unfreeze_chain_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::UnfreezeChain { + chain: ChainNameRaw::try_from("ethereum").unwrap(), + }, + ); + assert_err_contains!( + result, + permission_control::Error, + permission_control::Error::PermissionDenied { .. } + ); +} + +#[test] +fn admin_or_governance_can_freeze_chain() { + 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::FreezeChain { + chain: ChainNameRaw::try_from("ethereum").unwrap() + } + )); + + assert_ok!(contract::execute( + deps.as_mut(), + mock_env(), + mock_info(params::GOVERNANCE, &[]), + ExecuteMsg::FreezeChain { + chain: ChainNameRaw::try_from("ethereum").unwrap() + } + )); +} + +#[test] +fn admin_or_governance_can_unfreeze_chain() { + 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::UnfreezeChain { + chain: ChainNameRaw::try_from("ethereum").unwrap() + } + )); + + assert_ok!(contract::execute( + deps.as_mut(), + mock_env(), + mock_info(params::GOVERNANCE, &[]), + ExecuteMsg::UnfreezeChain { + chain: ChainNameRaw::try_from("ethereum").unwrap() + } + )); +}