From 10cca04234b51e1af8bfba5603a8224d24b8dbd7 Mon Sep 17 00:00:00 2001 From: Haiyi Zhong Date: Thu, 10 Oct 2024 22:34:03 +0800 Subject: [PATCH] feat(minor-axelarnet-gateway): route messages to nexus --- contracts/axelarnet-gateway/Cargo.toml | 1 + contracts/axelarnet-gateway/src/contract.rs | 6 +- .../axelarnet-gateway/src/contract/execute.rs | 104 +++++-- contracts/axelarnet-gateway/tests/execute.rs | 105 ++++++- ...route_from_router_to_nexus_succeeds.golden | 256 ++++++++++++++++++ 5 files changed, 427 insertions(+), 45 deletions(-) create mode 100644 contracts/axelarnet-gateway/tests/testdata/route_from_router_to_nexus_succeeds.golden diff --git a/contracts/axelarnet-gateway/Cargo.toml b/contracts/axelarnet-gateway/Cargo.toml index e0a49c45e..db86e3a18 100644 --- a/contracts/axelarnet-gateway/Cargo.toml +++ b/contracts/axelarnet-gateway/Cargo.toml @@ -51,6 +51,7 @@ thiserror = { workspace = true } [dev-dependencies] assert_ok = { workspace = true } +axelar-core-std = { workspace = true, features = ["test"] } cw-multi-test = { workspace = true } goldie = { workspace = true } hex = { workspace = true } diff --git a/contracts/axelarnet-gateway/src/contract.rs b/contracts/axelarnet-gateway/src/contract.rs index c85fbdbdd..041c360a9 100644 --- a/contracts/axelarnet-gateway/src/contract.rs +++ b/contracts/axelarnet-gateway/src/contract.rs @@ -86,8 +86,10 @@ pub fn execute( }, ) .change_context(Error::CallContract), - ExecuteMsg::RouteMessages(msgs) => execute::route_messages(deps.storage, info.sender, msgs) - .change_context(Error::RouteMessages), + ExecuteMsg::RouteMessages(msgs) => { + execute::route_messages(deps.storage, deps.querier, info.sender, msgs) + .change_context(Error::RouteMessages) + } ExecuteMsg::Execute { cc_id, payload } => { execute::execute(deps, cc_id, payload).change_context(Error::Execute) } diff --git a/contracts/axelarnet-gateway/src/contract/execute.rs b/contracts/axelarnet-gateway/src/contract/execute.rs index 8047eff37..621f8966b 100644 --- a/contracts/axelarnet-gateway/src/contract/execute.rs +++ b/contracts/axelarnet-gateway/src/contract/execute.rs @@ -7,7 +7,8 @@ use axelar_wasm_std::token::GetToken; use axelar_wasm_std::{address, FnExt, IntoContractError}; use cosmwasm_schema::cw_serde; use cosmwasm_std::{ - Addr, BankMsg, Coin, DepsMut, HexBinary, MessageInfo, QuerierWrapper, Response, Storage, + Addr, BankMsg, Coin, CosmosMsg, DepsMut, HexBinary, MessageInfo, QuerierWrapper, Response, + Storage, }; use error_stack::{bail, ensure, report, ResultExt}; use itertools::Itertools; @@ -24,8 +25,8 @@ use crate::{state, AxelarExecutableMsg}; pub enum Error { #[error("failed to save executable message")] SaveExecutableMessage, - #[error("failed to access executable message")] - ExecutableMessageAccess, + #[error("failed to access routable message")] + RoutableMessageAccess, #[error("message with ID {0} does not match the expected message")] MessageMismatch(CrossChainId), #[error("failed to mark message with ID {0} as executed")] @@ -79,6 +80,7 @@ impl CallContractData { enum RoutingDestination { Nexus, Router, + Axelarnet, } type Result = error_stack::Result; @@ -97,7 +99,7 @@ pub fn call_contract( let client: nexus::Client = client::CosmosClient::new(querier).into(); - let id = unique_cross_chain_id(&client, chain_name)?; + let id = unique_cross_chain_id(&client, chain_name.clone())?; let source_address = Address::from_str(info.sender.as_str()) .change_context(Error::InvalidSourceAddress(info.sender.clone()))?; let msg = call_contract.to_message(id, source_address); @@ -113,8 +115,10 @@ pub fn call_contract( token: token.clone(), }; - let res = match determine_routing_destination(&client, &msg.destination_chain)? { - RoutingDestination::Nexus => route_to_nexus(&client, nexus, msg, token)?, + let res = match determine_routing_destination(&client, &msg.destination_chain, &chain_name)? { + RoutingDestination::Nexus => { + Response::new().add_messages(route_to_nexus(&client, &nexus, msg, token)?) + } RoutingDestination::Router if token.is_none() => { route_to_router(storage, &Router::new(router), vec![msg])? } @@ -127,20 +131,46 @@ pub fn call_contract( pub fn route_messages( storage: &mut dyn Storage, + querier: QuerierWrapper, sender: Addr, msgs: Vec, ) -> Result> { let Config { - chain_name, router, .. + chain_name, + router, + nexus, } = state::load_config(storage); + let router = Router::new(router); + let client: nexus::Client = client::CosmosClient::new(querier).into(); - if sender == router.address { - Ok(prepare_msgs_for_execution(storage, chain_name, msgs)?) - } else { - // Messages initiated via call contract can be routed again - Ok(route_to_router(storage, &router, msgs)?) - } + msgs.iter() + .group_by(|msg| msg.destination_chain.to_owned()) + .into_iter() + .try_fold(Response::new(), |mut acc, (dest_chain, msgs)| { + let response = match determine_routing_destination(&client, &dest_chain, &chain_name)? { + // Allow re-routing of CallContract initiated messages + RoutingDestination::Router if sender != router.address => { + route_to_router(storage, &router, msgs.collect()) + } + // Ensure only router routes to Axelarnet + RoutingDestination::Axelarnet => { + ensure!(sender == router.address, Error::InvalidRoutingDestination); + prepare_msgs_for_execution(storage, chain_name.clone(), msgs.collect()) + } + // Ensure only router routes to Nexus + RoutingDestination::Nexus => { + ensure!(sender == router.address, Error::InvalidRoutingDestination); + route_messages_to_nexus(&client, &nexus, msgs.collect()) + } + _ => bail!(Error::InvalidRoutingDestination), + }?; + + acc.messages.extend(response.messages); + acc.events.extend(response.events); + + Ok(acc) + }) } pub fn execute( @@ -228,7 +258,7 @@ fn route_to_router( let msgs: Vec<_> = msgs .into_iter() .unique() - .map(|msg| try_load_executable_msg(store, msg)) + .map(|msg| try_load_routable_msg(store, msg)) .filter_map_ok(|msg| msg) .try_collect()?; @@ -242,9 +272,9 @@ fn route_to_router( /// Verify that the message is stored and matches the one we're trying to route. Returns Ok(None) if /// the message is not stored. -fn try_load_executable_msg(store: &mut dyn Storage, msg: Message) -> Result> { +fn try_load_routable_msg(store: &mut dyn Storage, msg: Message) -> Result> { let stored_msg = state::may_load_routable_msg(store, &msg.cc_id) - .change_context(Error::ExecutableMessageAccess)?; + .change_context(Error::RoutableMessageAccess)?; match stored_msg { Some(stored_msg) if stored_msg != msg => { @@ -273,26 +303,29 @@ fn unique_cross_chain_id(client: &nexus::Client, chain_name: ChainName) -> Resul /// Query Nexus module in core to decide should route message to core fn determine_routing_destination( client: &nexus::Client, - name: &ChainName, + dest_chain: &ChainName, + axelar_chain: &ChainName, ) -> Result { - let dest = match client - .is_chain_registered(name) - .change_context(Error::Nexus)? - { - true => RoutingDestination::Nexus, - false => RoutingDestination::Router, - }; - - Ok(dest) + Ok(match dest_chain { + dest_chain if dest_chain == axelar_chain => RoutingDestination::Axelarnet, + dest_chain + if client + .is_chain_registered(dest_chain) + .change_context(Error::Nexus)? => + { + RoutingDestination::Nexus + } + _ => RoutingDestination::Router, + }) } /// Route message to the Nexus module fn route_to_nexus( client: &nexus::Client, - nexus: Addr, + nexus: &Addr, msg: Message, token: Option, -) -> Result> { +) -> Result>> { let msg: nexus::execute::Message = (msg, token.clone()).into(); token @@ -304,6 +337,19 @@ fn route_to_nexus( .into_iter() .chain(iter::once(client.route_message(msg))) .collect::>() - .then(|msgs| Response::new().add_messages(msgs)) .then(Ok) } + +pub fn route_messages_to_nexus( + client: &nexus::Client, + nexus: &Addr, + msgs: Vec, +) -> Result> { + let msgs = msgs + .into_iter() + .map(|msg| route_to_nexus(client, nexus, msg, None)) + .collect::>>()? + .then(|msgs| msgs.concat()); + + Ok(Response::new().add_messages(msgs)) +} diff --git a/contracts/axelarnet-gateway/tests/execute.rs b/contracts/axelarnet-gateway/tests/execute.rs index fe8cc957b..de114f055 100644 --- a/contracts/axelarnet-gateway/tests/execute.rs +++ b/contracts/axelarnet-gateway/tests/execute.rs @@ -1,4 +1,5 @@ use assert_ok::assert_ok; +use axelar_core_std::nexus::test_utils::reply_with_is_chain_registered; use axelar_wasm_std::assert_err_contains; use axelar_wasm_std::response::inspect_response_msg; use axelarnet_gateway::contract::ExecuteError; @@ -100,7 +101,10 @@ fn execute_approved_message_once_returns_correct_events() { #[test] fn route_from_router_with_destination_chain_not_matching_contract_fails() { - let mut deps = mock_dependencies(); + let mut deps = mock_axelar_dependencies(); + deps.querier = deps + .querier + .with_custom_handler(reply_with_is_chain_registered(false)); let msg = messages::dummy_from_router(&[1, 2, 3]); let msg_with_wrong_destination = Message { @@ -108,54 +112,127 @@ fn route_from_router_with_destination_chain_not_matching_contract_fails() { ..msg }; - utils::instantiate_contract(deps.as_mut()).unwrap(); + utils::instantiate_contract(deps.as_default_mut()).unwrap(); assert_err_contains!( - utils::route_from_router(deps.as_mut(), vec![msg_with_wrong_destination]), + utils::route_from_router(deps.as_default_mut(), vec![msg_with_wrong_destination]), ExecuteError, - ExecuteError::InvalidDestination { .. } + ExecuteError::InvalidRoutingDestination, ); } #[test] fn route_from_router_same_message_multiple_times_succeeds() { - let mut deps = mock_dependencies(); + let mut deps = mock_axelar_dependencies(); + deps.querier = deps + .querier + .with_custom_handler(reply_with_is_chain_registered(false)); let msgs = vec![messages::dummy_from_router(&[1, 2, 3])]; - utils::instantiate_contract(deps.as_mut()).unwrap(); + utils::instantiate_contract(deps.as_default_mut()).unwrap(); - let response = assert_ok!(utils::route_from_router(deps.as_mut(), msgs)); + let response = assert_ok!(utils::route_from_router(deps.as_default_mut(), msgs)); goldie::assert_json!(response); } #[test] fn route_from_router_multiple_times_with_data_mismatch_fails() { - let mut deps = mock_dependencies(); + let mut deps = mock_axelar_dependencies(); + deps.querier = deps + .querier + .with_custom_handler(reply_with_is_chain_registered(false)); let mut msgs = vec![messages::dummy_from_router(&[1, 2, 3])]; - utils::instantiate_contract(deps.as_mut()).unwrap(); - utils::route_from_router(deps.as_mut(), msgs.clone()).unwrap(); + utils::instantiate_contract(deps.as_default_mut()).unwrap(); + utils::route_from_router(deps.as_default_mut(), msgs.clone()).unwrap(); msgs[0].source_address = "wrong-address".parse().unwrap(); assert_err_contains!( - utils::route_from_router(deps.as_mut(), msgs), + utils::route_from_router(deps.as_default_mut(), msgs), StateError, StateError::MessageMismatch(..) ); } +#[test] +fn route_to_nexus_from_non_router_sender_fails() { + let mut deps = mock_axelar_dependencies(); + deps.querier = deps + .querier + .with_custom_handler(reply_with_is_chain_registered(true)); + + let mut msg = messages::dummy_from_router(&[1, 2, 3]); + msg.destination_chain = "legacy-chain".parse().unwrap(); + + utils::instantiate_contract(deps.as_default_mut()).unwrap(); + assert_err_contains!( + utils::route_to_router(deps.as_default_mut(), vec![msg]), + ExecuteError, + ExecuteError::InvalidRoutingDestination, + ); +} + +#[test] +fn route_to_axelarnet_from_non_router_sender_fails() { + let mut deps = mock_axelar_dependencies(); + deps.querier = deps + .querier + .with_custom_handler(reply_with_is_chain_registered(false)); + + let msgs = vec![messages::dummy_from_router(&[1, 2, 3])]; + + utils::instantiate_contract(deps.as_default_mut()).unwrap(); + assert_err_contains!( + utils::route_to_router(deps.as_default_mut(), msgs), + ExecuteError, + ExecuteError::InvalidRoutingDestination, + ); +} + +#[test] +fn route_from_router_to_nexus_succeeds() { + let mut deps = mock_axelar_dependencies(); + deps.querier = deps + .querier + .with_custom_handler(reply_with_is_chain_registered(true)); + + let msg = messages::dummy_from_router(&[1, 2, 3]); + let msgs = vec![ + Message { + destination_chain: "legacy-chain-1".parse().unwrap(), + ..msg.clone() + }, + Message { + destination_chain: "legacy-chain-2".parse().unwrap(), + ..msg.clone() + }, + Message { + destination_chain: "legacy-chain-2".parse().unwrap(), + ..msg + }, + ]; + + utils::instantiate_contract(deps.as_default_mut()).unwrap(); + + let response = assert_ok!(utils::route_from_router(deps.as_default_mut(), msgs)); + goldie::assert_json!(response); +} + #[test] fn route_to_router_without_contract_call_ignores_message() { - let mut deps = mock_dependencies(); + let mut deps = mock_axelar_dependencies(); + deps.querier = deps + .querier + .with_custom_handler(reply_with_is_chain_registered(false)); let msg = messages::dummy_to_router(&vec![1, 2, 3]); - utils::instantiate_contract(deps.as_mut()).unwrap(); + utils::instantiate_contract(deps.as_default_mut()).unwrap(); - let response = assert_ok!(utils::route_to_router(deps.as_mut(), vec![msg])); + let response = assert_ok!(utils::route_to_router(deps.as_default_mut(), vec![msg])); assert_eq!(response.messages.len(), 0); } diff --git a/contracts/axelarnet-gateway/tests/testdata/route_from_router_to_nexus_succeeds.golden b/contracts/axelarnet-gateway/tests/testdata/route_from_router_to_nexus_succeeds.golden new file mode 100644 index 000000000..05c707de9 --- /dev/null +++ b/contracts/axelarnet-gateway/tests/testdata/route_from_router_to_nexus_succeeds.golden @@ -0,0 +1,256 @@ +{ + "messages": [ + { + "id": 0, + "msg": { + "custom": { + "source_chain": "source-chain", + "source_address": "source-address", + "destination_chain": "legacy-chain-1", + "destination_address": "destination-address", + "payload_hash": [ + 241, + 136, + 94, + 218, + 84, + 183, + 160, + 83, + 49, + 140, + 212, + 30, + 32, + 147, + 34, + 13, + 171, + 21, + 214, + 83, + 129, + 177, + 21, + 122, + 54, + 51, + 168, + 59, + 253, + 92, + 146, + 57 + ], + "source_tx_id": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "source_tx_index": 0, + "id": "hash-index" + } + }, + "gas_limit": null, + "reply_on": "never" + }, + { + "id": 0, + "msg": { + "custom": { + "source_chain": "source-chain", + "source_address": "source-address", + "destination_chain": "legacy-chain-2", + "destination_address": "destination-address", + "payload_hash": [ + 241, + 136, + 94, + 218, + 84, + 183, + 160, + 83, + 49, + 140, + 212, + 30, + 32, + 147, + 34, + 13, + 171, + 21, + 214, + 83, + 129, + 177, + 21, + 122, + 54, + 51, + 168, + 59, + 253, + 92, + 146, + 57 + ], + "source_tx_id": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "source_tx_index": 0, + "id": "hash-index" + } + }, + "gas_limit": null, + "reply_on": "never" + }, + { + "id": 0, + "msg": { + "custom": { + "source_chain": "source-chain", + "source_address": "source-address", + "destination_chain": "legacy-chain-2", + "destination_address": "destination-address", + "payload_hash": [ + 241, + 136, + 94, + 218, + 84, + 183, + 160, + 83, + 49, + 140, + 212, + 30, + 32, + 147, + 34, + 13, + 171, + 21, + 214, + 83, + 129, + 177, + 21, + 122, + 54, + 51, + 168, + 59, + 253, + 92, + 146, + 57 + ], + "source_tx_id": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "source_tx_index": 0, + "id": "hash-index" + } + }, + "gas_limit": null, + "reply_on": "never" + } + ], + "attributes": [], + "events": [], + "data": null +} \ No newline at end of file