Skip to content

Commit

Permalink
feat(minor-axelarnet-gateway): route messages to nexus
Browse files Browse the repository at this point in the history
  • Loading branch information
haiyizxx committed Oct 10, 2024
1 parent 7ea208f commit 10cca04
Show file tree
Hide file tree
Showing 5 changed files with 427 additions and 45 deletions.
1 change: 1 addition & 0 deletions contracts/axelarnet-gateway/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
6 changes: 4 additions & 2 deletions contracts/axelarnet-gateway/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
104 changes: 75 additions & 29 deletions contracts/axelarnet-gateway/src/contract/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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")]
Expand Down Expand Up @@ -79,6 +80,7 @@ impl CallContractData {
enum RoutingDestination {
Nexus,
Router,
Axelarnet,
}

type Result<T> = error_stack::Result<T, Error>;
Expand All @@ -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);
Expand All @@ -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])?
}
Expand All @@ -127,20 +131,46 @@ pub fn call_contract(

pub fn route_messages(
storage: &mut dyn Storage,
querier: QuerierWrapper,
sender: Addr,
msgs: Vec<Message>,
) -> Result<Response<nexus::execute::Message>> {
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())

Check failure on line 154 in contracts/axelarnet-gateway/src/contract/execute.rs

View workflow job for this annotation

GitHub Actions / Test Suite

a value of type `std::vec::Vec<router_api::Message>` cannot be built from an iterator over elements of type `&router_api::Message`

Check failure on line 154 in contracts/axelarnet-gateway/src/contract/execute.rs

View workflow job for this annotation

GitHub Actions / Lints

a value of type `std::vec::Vec<router_api::Message>` cannot be built from an iterator over elements of type `&router_api::Message`
}
// Ensure only router routes to Axelarnet
RoutingDestination::Axelarnet => {
ensure!(sender == router.address, Error::InvalidRoutingDestination);
prepare_msgs_for_execution(storage, chain_name.clone(), msgs.collect())

Check failure on line 159 in contracts/axelarnet-gateway/src/contract/execute.rs

View workflow job for this annotation

GitHub Actions / Test Suite

a value of type `std::vec::Vec<router_api::Message>` cannot be built from an iterator over elements of type `&router_api::Message`

Check failure on line 159 in contracts/axelarnet-gateway/src/contract/execute.rs

View workflow job for this annotation

GitHub Actions / Lints

a value of type `std::vec::Vec<router_api::Message>` cannot be built from an iterator over elements of type `&router_api::Message`
}
// Ensure only router routes to Nexus
RoutingDestination::Nexus => {
ensure!(sender == router.address, Error::InvalidRoutingDestination);
route_messages_to_nexus(&client, &nexus, msgs.collect())

Check failure on line 164 in contracts/axelarnet-gateway/src/contract/execute.rs

View workflow job for this annotation

GitHub Actions / Test Suite

a value of type `std::vec::Vec<router_api::Message>` cannot be built from an iterator over elements of type `&router_api::Message`

Check failure on line 164 in contracts/axelarnet-gateway/src/contract/execute.rs

View workflow job for this annotation

GitHub Actions / Lints

a value of type `std::vec::Vec<router_api::Message>` cannot be built from an iterator over elements of type `&router_api::Message`
}
_ => bail!(Error::InvalidRoutingDestination),
}?;

acc.messages.extend(response.messages);
acc.events.extend(response.events);

Ok(acc)
})
}

pub fn execute(
Expand Down Expand Up @@ -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()?;

Expand All @@ -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<Option<Message>> {
fn try_load_routable_msg(store: &mut dyn Storage, msg: Message) -> Result<Option<Message>> {
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 => {
Expand Down Expand Up @@ -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<RoutingDestination> {
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<Coin>,
) -> Result<Response<nexus::execute::Message>> {
) -> Result<Vec<CosmosMsg<nexus::execute::Message>>> {
let msg: nexus::execute::Message = (msg, token.clone()).into();

token
Expand All @@ -304,6 +337,19 @@ fn route_to_nexus(
.into_iter()
.chain(iter::once(client.route_message(msg)))
.collect::<Vec<_>>()
.then(|msgs| Response::new().add_messages(msgs))
.then(Ok)
}

pub fn route_messages_to_nexus(
client: &nexus::Client,
nexus: &Addr,
msgs: Vec<Message>,
) -> Result<Response<nexus::execute::Message>> {
let msgs = msgs
.into_iter()
.map(|msg| route_to_nexus(client, nexus, msg, None))
.collect::<Result<Vec<_>>>()?
.then(|msgs| msgs.concat());

Ok(Response::new().add_messages(msgs))
}
105 changes: 91 additions & 14 deletions contracts/axelarnet-gateway/tests/execute.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -100,62 +101,138 @@ 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 {
destination_chain: "wrong-chain".parse().unwrap(),
..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);
}

Expand Down
Loading

0 comments on commit 10cca04

Please sign in to comment.