From 56ffd0d86fb668830348093858601140c207b383 Mon Sep 17 00:00:00 2001 From: Christian Gorenflo Date: Mon, 12 Aug 2024 22:49:10 -0400 Subject: [PATCH 01/23] refactor(axelarnet-gateway-minor): simplify the initial implementation --- .../{executable.rs => clients/external.rs} | 44 +- .../src/{client.rs => clients/gateway.rs} | 17 +- .../axelarnet-gateway/src/clients/mod.rs | 5 + contracts/axelarnet-gateway/src/contract.rs | 122 +-- .../axelarnet-gateway/src/contract/execute.rs | 703 ++++++++++-------- .../axelarnet-gateway/src/contract/query.rs | 312 ++++---- contracts/axelarnet-gateway/src/lib.rs | 7 +- contracts/axelarnet-gateway/src/msg.rs | 42 +- contracts/axelarnet-gateway/src/state.rs | 544 +++++++------- contracts/gateway/src/contract/query.rs | 18 +- contracts/gateway/src/state.rs | 8 +- packages/axelar-wasm-std/src/error.rs | 21 + 12 files changed, 929 insertions(+), 914 deletions(-) rename contracts/axelarnet-gateway/src/{executable.rs => clients/external.rs} (62%) rename contracts/axelarnet-gateway/src/{client.rs => clients/gateway.rs} (90%) create mode 100644 contracts/axelarnet-gateway/src/clients/mod.rs diff --git a/contracts/axelarnet-gateway/src/executable.rs b/contracts/axelarnet-gateway/src/clients/external.rs similarity index 62% rename from contracts/axelarnet-gateway/src/executable.rs rename to contracts/axelarnet-gateway/src/clients/external.rs index 00148a813..94914e974 100644 --- a/contracts/axelarnet-gateway/src/executable.rs +++ b/contracts/axelarnet-gateway/src/clients/external.rs @@ -1,5 +1,6 @@ +use axelar_wasm_std::address; use cosmwasm_schema::cw_serde; -use cosmwasm_std::{HexBinary, WasmMsg}; +use cosmwasm_std::{Deps, HexBinary, WasmMsg}; use router_api::{Address, CrossChainId}; /// `AxelarExecutableMsg` is a struct containing the args used by the axelarnet gateway to execute a destination contract on Axelar. @@ -11,36 +12,35 @@ pub struct AxelarExecutableMsg { pub payload: HexBinary, } -/// Crate-specific `ExecuteMsg` type wraps the `AxelarExecutableMsg` for the AxelarExecutable client. -#[cw_serde] -pub(crate) enum AxelarExecutableExecuteMsg { - /// Execute the message at the destination contract with the corresponding payload. - Execute(AxelarExecutableMsg), +pub struct PayloadExecutor<'a> { + client: client::Client<'a, AxelarExecutableMsg, ()>, } -impl<'a> From> for AxelarExecutableClient<'a> { - fn from(client: client::Client<'a, AxelarExecutableExecuteMsg, ()>) -> Self { - AxelarExecutableClient { client } +impl<'a> From> for PayloadExecutor<'a> { + fn from(client: client::Client<'a, AxelarExecutableMsg, ()>) -> Self { + PayloadExecutor { client } } } -pub struct AxelarExecutableClient<'a> { - client: client::Client<'a, AxelarExecutableExecuteMsg, ()>, -} +impl<'a> PayloadExecutor<'a> { + pub fn new(deps: Deps<'a>, destination: &str) -> error_stack::Result { + let destination = address::validate_cosmwasm_address(deps.api, destination)?; + Ok(PayloadExecutor { + client: client::Client::new(deps.querier, destination), + }) + } -impl<'a> AxelarExecutableClient<'a> { pub fn execute( &self, cc_id: CrossChainId, source_address: Address, payload: HexBinary, ) -> WasmMsg { - self.client - .execute(&AxelarExecutableExecuteMsg::Execute(AxelarExecutableMsg { - cc_id, - source_address, - payload, - })) + self.client.execute(&AxelarExecutableMsg { + cc_id, + source_address, + payload, + }) } } @@ -54,7 +54,7 @@ mod test { #[test] fn execute_message() { let (querier, addr) = setup(); - let client: AxelarExecutableClient = + let client: PayloadExecutor = client::Client::new(QuerierWrapper::new(&querier), addr.clone()).into(); let cc_id = CrossChainId::new("source-chain", "message-id").unwrap(); @@ -67,11 +67,11 @@ mod test { msg, WasmMsg::Execute { contract_addr: addr.to_string(), - msg: to_json_binary(&AxelarExecutableExecuteMsg::Execute(AxelarExecutableMsg { + msg: to_json_binary(&AxelarExecutableMsg { cc_id, source_address, payload, - })) + }) .unwrap(), funds: vec![], } diff --git a/contracts/axelarnet-gateway/src/client.rs b/contracts/axelarnet-gateway/src/clients/gateway.rs similarity index 90% rename from contracts/axelarnet-gateway/src/client.rs rename to contracts/axelarnet-gateway/src/clients/gateway.rs index 558078971..9780c54bf 100644 --- a/contracts/axelarnet-gateway/src/client.rs +++ b/contracts/axelarnet-gateway/src/clients/gateway.rs @@ -2,7 +2,7 @@ use axelar_wasm_std::vec::VecExt; use cosmwasm_std::{HexBinary, WasmMsg}; use router_api::{Address, ChainName, CrossChainId, Message}; -use crate::msg::{ExecuteMsg, QueryMsg}; +use crate::msg::{CallContractData, ExecuteMsg, QueryMsg}; impl<'a> From> for Client<'a> { fn from(client: client::Client<'a, ExecuteMsg, QueryMsg>) -> Self { @@ -21,11 +21,12 @@ impl<'a> Client<'a> { destination_address: Address, payload: HexBinary, ) -> WasmMsg { - self.client.execute(&ExecuteMsg::CallContract { - destination_chain, - destination_address, - payload, - }) + self.client + .execute(&ExecuteMsg::CallContract(CallContractData { + destination_chain, + destination_address, + payload, + })) } pub fn execute(&self, cc_id: CrossChainId, payload: HexBinary) -> WasmMsg { @@ -67,11 +68,11 @@ mod test { msg, WasmMsg::Execute { contract_addr: addr.to_string(), - msg: to_json_binary(&ExecuteMsg::CallContract { + msg: to_json_binary(&ExecuteMsg::CallContract(CallContractData { destination_chain, destination_address, payload, - }) + })) .unwrap(), funds: vec![], } diff --git a/contracts/axelarnet-gateway/src/clients/mod.rs b/contracts/axelarnet-gateway/src/clients/mod.rs new file mode 100644 index 000000000..ae910c897 --- /dev/null +++ b/contracts/axelarnet-gateway/src/clients/mod.rs @@ -0,0 +1,5 @@ +mod external; +mod gateway; + +pub use external::*; +pub use gateway::Client as GatewayClient; diff --git a/contracts/axelarnet-gateway/src/contract.rs b/contracts/axelarnet-gateway/src/contract.rs index 53f0bc44b..77af5308c 100644 --- a/contracts/axelarnet-gateway/src/contract.rs +++ b/contracts/axelarnet-gateway/src/contract.rs @@ -1,10 +1,9 @@ -use axelar_wasm_std::FnExt; +use axelar_wasm_std::error::ContractError; +use axelar_wasm_std::{address, FnExt, IntoContractError}; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{to_json_binary, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response}; use error_stack::ResultExt; -use router_api::client::Router; -use router_api::CrossChainId; use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; use crate::state::{self, Config}; @@ -15,34 +14,18 @@ mod query; const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); -#[derive(thiserror::Error, Debug)] +#[derive(thiserror::Error, Debug, IntoContractError)] pub enum Error { - #[error("contract config is missing")] - ConfigMissing, - #[error("invalid store access")] - InvalidStoreAccess, - #[error("failed to serialize the response")] - SerializeResponse, - #[error("failed to serialize wasm message")] - SerializeWasmMsg, - #[error("invalid address {0}")] - InvalidAddress(String), - #[error("invalid message id")] - InvalidMessageId, - #[error("failed to save outgoing message")] - SaveOutgoingMessage, - #[error("message with ID {0} not found")] - MessageNotFound(CrossChainId), - #[error("message with ID {0} is different")] - MessageMismatch(CrossChainId), - #[error("message with ID {0} not in approved status")] - MessageNotApproved(CrossChainId), - #[error("failed to set message with ID {0} as executed")] - SetMessageStatusExecutedFailed(CrossChainId), - #[error("payload hash doesn't match message")] - PayloadHashMismatch, - #[error("failed to route messages")] - RoutingFailed, + #[error("failed to make a cross-chain contract call")] + CallContract, + #[error("failed to route messages on the gateway")] + RouteMessages, + #[error("failed to execute a cross-chain execution payload")] + Execute, + #[error("failed to query cross-chain contract call messages")] + QueryContractCallMessages, + #[error("failed to query executable messages")] + QueryExecutableMessages, } #[cfg_attr(not(feature = "library"), entry_point)] @@ -64,21 +47,13 @@ pub fn instantiate( _env: Env, _info: MessageInfo, msg: InstantiateMsg, -) -> Result { +) -> Result { cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - let router = deps - .api - .addr_validate(&msg.router_address) - .change_context(Error::InvalidAddress(msg.router_address.clone()))?; - - let config = Config { - chain_name: msg.chain_name, - router, - }; - - state::save_config(deps.storage, &config).change_context(Error::InvalidStoreAccess)?; + let router = address::validate_cosmwasm_address(deps.api, &msg.router_address)?; + let chain_name = msg.chain_name; + state::save_config(deps.storage, &Config { chain_name, router })?; Ok(Response::new()) } @@ -88,59 +63,34 @@ pub fn execute( env: Env, info: MessageInfo, msg: ExecuteMsg, -) -> Result { - let msg = msg.ensure_permissions(deps.storage, &info.sender)?; - - let config = state::load_config(deps.storage).change_context(Error::ConfigMissing)?; - - let router = Router { - address: config.router, - }; - let chain_name = config.chain_name; - - match msg { - ExecuteMsg::CallContract { - destination_chain, - destination_address, - payload, - } => execute::call_contract( - deps.storage, - env.block.height, - &router, - chain_name, - info.sender, - destination_chain, - destination_address, - payload, - ), - ExecuteMsg::RouteMessages(msgs) => { - if info.sender == router.address { - execute::receive_messages(deps.storage, chain_name, msgs) - } else { - // Messages initiated via call contract can be routed again - execute::send_messages(deps.storage, &router, msgs) - } +) -> Result { + match msg.ensure_permissions(deps.storage, &info.sender)? { + ExecuteMsg::CallContract(data) => { + execute::call_contract(deps.storage, env.block.height, info.sender, data) + .change_context(Error::CallContract) } + ExecuteMsg::RouteMessages(msgs) => execute::route_messages(deps.storage, info.sender, msgs) + .change_context(Error::RouteMessages), ExecuteMsg::Execute { cc_id, payload } => { - execute::execute(deps.storage, deps.api, deps.querier, cc_id, payload) + execute::execute(deps, cc_id, payload).change_context(Error::Execute) } }? .then(Ok) } #[cfg_attr(not(feature = "library"), entry_point)] -pub fn query( - deps: Deps, - _env: Env, - msg: QueryMsg, -) -> Result { +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result { match msg { - QueryMsg::SentMessages { cc_ids } => to_json_binary(&query::sent_messages(deps, cc_ids)?), - QueryMsg::ReceivedMessages { cc_ids } => { - to_json_binary(&query::received_messages(deps, cc_ids)?) - } - } - .map_err(axelar_wasm_std::error::ContractError::from) + QueryMsg::ContractCallMessages { cc_ids } => to_json_binary( + &query::contract_call_messages(deps, cc_ids) + .change_context(Error::QueryContractCallMessages)?, + ), + QueryMsg::ExecutableMessages { cc_ids } => to_json_binary( + &query::executable_messages(deps, cc_ids) + .change_context(Error::QueryExecutableMessages)?, + ), + }? + .then(Ok) } #[cfg(test)] diff --git a/contracts/axelarnet-gateway/src/contract/execute.rs b/contracts/axelarnet-gateway/src/contract/execute.rs index 6f442ce28..4e63d1a52 100644 --- a/contracts/axelarnet-gateway/src/contract/execute.rs +++ b/contracts/axelarnet-gateway/src/contract/execute.rs @@ -1,76 +1,158 @@ +use std::str::FromStr; + +use axelar_wasm_std::error::accumulate_reports; use axelar_wasm_std::msg_id::HexTxHashAndEventIndex; -use cosmwasm_std::{ - Addr, Api, Event, HexBinary, QuerierWrapper, Response, Storage, Uint256, WasmMsg, -}; -use error_stack::{report, Result, ResultExt}; +use axelar_wasm_std::{FnExt, IntoContractError}; +use cosmwasm_std::{Addr, DepsMut, HexBinary, Response, Storage, Uint256}; +use error_stack::{bail, ensure, report, Result, ResultExt}; use router_api::client::Router; use router_api::{Address, ChainName, CrossChainId, Message}; use sha3::{Digest, Keccak256}; -use crate::contract::Error; +use crate::clients::PayloadExecutor; use crate::events::AxelarnetGatewayEvent; -use crate::executable::AxelarExecutableClient; -use crate::state::{self}; +use crate::{msg, state}; + +#[derive(thiserror::Error, Debug, IntoContractError)] +pub enum Error { + #[error("failed to save executable message")] + SaveExecutableMessage, + #[error("failed to access executable message")] + ExecutableMessageAccess, + #[error("message with ID {0} is different")] + MessageMismatch(CrossChainId), + #[error("failed to mark message with ID {0} as executed")] + MarkExecuted(CrossChainId), + #[error("payload hash doesn't match message")] + PayloadHashMismatch, + #[error("expected destination chain {expected}, got {actual}")] + InvalidDestination { + expected: ChainName, + actual: ChainName, + }, + #[error("failed to generate cross chain id")] + CrossChainIdGeneration, + #[error("unable to load the contract config")] + ConfigAccess, + #[error("unable to save the message before routing")] + SaveContractCallMessage, + #[error("invalid cross-chain id")] + InvalidCrossChainId, + #[error("failed to create executor")] + CreateExecutor, + #[error("not allowed to execute payload")] + PayloadNotApproved, + #[error("unable to generate event index")] + EventIndex, + #[error("invalid source address {0}")] + InvalidSourceAddress(Addr), +} -#[allow(clippy::too_many_arguments)] -pub(crate) fn call_contract( - store: &mut dyn Storage, +pub fn call_contract( + storage: &mut dyn Storage, block_height: u64, - router: &Router, - chain_name: ChainName, sender: Addr, - destination_chain: ChainName, - destination_address: Address, - payload: HexBinary, + call_contract: msg::CallContractData, ) -> Result { - let counter = state::increment_msg_counter(store).change_context(Error::InvalidStoreAccess)?; + let payload = call_contract.payload.clone(); - // TODO: Retrieve the actual tx hash from core, since cosmwasm doesn't provide it. Use the block height as the placeholder in the meantime. - let message_id = HexTxHashAndEventIndex { - tx_hash: Uint256::from(block_height).to_be_bytes(), - event_index: counter, - } - .into(); + let id = generate_cross_chain_id(storage, block_height) + .change_context(Error::CrossChainIdGeneration)?; + let source_address = Address::from_str(sender.as_str()) + .change_context(Error::InvalidSourceAddress(sender.clone()))?; + let msg = call_contract.into_message(id, source_address); - let cc_id = CrossChainId { - source_chain: chain_name.into(), - message_id, - }; + state::save_unique_contract_call_msg(storage, &msg.cc_id, &msg) + .change_context(Error::SaveContractCallMessage)?; - let payload_hash = Keccak256::digest(payload.as_slice()).into(); + Ok(route_messages(storage, sender, vec![msg.clone()])? + .add_event(AxelarnetGatewayEvent::ContractCalled { msg, payload }.into())) +} - let msg = Message { - cc_id: cc_id.clone(), - source_address: Address::try_from(sender.clone().into_string()) - .expect("failed to convert sender address"), - destination_chain, - destination_address, - payload_hash, +pub fn route_messages( + storage: &mut dyn Storage, + sender: Addr, + msgs: Vec, +) -> Result { + let config = state::load_config(storage).change_context(Error::ConfigAccess)?; + let chain_name = config.chain_name; + let router = Router { + address: config.router, }; - state::save_sent_msg(store, cc_id, &msg).change_context(Error::InvalidStoreAccess)?; + 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)?) + } +} - let (wasm_msg, events) = route(router, vec![msg.clone()])?; +pub fn execute(deps: DepsMut, cc_id: CrossChainId, payload: HexBinary) -> Result { + let msg = approved_msg(deps.storage, &cc_id, &payload)?; - Ok(Response::new() - .add_message(wasm_msg) - .add_event(AxelarnetGatewayEvent::ContractCalled { msg, payload }.into()) - .add_events(events)) + state::mark_msg_as_executed(deps.storage, &cc_id) + .change_context(Error::MarkExecuted(cc_id.clone()))?; + + let executor = PayloadExecutor::new(deps.as_ref(), &msg.destination_address) + .change_context(Error::CreateExecutor)?; + + Response::new() + .add_message(executor.execute(cc_id, msg.source_address.clone(), payload)) + .add_event(AxelarnetGatewayEvent::MessageExecuted { msg }.into()) + .then(Ok) +} + +fn approved_msg( + storage: &dyn Storage, + cc_id: &CrossChainId, + payload: &HexBinary, +) -> Result { + let msg = state::load_executable_msg(storage, &cc_id) + .change_context(Error::PayloadNotApproved)? + .msg_owned(); + + let payload_hash: [u8; 32] = Keccak256::digest(payload.as_slice()).into(); + ensure!(payload_hash == msg.payload_hash, Error::PayloadHashMismatch); + + Ok(msg) +} + +fn generate_cross_chain_id( + storage: &mut dyn Storage, + block_height: u64, +) -> Result { + // TODO: Retrieve the actual tx hash from core, since cosmwasm doesn't provide it. + // Use the block height as the placeholder in the meantime. + let message_id = HexTxHashAndEventIndex { + tx_hash: Uint256::from(block_height).to_be_bytes(), + event_index: state::MESSAGES_FROM_CONTRACT_CALL_COUNTER + .incr(storage) + .change_context(Error::EventIndex)?, + }; + + let config = state::load_config(storage).change_context(Error::ConfigAccess)?; + Ok(CrossChainId::new(config.chain_name, message_id) + .change_context(Error::InvalidCrossChainId)?) } // Because the messages came from the router, we can assume they are already verified -pub(crate) fn receive_messages( +fn prepare_msgs_for_execution( store: &mut dyn Storage, chain_name: ChainName, msgs: Vec, ) -> Result { for msg in msgs.iter() { - if chain_name != msg.destination_chain { - panic!("message destination chain should match chain name in the gateway") - } + ensure!( + chain_name == msg.destination_chain, + Error::InvalidDestination { + expected: chain_name, + actual: msg.destination_chain.clone() + } + ); - state::save_received_msg(store, msg.cc_id.clone(), msg.clone()) - .change_context(Error::SaveOutgoingMessage)?; + state::save_executable_msg(store, &msg.cc_id, msg.clone()) + .change_context(Error::SaveExecutableMessage)?; } Ok(Response::new().add_events( @@ -79,297 +161,262 @@ pub(crate) fn receive_messages( )) } -pub(crate) fn send_messages( +/// Route messages to the router, ignore unknown messages. +fn route_to_router( store: &mut dyn Storage, router: &Router, msgs: Vec, ) -> Result { - for msg in msgs.iter() { - let stored_msg = state::may_load_sent_msg(store, &msg.cc_id) - .change_context(Error::InvalidStoreAccess)?; - - match stored_msg { - Some(message) if msg != &message => { - Err(report!(Error::MessageMismatch(msg.cc_id.clone()))) - } - Some(_) => Ok(()), - None => Err(report!(Error::MessageNotFound(msg.cc_id.clone()))), - }? - } - - let (wasm_msg, events) = route(router, msgs)?; - - Ok(Response::new().add_message(wasm_msg).add_events(events)) -} - -fn route( - router: &Router, - msgs: Vec, -) -> Result<(WasmMsg, impl IntoIterator), Error> { - Ok(( - router.route(msgs.clone()).ok_or(Error::RoutingFailed)?, - msgs.into_iter() - .map(|msg| AxelarnetGatewayEvent::Routing { msg }.into()), - )) -} - -pub(crate) fn execute( - store: &mut dyn Storage, - api: &dyn Api, - querier: QuerierWrapper, - cc_id: CrossChainId, - payload: HexBinary, -) -> Result { - let msg = state::set_msg_as_executed(store, cc_id.clone()) - .change_context(Error::SetMessageStatusExecutedFailed(cc_id))?; - - let payload_hash: [u8; 32] = Keccak256::digest(payload.as_slice()).into(); - if payload_hash != msg.payload_hash { - return Err(report!(Error::PayloadHashMismatch)); - } - - let destination_contract = api - .addr_validate(&msg.destination_address) - .change_context(Error::InvalidAddress(msg.destination_address.to_string()))?; + let msgs = msgs + .iter() + .filter_map(|msg| verify_message(store, msg).transpose()) + .fold(Ok(vec![]), accumulate_reports)?; - let executable: AxelarExecutableClient = - client::Client::new(querier, destination_contract).into(); - - // Call the destination contract - // Apps are required to expose AxelarExecutableMsg::Execute interface Ok(Response::new() - .add_message(executable.execute(msg.cc_id.clone(), msg.source_address.clone(), payload)) - .add_event(AxelarnetGatewayEvent::MessageExecuted { msg }.into())) + .add_messages(router.route(msgs.clone())) + .add_events( + msgs.into_iter() + .map(|msg| AxelarnetGatewayEvent::Routing { msg }.into()), + )) } -#[cfg(test)] -mod tests { - use axelar_wasm_std::err_contains; - use cosmwasm_std::testing::{ - mock_dependencies, mock_env, mock_info, MockApi, MockQuerier, MockStorage, - }; - use cosmwasm_std::{Addr, CosmosMsg, Empty, Env, MessageInfo, OwnedDeps}; - use router_api::{ChainName, CrossChainId, Message}; - - use super::*; - use crate::contract::{execute, instantiate}; - use crate::msg::{ExecuteMsg, InstantiateMsg}; - use crate::state::{self, MessageStatus}; - - const CHAIN: &str = "chain"; - const SOURCE_CHAIN: &str = "source-chain"; - const ROUTER: &str = "router"; - const PAYLOAD: [u8; 3] = [1, 2, 3]; - const SENDER: &str = "sender"; - - fn setup() -> ( - OwnedDeps, - Env, - MessageInfo, - ) { - let mut deps = mock_dependencies(); - let env = mock_env(); - let info = mock_info(SENDER, &[]); - - let chain_name: ChainName = CHAIN.parse().unwrap(); - let router = Addr::unchecked(ROUTER); - - let msg = InstantiateMsg { - chain_name: chain_name.clone(), - router_address: router.to_string(), - }; +/// 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 verify_message(store: &mut dyn Storage, msg: &Message) -> Result, Error> { + let stored_msg = state::may_load_contract_call_msg(store, &msg.cc_id) + .change_context(Error::ExecutableMessageAccess)?; - let _res = instantiate(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); - - (deps, env, info) - } - - fn dummy_message() -> Message { - Message { - cc_id: CrossChainId::new(SOURCE_CHAIN, "message-id").unwrap(), - source_address: "source-address".parse().unwrap(), - destination_chain: CHAIN.parse().unwrap(), - destination_address: "destination-address".parse().unwrap(), - payload_hash: Keccak256::digest(PAYLOAD).into(), + match stored_msg { + Some(stored_msg) if stored_msg != *msg => { + bail!(Error::MessageMismatch(msg.cc_id.clone())) } - } - - #[test] - fn call_contract_and_send_message() { - let (mut deps, env, info) = setup(); - - let expected_message_id = HexTxHashAndEventIndex { - tx_hash: Uint256::from(env.block.height).to_be_bytes(), - event_index: 1, - }; - let expected_cc_id = CrossChainId::new(CHAIN, expected_message_id).unwrap(); - let message = Message { - cc_id: expected_cc_id.clone(), - source_address: info.sender.clone().into_string().parse().unwrap(), - destination_chain: "destination-chain".parse().unwrap(), - destination_address: "destination-address".parse().unwrap(), - payload_hash: Keccak256::digest(PAYLOAD).into(), - }; - - let msg = ExecuteMsg::CallContract { - destination_chain: message.destination_chain.clone(), - destination_address: message.destination_address.clone(), - payload: PAYLOAD.into(), - }; - - let res = execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); - let sent_message = state::may_load_sent_msg(deps.as_mut().storage, &expected_cc_id) - .unwrap() - .unwrap(); - assert_eq!(sent_message, message); - - let router: Router = Router { - address: Addr::unchecked(ROUTER), - }; - assert_eq!(res.messages.len(), 1); - assert_eq!( - res.messages[0].msg, - CosmosMsg::Wasm(router.route(vec![message.clone()]).unwrap()) - ); - - // Re-route the message again - let msg = ExecuteMsg::RouteMessages(vec![message.clone()]); - let res = execute(deps.as_mut(), env, info, msg).unwrap(); - assert_eq!(res.messages.len(), 1); - assert_eq!( - res.messages[0].msg, - CosmosMsg::Wasm(router.route(vec![message]).unwrap()) - ); - } - - #[test] - fn route_messages_from_router() { - let (mut deps, env, _) = setup(); - - let message = dummy_message(); - let msg = ExecuteMsg::RouteMessages(vec![message.clone()]); - - // Execute RouteMessages as if it's coming from the router - let info = mock_info(ROUTER, &[]); - execute(deps.as_mut(), env, info, msg).unwrap(); - - // Check that the message was saved as received - let received_message = state::may_load_received_msg(deps.as_mut().storage, &message.cc_id) - .unwrap() - .unwrap(); - assert_eq!(received_message.msg, message); - assert!(matches!(received_message.status, MessageStatus::Approved)); - } - - #[test] - fn execute_message() { - let (mut deps, env, info) = setup(); - - let message = dummy_message(); - let cc_id = message.cc_id.clone(); - - // Save the message as received - state::save_received_msg(deps.as_mut().storage, cc_id.clone(), message).unwrap(); - - let msg = ExecuteMsg::Execute { - cc_id: cc_id.clone(), - payload: PAYLOAD.into(), - }; - - let res = execute(deps.as_mut(), env, info, msg).unwrap(); - - // Check that a message was sent to the destination contract - assert_eq!(res.messages.len(), 1); - - // Check that the message status was updated to Executed - let executed_message = state::may_load_received_msg(deps.as_mut().storage, &cc_id) - .unwrap() - .unwrap(); - assert!(matches!(executed_message.status, MessageStatus::Executed)); - } - - #[test] - fn execute_not_found() { - let (mut deps, env, info) = setup(); - - let cc_id = CrossChainId::new(SOURCE_CHAIN, "message-id").unwrap(); - let msg = ExecuteMsg::Execute { - cc_id: cc_id.clone(), - payload: PAYLOAD.into(), - }; - - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert!(err_contains!( - err.report, - state::Error, - state::Error::MessageNotApproved(..) - )); - assert!(err_contains!( - err.report, - Error, - Error::SetMessageStatusExecutedFailed(..) - )); - } - - #[test] - fn execute_already_executed() { - let (mut deps, env, info) = setup(); - - let message = dummy_message(); - let cc_id = message.cc_id.clone(); - - // Save the message as already executed - state::save_received_msg(deps.as_mut().storage, cc_id.clone(), message).unwrap(); - state::set_msg_as_executed(deps.as_mut().storage, cc_id.clone()).unwrap(); - - let msg = ExecuteMsg::Execute { - cc_id, - payload: PAYLOAD.into(), - }; - - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert!(err_contains!( - err.report, - state::Error, - state::Error::MessageAlreadyExecuted(..) - )); - assert!(err_contains!( - err.report, - Error, - Error::SetMessageStatusExecutedFailed(..) - )); - } - - #[test] - fn execute_payload_mismatch() { - let (mut deps, env, info) = setup(); - - let message = dummy_message(); - let cc_id = message.cc_id.clone(); - - state::save_received_msg(deps.as_mut().storage, cc_id.clone(), message).unwrap(); - - let msg = ExecuteMsg::Execute { - cc_id, - payload: [4, 5, 6].into(), - }; - - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert!(err_contains!(err.report, Error, Error::PayloadHashMismatch)); - } - - #[test] - #[should_panic(expected = "should match chain name")] - fn receive_messages_wrong_chain() { - let (mut deps, _, _) = setup(); - - let mut message = dummy_message(); - message.destination_chain = "wrong-chain".parse().unwrap(); - - let msg = ExecuteMsg::RouteMessages(vec![message]); - let info = mock_info(ROUTER, &[]); - - // This should panic because the destination chain doesn't match the gateway's chain name - execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + Some(stored_msg) => Ok(Some(stored_msg)), + None => Ok(None), } } + +// #[cfg(test)] +// mod tests { +// use axelar_wasm_std::err_contains; +// use cosmwasm_std::testing::{ +// mock_dependencies, mock_env, mock_info, MockApi, MockQuerier, MockStorage, +// }; +// use cosmwasm_std::{Addr, CosmosMsg, Empty, Env, MessageInfo, OwnedDeps}; +// use router_api::{ChainName, CrossChainId, Message}; +// +// use super::*; +// use crate::contract::{execute, instantiate}; +// use crate::msg::{CallContractData, ExecuteMsg, InstantiateMsg}; +// use crate::state::{self}; +// +// const CHAIN: &str = "chain"; +// const SOURCE_CHAIN: &str = "source-chain"; +// const ROUTER: &str = "router"; +// const PAYLOAD: [u8; 3] = [1, 2, 3]; +// const SENDER: &str = "sender"; +// +// fn setup() -> ( +// OwnedDeps, +// Env, +// MessageInfo, +// ) { +// let mut deps = mock_dependencies(); +// let env = mock_env(); +// let info = mock_info(SENDER, &[]); +// +// let chain_name: ChainName = CHAIN.parse().unwrap(); +// let router = Addr::unchecked(ROUTER); +// +// let msg = InstantiateMsg { +// chain_name: chain_name.clone(), +// router_address: router.to_string(), +// }; +// +// let _res = instantiate(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); +// +// (deps, env, info) +// } +// +// fn dummy_message() -> Message { +// Message { +// cc_id: CrossChainId::new(SOURCE_CHAIN, "message-id").unwrap(), +// source_address: "source-address".parse().unwrap(), +// destination_chain: CHAIN.parse().unwrap(), +// destination_address: "destination-address".parse().unwrap(), +// payload_hash: Keccak256::digest(PAYLOAD).into(), +// } +// } +// +// #[test] +// fn call_contract_and_send_message() { +// let (mut deps, env, info) = setup(); +// +// let expected_message_id = HexTxHashAndEventIndex { +// tx_hash: Uint256::from(env.block.height).to_be_bytes(), +// event_index: 1, +// }; +// let expected_cc_id = CrossChainId::new(CHAIN, expected_message_id).unwrap(); +// let message = Message { +// cc_id: expected_cc_id.clone(), +// source_address: info.sender.clone().into_string().parse().unwrap(), +// destination_chain: "destination-chain".parse().unwrap(), +// destination_address: "destination-address".parse().unwrap(), +// payload_hash: Keccak256::digest(PAYLOAD).into(), +// }; +// +// let msg = ExecuteMsg::CallContract(CallContractData { +// destination_chain: message.destination_chain.clone(), +// destination_address: message.destination_address.clone(), +// payload: PAYLOAD.into(), +// }); +// +// let res = execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); +// let sent_message = +// state::may_load_contract_call_msg(deps.as_mut().storage, &expected_cc_id) +// .unwrap() +// .unwrap(); +// assert_eq!(sent_message, message); +// +// let router: Router = Router { +// address: Addr::unchecked(ROUTER), +// }; +// assert_eq!(res.messages.len(), 1); +// assert_eq!( +// res.messages[0].msg, +// CosmosMsg::Wasm(router.route(vec![message.clone()]).unwrap()) +// ); +// +// // Re-route the message again +// let msg = ExecuteMsg::RouteMessages(vec![message.clone()]); +// let res = execute(deps.as_mut(), env, info, msg).unwrap(); +// assert_eq!(res.messages.len(), 1); +// assert_eq!( +// res.messages[0].msg, +// CosmosMsg::Wasm(router.route(vec![message]).unwrap()) +// ); +// } +// +// #[test] +// fn route_messages_from_router() { +// let (mut deps, env, _) = setup(); +// +// let message = dummy_message(); +// let msg = ExecuteMsg::RouteMessages(vec![message.clone()]); +// +// // Execute RouteMessages as if it's coming from the router +// let info = mock_info(ROUTER, &[]); +// execute(deps.as_mut(), env, info, msg).unwrap(); +// +// // Check that the message was saved as received +// let received_message = +// state::may_load_executable_msg(deps.as_mut().storage, &message.cc_id) +// .unwrap() +// .unwrap(); +// assert_eq!(received_message.msg, message); +// assert!(matches!(received_message.status, MessageStatus::Approved)); +// } +// +// #[test] +// fn execute_message() { +// let (mut deps, env, info) = setup(); +// +// let message = dummy_message(); +// let cc_id = message.cc_id.clone(); +// +// // Save the message as received +// state::save_executable_msg(deps.as_mut().storage, cc_id.clone(), message).unwrap(); +// +// let msg = ExecuteMsg::Execute { +// cc_id: cc_id.clone(), +// payload: PAYLOAD.into(), +// }; +// +// let res = execute(deps.as_mut(), env, info, msg).unwrap(); +// +// // Check that a message was sent to the destination contract +// assert_eq!(res.messages.len(), 1); +// +// // Check that the message status was updated to Executed +// let executed_message = state::may_load_executable_msg(deps.as_mut().storage, &cc_id) +// .unwrap() +// .unwrap(); +// assert!(matches!(executed_message.status, MessageStatus::Executed)); +// } +// +// #[test] +// fn execute_not_found() { +// let (mut deps, env, info) = setup(); +// +// let cc_id = CrossChainId::new(SOURCE_CHAIN, "message-id").unwrap(); +// let msg = ExecuteMsg::Execute { +// cc_id: cc_id.clone(), +// payload: PAYLOAD.into(), +// }; +// +// let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); +// assert!(err_contains!( +// err.report, +// state::Error, +// state::Error::MessageNotApproved(..) +// )); +// assert!(err_contains!(err.report, Error, Error::MarkExecuted(..))); +// } +// +// #[test] +// fn execute_already_executed() { +// let (mut deps, env, info) = setup(); +// +// let message = dummy_message(); +// let cc_id = message.cc_id.clone(); +// +// // Save the message as already executed +// state::save_executable_msg(deps.as_mut().storage, cc_id.clone(), message).unwrap(); +// state::mark_msg_as_executed(deps.as_mut().storage, cc_id.clone()).unwrap(); +// +// let msg = ExecuteMsg::Execute { +// cc_id, +// payload: PAYLOAD.into(), +// }; +// +// let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); +// assert!(err_contains!( +// err.report, +// state::Error, +// state::Error::MessageAlreadyExecuted(..) +// )); +// assert!(err_contains!(err.report, Error, Error::MarkExecuted(..))); +// } +// +// #[test] +// fn execute_payload_mismatch() { +// let (mut deps, env, info) = setup(); +// +// let message = dummy_message(); +// let cc_id = message.cc_id.clone(); +// +// state::save_executable_msg(deps.as_mut().storage, cc_id.clone(), message).unwrap(); +// +// let msg = ExecuteMsg::Execute { +// cc_id, +// payload: [4, 5, 6].into(), +// }; +// +// let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); +// assert!(err_contains!(err.report, Error, Error::PayloadHashMismatch)); +// } +// +// #[test] +// #[should_panic(expected = "should match chain name")] +// fn receive_messages_wrong_chain() { +// let (mut deps, _, _) = setup(); +// +// let mut message = dummy_message(); +// message.destination_chain = "wrong-chain".parse().unwrap(); +// +// let msg = ExecuteMsg::RouteMessages(vec![message]); +// let info = mock_info(ROUTER, &[]); +// +// // This should panic because the destination chain doesn't match the gateway's chain name +// execute(deps.as_mut(), mock_env(), info, msg).unwrap(); +// } +// } diff --git a/contracts/axelarnet-gateway/src/contract/query.rs b/contracts/axelarnet-gateway/src/contract/query.rs index 8476227ae..488752c7f 100644 --- a/contracts/axelarnet-gateway/src/contract/query.rs +++ b/contracts/axelarnet-gateway/src/contract/query.rs @@ -1,165 +1,173 @@ +use axelar_wasm_std::error::accumulate_errs; use cosmwasm_std::Deps; +use error_stack::Result; use router_api::{CrossChainId, Message}; -use crate::state::{self, MessageWithStatus}; +use crate::state::{self, ExecutableMessage}; -pub fn sent_messages(deps: Deps, cc_ids: Vec) -> Result, state::Error> { +pub fn contract_call_messages( + deps: Deps, + cc_ids: Vec, +) -> Result, state::Error> { cc_ids .into_iter() - .map(|cc_id| { - state::may_load_sent_msg(deps.storage, &cc_id)? - .ok_or(state::Error::MessageNotFound(cc_id)) - }) - .collect::, _>>() + .map(|cc_id| state::load_contract_call_msg(deps.storage, &cc_id)) + .fold(Ok(vec![]), accumulate_errs) } -pub fn received_messages( +pub fn executable_messages( deps: Deps, cc_ids: Vec, -) -> Result, state::Error> { +) -> Result, state::Error> { cc_ids .into_iter() - .map(|cc_id| { - state::may_load_received_msg(deps.storage, &cc_id)? - .ok_or(state::Error::MessageNotFound(cc_id)) - }) - .collect::, _>>() -} - -#[cfg(test)] -mod tests { - use axelar_wasm_std::{err_contains, FnExt}; - use cosmwasm_std::from_json; - use cosmwasm_std::testing::{mock_dependencies, mock_env}; - use router_api::{CrossChainId, Message}; - use serde::de::DeserializeOwned; - use state::MessageStatus; - - use super::*; - use crate::contract; - use crate::msg::QueryMsg; - - const SOURCE_CHAIN: &str = "source-chain"; - const DESTINATION_CHAIN: &str = "destination-chain"; - - fn dummy_message(id: &str) -> Message { - Message { - cc_id: CrossChainId::new(SOURCE_CHAIN, id).unwrap(), - source_address: "source-address".parse().unwrap(), - destination_chain: DESTINATION_CHAIN.parse().unwrap(), - destination_address: "destination-address".parse().unwrap(), - payload_hash: [0; 32], - } - } - - // Query a msg and deserialize it. If the query fails, the error is returned - fn query( - deps: Deps, - msg: QueryMsg, - ) -> Result { - contract::query(deps, mock_env(), msg)? - .then(from_json::) - .unwrap() - .then(Ok) - } - - #[test] - fn query_sent_messages() { - let mut deps = mock_dependencies(); - - let message1 = dummy_message("message-1"); - let message2 = dummy_message("message-2"); - let message3 = dummy_message("message-3"); - - // Save messages - state::save_sent_msg(deps.as_mut().storage, message1.cc_id.clone(), &message1).unwrap(); - state::save_sent_msg(deps.as_mut().storage, message2.cc_id.clone(), &message2).unwrap(); - - // Query existing messages - let result: Vec = query( - deps.as_ref(), - QueryMsg::SentMessages { - cc_ids: vec![message1.cc_id.clone(), message2.cc_id.clone()], - }, - ) - .unwrap(); - assert_eq!(result, vec![message1, message2]); - - // Query with non-existent message - let err = query::>( - deps.as_ref(), - QueryMsg::SentMessages { - cc_ids: vec![message3.cc_id], - }, - ) - .unwrap_err(); - assert!(err_contains!( - err.report, - state::Error, - state::Error::MessageNotFound(..) - )); - } - - #[test] - fn query_received_messages() { - let mut deps = mock_dependencies(); - - let message1 = dummy_message("message-1"); - let message2 = dummy_message("message-2"); - let message3 = dummy_message("message-3"); - - // Save messages - state::save_received_msg( - deps.as_mut().storage, - message1.cc_id.clone(), - message1.clone(), - ) - .unwrap(); - state::save_received_msg( - deps.as_mut().storage, - message2.cc_id.clone(), - message2.clone(), - ) - .unwrap(); - - // Set message2 as executed - state::set_msg_as_executed(deps.as_mut().storage, message2.cc_id.clone()).unwrap(); - - // Query existing messages - let result: Vec = query( - deps.as_ref(), - QueryMsg::ReceivedMessages { - cc_ids: vec![message1.cc_id.clone(), message2.cc_id.clone()], - }, - ) - .unwrap(); - - assert_eq!( - result, - vec![ - MessageWithStatus { - msg: message1, - status: MessageStatus::Approved - }, - MessageWithStatus { - msg: message2, - status: MessageStatus::Executed - } - ] - ); - - // Query with non-existent message - let err = query::>( - deps.as_ref(), - QueryMsg::ReceivedMessages { - cc_ids: vec![message3.cc_id], - }, - ) - .unwrap_err(); - assert!(err_contains!( - err.report, - state::Error, - state::Error::MessageNotFound(..) - )); - } + .map(|cc_id| state::load_executable_msg(deps.storage, &cc_id)) + .fold(Ok(vec![]), accumulate_errs) } +// +// #[cfg(test)] +// mod tests { +// use axelar_wasm_std::{err_contains, FnExt}; +// use cosmwasm_std::from_json; +// use cosmwasm_std::testing::{mock_dependencies, mock_env}; +// use router_api::{CrossChainId, Message}; +// use serde::de::DeserializeOwned; +// +// use super::*; +// use crate::contract; +// use crate::msg::QueryMsg; +// +// const SOURCE_CHAIN: &str = "source-chain"; +// const DESTINATION_CHAIN: &str = "destination-chain"; +// +// fn dummy_message(id: &str) -> Message { +// Message { +// cc_id: CrossChainId::new(SOURCE_CHAIN, id).unwrap(), +// source_address: "source-address".parse().unwrap(), +// destination_chain: DESTINATION_CHAIN.parse().unwrap(), +// destination_address: "destination-address".parse().unwrap(), +// payload_hash: [0; 32], +// } +// } +// +// // Query a msg and deserialize it. If the query fails, the error is returned +// fn query( +// deps: Deps, +// msg: QueryMsg, +// ) -> Result { +// contract::query(deps, mock_env(), msg)? +// .then(from_json::) +// .unwrap() +// .then(Ok) +// } +// +// #[test] +// fn query_sent_messages() { +// let mut deps = mock_dependencies(); +// +// let message1 = dummy_message("message-1"); +// let message2 = dummy_message("message-2"); +// let message3 = dummy_message("message-3"); +// +// // Save messages +// state::save_unique_contract_call_msg( +// deps.as_mut().storage, +// message1.cc_id.clone(), +// &message1, +// ) +// .unwrap(); +// state::save_unique_contract_call_msg( +// deps.as_mut().storage, +// message2.cc_id.clone(), +// &message2, +// ) +// .unwrap(); +// +// // Query existing messages +// let result: Vec = query( +// deps.as_ref(), +// QueryMsg::ContractCallMessages { +// cc_ids: vec![message1.cc_id.clone(), message2.cc_id.clone()], +// }, +// ) +// .unwrap(); +// assert_eq!(result, vec![message1, message2]); +// +// // Query with non-existent message +// let err = query::>( +// deps.as_ref(), +// QueryMsg::ContractCallMessages { +// cc_ids: vec![message3.cc_id], +// }, +// ) +// .unwrap_err(); +// assert!(err_contains!( +// err.report, +// state::Error, +// state::Error::MessageNotFound(..) +// )); +// } +// +// #[test] +// fn query_received_messages() { +// let mut deps = mock_dependencies(); +// +// let message1 = dummy_message("message-1"); +// let message2 = dummy_message("message-2"); +// let message3 = dummy_message("message-3"); +// +// // Save messages +// state::save_executable_msg( +// deps.as_mut().storage, +// message1.cc_id.clone(), +// message1.clone(), +// ) +// .unwrap(); +// state::save_executable_msg( +// deps.as_mut().storage, +// message2.cc_id.clone(), +// message2.clone(), +// ) +// .unwrap(); +// +// // Set message2 as executed +// state::mark_msg_as_executed(deps.as_mut().storage, message2.cc_id.clone()).unwrap(); +// +// // Query existing messages +// let result: Vec = query( +// deps.as_ref(), +// QueryMsg::ExecutableMessages { +// cc_ids: vec![message1.cc_id.clone(), message2.cc_id.clone()], +// }, +// ) +// .unwrap(); +// +// assert_eq!( +// result, +// vec![ +// ExecutableMessage { +// msg: message1, +// status: MessageStatus::Approved +// }, +// ExecutableMessage { +// msg: message2, +// status: MessageStatus::Executed +// } +// ] +// ); +// +// // Query with non-existent message +// let err = query::>( +// deps.as_ref(), +// QueryMsg::ExecutableMessages { +// cc_ids: vec![message3.cc_id], +// }, +// ) +// .unwrap_err(); +// assert!(err_contains!( +// err.report, +// state::Error, +// state::Error::MessageNotFound(..) +// )); +// } +// } diff --git a/contracts/axelarnet-gateway/src/lib.rs b/contracts/axelarnet-gateway/src/lib.rs index c7af4a4da..8262fe80f 100644 --- a/contracts/axelarnet-gateway/src/lib.rs +++ b/contracts/axelarnet-gateway/src/lib.rs @@ -3,8 +3,5 @@ pub mod events; pub mod msg; pub mod state; -mod client; -pub use client::Client; - -mod executable; -pub use executable::AxelarExecutableMsg; +mod clients; +pub use clients::{AxelarExecutableMsg, GatewayClient}; diff --git a/contracts/axelarnet-gateway/src/msg.rs b/contracts/axelarnet-gateway/src/msg.rs index a28ef14e6..aa33ad590 100644 --- a/contracts/axelarnet-gateway/src/msg.rs +++ b/contracts/axelarnet-gateway/src/msg.rs @@ -2,8 +2,9 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::HexBinary; use msgs_derive::EnsurePermissions; use router_api::{Address, ChainName, CrossChainId, Message}; +use sha3::{Digest, Keccak256}; -use crate::state::MessageWithStatus; +use crate::state::ExecutableMessage; #[cw_serde] pub struct InstantiateMsg { @@ -16,15 +17,6 @@ pub struct InstantiateMsg { #[cw_serde] #[derive(EnsurePermissions)] pub enum ExecuteMsg { - /// Initiate a cross-chain contract call from Axelarnet to another chain. - /// The message will be routed to the destination chain's gateway via the router. - #[permission(Any)] - CallContract { - destination_chain: ChainName, - destination_address: Address, - payload: HexBinary, - }, - /// Forward the given messages to the next step of the routing layer. /// Messages initiated via `CallContract` can be forwarded again to the router. /// If the messages are coming from the router, then they are marked ready for execution. @@ -38,6 +30,30 @@ pub enum ExecuteMsg { cc_id: CrossChainId, payload: HexBinary, }, + + /// Initiate a cross-chain contract call from Axelarnet to another chain. + /// The message will be routed to the destination chain's gateway via the router. + #[permission(Any)] + CallContract(CallContractData), +} + +#[cw_serde] +pub struct CallContractData { + pub destination_chain: ChainName, + pub destination_address: Address, + pub payload: HexBinary, +} + +impl CallContractData { + pub fn into_message(self, id: CrossChainId, source_address: Address) -> Message { + Message { + cc_id: id, + source_address, + destination_chain: self.destination_chain, + destination_address: self.destination_address, + payload_hash: Keccak256::digest(self.payload.as_slice()).into(), + } + } } #[cw_serde] @@ -45,9 +61,9 @@ pub enum ExecuteMsg { pub enum QueryMsg { /// Returns the sent messages for the given cross-chain ids. #[returns(Vec)] - SentMessages { cc_ids: Vec }, + ContractCallMessages { cc_ids: Vec }, /// Returns the received messages with their status for the given cross-chain ids. - #[returns(Vec)] - ReceivedMessages { cc_ids: Vec }, + #[returns(Vec)] + ExecutableMessages { cc_ids: Vec }, } diff --git a/contracts/axelarnet-gateway/src/state.rs b/contracts/axelarnet-gateway/src/state.rs index 7df260c86..27383fff3 100644 --- a/contracts/axelarnet-gateway/src/state.rs +++ b/contracts/axelarnet-gateway/src/state.rs @@ -1,39 +1,16 @@ use axelar_wasm_std::counter::Counter; -use axelar_wasm_std::{FnExt, IntoContractError}; +use axelar_wasm_std::IntoContractError; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, StdError, Storage}; use cw_storage_plus::{Item, Map}; +use error_stack::report; use router_api::{ChainName, CrossChainId, Message}; -#[cw_serde] -pub(crate) struct Config { - pub chain_name: ChainName, - pub router: Addr, -} - -#[cw_serde] -pub(crate) enum MessageStatus { - Approved, - Executed, -} - -#[cw_serde] -pub(crate) struct MessageWithStatus { - pub msg: Message, - pub status: MessageStatus, -} - -const CONFIG_NAME: &str = "config"; -const CONFIG: Item = Item::new(CONFIG_NAME); - -const SENT_MESSAGE_COUNTER_NAME: &str = "sent_message_counter"; -const SENT_MESSAGE_COUNTER: Counter = Counter::new(SENT_MESSAGE_COUNTER_NAME); - -const SENT_MESSAGES_NAME: &str = "sent_messages"; -const SENT_MESSAGES: Map = Map::new(SENT_MESSAGES_NAME); - -const RECEIVED_MESSAGES_NAME: &str = "received_messages"; -const RECEIVED_MESSAGES: Map = Map::new(RECEIVED_MESSAGES_NAME); +const CONFIG: Item = Item::new("config"); +const MESSAGES_FROM_CONTRACT_CALL: Map<&CrossChainId, Message> = Map::new("sent_messages"); +pub(crate) const MESSAGES_FROM_CONTRACT_CALL_COUNTER: Counter = + Counter::new("sent_message_counter"); +const MESSAGES_TO_EXECUTE: Map<&CrossChainId, ExecutableMessage> = Map::new("received_messages"); #[derive(thiserror::Error, Debug, PartialEq, IntoContractError)] pub enum Error { @@ -53,286 +30,297 @@ pub enum Error { MessageAlreadyExists(CrossChainId), } +#[cw_serde] +pub(crate) struct Config { + pub chain_name: ChainName, + pub router: Addr, +} + +#[cw_serde] +pub enum ExecutableMessage { + NotExecuted(Message), + Executed(Message), +} + +impl ExecutableMessage { + pub fn msg(&self) -> &Message { + match self { + ExecutableMessage::NotExecuted(msg) | ExecutableMessage::Executed(msg) => msg, + } + } + + pub fn msg_owned(self) -> Message { + match self { + ExecutableMessage::NotExecuted(msg) | ExecutableMessage::Executed(msg) => msg, + } + } +} + pub(crate) fn save_config(storage: &mut dyn Storage, value: &Config) -> Result<(), Error> { - CONFIG.save(storage, value).map_err(Error::from) + Ok(CONFIG.save(storage, value)?) } pub(crate) fn load_config(storage: &dyn Storage) -> Result { - CONFIG - .may_load(storage) - .map_err(Error::from)? - .ok_or(Error::MissingConfig) + CONFIG.may_load(storage)?.ok_or(Error::MissingConfig) } -pub(crate) fn save_sent_msg( +pub(crate) fn save_unique_contract_call_msg( storage: &mut dyn Storage, - key: CrossChainId, + cc_id: &CrossChainId, msg: &Message, ) -> Result<(), Error> { - match SENT_MESSAGES.may_load(storage, key.clone())? { - Some(_) => Err(Error::MessageAlreadyExists(key)), - None => SENT_MESSAGES.save(storage, key, msg).map_err(Error::from), + // these cc IDs are generated by the gateway and should be unique. + // If there is a collision, there must be a bug in the logic of the caller. + if MESSAGES_FROM_CONTRACT_CALL.has(storage, cc_id) { + return Err(Error::MessageAlreadyExists(cc_id.clone())); } + + Ok(MESSAGES_FROM_CONTRACT_CALL.save(storage, cc_id, msg)?) } -pub(crate) fn may_load_sent_msg( +pub(crate) fn may_load_contract_call_msg( storage: &dyn Storage, id: &CrossChainId, ) -> Result, Error> { - SENT_MESSAGES - .may_load(storage, id.clone()) - .map_err(Error::from) + Ok(MESSAGES_FROM_CONTRACT_CALL.may_load(storage, id)?) } -pub(crate) fn may_load_received_msg( - storage: &dyn Storage, - cc_id: &CrossChainId, -) -> Result, Error> { - RECEIVED_MESSAGES - .may_load(storage, cc_id.clone()) - .map_err(Error::from) +pub fn load_contract_call_msg(storage: &dyn Storage, id: &CrossChainId) -> Result { + may_load_contract_call_msg(storage, id)?.ok_or_else(|| Error::MessageNotFound(id.clone())) } -pub(crate) fn save_received_msg( +pub(crate) fn save_executable_msg( storage: &mut dyn Storage, - cc_id: CrossChainId, + cc_id: &CrossChainId, msg: Message, ) -> Result<(), Error> { - let existing = RECEIVED_MESSAGES - .may_load(storage, cc_id.clone()) - .map_err(Error::from)?; + let existing = may_load_executable_msg(storage, cc_id)?; match existing { - Some(MessageWithStatus { - msg: existing_msg, .. - }) if msg != existing_msg => Err(Error::MessageMismatch(msg.cc_id.clone())), + Some(with_status) if *with_status.msg() != msg => { + Err(Error::MessageMismatch(msg.cc_id.clone())) + } Some(_) => Ok(()), // new message is identical, no need to store it - None => RECEIVED_MESSAGES - .save( - storage, - cc_id, - &MessageWithStatus { - msg, - status: MessageStatus::Approved, - }, - ) - .map_err(Error::from)? - .then(Ok), - } -} - -/// Update the status of a message to executed if it is in approved status, error otherwise. -pub(crate) fn set_msg_as_executed( - storage: &mut dyn Storage, - cc_id: CrossChainId, -) -> Result { - let existing = RECEIVED_MESSAGES - .may_load(storage, cc_id.clone()) - .map_err(Error::from)?; - - match existing { - Some(MessageWithStatus { - msg, - status: MessageStatus::Approved, - }) => { - RECEIVED_MESSAGES - .save( - storage, - cc_id, - &MessageWithStatus { - msg: msg.clone(), - status: MessageStatus::Executed, - }, - ) - .map_err(Error::from)?; - - Ok(msg) + None => { + Ok(MESSAGES_TO_EXECUTE.save(storage, cc_id, &ExecutableMessage::NotExecuted(msg))?) } - Some(MessageWithStatus { - status: MessageStatus::Executed, - .. - }) => Err(Error::MessageAlreadyExecuted(cc_id)), - _ => Err(Error::MessageNotApproved(cc_id)), } } -pub(crate) fn increment_msg_counter(storage: &mut dyn Storage) -> Result { - SENT_MESSAGE_COUNTER.incr(storage).map_err(Error::from) +pub(crate) fn may_load_executable_msg( + storage: &dyn Storage, + cc_id: &CrossChainId, +) -> Result, Error> { + Ok(MESSAGES_TO_EXECUTE.may_load(storage, cc_id)?) } -#[cfg(test)] -mod tests { - use cosmwasm_std::testing::mock_dependencies; - use cosmwasm_std::Addr; - use router_api::{CrossChainId, Message}; - - use super::*; - - fn create_test_message() -> Message { - Message { - cc_id: CrossChainId::new("source-chain", "message-id").unwrap(), - source_address: "source-address".parse().unwrap(), - destination_chain: "destination-chain".parse().unwrap(), - destination_address: "destination-address".parse().unwrap(), - payload_hash: [1; 32], - } - } - - #[test] - fn config_storage() { - let mut deps = mock_dependencies(); - - let config = Config { - chain_name: "test-chain".parse().unwrap(), - router: Addr::unchecked("router-address"), - }; - - // Test saving config - super::save_config(deps.as_mut().storage, &config).unwrap(); - - // Test loading config - let loaded_config = super::load_config(deps.as_ref().storage).unwrap(); - assert_eq!(config, loaded_config); - - // Test loading non-existent config - CONFIG.remove(deps.as_mut().storage); - let result = super::load_config(deps.as_ref().storage); - assert_eq!(result, Err(Error::MissingConfig)); - } - - #[test] - fn sent_message_storage() { - let mut deps = mock_dependencies(); - let message = create_test_message(); - - // Test saving sent message - super::save_sent_msg(deps.as_mut().storage, message.cc_id.clone(), &message).unwrap(); - - // Test loading sent message - let loaded_message = - super::may_load_sent_msg(deps.as_ref().storage, &message.cc_id).unwrap(); - assert_eq!(Some(message.clone()), loaded_message); - - // Test loading non-existent message - let non_existent_id = CrossChainId::new("non-existent", "id").unwrap(); - assert_eq!( - None, - super::may_load_sent_msg(deps.as_ref().storage, &non_existent_id).unwrap() - ); - - // Test saving duplicate message - let result = super::save_sent_msg(deps.as_mut().storage, message.cc_id.clone(), &message); - assert_eq!(result, Err(Error::MessageAlreadyExists(message.cc_id))); - } - - #[test] - fn received_message_storage() { - let mut deps = mock_dependencies(); - let message = create_test_message(); - - // Test saving received message - super::save_received_msg( - deps.as_mut().storage, - message.cc_id.clone(), - message.clone(), - ) - .unwrap(); - - // Test loading received message - let loaded_message = - super::may_load_received_msg(deps.as_ref().storage, &message.cc_id).unwrap(); - assert_eq!( - Some(MessageWithStatus { - msg: message.clone(), - status: MessageStatus::Approved - }), - loaded_message - ); - - // Test loading non-existent message - let non_existent_id = CrossChainId::new("non-existent", "id").unwrap(); - assert_eq!( - None, - super::may_load_received_msg(deps.as_ref().storage, &non_existent_id).unwrap() - ); - - // Test saving duplicate message (should not error, but also not change the stored message) - super::save_received_msg( - deps.as_mut().storage, - message.cc_id.clone(), - message.clone(), - ) - .unwrap(); - let loaded_message = - super::may_load_received_msg(deps.as_ref().storage, &message.cc_id).unwrap(); - assert_eq!( - Some(MessageWithStatus { - msg: message.clone(), - status: MessageStatus::Approved - }), - loaded_message - ); - - // Test saving mismatched message - let mismatched_message = Message { - cc_id: message.cc_id.clone(), - source_address: "different-address".parse().unwrap(), - ..message.clone() - }; - let result = super::save_received_msg( - deps.as_mut().storage, - message.cc_id.clone(), - mismatched_message, - ); - assert_eq!(result, Err(Error::MessageMismatch(message.cc_id))); - } - - #[test] - fn set_msg_as_executed() { - let mut deps = mock_dependencies(); - let message = create_test_message(); - - // Save a received message - super::save_received_msg( - deps.as_mut().storage, - message.cc_id.clone(), - message.clone(), - ) - .unwrap(); - - // Test setting message as executed - let executed_message = - super::set_msg_as_executed(deps.as_mut().storage, message.cc_id.clone()).unwrap(); - assert_eq!(message, executed_message); - - // Verify the message status is now Executed - let loaded_message = - super::may_load_received_msg(deps.as_ref().storage, &message.cc_id).unwrap(); - assert_eq!( - Some(MessageWithStatus { - msg: message.clone(), - status: MessageStatus::Executed - }), - loaded_message - ); - - // Test setting an already executed message - let result = super::set_msg_as_executed(deps.as_mut().storage, message.cc_id.clone()); - assert_eq!(result, Err(Error::MessageAlreadyExecuted(message.cc_id))); - - // Test setting a non-existent message - let non_existent_id = CrossChainId::new("non-existent", "id").unwrap(); - let result = super::set_msg_as_executed(deps.as_mut().storage, non_existent_id.clone()); - assert_eq!(result, Err(Error::MessageNotApproved(non_existent_id))); - } +pub fn load_executable_msg( + storage: &dyn Storage, + cc_id: &CrossChainId, +) -> Result { + may_load_executable_msg(storage, cc_id)?.ok_or_else(|| Error::MessageNotApproved(cc_id.clone())) +} - #[test] - fn increment_msg_counter() { - let mut deps = mock_dependencies(); +/// Update the status of a message to executed if it is in approved status, error otherwise. +pub(crate) fn mark_msg_as_executed( + storage: &mut dyn Storage, + cc_id: &CrossChainId, +) -> Result<(), Error> { + let existing = may_load_executable_msg(storage, cc_id)? + .ok_or_else(|| Error::MessageNotApproved(cc_id.clone()))?; - for i in 1..=3 { - let count = super::increment_msg_counter(deps.as_mut().storage).unwrap(); - assert_eq!(i, count); - } + match existing { + ExecutableMessage::NotExecuted(msg) => Ok(MESSAGES_TO_EXECUTE.save( + storage, + cc_id, + &ExecutableMessage::Executed(msg.clone()), + )?), + ExecutableMessage::Executed(_) => Err(Error::MessageAlreadyExecuted(cc_id.clone())), } } +// +// #[cfg(test)] +// mod tests { +// use cosmwasm_std::testing::mock_dependencies; +// use cosmwasm_std::Addr; +// use router_api::{CrossChainId, Message}; +// +// use super::*; +// +// fn create_test_message() -> Message { +// Message { +// cc_id: CrossChainId::new("source-chain", "message-id").unwrap(), +// source_address: "source-address".parse().unwrap(), +// destination_chain: "destination-chain".parse().unwrap(), +// destination_address: "destination-address".parse().unwrap(), +// payload_hash: [1; 32], +// } +// } +// +// #[test] +// fn config_storage() { +// let mut deps = mock_dependencies(); +// +// let config = Config { +// chain_name: "test-chain".parse().unwrap(), +// router: Addr::unchecked("router-address"), +// }; +// +// // Test saving config +// super::save_config(deps.as_mut().storage, &config).unwrap(); +// +// // Test loading config +// let loaded_config = super::load_config(deps.as_ref().storage).unwrap(); +// assert_eq!(config, loaded_config); +// +// // Test loading non-existent config +// CONFIG.remove(deps.as_mut().storage); +// let result = super::load_config(deps.as_ref().storage); +// assert_eq!(result, Err(Error::MissingConfig)); +// } +// +// #[test] +// fn sent_message_storage() { +// let mut deps = mock_dependencies(); +// let message = create_test_message(); +// +// // Test saving sent message +// super::save_unique_contract_call_msg( +// deps.as_mut().storage, +// message.cc_id.clone(), +// &message, +// ) +// .unwrap(); +// +// // Test loading sent message +// let loaded_message = +// super::may_load_contract_call_msg(deps.as_ref().storage, &message.cc_id).unwrap(); +// assert_eq!(Some(message.clone()), loaded_message); +// +// // Test loading non-existent message +// let non_existent_id = CrossChainId::new("non-existent", "id").unwrap(); +// assert_eq!( +// None, +// super::may_load_contract_call_msg(deps.as_ref().storage, &non_existent_id).unwrap() +// ); +// +// // Test saving duplicate message +// let result = super::save_unique_contract_call_msg( +// deps.as_mut().storage, +// message.cc_id.clone(), +// &message, +// ); +// assert_eq!(result, Err(Error::MessageAlreadyExists(message.cc_id))); +// } +// +// #[test] +// fn received_message_storage() { +// let mut deps = mock_dependencies(); +// let message = create_test_message(); +// +// // Test saving received message +// super::save_executable_msg( +// deps.as_mut().storage, +// message.cc_id.clone(), +// message.clone(), +// ) +// .unwrap(); +// +// // Test loading received message +// let loaded_message = +// super::may_load_executable_msg(deps.as_ref().storage, &message.cc_id).unwrap(); +// assert_eq!( +// Some(ExecutableMessage { +// msg: message.clone(), +// status: MessageStatus::Approved +// }), +// loaded_message +// ); +// +// // Test loading non-existent message +// let non_existent_id = CrossChainId::new("non-existent", "id").unwrap(); +// assert_eq!( +// None, +// super::may_load_executable_msg(deps.as_ref().storage, &non_existent_id).unwrap() +// ); +// +// // Test saving duplicate message (should not error, but also not change the stored message) +// super::save_executable_msg( +// deps.as_mut().storage, +// message.cc_id.clone(), +// message.clone(), +// ) +// .unwrap(); +// let loaded_message = +// super::may_load_executable_msg(deps.as_ref().storage, &message.cc_id).unwrap(); +// assert_eq!( +// Some(ExecutableMessage { +// msg: message.clone(), +// status: MessageStatus::Approved +// }), +// loaded_message +// ); +// +// // Test saving mismatched message +// let mismatched_message = Message { +// cc_id: message.cc_id.clone(), +// source_address: "different-address".parse().unwrap(), +// ..message.clone() +// }; +// let result = super::save_executable_msg( +// deps.as_mut().storage, +// message.cc_id.clone(), +// mismatched_message, +// ); +// assert_eq!(result, Err(Error::MessageMismatch(message.cc_id))); +// } +// +// #[test] +// fn set_msg_as_executed() { +// let mut deps = mock_dependencies(); +// let message = create_test_message(); +// +// // Save a received message +// super::save_executable_msg( +// deps.as_mut().storage, +// message.cc_id.clone(), +// message.clone(), +// ) +// .unwrap(); +// +// // Test setting message as executed +// let executed_message = +// super::mark_msg_as_executed(deps.as_mut().storage, message.cc_id.clone()).unwrap(); +// assert_eq!(message, executed_message); +// +// // Verify the message status is now Executed +// let loaded_message = +// super::may_load_executable_msg(deps.as_ref().storage, &message.cc_id).unwrap(); +// assert_eq!( +// Some(ExecutableMessage { +// msg: message.clone(), +// status: MessageStatus::Executed +// }), +// loaded_message +// ); +// +// // Test setting an already executed message +// let result = super::mark_msg_as_executed(deps.as_mut().storage, message.cc_id.clone()); +// assert_eq!(result, Err(Error::MessageAlreadyExecuted(message.cc_id))); +// +// // Test setting a non-existent message +// let non_existent_id = CrossChainId::new("non-existent", "id").unwrap(); +// let result = super::mark_msg_as_executed(deps.as_mut().storage, non_existent_id.clone()); +// assert_eq!(result, Err(Error::MessageNotApproved(non_existent_id))); +// } +// +// #[test] +// fn increment_msg_counter() { +// let mut deps = mock_dependencies(); +// +// for i in 1..=3 { +// let count = super::increment_msg_counter(deps.as_mut().storage).unwrap(); +// assert_eq!(i, count); +// } +// } +// } diff --git a/contracts/gateway/src/contract/query.rs b/contracts/gateway/src/contract/query.rs index 59cc4c4e5..64d081a13 100644 --- a/contracts/gateway/src/contract/query.rs +++ b/contracts/gateway/src/contract/query.rs @@ -1,7 +1,7 @@ -use axelar_wasm_std::error::extend_err; +use axelar_wasm_std::error::accumulate_errs; use cosmwasm_std::{to_json_binary, Binary, Storage}; use error_stack::Result; -use router_api::{CrossChainId, Message}; +use router_api::CrossChainId; use crate::state; @@ -16,20 +16,6 @@ pub fn outgoing_messages<'a>( Ok(to_json_binary(&msgs).map_err(state::Error::from)?) } -fn accumulate_errs( - acc: Result, state::Error>, - msg: std::result::Result, -) -> Result, state::Error> { - match (acc, msg) { - (Ok(mut msgs), Ok(msg)) => { - msgs.push(msg); - Ok(msgs) - } - (Err(report), Ok(_)) => Err(report), - (acc, Err(msg_err)) => extend_err(acc, msg_err.into()), - } -} - #[cfg(test)] mod test { use cosmwasm_std::from_json; diff --git a/contracts/gateway/src/state.rs b/contracts/gateway/src/state.rs index 445eec117..ab3d7c622 100644 --- a/contracts/gateway/src/state.rs +++ b/contracts/gateway/src/state.rs @@ -26,10 +26,7 @@ pub enum Error { } pub(crate) fn load_config(storage: &dyn Storage) -> Result { - CONFIG - .may_load(storage) - .map_err(Error::from)? - .ok_or(Error::MissingConfig) + CONFIG.may_load(storage)?.ok_or(Error::MissingConfig) } pub(crate) fn save_config(storage: &mut dyn Storage, config: &Config) -> Result<(), Error> { @@ -41,8 +38,7 @@ pub(crate) fn load_outgoing_message( cc_id: &CrossChainId, ) -> Result { OUTGOING_MESSAGES - .may_load(storage, cc_id) - .map_err(Error::from)? + .may_load(storage, cc_id)? .ok_or_else(|| Error::MessageNotFound(cc_id.clone())) } diff --git a/packages/axelar-wasm-std/src/error.rs b/packages/axelar-wasm-std/src/error.rs index 1e2922591..05f5d214e 100644 --- a/packages/axelar-wasm-std/src/error.rs +++ b/packages/axelar-wasm-std/src/error.rs @@ -77,6 +77,27 @@ pub fn extend_err( } } +pub fn accumulate_errs( + acc: error_stack::Result, E>, + value: Result, +) -> error_stack::Result, E> { + accumulate_reports(acc, value.map_err(|err| err.into())) +} + +pub fn accumulate_reports( + acc: error_stack::Result, E>, + value: Result>, +) -> error_stack::Result, E> { + match (acc, value) { + (Ok(mut values), Ok(value)) => { + values.push(value); + Ok(values) + } + (Err(report), Ok(_)) => Err(report), + (acc, Err(err)) => extend_err(acc, err), + } +} + #[macro_export] macro_rules! err_contains { ($expression:expr, $error_type:ty, $pattern:pat $(if $guard:expr)? $(,)?) => { From ecd5724f56f137c498865acccadaae46f8f06b96 Mon Sep 17 00:00:00 2001 From: Christian Gorenflo Date: Mon, 12 Aug 2024 22:57:51 -0400 Subject: [PATCH 02/23] clippy --- contracts/axelarnet-gateway/src/contract/execute.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/contracts/axelarnet-gateway/src/contract/execute.rs b/contracts/axelarnet-gateway/src/contract/execute.rs index 4e63d1a52..5e66a9a18 100644 --- a/contracts/axelarnet-gateway/src/contract/execute.rs +++ b/contracts/axelarnet-gateway/src/contract/execute.rs @@ -108,7 +108,7 @@ fn approved_msg( cc_id: &CrossChainId, payload: &HexBinary, ) -> Result { - let msg = state::load_executable_msg(storage, &cc_id) + let msg = state::load_executable_msg(storage, cc_id) .change_context(Error::PayloadNotApproved)? .msg_owned(); @@ -132,8 +132,7 @@ fn generate_cross_chain_id( }; let config = state::load_config(storage).change_context(Error::ConfigAccess)?; - Ok(CrossChainId::new(config.chain_name, message_id) - .change_context(Error::InvalidCrossChainId)?) + CrossChainId::new(config.chain_name, message_id).change_context(Error::InvalidCrossChainId) } // Because the messages came from the router, we can assume they are already verified From a5a674c0e669c97c4938cdb6d6c23ff8e46198c9 Mon Sep 17 00:00:00 2001 From: Christian Gorenflo Date: Tue, 13 Aug 2024 16:47:07 -0400 Subject: [PATCH 03/23] improvements and panic --- contracts/axelarnet-gateway/src/contract.rs | 4 +-- .../axelarnet-gateway/src/contract/execute.rs | 20 ++++++++--- .../axelarnet-gateway/src/contract/query.rs | 4 +-- contracts/axelarnet-gateway/src/msg.rs | 2 +- contracts/axelarnet-gateway/src/state.rs | 36 +++++++++---------- 5 files changed, 37 insertions(+), 29 deletions(-) diff --git a/contracts/axelarnet-gateway/src/contract.rs b/contracts/axelarnet-gateway/src/contract.rs index 77af5308c..e372f6445 100644 --- a/contracts/axelarnet-gateway/src/contract.rs +++ b/contracts/axelarnet-gateway/src/contract.rs @@ -81,8 +81,8 @@ pub fn execute( #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result { match msg { - QueryMsg::ContractCallMessages { cc_ids } => to_json_binary( - &query::contract_call_messages(deps, cc_ids) + QueryMsg::RoutableMessages { cc_ids } => to_json_binary( + &query::routable_messages(deps, cc_ids) .change_context(Error::QueryContractCallMessages)?, ), QueryMsg::ExecutableMessages { cc_ids } => to_json_binary( diff --git a/contracts/axelarnet-gateway/src/contract/execute.rs b/contracts/axelarnet-gateway/src/contract/execute.rs index 5e66a9a18..4f6e77a96 100644 --- a/contracts/axelarnet-gateway/src/contract/execute.rs +++ b/contracts/axelarnet-gateway/src/contract/execute.rs @@ -35,7 +35,7 @@ pub enum Error { #[error("unable to load the contract config")] ConfigAccess, #[error("unable to save the message before routing")] - SaveContractCallMessage, + SaveRoutableMessage, #[error("invalid cross-chain id")] InvalidCrossChainId, #[error("failed to create executor")] @@ -62,8 +62,9 @@ pub fn call_contract( .change_context(Error::InvalidSourceAddress(sender.clone()))?; let msg = call_contract.into_message(id, source_address); - state::save_unique_contract_call_msg(storage, &msg.cc_id, &msg) - .change_context(Error::SaveContractCallMessage)?; + state::save_unique_routable_msg(storage, &msg.cc_id, &msg) + .inspect_err(|err| panic_if_already_exists(err, &msg.cc_id)) + .change_context(Error::SaveRoutableMessage)?; Ok(route_messages(storage, sender, vec![msg.clone()])? .add_event(AxelarnetGatewayEvent::ContractCalled { msg, payload }.into())) @@ -126,7 +127,7 @@ fn generate_cross_chain_id( // Use the block height as the placeholder in the meantime. let message_id = HexTxHashAndEventIndex { tx_hash: Uint256::from(block_height).to_be_bytes(), - event_index: state::MESSAGES_FROM_CONTRACT_CALL_COUNTER + event_index: state::ROUTABLE_MESSAGES_INDEX .incr(storage) .change_context(Error::EventIndex)?, }; @@ -135,6 +136,15 @@ fn generate_cross_chain_id( CrossChainId::new(config.chain_name, message_id).change_context(Error::InvalidCrossChainId) } +fn panic_if_already_exists(err: &state::Error, cc_id: &CrossChainId) { + if matches!(err, state::Error::MessageAlreadyExists(..)) { + panic!( + "violated invariant: message with ID {0} already exists", + cc_id + ) + } +} + // Because the messages came from the router, we can assume they are already verified fn prepare_msgs_for_execution( store: &mut dyn Storage, @@ -182,7 +192,7 @@ 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 verify_message(store: &mut dyn Storage, msg: &Message) -> Result, Error> { - let stored_msg = state::may_load_contract_call_msg(store, &msg.cc_id) + let stored_msg = state::may_load_routable_msg(store, &msg.cc_id) .change_context(Error::ExecutableMessageAccess)?; match stored_msg { diff --git a/contracts/axelarnet-gateway/src/contract/query.rs b/contracts/axelarnet-gateway/src/contract/query.rs index 488752c7f..177789fa4 100644 --- a/contracts/axelarnet-gateway/src/contract/query.rs +++ b/contracts/axelarnet-gateway/src/contract/query.rs @@ -5,13 +5,13 @@ use router_api::{CrossChainId, Message}; use crate::state::{self, ExecutableMessage}; -pub fn contract_call_messages( +pub fn routable_messages( deps: Deps, cc_ids: Vec, ) -> Result, state::Error> { cc_ids .into_iter() - .map(|cc_id| state::load_contract_call_msg(deps.storage, &cc_id)) + .map(|cc_id| state::load_routable_msg(deps.storage, &cc_id)) .fold(Ok(vec![]), accumulate_errs) } diff --git a/contracts/axelarnet-gateway/src/msg.rs b/contracts/axelarnet-gateway/src/msg.rs index aa33ad590..cd17c1a2a 100644 --- a/contracts/axelarnet-gateway/src/msg.rs +++ b/contracts/axelarnet-gateway/src/msg.rs @@ -61,7 +61,7 @@ impl CallContractData { pub enum QueryMsg { /// Returns the sent messages for the given cross-chain ids. #[returns(Vec)] - ContractCallMessages { cc_ids: Vec }, + RoutableMessages { cc_ids: Vec }, /// Returns the received messages with their status for the given cross-chain ids. #[returns(Vec)] diff --git a/contracts/axelarnet-gateway/src/state.rs b/contracts/axelarnet-gateway/src/state.rs index 27383fff3..e13596b7f 100644 --- a/contracts/axelarnet-gateway/src/state.rs +++ b/contracts/axelarnet-gateway/src/state.rs @@ -7,10 +7,9 @@ use error_stack::report; use router_api::{ChainName, CrossChainId, Message}; const CONFIG: Item = Item::new("config"); -const MESSAGES_FROM_CONTRACT_CALL: Map<&CrossChainId, Message> = Map::new("sent_messages"); -pub(crate) const MESSAGES_FROM_CONTRACT_CALL_COUNTER: Counter = - Counter::new("sent_message_counter"); -const MESSAGES_TO_EXECUTE: Map<&CrossChainId, ExecutableMessage> = Map::new("received_messages"); +const ROUTABLE_MESSAGES: Map<&CrossChainId, Message> = Map::new("routable_messages"); +pub(crate) const ROUTABLE_MESSAGES_INDEX: Counter = Counter::new("routable_message_index"); +const EXECUTABLE_MESSAGES: Map<&CrossChainId, ExecutableMessage> = Map::new("executable_messages"); #[derive(thiserror::Error, Debug, PartialEq, IntoContractError)] pub enum Error { @@ -38,20 +37,21 @@ pub(crate) struct Config { #[cw_serde] pub enum ExecutableMessage { - NotExecuted(Message), + // sent by the router, but not executed yet + Approved(Message), Executed(Message), } impl ExecutableMessage { pub fn msg(&self) -> &Message { match self { - ExecutableMessage::NotExecuted(msg) | ExecutableMessage::Executed(msg) => msg, + ExecutableMessage::Approved(msg) | ExecutableMessage::Executed(msg) => msg, } } pub fn msg_owned(self) -> Message { match self { - ExecutableMessage::NotExecuted(msg) | ExecutableMessage::Executed(msg) => msg, + ExecutableMessage::Approved(msg) | ExecutableMessage::Executed(msg) => msg, } } } @@ -64,29 +64,29 @@ pub(crate) fn load_config(storage: &dyn Storage) -> Result { CONFIG.may_load(storage)?.ok_or(Error::MissingConfig) } -pub(crate) fn save_unique_contract_call_msg( +pub(crate) fn save_unique_routable_msg( storage: &mut dyn Storage, cc_id: &CrossChainId, msg: &Message, ) -> Result<(), Error> { // these cc IDs are generated by the gateway and should be unique. // If there is a collision, there must be a bug in the logic of the caller. - if MESSAGES_FROM_CONTRACT_CALL.has(storage, cc_id) { + if ROUTABLE_MESSAGES.has(storage, cc_id) { return Err(Error::MessageAlreadyExists(cc_id.clone())); } - Ok(MESSAGES_FROM_CONTRACT_CALL.save(storage, cc_id, msg)?) + Ok(ROUTABLE_MESSAGES.save(storage, cc_id, msg)?) } -pub(crate) fn may_load_contract_call_msg( +pub(crate) fn may_load_routable_msg( storage: &dyn Storage, id: &CrossChainId, ) -> Result, Error> { - Ok(MESSAGES_FROM_CONTRACT_CALL.may_load(storage, id)?) + Ok(ROUTABLE_MESSAGES.may_load(storage, id)?) } -pub fn load_contract_call_msg(storage: &dyn Storage, id: &CrossChainId) -> Result { - may_load_contract_call_msg(storage, id)?.ok_or_else(|| Error::MessageNotFound(id.clone())) +pub fn load_routable_msg(storage: &dyn Storage, id: &CrossChainId) -> Result { + may_load_routable_msg(storage, id)?.ok_or_else(|| Error::MessageNotFound(id.clone())) } pub(crate) fn save_executable_msg( @@ -101,9 +101,7 @@ pub(crate) fn save_executable_msg( Err(Error::MessageMismatch(msg.cc_id.clone())) } Some(_) => Ok(()), // new message is identical, no need to store it - None => { - Ok(MESSAGES_TO_EXECUTE.save(storage, cc_id, &ExecutableMessage::NotExecuted(msg))?) - } + None => Ok(EXECUTABLE_MESSAGES.save(storage, cc_id, &ExecutableMessage::Approved(msg))?), } } @@ -111,7 +109,7 @@ pub(crate) fn may_load_executable_msg( storage: &dyn Storage, cc_id: &CrossChainId, ) -> Result, Error> { - Ok(MESSAGES_TO_EXECUTE.may_load(storage, cc_id)?) + Ok(EXECUTABLE_MESSAGES.may_load(storage, cc_id)?) } pub fn load_executable_msg( @@ -130,7 +128,7 @@ pub(crate) fn mark_msg_as_executed( .ok_or_else(|| Error::MessageNotApproved(cc_id.clone()))?; match existing { - ExecutableMessage::NotExecuted(msg) => Ok(MESSAGES_TO_EXECUTE.save( + ExecutableMessage::Approved(msg) => Ok(EXECUTABLE_MESSAGES.save( storage, cc_id, &ExecutableMessage::Executed(msg.clone()), From d4d66968a79eca70aef3d05dfd061680c2362705 Mon Sep 17 00:00:00 2001 From: Christian Gorenflo Date: Thu, 22 Aug 2024 15:56:44 -0400 Subject: [PATCH 04/23] refactor(axelarnet-gateeway): improve contract structure --- contracts/axelarnet-gateway/Cargo.toml | 1 + .../axelarnet-gateway/src/clients/gateway.rs | 17 ++-- contracts/axelarnet-gateway/src/contract.rs | 19 +++- .../axelarnet-gateway/src/contract/execute.rs | 89 +++++++++++-------- .../axelarnet-gateway/src/contract/query.rs | 2 +- contracts/axelarnet-gateway/src/msg.rs | 26 ++---- contracts/axelarnet-gateway/src/state.rs | 26 +++--- contracts/gateway/src/contract/execute.rs | 23 ++--- 8 files changed, 99 insertions(+), 104 deletions(-) diff --git a/contracts/axelarnet-gateway/Cargo.toml b/contracts/axelarnet-gateway/Cargo.toml index 4c5499714..adf68a8e0 100644 --- a/contracts/axelarnet-gateway/Cargo.toml +++ b/contracts/axelarnet-gateway/Cargo.toml @@ -46,6 +46,7 @@ serde = { workspace = true } serde_json = { workspace = true } sha3 = { workspace = true } thiserror = { workspace = true } +itertools = { workspace = true } [dev-dependencies] cw-multi-test = "0.15.1" diff --git a/contracts/axelarnet-gateway/src/clients/gateway.rs b/contracts/axelarnet-gateway/src/clients/gateway.rs index 9780c54bf..558078971 100644 --- a/contracts/axelarnet-gateway/src/clients/gateway.rs +++ b/contracts/axelarnet-gateway/src/clients/gateway.rs @@ -2,7 +2,7 @@ use axelar_wasm_std::vec::VecExt; use cosmwasm_std::{HexBinary, WasmMsg}; use router_api::{Address, ChainName, CrossChainId, Message}; -use crate::msg::{CallContractData, ExecuteMsg, QueryMsg}; +use crate::msg::{ExecuteMsg, QueryMsg}; impl<'a> From> for Client<'a> { fn from(client: client::Client<'a, ExecuteMsg, QueryMsg>) -> Self { @@ -21,12 +21,11 @@ impl<'a> Client<'a> { destination_address: Address, payload: HexBinary, ) -> WasmMsg { - self.client - .execute(&ExecuteMsg::CallContract(CallContractData { - destination_chain, - destination_address, - payload, - })) + self.client.execute(&ExecuteMsg::CallContract { + destination_chain, + destination_address, + payload, + }) } pub fn execute(&self, cc_id: CrossChainId, payload: HexBinary) -> WasmMsg { @@ -68,11 +67,11 @@ mod test { msg, WasmMsg::Execute { contract_addr: addr.to_string(), - msg: to_json_binary(&ExecuteMsg::CallContract(CallContractData { + msg: to_json_binary(&ExecuteMsg::CallContract { destination_chain, destination_address, payload, - })) + }) .unwrap(), funds: vec![], } diff --git a/contracts/axelarnet-gateway/src/contract.rs b/contracts/axelarnet-gateway/src/contract.rs index e372f6445..a26165ccf 100644 --- a/contracts/axelarnet-gateway/src/contract.rs +++ b/contracts/axelarnet-gateway/src/contract.rs @@ -65,10 +65,21 @@ pub fn execute( msg: ExecuteMsg, ) -> Result { match msg.ensure_permissions(deps.storage, &info.sender)? { - ExecuteMsg::CallContract(data) => { - execute::call_contract(deps.storage, env.block.height, info.sender, data) - .change_context(Error::CallContract) - } + ExecuteMsg::CallContract { + destination_chain, + destination_address, + payload, + } => execute::call_contract( + deps.storage, + env.block.height, + info.sender, + execute::CallContractData { + destination_chain, + destination_address, + payload, + }, + ) + .change_context(Error::CallContract), ExecuteMsg::RouteMessages(msgs) => execute::route_messages(deps.storage, info.sender, msgs) .change_context(Error::RouteMessages), ExecuteMsg::Execute { cc_id, payload } => { diff --git a/contracts/axelarnet-gateway/src/contract/execute.rs b/contracts/axelarnet-gateway/src/contract/execute.rs index 4f6e77a96..c21ad87af 100644 --- a/contracts/axelarnet-gateway/src/contract/execute.rs +++ b/contracts/axelarnet-gateway/src/contract/execute.rs @@ -1,17 +1,19 @@ use std::str::FromStr; -use axelar_wasm_std::error::accumulate_reports; use axelar_wasm_std::msg_id::HexTxHashAndEventIndex; use axelar_wasm_std::{FnExt, IntoContractError}; +use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, DepsMut, HexBinary, Response, Storage, Uint256}; use error_stack::{bail, ensure, report, Result, ResultExt}; +use itertools::Itertools; use router_api::client::Router; use router_api::{Address, ChainName, CrossChainId, Message}; use sha3::{Digest, Keccak256}; use crate::clients::PayloadExecutor; use crate::events::AxelarnetGatewayEvent; -use crate::{msg, state}; +use crate::state; +use crate::state::Config; #[derive(thiserror::Error, Debug, IntoContractError)] pub enum Error { @@ -23,8 +25,6 @@ pub enum Error { MessageMismatch(CrossChainId), #[error("failed to mark message with ID {0} as executed")] MarkExecuted(CrossChainId), - #[error("payload hash doesn't match message")] - PayloadHashMismatch, #[error("expected destination chain {expected}, got {actual}")] InvalidDestination { expected: ChainName, @@ -48,15 +48,37 @@ pub enum Error { InvalidSourceAddress(Addr), } +#[cw_serde] +pub struct CallContractData { + pub destination_chain: ChainName, + pub destination_address: Address, + pub payload: HexBinary, +} + +impl CallContractData { + pub fn into_message(self, id: CrossChainId, source_address: Address) -> Message { + Message { + cc_id: id, + source_address, + destination_chain: self.destination_chain, + destination_address: self.destination_address, + payload_hash: Keccak256::digest(self.payload.as_slice()).into(), + } + } +} + pub fn call_contract( storage: &mut dyn Storage, block_height: u64, sender: Addr, - call_contract: msg::CallContractData, + call_contract: CallContractData, ) -> Result { let payload = call_contract.payload.clone(); - let id = generate_cross_chain_id(storage, block_height) + let Config { router, chain_name } = + state::load_config(storage).change_context(Error::ConfigAccess)?; + + let id = generate_cross_chain_id(storage, block_height, chain_name) .change_context(Error::CrossChainIdGeneration)?; let source_address = Address::from_str(sender.as_str()) .change_context(Error::InvalidSourceAddress(sender.clone()))?; @@ -66,8 +88,10 @@ pub fn call_contract( .inspect_err(|err| panic_if_already_exists(err, &msg.cc_id)) .change_context(Error::SaveRoutableMessage)?; - Ok(route_messages(storage, sender, vec![msg.clone()])? - .add_event(AxelarnetGatewayEvent::ContractCalled { msg, payload }.into())) + Ok( + route_to_router(storage, &Router { address: router }, vec![msg.clone()])? + .add_event(AxelarnetGatewayEvent::ContractCalled { msg, payload }.into()), + ) } pub fn route_messages( @@ -75,11 +99,9 @@ pub fn route_messages( sender: Addr, msgs: Vec, ) -> Result { - let config = state::load_config(storage).change_context(Error::ConfigAccess)?; - let chain_name = config.chain_name; - let router = Router { - address: config.router, - }; + let Config { chain_name, router } = + state::load_config(storage).change_context(Error::ConfigAccess)?; + let router = Router { address: router }; if sender == router.address { Ok(prepare_msgs_for_execution(storage, chain_name, msgs)?) @@ -90,10 +112,13 @@ pub fn route_messages( } pub fn execute(deps: DepsMut, cc_id: CrossChainId, payload: HexBinary) -> Result { - let msg = approved_msg(deps.storage, &cc_id, &payload)?; - - state::mark_msg_as_executed(deps.storage, &cc_id) - .change_context(Error::MarkExecuted(cc_id.clone()))?; + let payload_hash: [u8; 32] = Keccak256::digest(payload.as_slice()).into(); + let msg = state::update_as_executed(deps.storage, &cc_id, |msg| { + (payload_hash == msg.payload_hash) + .then_some(msg) + .ok_or(state::Error::PayloadHashMismatch) + }) + .change_context(Error::MarkExecuted(cc_id.clone()))?; let executor = PayloadExecutor::new(deps.as_ref(), &msg.destination_address) .change_context(Error::CreateExecutor)?; @@ -104,24 +129,10 @@ pub fn execute(deps: DepsMut, cc_id: CrossChainId, payload: HexBinary) -> Result .then(Ok) } -fn approved_msg( - storage: &dyn Storage, - cc_id: &CrossChainId, - payload: &HexBinary, -) -> Result { - let msg = state::load_executable_msg(storage, cc_id) - .change_context(Error::PayloadNotApproved)? - .msg_owned(); - - let payload_hash: [u8; 32] = Keccak256::digest(payload.as_slice()).into(); - ensure!(payload_hash == msg.payload_hash, Error::PayloadHashMismatch); - - Ok(msg) -} - fn generate_cross_chain_id( storage: &mut dyn Storage, block_height: u64, + chain_name: ChainName, ) -> Result { // TODO: Retrieve the actual tx hash from core, since cosmwasm doesn't provide it. // Use the block height as the placeholder in the meantime. @@ -132,8 +143,7 @@ fn generate_cross_chain_id( .change_context(Error::EventIndex)?, }; - let config = state::load_config(storage).change_context(Error::ConfigAccess)?; - CrossChainId::new(config.chain_name, message_id).change_context(Error::InvalidCrossChainId) + CrossChainId::new(chain_name, message_id).change_context(Error::InvalidCrossChainId) } fn panic_if_already_exists(err: &state::Error, cc_id: &CrossChainId) { @@ -176,10 +186,11 @@ fn route_to_router( router: &Router, msgs: Vec, ) -> Result { - let msgs = msgs - .iter() + let msgs: Vec<_> = msgs + .into_iter() + .unique() .filter_map(|msg| verify_message(store, msg).transpose()) - .fold(Ok(vec![]), accumulate_reports)?; + .try_collect()?; Ok(Response::new() .add_messages(router.route(msgs.clone())) @@ -191,12 +202,12 @@ 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 verify_message(store: &mut dyn Storage, msg: &Message) -> Result, Error> { +fn verify_message(store: &mut dyn Storage, msg: Message) -> Result, Error> { let stored_msg = state::may_load_routable_msg(store, &msg.cc_id) .change_context(Error::ExecutableMessageAccess)?; match stored_msg { - Some(stored_msg) if stored_msg != *msg => { + Some(stored_msg) if stored_msg != msg => { bail!(Error::MessageMismatch(msg.cc_id.clone())) } Some(stored_msg) => Ok(Some(stored_msg)), diff --git a/contracts/axelarnet-gateway/src/contract/query.rs b/contracts/axelarnet-gateway/src/contract/query.rs index 177789fa4..cace9b105 100644 --- a/contracts/axelarnet-gateway/src/contract/query.rs +++ b/contracts/axelarnet-gateway/src/contract/query.rs @@ -45,7 +45,7 @@ pub fn executable_messages( // cc_id: CrossChainId::new(SOURCE_CHAIN, id).unwrap(), // source_address: "source-address".parse().unwrap(), // destination_chain: DESTINATION_CHAIN.parse().unwrap(), -// destination_address: "destination-address".parse().unwrap(), +// destination_address: "destination-address".parse().unwrpub(crate)ap(), // payload_hash: [0; 32], // } // } diff --git a/contracts/axelarnet-gateway/src/msg.rs b/contracts/axelarnet-gateway/src/msg.rs index cd17c1a2a..29e2ee71b 100644 --- a/contracts/axelarnet-gateway/src/msg.rs +++ b/contracts/axelarnet-gateway/src/msg.rs @@ -2,7 +2,6 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::HexBinary; use msgs_derive::EnsurePermissions; use router_api::{Address, ChainName, CrossChainId, Message}; -use sha3::{Digest, Keccak256}; use crate::state::ExecutableMessage; @@ -34,26 +33,11 @@ pub enum ExecuteMsg { /// Initiate a cross-chain contract call from Axelarnet to another chain. /// The message will be routed to the destination chain's gateway via the router. #[permission(Any)] - CallContract(CallContractData), -} - -#[cw_serde] -pub struct CallContractData { - pub destination_chain: ChainName, - pub destination_address: Address, - pub payload: HexBinary, -} - -impl CallContractData { - pub fn into_message(self, id: CrossChainId, source_address: Address) -> Message { - Message { - cc_id: id, - source_address, - destination_chain: self.destination_chain, - destination_address: self.destination_address, - payload_hash: Keccak256::digest(self.payload.as_slice()).into(), - } - } + CallContract { + destination_chain: ChainName, + destination_address: Address, + payload: HexBinary, + }, } #[cw_serde] diff --git a/contracts/axelarnet-gateway/src/state.rs b/contracts/axelarnet-gateway/src/state.rs index e13596b7f..fe7895366 100644 --- a/contracts/axelarnet-gateway/src/state.rs +++ b/contracts/axelarnet-gateway/src/state.rs @@ -27,6 +27,8 @@ pub enum Error { MessageAlreadyExecuted(CrossChainId), #[error("sent message with ID {0} already exists")] MessageAlreadyExists(CrossChainId), + #[error("payload hash doesn't match message")] + PayloadHashMismatch, } #[cw_serde] @@ -120,22 +122,22 @@ pub fn load_executable_msg( } /// Update the status of a message to executed if it is in approved status, error otherwise. -pub(crate) fn mark_msg_as_executed( +pub(crate) fn update_as_executed( storage: &mut dyn Storage, cc_id: &CrossChainId, -) -> Result<(), Error> { - let existing = may_load_executable_msg(storage, cc_id)? - .ok_or_else(|| Error::MessageNotApproved(cc_id.clone()))?; + action: impl FnOnce(Message) -> Result, +) -> Result { + let msg = match may_load_executable_msg(storage, cc_id)? { + None => Err(Error::MessageNotApproved(cc_id.clone())), + Some(ExecutableMessage::Executed(_)) => Err(Error::MessageAlreadyExecuted(cc_id.clone())), + Some(ExecutableMessage::Approved(msg)) => Ok(action(msg)?), + }?; - match existing { - ExecutableMessage::Approved(msg) => Ok(EXECUTABLE_MESSAGES.save( - storage, - cc_id, - &ExecutableMessage::Executed(msg.clone()), - )?), - ExecutableMessage::Executed(_) => Err(Error::MessageAlreadyExecuted(cc_id.clone())), - } + EXECUTABLE_MESSAGES.save(storage, cc_id, &ExecutableMessage::Executed(msg.clone()))?; + + Ok(msg) } + // // #[cfg(test)] // mod tests { diff --git a/contracts/gateway/src/contract/execute.rs b/contracts/gateway/src/contract/execute.rs index d72f98527..5c44ca465 100644 --- a/contracts/gateway/src/contract/execute.rs +++ b/contracts/gateway/src/contract/execute.rs @@ -34,7 +34,7 @@ pub(crate) fn route_outgoing_messages( store: &mut dyn Storage, verified: Vec, ) -> Result { - let msgs = check_for_duplicates(verified)?; + let msgs: Vec<_> = verified.into_iter().unique().collect(); for msg in msgs.iter() { state::save_outgoing_message(store, &msg.cc_id, msg) @@ -52,8 +52,10 @@ fn apply( msgs: Vec, action: impl Fn(Vec<(VerificationStatus, Vec)>) -> (Option, Vec), ) -> Result { - check_for_duplicates(msgs)? - .then(|msgs| verifier.messages_status(msgs)) + let unique_msgs = msgs.into_iter().unique().collect(); + + verifier + .messages_status(unique_msgs) .change_context(Error::MessageStatus)? .then(group_by_status) .then(action) @@ -61,21 +63,6 @@ fn apply( .then(Ok) } -fn check_for_duplicates(msgs: Vec) -> Result, Error> { - let duplicates: Vec<_> = msgs - .iter() - // the following two map instructions are separated on purpose - // so the duplicate check is done on the typed id instead of just a string - .map(|m| &m.cc_id) - .duplicates() - .map(|cc_id| cc_id.to_string()) - .collect(); - if !duplicates.is_empty() { - return Err(Error::DuplicateMessageIds).attach_printable(duplicates.iter().join(", ")); - } - Ok(msgs) -} - fn group_by_status( msgs_with_status: impl IntoIterator, ) -> Vec<(VerificationStatus, Vec)> { From d1a4c6b38f0c4eb3b06d1dde07b0d90f39040403 Mon Sep 17 00:00:00 2001 From: Christian Gorenflo Date: Thu, 22 Aug 2024 16:34:37 -0400 Subject: [PATCH 05/23] improve naming --- Cargo.lock | 1 + .../axelarnet-gateway/src/clients/external.rs | 46 +++++++----- .../axelarnet-gateway/src/contract/execute.rs | 6 +- contracts/axelarnet-gateway/tests/execute.rs | 74 +++++++++++++++++++ .../axelarnet-gateway/tests/instantiate.rs | 28 +++++++ .../tests/utils/instantiate.rs | 19 +++++ .../axelarnet-gateway/tests/utils/mod.rs | 2 + .../axelarnet-gateway/tests/utils/params.rs | 2 + 8 files changed, 157 insertions(+), 21 deletions(-) create mode 100644 contracts/axelarnet-gateway/tests/execute.rs create mode 100644 contracts/axelarnet-gateway/tests/instantiate.rs create mode 100644 contracts/axelarnet-gateway/tests/utils/instantiate.rs create mode 100644 contracts/axelarnet-gateway/tests/utils/mod.rs create mode 100644 contracts/axelarnet-gateway/tests/utils/params.rs diff --git a/Cargo.lock b/Cargo.lock index 92d7f7b64..511ea6a4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -845,6 +845,7 @@ dependencies = [ "cw-storage-plus 1.2.0", "cw2 1.1.2", "error-stack", + "itertools 0.11.0", "msgs-derive", "report", "router-api", diff --git a/contracts/axelarnet-gateway/src/clients/external.rs b/contracts/axelarnet-gateway/src/clients/external.rs index 94914e974..40ef540c9 100644 --- a/contracts/axelarnet-gateway/src/clients/external.rs +++ b/contracts/axelarnet-gateway/src/clients/external.rs @@ -12,35 +12,44 @@ pub struct AxelarExecutableMsg { pub payload: HexBinary, } -pub struct PayloadExecutor<'a> { - client: client::Client<'a, AxelarExecutableMsg, ()>, +pub struct CrossChainExecutor<'a> { + client: client::Client<'a, ExecuteMsg, ()>, } -impl<'a> From> for PayloadExecutor<'a> { - fn from(client: client::Client<'a, AxelarExecutableMsg, ()>) -> Self { - PayloadExecutor { client } - } -} - -impl<'a> PayloadExecutor<'a> { +impl<'a> CrossChainExecutor<'a> { pub fn new(deps: Deps<'a>, destination: &str) -> error_stack::Result { let destination = address::validate_cosmwasm_address(deps.api, destination)?; - Ok(PayloadExecutor { + Ok(CrossChainExecutor { client: client::Client::new(deps.querier, destination), }) } - pub fn execute( + pub fn prepare_execute_msg( &self, cc_id: CrossChainId, source_address: Address, payload: HexBinary, ) -> WasmMsg { - self.client.execute(&AxelarExecutableMsg { - cc_id, - source_address, - payload, - }) + self.client + .execute(&ExecuteMsg::Execute(AxelarExecutableMsg { + cc_id, + source_address, + payload, + })) + } +} + +/// By convention, amplifier-compatible contracts must expose this `Execute` variant. +/// Due to identical json serialization, we can imitate it here so the gateway can call it. +#[cw_serde] +enum ExecuteMsg { + /// Execute the message at the destination contract with the corresponding payload. + Execute(AxelarExecutableMsg), +} + +impl<'a> From> for CrossChainExecutor<'a> { + fn from(client: client::Client<'a, ExecuteMsg, ()>) -> Self { + CrossChainExecutor { client } } } @@ -54,14 +63,15 @@ mod test { #[test] fn execute_message() { let (querier, addr) = setup(); - let client: PayloadExecutor = + let client: CrossChainExecutor = client::Client::new(QuerierWrapper::new(&querier), addr.clone()).into(); let cc_id = CrossChainId::new("source-chain", "message-id").unwrap(); let source_address: Address = "source-address".parse().unwrap(); let payload = HexBinary::from(vec![1, 2, 3]); - let msg = client.execute(cc_id.clone(), source_address.clone(), payload.clone()); + let msg = + client.prepare_execute_msg(cc_id.clone(), source_address.clone(), payload.clone()); assert_eq!( msg, diff --git a/contracts/axelarnet-gateway/src/contract/execute.rs b/contracts/axelarnet-gateway/src/contract/execute.rs index c21ad87af..90c729e9f 100644 --- a/contracts/axelarnet-gateway/src/contract/execute.rs +++ b/contracts/axelarnet-gateway/src/contract/execute.rs @@ -10,7 +10,7 @@ use router_api::client::Router; use router_api::{Address, ChainName, CrossChainId, Message}; use sha3::{Digest, Keccak256}; -use crate::clients::PayloadExecutor; +use crate::clients::CrossChainExecutor; use crate::events::AxelarnetGatewayEvent; use crate::state; use crate::state::Config; @@ -120,11 +120,11 @@ pub fn execute(deps: DepsMut, cc_id: CrossChainId, payload: HexBinary) -> Result }) .change_context(Error::MarkExecuted(cc_id.clone()))?; - let executor = PayloadExecutor::new(deps.as_ref(), &msg.destination_address) + let executor = CrossChainExecutor::new(deps.as_ref(), &msg.destination_address) .change_context(Error::CreateExecutor)?; Response::new() - .add_message(executor.execute(cc_id, msg.source_address.clone(), payload)) + .add_message(executor.prepare_execute_msg(cc_id, msg.source_address.clone(), payload)) .add_event(AxelarnetGatewayEvent::MessageExecuted { msg }.into()) .then(Ok) } diff --git a/contracts/axelarnet-gateway/tests/execute.rs b/contracts/axelarnet-gateway/tests/execute.rs new file mode 100644 index 000000000..07368ee5e --- /dev/null +++ b/contracts/axelarnet-gateway/tests/execute.rs @@ -0,0 +1,74 @@ +use axelar_wasm_std::error::ContractError; +use axelarnet_gateway::contract; +use axelarnet_gateway::msg::ExecuteMsg; +use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; +use cosmwasm_std::{DepsMut, HexBinary, Response}; +use router_api::{CrossChainId, Message}; +use sha3::Digest; + +use crate::utils::instantiate::instantiate_contract; +use crate::utils::params; + +mod utils; + +#[test] +fn message_not_approved() { + let mut deps = mock_dependencies(); + + instantiate_contract(deps.as_mut()).unwrap(); + + assert!(contract::execute( + deps.as_mut(), + mock_env(), + mock_info("sender", &[]), + ExecuteMsg::Execute { + cc_id: CrossChainId::new("source-chain", "hash-index").unwrap(), + payload: vec![1, 2, 3].into(), + }, + ) + .is_ok()); +} + +#[test] +fn message_already_executed() { + let mut deps = mock_dependencies(); + + let cc_id = CrossChainId::new("chain", "hash-index").unwrap(); + let payload: HexBinary = vec![1, 2, 3].into(); + + let msgs = vec![Message { + cc_id: cc_id.clone(), + source_address: "source-address".parse().unwrap(), + destination_chain: "axelarnet".parse().unwrap(), + destination_address: "destination-address".parse().unwrap(), + payload_hash: sha3::Keccak256::digest(&payload).into(), + }]; + + instantiate_contract(deps.as_mut()).unwrap(); + route_from_router(deps.as_mut(), msgs).unwrap(); + execute_payload(deps.as_mut(), cc_id.clone(), payload.clone()).unwrap(); + + assert!(execute_payload(deps.as_mut(), cc_id, payload).is_err()); +} + +fn execute_payload( + deps: DepsMut, + cc_id: CrossChainId, + payload: HexBinary, +) -> Result { + contract::execute( + deps, + mock_env(), + mock_info("sender", &[]), + ExecuteMsg::Execute { cc_id, payload }.clone(), + ) +} + +fn route_from_router(deps: DepsMut, msgs: Vec) -> Result { + contract::execute( + deps, + mock_env(), + mock_info(params::ROUTER, &[]), + ExecuteMsg::RouteMessages(msgs), + ) +} diff --git a/contracts/axelarnet-gateway/tests/instantiate.rs b/contracts/axelarnet-gateway/tests/instantiate.rs new file mode 100644 index 000000000..9a3192b57 --- /dev/null +++ b/contracts/axelarnet-gateway/tests/instantiate.rs @@ -0,0 +1,28 @@ +use axelarnet_gateway::contract; +use axelarnet_gateway::msg::InstantiateMsg; +use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; + +use crate::utils::instantiate::instantiate_contract; +use crate::utils::params; + +mod utils; +#[test] +fn instantiate_works() { + let mut deps = mock_dependencies(); + + assert!(instantiate_contract(deps.as_mut()).is_ok()); +} + +#[test] +fn invalid_router_address() { + let mut deps = mock_dependencies(); + + let msg = InstantiateMsg { + chain_name: params::AXELARNET.parse().unwrap(), + router_address: "".to_string(), + }; + + assert!( + contract::instantiate(deps.as_mut(), mock_env(), mock_info("sender", &[]), msg).is_err() + ); +} diff --git a/contracts/axelarnet-gateway/tests/utils/instantiate.rs b/contracts/axelarnet-gateway/tests/utils/instantiate.rs new file mode 100644 index 000000000..3de8b6abb --- /dev/null +++ b/contracts/axelarnet-gateway/tests/utils/instantiate.rs @@ -0,0 +1,19 @@ +use axelar_wasm_std::error::ContractError; +use axelarnet_gateway::contract; +use axelarnet_gateway::msg::InstantiateMsg; +use cosmwasm_std::testing::{mock_env, mock_info}; +use cosmwasm_std::{DepsMut, Response}; + +use crate::utils::params; + +pub fn instantiate_contract(deps: DepsMut) -> Result { + contract::instantiate( + deps, + mock_env(), + mock_info("sender", &[]), + InstantiateMsg { + chain_name: params::AXELARNET.parse().unwrap(), + router_address: params::ROUTER.parse().unwrap(), + }, + ) +} diff --git a/contracts/axelarnet-gateway/tests/utils/mod.rs b/contracts/axelarnet-gateway/tests/utils/mod.rs new file mode 100644 index 000000000..ad374342f --- /dev/null +++ b/contracts/axelarnet-gateway/tests/utils/mod.rs @@ -0,0 +1,2 @@ +pub mod instantiate; +pub mod params; diff --git a/contracts/axelarnet-gateway/tests/utils/params.rs b/contracts/axelarnet-gateway/tests/utils/params.rs new file mode 100644 index 000000000..53c73f6e4 --- /dev/null +++ b/contracts/axelarnet-gateway/tests/utils/params.rs @@ -0,0 +1,2 @@ +pub const AXELARNET: &'static str = "axelarnet"; +pub const ROUTER: &'static str = "router"; From 42953ac1107075c40803c642bc331a8629ed2858 Mon Sep 17 00:00:00 2001 From: Christian Gorenflo Date: Thu, 22 Aug 2024 16:43:16 -0400 Subject: [PATCH 06/23] merge conflicts --- contracts/axelarnet-gateway/src/contract/query.rs | 2 +- contracts/axelarnet-gateway/src/state.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/axelarnet-gateway/src/contract/query.rs b/contracts/axelarnet-gateway/src/contract/query.rs index cace9b105..177789fa4 100644 --- a/contracts/axelarnet-gateway/src/contract/query.rs +++ b/contracts/axelarnet-gateway/src/contract/query.rs @@ -45,7 +45,7 @@ pub fn executable_messages( // cc_id: CrossChainId::new(SOURCE_CHAIN, id).unwrap(), // source_address: "source-address".parse().unwrap(), // destination_chain: DESTINATION_CHAIN.parse().unwrap(), -// destination_address: "destination-address".parse().unwrpub(crate)ap(), +// destination_address: "destination-address".parse().unwrap(), // payload_hash: [0; 32], // } // } diff --git a/contracts/axelarnet-gateway/src/state.rs b/contracts/axelarnet-gateway/src/state.rs index 9057037bb..53b4fbe9a 100644 --- a/contracts/axelarnet-gateway/src/state.rs +++ b/contracts/axelarnet-gateway/src/state.rs @@ -8,7 +8,7 @@ use router_api::{ChainName, CrossChainId, Message}; const CONFIG: Item = Item::new("config"); const ROUTABLE_MESSAGES: Map<&CrossChainId, Message> = Map::new("routable_messages"); -pub(crate) const ROUTABLE_MESSAGES_INDEX: Counter = Counter::new("routable_message_index"); +pub const ROUTABLE_MESSAGES_INDEX: Counter = Counter::new("routable_message_index"); const EXECUTABLE_MESSAGES: Map<&CrossChainId, ExecutableMessage> = Map::new("executable_messages"); #[derive(thiserror::Error, Debug, PartialEq, IntoContractError)] @@ -32,7 +32,7 @@ pub enum Error { } #[cw_serde] -pub(crate) struct Config { +pub struct Config { pub chain_name: ChainName, pub router: Addr, } @@ -107,7 +107,7 @@ pub fn save_executable_msg( } } -pub(crate) fn may_load_executable_msg( +pub fn may_load_executable_msg( storage: &dyn Storage, cc_id: &CrossChainId, ) -> Result, Error> { From 65d4dbbe1bf5d01e55303813e88e94d7f7587da8 Mon Sep 17 00:00:00 2001 From: Christian Gorenflo Date: Fri, 23 Aug 2024 00:19:49 -0400 Subject: [PATCH 07/23] finish tests --- Cargo.lock | 64 +++- Cargo.toml | 1 + contracts/axelarnet-gateway/Cargo.toml | 3 +- .../axelarnet-gateway/src/clients/external.rs | 20 +- .../axelarnet-gateway/src/contract/execute.rs | 228 +------------ .../axelarnet-gateway/src/contract/query.rs | 147 -------- contracts/axelarnet-gateway/src/lib.rs | 3 +- contracts/axelarnet-gateway/src/state.rs | 201 +---------- contracts/axelarnet-gateway/tests/execute.rs | 322 +++++++++++++++--- .../axelarnet-gateway/tests/instantiate.rs | 3 +- contracts/axelarnet-gateway/tests/query.rs | 252 ++++++++++++++ ...ontract_call_returns_correct_events.golden | 64 ++++ ...ntract_call_returns_correct_message.golden | 14 + ...message_once_returns_correct_events.golden | 31 ++ ...essage_once_returns_correct_message.golden | 10 + ...ble_messages_gets_expected_messages.golden | 110 ++++++ ...ame_message_multiple_times_succeeds.golden | 36 ++ ...er_contract_call_ignores_duplicates.golden | 14 + ...ntract_call_succeeds_multiple_times.golden | 14 + .../axelarnet-gateway/tests/utils/execute.rs | 48 +++ .../axelarnet-gateway/tests/utils/messages.rs | 44 +++ .../axelarnet-gateway/tests/utils/mod.rs | 12 +- 22 files changed, 998 insertions(+), 643 deletions(-) create mode 100644 contracts/axelarnet-gateway/tests/query.rs create mode 100644 contracts/axelarnet-gateway/tests/testdata/contract_call_returns_correct_events.golden create mode 100644 contracts/axelarnet-gateway/tests/testdata/contract_call_returns_correct_message.golden create mode 100644 contracts/axelarnet-gateway/tests/testdata/execute_approved_message_once_returns_correct_events.golden create mode 100644 contracts/axelarnet-gateway/tests/testdata/execute_approved_message_once_returns_correct_message.golden create mode 100644 contracts/axelarnet-gateway/tests/testdata/query_executable_messages_gets_expected_messages.golden create mode 100644 contracts/axelarnet-gateway/tests/testdata/route_from_router_same_message_multiple_times_succeeds.golden create mode 100644 contracts/axelarnet-gateway/tests/testdata/route_to_router_after_contract_call_ignores_duplicates.golden create mode 100644 contracts/axelarnet-gateway/tests/testdata/route_to_router_after_contract_call_succeeds_multiple_times.golden create mode 100644 contracts/axelarnet-gateway/tests/utils/execute.rs create mode 100644 contracts/axelarnet-gateway/tests/utils/messages.rs diff --git a/Cargo.lock b/Cargo.lock index 767583da5..9db0763f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -807,7 +807,7 @@ dependencies = [ "bs58 0.5.1", "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test", + "cw-multi-test 0.15.1", "cw-storage-plus 1.2.0", "cw2 1.1.2", "error-stack", @@ -850,10 +850,11 @@ dependencies = [ "client", "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test", + "cw-multi-test 1.2.0", "cw-storage-plus 1.2.0", "cw2 1.1.2", "error-stack", + "goldie", "itertools 0.11.0", "msgs-derive", "report", @@ -1045,6 +1046,12 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" +[[package]] +name = "bech32" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" + [[package]] name = "beef" version = "0.5.2" @@ -1644,7 +1651,7 @@ dependencies = [ "axelar-wasm-std", "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test", + "cw-multi-test 0.15.1", "cw-storage-plus 1.2.0", "cw2 1.1.2", "error-stack", @@ -1768,7 +1775,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c1556156fdf892a55cced6115968b961eaaadd6f724a2c2cb7d1e168e32dd3" dependencies = [ "base64 0.21.7", - "bech32", + "bech32 0.9.1", "bnum", "cosmwasm-crypto", "cosmwasm-derive", @@ -1941,6 +1948,26 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cw-multi-test" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91fc33b1d65c102d72f46548c64dca423c337e528d6747d0c595316aa65f887b" +dependencies = [ + "anyhow", + "bech32 0.11.0", + "cosmwasm-std", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "derivative", + "itertools 0.13.0", + "prost 0.12.6", + "schemars", + "serde", + "sha2 0.10.8", + "thiserror", +] + [[package]] name = "cw-storage-macro" version = "1.2.0" @@ -2798,7 +2825,7 @@ dependencies = [ "ark-serialize 0.4.2", "auto_ops", "base64ct", - "bech32", + "bech32 0.9.1", "bincode", "blake2", "blst", @@ -3202,7 +3229,7 @@ dependencies = [ "client", "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test", + "cw-multi-test 0.15.1", "cw-storage-plus 1.2.0", "cw2 1.1.2", "error-stack", @@ -4059,7 +4086,7 @@ dependencies = [ "axelar-wasm-std", "coordinator", "cosmwasm-std", - "cw-multi-test", + "cw-multi-test 0.15.1", "error-stack", "gateway", "gateway-api", @@ -4159,6 +4186,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" @@ -4984,7 +5020,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "curve25519-dalek 4.1.3", - "cw-multi-test", + "cw-multi-test 0.15.1", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", "cw2 1.1.2", @@ -5017,7 +5053,7 @@ dependencies = [ "coordinator", "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test", + "cw-multi-test 0.15.1", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", "cw2 1.1.2", @@ -5055,7 +5091,7 @@ checksum = "5cb2f8dd4a17ce9c9fa1ab3d80152929702968be6536499f32bd7e2278c2e0fb" dependencies = [ "anyhow", "base64 0.22.1", - "bech32", + "bech32 0.9.1", "bip39", "hex", "hmac", @@ -6689,7 +6725,7 @@ dependencies = [ "axelar-wasm-std", "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test", + "cw-multi-test 0.15.1", "cw-storage-plus 1.2.0", "cw2 1.1.2", "error-stack", @@ -6800,7 +6836,7 @@ dependencies = [ "axelar-wasm-std", "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test", + "cw-multi-test 0.15.1", "cw-storage-plus 1.2.0", "cw2 1.1.2", "error-stack", @@ -7494,7 +7530,7 @@ dependencies = [ "coordinator", "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test", + "cw-multi-test 0.15.1", "cw-storage-plus 1.2.0", "cw2 1.1.2", "error-stack", @@ -9308,7 +9344,7 @@ dependencies = [ "client", "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test", + "cw-multi-test 0.15.1", "cw-storage-plus 1.2.0", "cw2 1.1.2", "error-stack", diff --git a/Cargo.toml b/Cargo.toml index f8833bb3f..1743b395a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,6 +62,7 @@ strum = { version = "0.25", default-features = false, features = ["derive"] } interchain-token-service = { version = "^0.1.0", path = "interchain-token-service" } goldie = { version = "0.5" } axelarnet-gateway = { version = "^0.1.0", path = "contracts/axelarnet-gateway" } +cw-multi-test = "1.2.0" [workspace.lints.clippy] arithmetic_side_effects = "deny" diff --git a/contracts/axelarnet-gateway/Cargo.toml b/contracts/axelarnet-gateway/Cargo.toml index adf68a8e0..f33010231 100644 --- a/contracts/axelarnet-gateway/Cargo.toml +++ b/contracts/axelarnet-gateway/Cargo.toml @@ -49,7 +49,8 @@ thiserror = { workspace = true } itertools = { workspace = true } [dev-dependencies] -cw-multi-test = "0.15.1" +cw-multi-test = { workspace = true } +goldie = { workspace = true } [lints] workspace = true diff --git a/contracts/axelarnet-gateway/src/clients/external.rs b/contracts/axelarnet-gateway/src/clients/external.rs index 40ef540c9..e414edadd 100644 --- a/contracts/axelarnet-gateway/src/clients/external.rs +++ b/contracts/axelarnet-gateway/src/clients/external.rs @@ -12,6 +12,14 @@ pub struct AxelarExecutableMsg { pub payload: HexBinary, } +/// By convention, amplifier-compatible contracts must expose this `Execute` variant. +/// Due to identical json serialization, we can imitate it here so the gateway can call it. +#[cw_serde] +pub enum ExecuteMsg { + /// Execute the message at the destination contract with the corresponding payload. + Execute(AxelarExecutableMsg), +} + pub struct CrossChainExecutor<'a> { client: client::Client<'a, ExecuteMsg, ()>, } @@ -39,14 +47,6 @@ impl<'a> CrossChainExecutor<'a> { } } -/// By convention, amplifier-compatible contracts must expose this `Execute` variant. -/// Due to identical json serialization, we can imitate it here so the gateway can call it. -#[cw_serde] -enum ExecuteMsg { - /// Execute the message at the destination contract with the corresponding payload. - Execute(AxelarExecutableMsg), -} - impl<'a> From> for CrossChainExecutor<'a> { fn from(client: client::Client<'a, ExecuteMsg, ()>) -> Self { CrossChainExecutor { client } @@ -77,11 +77,11 @@ mod test { msg, WasmMsg::Execute { contract_addr: addr.to_string(), - msg: to_json_binary(&AxelarExecutableMsg { + msg: to_json_binary(&ExecuteMsg::Execute(AxelarExecutableMsg { cc_id, source_address, payload, - }) + })) .unwrap(), funds: vec![], } diff --git a/contracts/axelarnet-gateway/src/contract/execute.rs b/contracts/axelarnet-gateway/src/contract/execute.rs index 90c729e9f..989ec6109 100644 --- a/contracts/axelarnet-gateway/src/contract/execute.rs +++ b/contracts/axelarnet-gateway/src/contract/execute.rs @@ -21,7 +21,7 @@ pub enum Error { SaveExecutableMessage, #[error("failed to access executable message")] ExecutableMessageAccess, - #[error("message with ID {0} is different")] + #[error("message with ID {0} does not match the expected message")] MessageMismatch(CrossChainId), #[error("failed to mark message with ID {0} as executed")] MarkExecuted(CrossChainId), @@ -214,229 +214,3 @@ fn verify_message(store: &mut dyn Storage, msg: Message) -> Result Ok(None), } } - -// #[cfg(test)] -// mod tests { -// use axelar_wasm_std::err_contains; -// use cosmwasm_std::testing::{ -// mock_dependencies, mock_env, mock_info, MockApi, MockQuerier, MockStorage, -// }; -// use cosmwasm_std::{Addr, CosmosMsg, Empty, Env, MessageInfo, OwnedDeps}; -// use router_api::{ChainName, CrossChainId, Message}; -// -// use super::*; -// use crate::contract::{execute, instantiate}; -// use crate::msg::{CallContractData, ExecuteMsg, InstantiateMsg}; -// use crate::state::{self}; -// -// const CHAIN: &str = "chain"; -// const SOURCE_CHAIN: &str = "source-chain"; -// const ROUTER: &str = "router"; -// const PAYLOAD: [u8; 3] = [1, 2, 3]; -// const SENDER: &str = "sender"; -// -// fn setup() -> ( -// OwnedDeps, -// Env, -// MessageInfo, -// ) { -// let mut deps = mock_dependencies(); -// let env = mock_env(); -// let info = mock_info(SENDER, &[]); -// -// let chain_name: ChainName = CHAIN.parse().unwrap(); -// let router = Addr::unchecked(ROUTER); -// -// let msg = InstantiateMsg { -// chain_name: chain_name.clone(), -// router_address: router.to_string(), -// }; -// -// let _res = instantiate(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); -// -// (deps, env, info) -// } -// -// fn dummy_message() -> Message { -// Message { -// cc_id: CrossChainId::new(SOURCE_CHAIN, "message-id").unwrap(), -// source_address: "source-address".parse().unwrap(), -// destination_chain: CHAIN.parse().unwrap(), -// destination_address: "destination-address".parse().unwrap(), -// payload_hash: Keccak256::digest(PAYLOAD).into(), -// } -// } -// -// #[test] -// fn call_contract_and_send_message() { -// let (mut deps, env, info) = setup(); -// -// let expected_message_id = HexTxHashAndEventIndex { -// tx_hash: Uint256::from(env.block.height).to_be_bytes(), -// event_index: 1, -// }; -// let expected_cc_id = CrossChainId::new(CHAIN, expected_message_id).unwrap(); -// let message = Message { -// cc_id: expected_cc_id.clone(), -// source_address: info.sender.clone().into_string().parse().unwrap(), -// destination_chain: "destination-chain".parse().unwrap(), -// destination_address: "destination-address".parse().unwrap(), -// payload_hash: Keccak256::digest(PAYLOAD).into(), -// }; -// -// let msg = ExecuteMsg::CallContract(CallContractData { -// destination_chain: message.destination_chain.clone(), -// destination_address: message.destination_address.clone(), -// payload: PAYLOAD.into(), -// }); -// -// let res = execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); -// let sent_message = -// state::may_load_contract_call_msg(deps.as_mut().storage, &expected_cc_id) -// .unwrap() -// .unwrap(); -// assert_eq!(sent_message, message); -// -// let router: Router = Router { -// address: Addr::unchecked(ROUTER), -// }; -// assert_eq!(res.messages.len(), 1); -// assert_eq!( -// res.messages[0].msg, -// CosmosMsg::Wasm(router.route(vec![message.clone()]).unwrap()) -// ); -// -// // Re-route the message again -// let msg = ExecuteMsg::RouteMessages(vec![message.clone()]); -// let res = execute(deps.as_mut(), env, info, msg).unwrap(); -// assert_eq!(res.messages.len(), 1); -// assert_eq!( -// res.messages[0].msg, -// CosmosMsg::Wasm(router.route(vec![message]).unwrap()) -// ); -// } -// -// #[test] -// fn route_messages_from_router() { -// let (mut deps, env, _) = setup(); -// -// let message = dummy_message(); -// let msg = ExecuteMsg::RouteMessages(vec![message.clone()]); -// -// // Execute RouteMessages as if it's coming from the router -// let info = mock_info(ROUTER, &[]); -// execute(deps.as_mut(), env, info, msg).unwrap(); -// -// // Check that the message was saved as received -// let received_message = -// state::may_load_executable_msg(deps.as_mut().storage, &message.cc_id) -// .unwrap() -// .unwrap(); -// assert_eq!(received_message.msg, message); -// assert!(matches!(received_message.status, MessageStatus::Approved)); -// } -// -// #[test] -// fn execute_message() { -// let (mut deps, env, info) = setup(); -// -// let message = dummy_message(); -// let cc_id = message.cc_id.clone(); -// -// // Save the message as received -// state::save_executable_msg(deps.as_mut().storage, cc_id.clone(), message).unwrap(); -// -// let msg = ExecuteMsg::Execute { -// cc_id: cc_id.clone(), -// payload: PAYLOAD.into(), -// }; -// -// let res = execute(deps.as_mut(), env, info, msg).unwrap(); -// -// // Check that a message was sent to the destination contract -// assert_eq!(res.messages.len(), 1); -// -// // Check that the message status was updated to Executed -// let executed_message = state::may_load_executable_msg(deps.as_mut().storage, &cc_id) -// .unwrap() -// .unwrap(); -// assert!(matches!(executed_message.status, MessageStatus::Executed)); -// } -// -// #[test] -// fn execute_not_found() { -// let (mut deps, env, info) = setup(); -// -// let cc_id = CrossChainId::new(SOURCE_CHAIN, "message-id").unwrap(); -// let msg = ExecuteMsg::Execute { -// cc_id: cc_id.clone(), -// payload: PAYLOAD.into(), -// }; -// -// let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); -// assert!(err_contains!( -// err.report, -// state::Error, -// state::Error::MessageNotApproved(..) -// )); -// assert!(err_contains!(err.report, Error, Error::MarkExecuted(..))); -// } -// -// #[test] -// fn execute_already_executed() { -// let (mut deps, env, info) = setup(); -// -// let message = dummy_message(); -// let cc_id = message.cc_id.clone(); -// -// // Save the message as already executed -// state::save_executable_msg(deps.as_mut().storage, cc_id.clone(), message).unwrap(); -// state::mark_msg_as_executed(deps.as_mut().storage, cc_id.clone()).unwrap(); -// -// let msg = ExecuteMsg::Execute { -// cc_id, -// payload: PAYLOAD.into(), -// }; -// -// let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); -// assert!(err_contains!( -// err.report, -// state::Error, -// state::Error::MessageAlreadyExecuted(..) -// )); -// assert!(err_contains!(err.report, Error, Error::MarkExecuted(..))); -// } -// -// #[test] -// fn execute_payload_mismatch() { -// let (mut deps, env, info) = setup(); -// -// let message = dummy_message(); -// let cc_id = message.cc_id.clone(); -// -// state::save_executable_msg(deps.as_mut().storage, cc_id.clone(), message).unwrap(); -// -// let msg = ExecuteMsg::Execute { -// cc_id, -// payload: [4, 5, 6].into(), -// }; -// -// let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); -// assert!(err_contains!(err.report, Error, Error::PayloadHashMismatch)); -// } -// -// #[test] -// #[should_panic(expected = "should match chain name")] -// fn receive_messages_wrong_chain() { -// let (mut deps, _, _) = setup(); -// -// let mut message = dummy_message(); -// message.destination_chain = "wrong-chain".parse().unwrap(); -// -// let msg = ExecuteMsg::RouteMessages(vec![message]); -// let info = mock_info(ROUTER, &[]); -// -// // This should panic because the destination chain doesn't match the gateway's chain name -// execute(deps.as_mut(), mock_env(), info, msg).unwrap(); -// } -// } diff --git a/contracts/axelarnet-gateway/src/contract/query.rs b/contracts/axelarnet-gateway/src/contract/query.rs index 177789fa4..90d575843 100644 --- a/contracts/axelarnet-gateway/src/contract/query.rs +++ b/contracts/axelarnet-gateway/src/contract/query.rs @@ -24,150 +24,3 @@ pub fn executable_messages( .map(|cc_id| state::load_executable_msg(deps.storage, &cc_id)) .fold(Ok(vec![]), accumulate_errs) } -// -// #[cfg(test)] -// mod tests { -// use axelar_wasm_std::{err_contains, FnExt}; -// use cosmwasm_std::from_json; -// use cosmwasm_std::testing::{mock_dependencies, mock_env}; -// use router_api::{CrossChainId, Message}; -// use serde::de::DeserializeOwned; -// -// use super::*; -// use crate::contract; -// use crate::msg::QueryMsg; -// -// const SOURCE_CHAIN: &str = "source-chain"; -// const DESTINATION_CHAIN: &str = "destination-chain"; -// -// fn dummy_message(id: &str) -> Message { -// Message { -// cc_id: CrossChainId::new(SOURCE_CHAIN, id).unwrap(), -// source_address: "source-address".parse().unwrap(), -// destination_chain: DESTINATION_CHAIN.parse().unwrap(), -// destination_address: "destination-address".parse().unwrap(), -// payload_hash: [0; 32], -// } -// } -// -// // Query a msg and deserialize it. If the query fails, the error is returned -// fn query( -// deps: Deps, -// msg: QueryMsg, -// ) -> Result { -// contract::query(deps, mock_env(), msg)? -// .then(from_json::) -// .unwrap() -// .then(Ok) -// } -// -// #[test] -// fn query_sent_messages() { -// let mut deps = mock_dependencies(); -// -// let message1 = dummy_message("message-1"); -// let message2 = dummy_message("message-2"); -// let message3 = dummy_message("message-3"); -// -// // Save messages -// state::save_unique_contract_call_msg( -// deps.as_mut().storage, -// message1.cc_id.clone(), -// &message1, -// ) -// .unwrap(); -// state::save_unique_contract_call_msg( -// deps.as_mut().storage, -// message2.cc_id.clone(), -// &message2, -// ) -// .unwrap(); -// -// // Query existing messages -// let result: Vec = query( -// deps.as_ref(), -// QueryMsg::ContractCallMessages { -// cc_ids: vec![message1.cc_id.clone(), message2.cc_id.clone()], -// }, -// ) -// .unwrap(); -// assert_eq!(result, vec![message1, message2]); -// -// // Query with non-existent message -// let err = query::>( -// deps.as_ref(), -// QueryMsg::ContractCallMessages { -// cc_ids: vec![message3.cc_id], -// }, -// ) -// .unwrap_err(); -// assert!(err_contains!( -// err.report, -// state::Error, -// state::Error::MessageNotFound(..) -// )); -// } -// -// #[test] -// fn query_received_messages() { -// let mut deps = mock_dependencies(); -// -// let message1 = dummy_message("message-1"); -// let message2 = dummy_message("message-2"); -// let message3 = dummy_message("message-3"); -// -// // Save messages -// state::save_executable_msg( -// deps.as_mut().storage, -// message1.cc_id.clone(), -// message1.clone(), -// ) -// .unwrap(); -// state::save_executable_msg( -// deps.as_mut().storage, -// message2.cc_id.clone(), -// message2.clone(), -// ) -// .unwrap(); -// -// // Set message2 as executed -// state::mark_msg_as_executed(deps.as_mut().storage, message2.cc_id.clone()).unwrap(); -// -// // Query existing messages -// let result: Vec = query( -// deps.as_ref(), -// QueryMsg::ExecutableMessages { -// cc_ids: vec![message1.cc_id.clone(), message2.cc_id.clone()], -// }, -// ) -// .unwrap(); -// -// assert_eq!( -// result, -// vec![ -// ExecutableMessage { -// msg: message1, -// status: MessageStatus::Approved -// }, -// ExecutableMessage { -// msg: message2, -// status: MessageStatus::Executed -// } -// ] -// ); -// -// // Query with non-existent message -// let err = query::>( -// deps.as_ref(), -// QueryMsg::ExecutableMessages { -// cc_ids: vec![message3.cc_id], -// }, -// ) -// .unwrap_err(); -// assert!(err_contains!( -// err.report, -// state::Error, -// state::Error::MessageNotFound(..) -// )); -// } -// } diff --git a/contracts/axelarnet-gateway/src/lib.rs b/contracts/axelarnet-gateway/src/lib.rs index 20977bb34..e9ff7ce8f 100644 --- a/contracts/axelarnet-gateway/src/lib.rs +++ b/contracts/axelarnet-gateway/src/lib.rs @@ -4,4 +4,5 @@ pub mod msg; mod state; mod clients; -pub use clients::{AxelarExecutableMsg, GatewayClient}; +pub use clients::{AxelarExecutableMsg, ExecuteMsg as ExternalExecuteMsg, GatewayClient}; +pub use state::ExecutableMessage; diff --git a/contracts/axelarnet-gateway/src/state.rs b/contracts/axelarnet-gateway/src/state.rs index 53b4fbe9a..d50bc4bad 100644 --- a/contracts/axelarnet-gateway/src/state.rs +++ b/contracts/axelarnet-gateway/src/state.rs @@ -50,12 +50,6 @@ impl ExecutableMessage { ExecutableMessage::Approved(msg) | ExecutableMessage::Executed(msg) => msg, } } - - pub fn msg_owned(self) -> Message { - match self { - ExecutableMessage::Approved(msg) | ExecutableMessage::Executed(msg) => msg, - } - } } pub fn save_config(storage: &mut dyn Storage, value: &Config) -> Result<(), Error> { @@ -96,12 +90,8 @@ pub fn save_executable_msg( cc_id: &CrossChainId, msg: Message, ) -> Result<(), Error> { - let existing = may_load_executable_msg(storage, cc_id)?; - - match existing { - Some(with_status) if *with_status.msg() != msg => { - Err(Error::MessageMismatch(msg.cc_id.clone())) - } + match may_load_executable_msg(storage, cc_id)? { + Some(existing) if *existing.msg() != msg => Err(Error::MessageMismatch(msg.cc_id.clone())), Some(_) => Ok(()), // new message is identical, no need to store it None => Ok(EXECUTABLE_MESSAGES.save(storage, cc_id, &ExecutableMessage::Approved(msg))?), } @@ -137,190 +127,3 @@ pub fn update_as_executed( Ok(msg) } - -// -// #[cfg(test)] -// mod tests { -// use cosmwasm_std::testing::mock_dependencies; -// use cosmwasm_std::Addr; -// use router_api::{CrossChainId, Message}; -// -// use super::*; -// -// fn create_test_message() -> Message { -// Message { -// cc_id: CrossChainId::new("source-chain", "message-id").unwrap(), -// source_address: "source-address".parse().unwrap(), -// destination_chain: "destination-chain".parse().unwrap(), -// destination_address: "destination-address".parse().unwrap(), -// payload_hash: [1; 32], -// } -// } -// -// #[test] -// fn config_storage() { -// let mut deps = mock_dependencies(); -// -// let config = Config { -// chain_name: "test-chain".parse().unwrap(), -// router: Addr::unchecked("router-address"), -// }; -// -// // Test saving config -// super::save_config(deps.as_mut().storage, &config).unwrap(); -// -// // Test loading config -// let loaded_config = super::load_config(deps.as_ref().storage).unwrap(); -// assert_eq!(config, loaded_config); -// -// // Test loading non-existent config -// CONFIG.remove(deps.as_mut().storage); -// let result = super::load_config(deps.as_ref().storage); -// assert_eq!(result, Err(Error::MissingConfig)); -// } -// -// #[test] -// fn sent_message_storage() { -// let mut deps = mock_dependencies(); -// let message = create_test_message(); -// -// // Test saving sent message -// super::save_unique_contract_call_msg( -// deps.as_mut().storage, -// message.cc_id.clone(), -// &message, -// ) -// .unwrap(); -// -// // Test loading sent message -// let loaded_message = -// super::may_load_contract_call_msg(deps.as_ref().storage, &message.cc_id).unwrap(); -// assert_eq!(Some(message.clone()), loaded_message); -// -// // Test loading non-existent message -// let non_existent_id = CrossChainId::new("non-existent", "id").unwrap(); -// assert_eq!( -// None, -// super::may_load_contract_call_msg(deps.as_ref().storage, &non_existent_id).unwrap() -// ); -// -// // Test saving duplicate message -// let result = super::save_unique_contract_call_msg( -// deps.as_mut().storage, -// message.cc_id.clone(), -// &message, -// ); -// assert_eq!(result, Err(Error::MessageAlreadyExists(message.cc_id))); -// } -// -// #[test] -// fn received_message_storage() { -// let mut deps = mock_dependencies(); -// let message = create_test_message(); -// -// // Test saving received message -// super::save_executable_msg( -// deps.as_mut().storage, -// message.cc_id.clone(), -// message.clone(), -// ) -// .unwrap(); -// -// // Test loading received message -// let loaded_message = -// super::may_load_executable_msg(deps.as_ref().storage, &message.cc_id).unwrap(); -// assert_eq!( -// Some(ExecutableMessage { -// msg: message.clone(), -// status: MessageStatus::Approved -// }), -// loaded_message -// ); -// -// // Test loading non-existent message -// let non_existent_id = CrossChainId::new("non-existent", "id").unwrap(); -// assert_eq!( -// None, -// super::may_load_executable_msg(deps.as_ref().storage, &non_existent_id).unwrap() -// ); -// -// // Test saving duplicate message (should not error, but also not change the stored message) -// super::save_executable_msg( -// deps.as_mut().storage, -// message.cc_id.clone(), -// message.clone(), -// ) -// .unwrap(); -// let loaded_message = -// super::may_load_executable_msg(deps.as_ref().storage, &message.cc_id).unwrap(); -// assert_eq!( -// Some(ExecutableMessage { -// msg: message.clone(), -// status: MessageStatus::Approved -// }), -// loaded_message -// ); -// -// // Test saving mismatched message -// let mismatched_message = Message { -// cc_id: message.cc_id.clone(), -// source_address: "different-address".parse().unwrap(), -// ..message.clone() -// }; -// let result = super::save_executable_msg( -// deps.as_mut().storage, -// message.cc_id.clone(), -// mismatched_message, -// ); -// assert_eq!(result, Err(Error::MessageMismatch(message.cc_id))); -// } -// -// #[test] -// fn set_msg_as_executed() { -// let mut deps = mock_dependencies(); -// let message = create_test_message(); -// -// // Save a received message -// super::save_executable_msg( -// deps.as_mut().storage, -// message.cc_id.clone(), -// message.clone(), -// ) -// .unwrap(); -// -// // Test setting message as executed -// let executed_message = -// super::mark_msg_as_executed(deps.as_mut().storage, message.cc_id.clone()).unwrap(); -// assert_eq!(message, executed_message); -// -// // Verify the message status is now Executed -// let loaded_message = -// super::may_load_executable_msg(deps.as_ref().storage, &message.cc_id).unwrap(); -// assert_eq!( -// Some(ExecutableMessage { -// msg: message.clone(), -// status: MessageStatus::Executed -// }), -// loaded_message -// ); -// -// // Test setting an already executed message -// let result = super::mark_msg_as_executed(deps.as_mut().storage, message.cc_id.clone()); -// assert_eq!(result, Err(Error::MessageAlreadyExecuted(message.cc_id))); -// -// // Test setting a non-existent message -// let non_existent_id = CrossChainId::new("non-existent", "id").unwrap(); -// let result = super::mark_msg_as_executed(deps.as_mut().storage, non_existent_id.clone()); -// assert_eq!(result, Err(Error::MessageNotApproved(non_existent_id))); -// } -// -// #[test] -// fn increment_msg_counter() { -// let mut deps = mock_dependencies(); -// -// for i in 1..=3 { -// let count = super::increment_msg_counter(deps.as_mut().storage).unwrap(); -// assert_eq!(i, count); -// } -// } -// } diff --git a/contracts/axelarnet-gateway/tests/execute.rs b/contracts/axelarnet-gateway/tests/execute.rs index 07368ee5e..9ba009422 100644 --- a/contracts/axelarnet-gateway/tests/execute.rs +++ b/contracts/axelarnet-gateway/tests/execute.rs @@ -1,74 +1,314 @@ +use std::str::FromStr; + use axelar_wasm_std::error::ContractError; -use axelarnet_gateway::contract; use axelarnet_gateway::msg::ExecuteMsg; +use axelarnet_gateway::{contract, ExternalExecuteMsg}; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; use cosmwasm_std::{DepsMut, HexBinary, Response}; -use router_api::{CrossChainId, Message}; -use sha3::Digest; +use router_api::msg::ExecuteMsg as RouterExecuteMsg; +use router_api::{Address, ChainName, CrossChainId, Message}; -use crate::utils::instantiate::instantiate_contract; -use crate::utils::params; +use crate::utils::messages; +use crate::utils::messages::inspect_response_msg; mod utils; #[test] -fn message_not_approved() { +fn execute_message_when_not_approved_fails() { + let mut deps = mock_dependencies(); + + let cc_id = CrossChainId::new("source-chain", "hash-index").unwrap(); + let payload = vec![1, 2, 3].into(); + + utils::instantiate_contract(deps.as_mut()).unwrap(); + + assert!(utils::execute_payload(deps.as_mut(), cc_id, payload).is_err()); +} + +#[test] +fn execute_approved_message_when_already_executed_fails() { + let mut deps = mock_dependencies(); + + let payload: HexBinary = vec![1, 2, 3].into(); + let msg = messages::dummy_from_router(&payload); + let cc_id = msg.cc_id.clone(); + + utils::instantiate_contract(deps.as_mut()).unwrap(); + utils::route_from_router(deps.as_mut(), vec![msg]).unwrap(); + utils::execute_payload(deps.as_mut(), cc_id.clone(), payload.clone()).unwrap(); + + assert!(utils::execute_payload(deps.as_mut(), cc_id, payload).is_err()); +} + +#[test] +fn execute_approved_message_when_payload_mismatch_fails() { + let mut deps = mock_dependencies(); + + let payload = vec![1, 2, 3]; + let mismatched_payload = vec![4, 5, 6].into(); + let msg = messages::dummy_from_router(&payload); + let cc_id = msg.cc_id.clone(); + + utils::instantiate_contract(deps.as_mut()).unwrap(); + utils::route_from_router(deps.as_mut(), vec![msg]).unwrap(); + + assert!(utils::execute_payload(deps.as_mut(), cc_id, mismatched_payload).is_err()); +} + +#[test] +fn execute_approved_message_once_returns_correct_message() { + let mut deps = mock_dependencies(); + + let payload = vec![1, 2, 3].into(); + let msg = messages::dummy_from_router(&payload); + let cc_id = msg.cc_id.clone(); + + utils::instantiate_contract(deps.as_mut()).unwrap(); + utils::route_from_router(deps.as_mut(), vec![msg]).unwrap(); + + let response = utils::execute_payload(deps.as_mut(), cc_id, payload); + assert!(response.is_ok()); + + let msg = inspect_response_msg::(response.unwrap()); + assert!(msg.is_ok()); + goldie::assert_json!(msg.unwrap()) +} + +#[test] +fn execute_approved_message_once_returns_correct_events() { let mut deps = mock_dependencies(); - instantiate_contract(deps.as_mut()).unwrap(); + let payload = vec![1, 2, 3].into(); + let msg = messages::dummy_from_router(&payload); + let cc_id = msg.cc_id.clone(); + + utils::instantiate_contract(deps.as_mut()).unwrap(); + utils::route_from_router(deps.as_mut(), vec![msg]).unwrap(); + + let response = utils::execute_payload(deps.as_mut(), cc_id, payload); + assert!(response.is_ok()); - assert!(contract::execute( + goldie::assert_json!(response.unwrap().events) +} + +#[test] +fn route_from_router_with_destination_chain_not_matching_contract_fails() { + let mut deps = mock_dependencies(); + + 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(); + + assert!(utils::route_from_router(deps.as_mut(), vec![msg_with_wrong_destination]).is_err()); +} + +#[test] +fn route_from_router_same_message_multiple_times_succeeds() { + let mut deps = mock_dependencies(); + + let msgs = vec![messages::dummy_from_router(&[1, 2, 3])]; + + utils::instantiate_contract(deps.as_mut()).unwrap(); + + let response = utils::route_from_router(deps.as_mut(), msgs); + assert!(response.is_ok()); + goldie::assert_json!(response.unwrap()); +} + +#[test] +fn route_from_router_multiple_times_with_data_mismatch_fails() { + let mut deps = mock_dependencies(); + + 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(); + + msgs[0].source_address = "wrong-address".parse().unwrap(); + + assert!(utils::route_from_router(deps.as_mut(), msgs).is_err()); +} + +#[test] +fn route_to_router_without_contract_call_ignores_message() { + let mut deps = mock_dependencies(); + + let msg = messages::dummy_to_router(&vec![1, 2, 3]); + + utils::instantiate_contract(deps.as_mut()).unwrap(); + + let response = route_to_router(deps.as_mut(), vec![msg]); + assert!(response.is_ok()); + assert_eq!(response.unwrap().messages.len(), 0); +} + +#[test] +fn route_to_router_after_contract_call_with_tempered_data_fails() { + let mut deps = mock_dependencies(); + + let destination_chain = "destination-chain".parse().unwrap(); + let destination_address = "destination-address".parse().unwrap(); + let payload = vec![1, 2, 3].into(); + + utils::instantiate_contract(deps.as_mut()).unwrap(); + let response = utils::call_contract( deps.as_mut(), - mock_env(), - mock_info("sender", &[]), - ExecuteMsg::Execute { - cc_id: CrossChainId::new("source-chain", "hash-index").unwrap(), - payload: vec![1, 2, 3].into(), - }, + destination_chain, + destination_address, + payload, ) - .is_ok()); + .unwrap(); + + let RouterExecuteMsg::RouteMessages(mut msgs) = inspect_response_msg(response).unwrap() else { + panic!("pattern must match") + }; + msgs[0].destination_chain = "wrong-chain".parse().unwrap(); + + assert!(route_to_router(deps.as_mut(), msgs.clone()).is_err()); } #[test] -fn message_already_executed() { +fn route_to_router_after_contract_call_succeeds_multiple_times() { let mut deps = mock_dependencies(); - let cc_id = CrossChainId::new("chain", "hash-index").unwrap(); - let payload: HexBinary = vec![1, 2, 3].into(); + let destination_chain = "destination-chain".parse().unwrap(); + let destination_address = "destination-address".parse().unwrap(); + let payload = vec![1, 2, 3].into(); - let msgs = vec![Message { - cc_id: cc_id.clone(), - source_address: "source-address".parse().unwrap(), - destination_chain: "axelarnet".parse().unwrap(), - destination_address: "destination-address".parse().unwrap(), - payload_hash: sha3::Keccak256::digest(&payload).into(), - }]; + utils::instantiate_contract(deps.as_mut()).unwrap(); + let response = utils::call_contract( + deps.as_mut(), + destination_chain, + destination_address, + payload, + ) + .unwrap(); - instantiate_contract(deps.as_mut()).unwrap(); - route_from_router(deps.as_mut(), msgs).unwrap(); - execute_payload(deps.as_mut(), cc_id.clone(), payload.clone()).unwrap(); + let RouterExecuteMsg::RouteMessages(msgs) = inspect_response_msg(response).unwrap() else { + panic!("pattern must match") + }; - assert!(execute_payload(deps.as_mut(), cc_id, payload).is_err()); + for _ in 0..10 { + let response = route_to_router(deps.as_mut(), msgs.clone()); + assert!(response.is_ok()); + let msg = inspect_response_msg::(response.unwrap()); + assert!(msg.is_ok()); + goldie::assert_json!(msg.unwrap()); + } } -fn execute_payload( - deps: DepsMut, - cc_id: CrossChainId, - payload: HexBinary, -) -> Result { - contract::execute( - deps, - mock_env(), - mock_info("sender", &[]), - ExecuteMsg::Execute { cc_id, payload }.clone(), +#[test] +fn route_to_router_after_contract_call_ignores_duplicates() { + let mut deps = mock_dependencies(); + + let destination_chain = "destination-chain".parse().unwrap(); + let destination_address = "destination-address".parse().unwrap(); + let payload = vec![1, 2, 3].into(); + + utils::instantiate_contract(deps.as_mut()).unwrap(); + let response = utils::call_contract( + deps.as_mut(), + destination_chain, + destination_address, + payload, ) + .unwrap(); + + let RouterExecuteMsg::RouteMessages(mut msgs) = inspect_response_msg(response).unwrap() else { + panic!("pattern must match") + }; + + msgs.append(&mut msgs.clone()); + msgs.append(&mut msgs.clone()); + assert_eq!(msgs.len(), 4); + + let response = route_to_router(deps.as_mut(), msgs); + assert!(response.is_ok()); + let msg = inspect_response_msg::(response.unwrap()); + assert!(msg.is_ok()); + goldie::assert_json!(msg.unwrap()); } -fn route_from_router(deps: DepsMut, msgs: Vec) -> Result { +#[test] +fn contract_call_returns_correct_message() { + let mut deps = mock_dependencies(); + + let destination_chain = "destination-chain".parse().unwrap(); + let destination_address = "destination-address".parse().unwrap(); + let payload = vec![1, 2, 3].into(); + + utils::instantiate_contract(deps.as_mut()).unwrap(); + let response = utils::call_contract( + deps.as_mut(), + destination_chain, + destination_address, + payload, + ); + + assert!(response.is_ok()); + + let msg = inspect_response_msg::(response.unwrap()); + assert!(msg.is_ok()); + goldie::assert_json!(msg.unwrap()) +} + +#[test] +fn contract_call_returns_correct_events() { + let mut deps = mock_dependencies(); + + let destination_chain = "destination-chain".parse().unwrap(); + let destination_address = "destination-address".parse().unwrap(); + let payload = vec![1, 2, 3].into(); + + utils::instantiate_contract(deps.as_mut()).unwrap(); + let response = utils::call_contract( + deps.as_mut(), + destination_chain, + destination_address, + payload, + ); + + assert!(response.is_ok()); + goldie::assert_json!(response.unwrap().events) +} + +#[test] +fn contract_call_multiple_times_results_in_different_messages() { + let mut deps = mock_dependencies(); + + let destination_chain = ChainName::from_str("destination-chain").unwrap(); + let destination_address = Address::from_str("destination-address").unwrap(); + let payload = HexBinary::from(vec![1, 2, 3]); + + utils::instantiate_contract(deps.as_mut()).unwrap(); + let response1 = utils::call_contract( + deps.as_mut(), + destination_chain.clone(), + destination_address.clone(), + payload.clone(), + ); + + let response2 = utils::call_contract( + deps.as_mut(), + destination_chain, + destination_address, + payload, + ); + + assert!(response1.is_ok()); + assert!(response2.is_ok()); + assert_ne!(response1.unwrap().messages, response2.unwrap().messages); +} + +fn route_to_router(deps: DepsMut, msgs: Vec) -> Result { contract::execute( deps, mock_env(), - mock_info(params::ROUTER, &[]), + mock_info("sender", &[]), ExecuteMsg::RouteMessages(msgs), ) } diff --git a/contracts/axelarnet-gateway/tests/instantiate.rs b/contracts/axelarnet-gateway/tests/instantiate.rs index 9a3192b57..4c28bee61 100644 --- a/contracts/axelarnet-gateway/tests/instantiate.rs +++ b/contracts/axelarnet-gateway/tests/instantiate.rs @@ -2,8 +2,7 @@ use axelarnet_gateway::contract; use axelarnet_gateway::msg::InstantiateMsg; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; -use crate::utils::instantiate::instantiate_contract; -use crate::utils::params; +use crate::utils::{instantiate_contract, params}; mod utils; #[test] diff --git a/contracts/axelarnet-gateway/tests/query.rs b/contracts/axelarnet-gateway/tests/query.rs new file mode 100644 index 000000000..cea540cb5 --- /dev/null +++ b/contracts/axelarnet-gateway/tests/query.rs @@ -0,0 +1,252 @@ +use axelarnet_gateway::msg::QueryMsg; +use axelarnet_gateway::{contract, ExecutableMessage}; +use cosmwasm_std::testing::{mock_dependencies, mock_env, MockApi, MockQuerier, MockStorage}; +use cosmwasm_std::{from_json, Deps, OwnedDeps}; +use router_api::msg::ExecuteMsg as RouterExecuteMsg; +use router_api::{CrossChainId, Message}; +use sha3::{Digest, Keccak256}; + +use crate::utils::messages::inspect_response_msg; +use crate::utils::params; + +mod utils; + +#[test] +fn query_routable_messages_gets_expected_messages() { + let mut deps = mock_dependencies(); + + utils::instantiate_contract(deps.as_mut()).unwrap(); + let mut expected = populate_routable_messages(&mut deps); + + expected.remove(3); + let cc_ids = expected.iter().map(|msg| &msg.cc_id).cloned().collect(); + + let result = query_routable_messages(deps.as_ref(), cc_ids); + + assert_eq!(result.unwrap(), expected); +} + +#[test] +fn query_executable_messages_gets_expected_messages() { + let mut deps = mock_dependencies(); + + utils::instantiate_contract(deps.as_mut()).unwrap(); + let mut cc_ids = populate_executable_messages(&mut deps); + cc_ids.remove(3); + + let result = query_executable_messages(deps.as_ref(), cc_ids); + + assert!(result.is_ok()); + goldie::assert_json!(result.unwrap()); +} + +fn query_routable_messages(deps: Deps, cc_ids: Vec) -> Result, ()> { + from_json( + contract::query(deps, mock_env(), QueryMsg::RoutableMessages { cc_ids }).map_err(|_| ())?, + ) + .map_err(|_| ()) +} + +fn query_executable_messages( + deps: Deps, + cc_ids: Vec, +) -> Result, ()> { + from_json( + contract::query(deps, mock_env(), QueryMsg::ExecutableMessages { cc_ids }) + .map_err(|_| ())?, + ) + .map_err(|_| ()) +} + +fn populate_routable_messages( + deps: &mut OwnedDeps, +) -> Vec { + (0..10) + .map(|i| { + let response = utils::call_contract( + deps.as_mut(), + format!("destination-chain-{}", i).parse().unwrap(), + format!("destination-address-{}", i).parse().unwrap(), + vec![i].into(), + ) + .unwrap(); + + let RouterExecuteMsg::RouteMessages(mut msgs) = inspect_response_msg(response).unwrap() + else { + panic!("pattern must match") + }; + + msgs.pop().unwrap() + }) + .collect() +} + +fn populate_executable_messages( + deps: &mut OwnedDeps, +) -> Vec { + let msgs: Vec<_> = (0..10) + .map(|i| Message { + cc_id: CrossChainId::new("source-chain", format!("hash-index-{}", i)).unwrap(), + source_address: "source-address".parse().unwrap(), + destination_chain: params::AXELARNET.parse().unwrap(), + destination_address: "destination-address".parse().unwrap(), + payload_hash: Keccak256::digest(vec![i]).into(), + }) + .collect(); + + utils::route_from_router(deps.as_mut(), msgs.clone()).unwrap(); + + utils::execute_payload(deps.as_mut(), msgs[0].cc_id.clone(), vec![0].into()).unwrap(); + utils::execute_payload(deps.as_mut(), msgs[5].cc_id.clone(), vec![5].into()).unwrap(); + utils::execute_payload(deps.as_mut(), msgs[7].cc_id.clone(), vec![7].into()).unwrap(); + + msgs.into_iter().map(|msg| msg.cc_id).collect() +} + +// +// #[cfg(test)] +// mod tests { +// use axelar_wasm_std::{err_contains, FnExt}; +// use cosmwasm_std::from_json; +// use cosmwasm_std::testing::{mock_dependencies, mock_env}; +// use router_api::{CrossChainId, Message}; +// use serde::de::DeserializeOwned; +// +// use super::*; +// use crate::contract; +// use crate::msg::QueryMsg; +// +// const SOURCE_CHAIN: &str = "source-chain"; +// const DESTINATION_CHAIN: &str = "destination-chain"; +// +// fn dummy_message(id: &str) -> Message { +// Message { +// cc_id: CrossChainId::new(SOURCE_CHAIN, id).unwrap(), +// source_address: "source-address".parse().unwrap(), +// destination_chain: DESTINATION_CHAIN.parse().unwrap(), +// destination_address: "destination-address".parse().unwrap(), +// payload_hash: [0; 32], +// } +// } +// +// // Query a msg and deserialize it. If the query fails, the error is returned +// fn query( +// deps: Deps, +// msg: QueryMsg, +// ) -> Result { +// contract::query(deps, mock_env(), msg)? +// .then(from_json::) +// .unwrap() +// .then(Ok) +// } +// +// #[test] +// fn query_sent_messages() { +// let mut deps = mock_dependencies(); +// +// let message1 = dummy_message("message-1"); +// let message2 = dummy_message("message-2"); +// let message3 = dummy_message("message-3"); +// +// // Save messages +// state::save_unique_contract_call_msg( +// deps.as_mut().storage, +// message1.cc_id.clone(), +// &message1, +// ) +// .unwrap(); +// state::save_unique_contract_call_msg( +// deps.as_mut().storage, +// message2.cc_id.clone(), +// &message2, +// ) +// .unwrap(); +// +// // Query existing messages +// let result: Vec = query( +// deps.as_ref(), +// QueryMsg::ContractCallMessages { +// cc_ids: vec![message1.cc_id.clone(), message2.cc_id.clone()], +// }, +// ) +// .unwrap(); +// assert_eq!(result, vec![message1, message2]); +// +// // Query with non-existent message +// let err = query::>( +// deps.as_ref(), +// QueryMsg::ContractCallMessages { +// cc_ids: vec![message3.cc_id], +// }, +// ) +// .unwrap_err(); +// assert!(err_contains!( +// err.report, +// state::Error, +// state::Error::MessageNotFound(..) +// )); +// } +// +// #[test] +// fn query_received_messages() { +// let mut deps = mock_dependencies(); +// +// let message1 = dummy_message("message-1"); +// let message2 = dummy_message("message-2"); +// let message3 = dummy_message("message-3"); +// +// // Save messages +// state::save_executable_msg( +// deps.as_mut().storage, +// message1.cc_id.clone(), +// message1.clone(), +// ) +// .unwrap(); +// state::save_executable_msg( +// deps.as_mut().storage, +// message2.cc_id.clone(), +// message2.clone(), +// ) +// .unwrap(); +// +// // Set message2 as executed +// state::mark_msg_as_executed(deps.as_mut().storage, message2.cc_id.clone()).unwrap(); +// +// // Query existing messages +// let result: Vec = query( +// deps.as_ref(), +// QueryMsg::ExecutableMessages { +// cc_ids: vec![message1.cc_id.clone(), message2.cc_id.clone()], +// }, +// ) +// .unwrap(); +// +// assert_eq!( +// result, +// vec![ +// ExecutableMessage { +// msg: message1, +// status: MessageStatus::Approved +// }, +// ExecutableMessage { +// msg: message2, +// status: MessageStatus::Executed +// } +// ] +// ); +// +// // Query with non-existent message +// let err = query::>( +// deps.as_ref(), +// QueryMsg::ExecutableMessages { +// cc_ids: vec![message3.cc_id], +// }, +// ) +// .unwrap_err(); +// assert!(err_contains!( +// err.report, +// state::Error, +// state::Error::MessageNotFound(..) +// )); +// } +// } diff --git a/contracts/axelarnet-gateway/tests/testdata/contract_call_returns_correct_events.golden b/contracts/axelarnet-gateway/tests/testdata/contract_call_returns_correct_events.golden new file mode 100644 index 000000000..1fb3d9671 --- /dev/null +++ b/contracts/axelarnet-gateway/tests/testdata/contract_call_returns_correct_events.golden @@ -0,0 +1,64 @@ +[ + { + "type": "routing", + "attributes": [ + { + "key": "message_id", + "value": "0x0000000000000000000000000000000000000000000000000000000000003039-1" + }, + { + "key": "source_chain", + "value": "axelarnet" + }, + { + "key": "source_address", + "value": "sender" + }, + { + "key": "destination_chain", + "value": "destination-chain" + }, + { + "key": "destination_address", + "value": "destination-address" + }, + { + "key": "payload_hash", + "value": "f1885eda54b7a053318cd41e2093220dab15d65381b1157a3633a83bfd5c9239" + } + ] + }, + { + "type": "contract_called", + "attributes": [ + { + "key": "message_id", + "value": "0x0000000000000000000000000000000000000000000000000000000000003039-1" + }, + { + "key": "source_chain", + "value": "axelarnet" + }, + { + "key": "source_address", + "value": "sender" + }, + { + "key": "destination_chain", + "value": "destination-chain" + }, + { + "key": "destination_address", + "value": "destination-address" + }, + { + "key": "payload_hash", + "value": "f1885eda54b7a053318cd41e2093220dab15d65381b1157a3633a83bfd5c9239" + }, + { + "key": "payload", + "value": "010203" + } + ] + } +] \ No newline at end of file diff --git a/contracts/axelarnet-gateway/tests/testdata/contract_call_returns_correct_message.golden b/contracts/axelarnet-gateway/tests/testdata/contract_call_returns_correct_message.golden new file mode 100644 index 000000000..4aeaa7194 --- /dev/null +++ b/contracts/axelarnet-gateway/tests/testdata/contract_call_returns_correct_message.golden @@ -0,0 +1,14 @@ +{ + "route_messages": [ + { + "cc_id": { + "source_chain": "axelarnet", + "message_id": "0x0000000000000000000000000000000000000000000000000000000000003039-1" + }, + "source_address": "sender", + "destination_chain": "destination-chain", + "destination_address": "destination-address", + "payload_hash": "f1885eda54b7a053318cd41e2093220dab15d65381b1157a3633a83bfd5c9239" + } + ] +} \ No newline at end of file diff --git a/contracts/axelarnet-gateway/tests/testdata/execute_approved_message_once_returns_correct_events.golden b/contracts/axelarnet-gateway/tests/testdata/execute_approved_message_once_returns_correct_events.golden new file mode 100644 index 000000000..6ee1845bc --- /dev/null +++ b/contracts/axelarnet-gateway/tests/testdata/execute_approved_message_once_returns_correct_events.golden @@ -0,0 +1,31 @@ +[ + { + "type": "message_executed", + "attributes": [ + { + "key": "message_id", + "value": "hash-index" + }, + { + "key": "source_chain", + "value": "source-chain" + }, + { + "key": "source_address", + "value": "source-address" + }, + { + "key": "destination_chain", + "value": "axelarnet" + }, + { + "key": "destination_address", + "value": "destination-address" + }, + { + "key": "payload_hash", + "value": "f1885eda54b7a053318cd41e2093220dab15d65381b1157a3633a83bfd5c9239" + } + ] + } +] \ No newline at end of file diff --git a/contracts/axelarnet-gateway/tests/testdata/execute_approved_message_once_returns_correct_message.golden b/contracts/axelarnet-gateway/tests/testdata/execute_approved_message_once_returns_correct_message.golden new file mode 100644 index 000000000..78d4efc59 --- /dev/null +++ b/contracts/axelarnet-gateway/tests/testdata/execute_approved_message_once_returns_correct_message.golden @@ -0,0 +1,10 @@ +{ + "execute": { + "cc_id": { + "source_chain": "source-chain", + "message_id": "hash-index" + }, + "source_address": "source-address", + "payload": "010203" + } +} \ No newline at end of file diff --git a/contracts/axelarnet-gateway/tests/testdata/query_executable_messages_gets_expected_messages.golden b/contracts/axelarnet-gateway/tests/testdata/query_executable_messages_gets_expected_messages.golden new file mode 100644 index 000000000..a4955f5f9 --- /dev/null +++ b/contracts/axelarnet-gateway/tests/testdata/query_executable_messages_gets_expected_messages.golden @@ -0,0 +1,110 @@ +[ + { + "executed": { + "cc_id": { + "source_chain": "source-chain", + "message_id": "hash-index-0" + }, + "source_address": "source-address", + "destination_chain": "axelarnet", + "destination_address": "destination-address", + "payload_hash": "bc36789e7a1e281436464229828f817d6612f7b477d66591ff96a9e064bcc98a" + } + }, + { + "approved": { + "cc_id": { + "source_chain": "source-chain", + "message_id": "hash-index-1" + }, + "source_address": "source-address", + "destination_chain": "axelarnet", + "destination_address": "destination-address", + "payload_hash": "5fe7f977e71dba2ea1a68e21057beebb9be2ac30c6410aa38d4f3fbe41dcffd2" + } + }, + { + "approved": { + "cc_id": { + "source_chain": "source-chain", + "message_id": "hash-index-2" + }, + "source_address": "source-address", + "destination_chain": "axelarnet", + "destination_address": "destination-address", + "payload_hash": "f2ee15ea639b73fa3db9b34a245bdfa015c260c598b211bf05a1ecc4b3e3b4f2" + } + }, + { + "approved": { + "cc_id": { + "source_chain": "source-chain", + "message_id": "hash-index-4" + }, + "source_address": "source-address", + "destination_chain": "axelarnet", + "destination_address": "destination-address", + "payload_hash": "f343681465b9efe82c933c3e8748c70cb8aa06539c361de20f72eac04e766393" + } + }, + { + "executed": { + "cc_id": { + "source_chain": "source-chain", + "message_id": "hash-index-5" + }, + "source_address": "source-address", + "destination_chain": "axelarnet", + "destination_address": "destination-address", + "payload_hash": "dbb8d0f4c497851a5043c6363657698cb1387682cac2f786c731f8936109d795" + } + }, + { + "approved": { + "cc_id": { + "source_chain": "source-chain", + "message_id": "hash-index-6" + }, + "source_address": "source-address", + "destination_chain": "axelarnet", + "destination_address": "destination-address", + "payload_hash": "d0591206d9e81e07f4defc5327957173572bcd1bca7838caa7be39b0c12b1873" + } + }, + { + "executed": { + "cc_id": { + "source_chain": "source-chain", + "message_id": "hash-index-7" + }, + "source_address": "source-address", + "destination_chain": "axelarnet", + "destination_address": "destination-address", + "payload_hash": "ee2a4bc7db81da2b7164e56b3649b1e2a09c58c455b15dabddd9146c7582cebc" + } + }, + { + "approved": { + "cc_id": { + "source_chain": "source-chain", + "message_id": "hash-index-8" + }, + "source_address": "source-address", + "destination_chain": "axelarnet", + "destination_address": "destination-address", + "payload_hash": "d33e25809fcaa2b6900567812852539da8559dc8b76a7ce3fc5ddd77e8d19a69" + } + }, + { + "approved": { + "cc_id": { + "source_chain": "source-chain", + "message_id": "hash-index-9" + }, + "source_address": "source-address", + "destination_chain": "axelarnet", + "destination_address": "destination-address", + "payload_hash": "b2e7b7a21d986ae84d62a7de4a916f006c4e42a596358b93bad65492d174c4ff" + } + } +] \ No newline at end of file diff --git a/contracts/axelarnet-gateway/tests/testdata/route_from_router_same_message_multiple_times_succeeds.golden b/contracts/axelarnet-gateway/tests/testdata/route_from_router_same_message_multiple_times_succeeds.golden new file mode 100644 index 000000000..73fe0e0c0 --- /dev/null +++ b/contracts/axelarnet-gateway/tests/testdata/route_from_router_same_message_multiple_times_succeeds.golden @@ -0,0 +1,36 @@ +{ + "messages": [], + "attributes": [], + "events": [ + { + "type": "routing", + "attributes": [ + { + "key": "message_id", + "value": "hash-index" + }, + { + "key": "source_chain", + "value": "source-chain" + }, + { + "key": "source_address", + "value": "source-address" + }, + { + "key": "destination_chain", + "value": "axelarnet" + }, + { + "key": "destination_address", + "value": "destination-address" + }, + { + "key": "payload_hash", + "value": "f1885eda54b7a053318cd41e2093220dab15d65381b1157a3633a83bfd5c9239" + } + ] + } + ], + "data": null +} \ No newline at end of file diff --git a/contracts/axelarnet-gateway/tests/testdata/route_to_router_after_contract_call_ignores_duplicates.golden b/contracts/axelarnet-gateway/tests/testdata/route_to_router_after_contract_call_ignores_duplicates.golden new file mode 100644 index 000000000..4aeaa7194 --- /dev/null +++ b/contracts/axelarnet-gateway/tests/testdata/route_to_router_after_contract_call_ignores_duplicates.golden @@ -0,0 +1,14 @@ +{ + "route_messages": [ + { + "cc_id": { + "source_chain": "axelarnet", + "message_id": "0x0000000000000000000000000000000000000000000000000000000000003039-1" + }, + "source_address": "sender", + "destination_chain": "destination-chain", + "destination_address": "destination-address", + "payload_hash": "f1885eda54b7a053318cd41e2093220dab15d65381b1157a3633a83bfd5c9239" + } + ] +} \ No newline at end of file diff --git a/contracts/axelarnet-gateway/tests/testdata/route_to_router_after_contract_call_succeeds_multiple_times.golden b/contracts/axelarnet-gateway/tests/testdata/route_to_router_after_contract_call_succeeds_multiple_times.golden new file mode 100644 index 000000000..4aeaa7194 --- /dev/null +++ b/contracts/axelarnet-gateway/tests/testdata/route_to_router_after_contract_call_succeeds_multiple_times.golden @@ -0,0 +1,14 @@ +{ + "route_messages": [ + { + "cc_id": { + "source_chain": "axelarnet", + "message_id": "0x0000000000000000000000000000000000000000000000000000000000003039-1" + }, + "source_address": "sender", + "destination_chain": "destination-chain", + "destination_address": "destination-address", + "payload_hash": "f1885eda54b7a053318cd41e2093220dab15d65381b1157a3633a83bfd5c9239" + } + ] +} \ No newline at end of file diff --git a/contracts/axelarnet-gateway/tests/utils/execute.rs b/contracts/axelarnet-gateway/tests/utils/execute.rs new file mode 100644 index 000000000..fe5d72248 --- /dev/null +++ b/contracts/axelarnet-gateway/tests/utils/execute.rs @@ -0,0 +1,48 @@ +use axelar_wasm_std::error::ContractError; +use axelarnet_gateway::contract; +use axelarnet_gateway::msg::ExecuteMsg; +use cosmwasm_std::testing::{mock_env, mock_info}; +use cosmwasm_std::{DepsMut, HexBinary, Response}; +use router_api::{Address, ChainName, CrossChainId, Message}; + +use crate::utils::params; + +pub fn call_contract( + deps: DepsMut, + destination_chain: ChainName, + destination_address: Address, + payload: HexBinary, +) -> Result { + contract::execute( + deps, + mock_env(), + mock_info("sender", &[]), + ExecuteMsg::CallContract { + destination_chain, + destination_address, + payload, + }, + ) +} + +pub fn route_from_router(deps: DepsMut, msgs: Vec) -> Result { + contract::execute( + deps, + mock_env(), + mock_info(params::ROUTER, &[]), + ExecuteMsg::RouteMessages(msgs), + ) +} + +pub fn execute_payload( + deps: DepsMut, + cc_id: CrossChainId, + payload: HexBinary, +) -> Result { + contract::execute( + deps, + mock_env(), + mock_info("sender", &[]), + ExecuteMsg::Execute { cc_id, payload }.clone(), + ) +} diff --git a/contracts/axelarnet-gateway/tests/utils/messages.rs b/contracts/axelarnet-gateway/tests/utils/messages.rs new file mode 100644 index 000000000..d8905b262 --- /dev/null +++ b/contracts/axelarnet-gateway/tests/utils/messages.rs @@ -0,0 +1,44 @@ +use cosmwasm_std::{from_json, CosmosMsg, Response, WasmMsg}; +use router_api::{CrossChainId, Message}; +use serde::de::DeserializeOwned; +use sha3::Digest; + +use crate::utils::params; + +pub fn dummy_from_router(payload: &impl AsRef<[u8]>) -> Message { + Message { + cc_id: CrossChainId::new("source-chain", "hash-index").unwrap(), + source_address: "source-address".parse().unwrap(), + destination_chain: params::AXELARNET.parse().unwrap(), + destination_address: "destination-address".parse().unwrap(), + payload_hash: sha3::Keccak256::digest(payload).into(), + } +} + +pub fn dummy_to_router(payload: &impl AsRef<[u8]>) -> Message { + Message { + cc_id: CrossChainId::new("source-chain", "hash-index").unwrap(), + source_address: params::AXELARNET.parse().unwrap(), + destination_chain: "destination-chain".parse().unwrap(), + destination_address: "destination-address".parse().unwrap(), + payload_hash: sha3::Keccak256::digest(payload).into(), + } +} + +pub fn inspect_response_msg(response: Response) -> Result +where + T: DeserializeOwned, +{ + let mut followup_messages = response.messages.into_iter(); + + let msg = followup_messages.next().ok_or(())?.msg; + + if let Some(_) = followup_messages.next() { + return Err(()); + } + + match msg { + CosmosMsg::Wasm(WasmMsg::Execute { msg, .. }) => from_json(&msg).map_err(|_| ()), + _ => Err(()), + } +} diff --git a/contracts/axelarnet-gateway/tests/utils/mod.rs b/contracts/axelarnet-gateway/tests/utils/mod.rs index ad374342f..e5c84b81f 100644 --- a/contracts/axelarnet-gateway/tests/utils/mod.rs +++ b/contracts/axelarnet-gateway/tests/utils/mod.rs @@ -1,2 +1,12 @@ -pub mod instantiate; +// because each test file is a module, the compiler complains about unused imports if one of the files doesn't use them. +// This circumvents that issue. +#![allow(dead_code)] + +#[allow(unused_imports)] +pub use execute::*; +pub use instantiate::*; + +mod execute; +mod instantiate; +pub mod messages; pub mod params; From 020d0eb3177958240544698daa01491ca3ba0295 Mon Sep 17 00:00:00 2001 From: Christian Gorenflo Date: Fri, 23 Aug 2024 00:22:46 -0400 Subject: [PATCH 08/23] remove commented out lines --- contracts/axelarnet-gateway/tests/query.rs | 148 --------------------- 1 file changed, 148 deletions(-) diff --git a/contracts/axelarnet-gateway/tests/query.rs b/contracts/axelarnet-gateway/tests/query.rs index cea540cb5..cac56ab77 100644 --- a/contracts/axelarnet-gateway/tests/query.rs +++ b/contracts/axelarnet-gateway/tests/query.rs @@ -102,151 +102,3 @@ fn populate_executable_messages( msgs.into_iter().map(|msg| msg.cc_id).collect() } - -// -// #[cfg(test)] -// mod tests { -// use axelar_wasm_std::{err_contains, FnExt}; -// use cosmwasm_std::from_json; -// use cosmwasm_std::testing::{mock_dependencies, mock_env}; -// use router_api::{CrossChainId, Message}; -// use serde::de::DeserializeOwned; -// -// use super::*; -// use crate::contract; -// use crate::msg::QueryMsg; -// -// const SOURCE_CHAIN: &str = "source-chain"; -// const DESTINATION_CHAIN: &str = "destination-chain"; -// -// fn dummy_message(id: &str) -> Message { -// Message { -// cc_id: CrossChainId::new(SOURCE_CHAIN, id).unwrap(), -// source_address: "source-address".parse().unwrap(), -// destination_chain: DESTINATION_CHAIN.parse().unwrap(), -// destination_address: "destination-address".parse().unwrap(), -// payload_hash: [0; 32], -// } -// } -// -// // Query a msg and deserialize it. If the query fails, the error is returned -// fn query( -// deps: Deps, -// msg: QueryMsg, -// ) -> Result { -// contract::query(deps, mock_env(), msg)? -// .then(from_json::) -// .unwrap() -// .then(Ok) -// } -// -// #[test] -// fn query_sent_messages() { -// let mut deps = mock_dependencies(); -// -// let message1 = dummy_message("message-1"); -// let message2 = dummy_message("message-2"); -// let message3 = dummy_message("message-3"); -// -// // Save messages -// state::save_unique_contract_call_msg( -// deps.as_mut().storage, -// message1.cc_id.clone(), -// &message1, -// ) -// .unwrap(); -// state::save_unique_contract_call_msg( -// deps.as_mut().storage, -// message2.cc_id.clone(), -// &message2, -// ) -// .unwrap(); -// -// // Query existing messages -// let result: Vec = query( -// deps.as_ref(), -// QueryMsg::ContractCallMessages { -// cc_ids: vec![message1.cc_id.clone(), message2.cc_id.clone()], -// }, -// ) -// .unwrap(); -// assert_eq!(result, vec![message1, message2]); -// -// // Query with non-existent message -// let err = query::>( -// deps.as_ref(), -// QueryMsg::ContractCallMessages { -// cc_ids: vec![message3.cc_id], -// }, -// ) -// .unwrap_err(); -// assert!(err_contains!( -// err.report, -// state::Error, -// state::Error::MessageNotFound(..) -// )); -// } -// -// #[test] -// fn query_received_messages() { -// let mut deps = mock_dependencies(); -// -// let message1 = dummy_message("message-1"); -// let message2 = dummy_message("message-2"); -// let message3 = dummy_message("message-3"); -// -// // Save messages -// state::save_executable_msg( -// deps.as_mut().storage, -// message1.cc_id.clone(), -// message1.clone(), -// ) -// .unwrap(); -// state::save_executable_msg( -// deps.as_mut().storage, -// message2.cc_id.clone(), -// message2.clone(), -// ) -// .unwrap(); -// -// // Set message2 as executed -// state::mark_msg_as_executed(deps.as_mut().storage, message2.cc_id.clone()).unwrap(); -// -// // Query existing messages -// let result: Vec = query( -// deps.as_ref(), -// QueryMsg::ExecutableMessages { -// cc_ids: vec![message1.cc_id.clone(), message2.cc_id.clone()], -// }, -// ) -// .unwrap(); -// -// assert_eq!( -// result, -// vec![ -// ExecutableMessage { -// msg: message1, -// status: MessageStatus::Approved -// }, -// ExecutableMessage { -// msg: message2, -// status: MessageStatus::Executed -// } -// ] -// ); -// -// // Query with non-existent message -// let err = query::>( -// deps.as_ref(), -// QueryMsg::ExecutableMessages { -// cc_ids: vec![message3.cc_id], -// }, -// ) -// .unwrap_err(); -// assert!(err_contains!( -// err.report, -// state::Error, -// state::Error::MessageNotFound(..) -// )); -// } -// } From 1ed43530f2f4530ce83c516fdfaa62687bf52bda Mon Sep 17 00:00:00 2001 From: Christian Gorenflo Date: Fri, 23 Aug 2024 00:43:55 -0400 Subject: [PATCH 09/23] fix tests --- Cargo.lock | 3 +- contracts/axelarnet-gateway/Cargo.toml | 2 +- contracts/gateway/Cargo.toml | 3 +- contracts/gateway/tests/contract.rs | 22 +++- ...licate_ids_should_ignore_duplicates.golden | 106 ++++++++++++++++++ 5 files changed, 130 insertions(+), 6 deletions(-) create mode 100644 contracts/gateway/tests/testdata/route_duplicate_ids_should_ignore_duplicates.golden diff --git a/Cargo.lock b/Cargo.lock index 9db0763f4..af3ba2322 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3229,11 +3229,12 @@ dependencies = [ "client", "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test 0.15.1", + "cw-multi-test 1.2.0", "cw-storage-plus 1.2.0", "cw2 1.1.2", "error-stack", "gateway-api", + "goldie", "itertools 0.11.0", "rand", "report", diff --git a/contracts/axelarnet-gateway/Cargo.toml b/contracts/axelarnet-gateway/Cargo.toml index f33010231..db4eed7af 100644 --- a/contracts/axelarnet-gateway/Cargo.toml +++ b/contracts/axelarnet-gateway/Cargo.toml @@ -39,6 +39,7 @@ cosmwasm-std = { workspace = true } cw-storage-plus = { workspace = true } cw2 = { workspace = true } error-stack = { workspace = true } +itertools = { workspace = true } msgs-derive = { workspace = true } report = { workspace = true } router-api = { workspace = true } @@ -46,7 +47,6 @@ serde = { workspace = true } serde_json = { workspace = true } sha3 = { workspace = true } thiserror = { workspace = true } -itertools = { workspace = true } [dev-dependencies] cw-multi-test = { workspace = true } diff --git a/contracts/gateway/Cargo.toml b/contracts/gateway/Cargo.toml index dce577e75..8fda2fbfe 100644 --- a/contracts/gateway/Cargo.toml +++ b/contracts/gateway/Cargo.toml @@ -52,7 +52,8 @@ thiserror = { workspace = true } voting-verifier = { workspace = true, features = ["library"] } [dev-dependencies] -cw-multi-test = "0.15.1" +cw-multi-test = { workspace = true } +goldie = { workspace = true } rand = "0.8.5" [lints] diff --git a/contracts/gateway/tests/contract.rs b/contracts/gateway/tests/contract.rs index cb86390aa..9eac300bd 100644 --- a/contracts/gateway/tests/contract.rs +++ b/contracts/gateway/tests/contract.rs @@ -9,7 +9,8 @@ use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info, MockQuerier} #[cfg(not(feature = "generate_golden_files"))] use cosmwasm_std::Response; use cosmwasm_std::{ - from_json, to_json_binary, Addr, ContractResult, DepsMut, QuerierResult, WasmQuery, + from_json, to_json_binary, Addr, ContractResult, CosmosMsg, DepsMut, QuerierResult, WasmMsg, + WasmQuery, }; use gateway::contract::*; use gateway::msg::InstantiateMsg; @@ -262,7 +263,7 @@ fn calls_with_duplicate_ids_should_fail() { } #[test] -fn route_duplicate_ids_should_fail() { +fn route_duplicate_ids_should_ignore_duplicates() { let (test_cases, handler) = test_cases_for_duplicate_msgs(); for msgs in test_cases { let mut deps = mock_dependencies(); @@ -277,10 +278,25 @@ fn route_duplicate_ids_should_fail() { ExecuteMsg::RouteMessages(msgs), ); - assert!(response.is_err()); + assert!(response.is_ok()); + let msgs: Vec = extract_messages_from_response(response.unwrap()); + goldie::assert_json!(msgs) } } +fn extract_messages_from_response(response: Response) -> Vec { + response + .messages + .into_iter() + .map(|sub_msg| { + let CosmosMsg::Wasm(WasmMsg::Execute { msg, .. }) = sub_msg.msg else { + panic!("pattern must match") + }; + from_json(msg).unwrap() + }) + .collect() +} + #[test] fn reject_reroute_outgoing_message_with_different_contents() { let mut msgs = generate_msgs(VerificationStatus::SucceededOnSourceChain, 10); diff --git a/contracts/gateway/tests/testdata/route_duplicate_ids_should_ignore_duplicates.golden b/contracts/gateway/tests/testdata/route_duplicate_ids_should_ignore_duplicates.golden new file mode 100644 index 000000000..284c674c7 --- /dev/null +++ b/contracts/gateway/tests/testdata/route_duplicate_ids_should_ignore_duplicates.golden @@ -0,0 +1,106 @@ +[ + { + "route_messages": [ + { + "cc_id": { + "source_chain": "mock-chain", + "message_id": "SucceededOnSourceChain0" + }, + "source_address": "idc", + "destination_chain": "mock-chain-2", + "destination_address": "idc", + "payload_hash": "0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "cc_id": { + "source_chain": "mock-chain", + "message_id": "SucceededOnSourceChain1" + }, + "source_address": "idc", + "destination_chain": "mock-chain-2", + "destination_address": "idc", + "payload_hash": "0101010101010101010101010101010101010101010101010101010101010101" + }, + { + "cc_id": { + "source_chain": "mock-chain", + "message_id": "SucceededOnSourceChain2" + }, + "source_address": "idc", + "destination_chain": "mock-chain-2", + "destination_address": "idc", + "payload_hash": "0202020202020202020202020202020202020202020202020202020202020202" + }, + { + "cc_id": { + "source_chain": "mock-chain", + "message_id": "SucceededOnSourceChain3" + }, + "source_address": "idc", + "destination_chain": "mock-chain-2", + "destination_address": "idc", + "payload_hash": "0303030303030303030303030303030303030303030303030303030303030303" + }, + { + "cc_id": { + "source_chain": "mock-chain", + "message_id": "SucceededOnSourceChain4" + }, + "source_address": "idc", + "destination_chain": "mock-chain-2", + "destination_address": "idc", + "payload_hash": "0404040404040404040404040404040404040404040404040404040404040404" + }, + { + "cc_id": { + "source_chain": "mock-chain", + "message_id": "SucceededOnSourceChain5" + }, + "source_address": "idc", + "destination_chain": "mock-chain-2", + "destination_address": "idc", + "payload_hash": "0505050505050505050505050505050505050505050505050505050505050505" + }, + { + "cc_id": { + "source_chain": "mock-chain", + "message_id": "SucceededOnSourceChain6" + }, + "source_address": "idc", + "destination_chain": "mock-chain-2", + "destination_address": "idc", + "payload_hash": "0606060606060606060606060606060606060606060606060606060606060606" + }, + { + "cc_id": { + "source_chain": "mock-chain", + "message_id": "SucceededOnSourceChain7" + }, + "source_address": "idc", + "destination_chain": "mock-chain-2", + "destination_address": "idc", + "payload_hash": "0707070707070707070707070707070707070707070707070707070707070707" + }, + { + "cc_id": { + "source_chain": "mock-chain", + "message_id": "SucceededOnSourceChain8" + }, + "source_address": "idc", + "destination_chain": "mock-chain-2", + "destination_address": "idc", + "payload_hash": "0808080808080808080808080808080808080808080808080808080808080808" + }, + { + "cc_id": { + "source_chain": "mock-chain", + "message_id": "SucceededOnSourceChain9" + }, + "source_address": "idc", + "destination_chain": "mock-chain-2", + "destination_address": "idc", + "payload_hash": "0909090909090909090909090909090909090909090909090909090909090909" + } + ] + } +] \ No newline at end of file From 61da7ea8c85b2e67ef693a62cfedaebdf54c2684 Mon Sep 17 00:00:00 2001 From: Christian Gorenflo Date: Fri, 23 Aug 2024 01:02:10 -0400 Subject: [PATCH 10/23] fix tests --- contracts/gateway/tests/contract.rs | 22 +- ...licate_ids_should_ignore_duplicates.golden | 306 ++++++++++++++++++ 2 files changed, 310 insertions(+), 18 deletions(-) create mode 100644 contracts/gateway/tests/testdata/verify_calls_with_duplicate_ids_should_ignore_duplicates.golden diff --git a/contracts/gateway/tests/contract.rs b/contracts/gateway/tests/contract.rs index 9eac300bd..b5c6802a0 100644 --- a/contracts/gateway/tests/contract.rs +++ b/contracts/gateway/tests/contract.rs @@ -227,7 +227,7 @@ fn route_incoming_with_faulty_verifier_fails() { } #[test] -fn calls_with_duplicate_ids_should_fail() { +fn verify_calls_with_duplicate_ids_should_ignore_duplicates() { let (test_cases, handler) = test_cases_for_duplicate_msgs(); for msgs in test_cases { let mut deps = mock_dependencies(); @@ -242,23 +242,9 @@ fn calls_with_duplicate_ids_should_fail() { mock_info("sender", &[]), ExecuteMsg::VerifyMessages(msgs.clone()), ); - assert!(response.is_err()); - - let response = execute( - deps.as_mut(), - mock_env(), - mock_info("sender", &[]), - ExecuteMsg::RouteMessages(msgs.clone()), - ); - assert!(response.is_err()); - - let response = execute( - deps.as_mut(), - mock_env(), - mock_info(router, &[]), - ExecuteMsg::RouteMessages(msgs), - ); - assert!(response.is_err()); + assert!(response.is_ok()); + let routed_msgs: Vec = extract_messages_from_response(response.unwrap()); + goldie::assert_json!(routed_msgs); } } diff --git a/contracts/gateway/tests/testdata/verify_calls_with_duplicate_ids_should_ignore_duplicates.golden b/contracts/gateway/tests/testdata/verify_calls_with_duplicate_ids_should_ignore_duplicates.golden new file mode 100644 index 000000000..5d32dfb12 --- /dev/null +++ b/contracts/gateway/tests/testdata/verify_calls_with_duplicate_ids_should_ignore_duplicates.golden @@ -0,0 +1,306 @@ +[ + { + "verify_messages": [ + { + "cc_id": { + "source_chain": "mock-chain", + "message_id": "NotFoundOnSourceChain0" + }, + "source_address": "idc", + "destination_chain": "mock-chain-2", + "destination_address": "idc", + "payload_hash": "0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "cc_id": { + "source_chain": "mock-chain", + "message_id": "NotFoundOnSourceChain1" + }, + "source_address": "idc", + "destination_chain": "mock-chain-2", + "destination_address": "idc", + "payload_hash": "0101010101010101010101010101010101010101010101010101010101010101" + }, + { + "cc_id": { + "source_chain": "mock-chain", + "message_id": "NotFoundOnSourceChain2" + }, + "source_address": "idc", + "destination_chain": "mock-chain-2", + "destination_address": "idc", + "payload_hash": "0202020202020202020202020202020202020202020202020202020202020202" + }, + { + "cc_id": { + "source_chain": "mock-chain", + "message_id": "NotFoundOnSourceChain3" + }, + "source_address": "idc", + "destination_chain": "mock-chain-2", + "destination_address": "idc", + "payload_hash": "0303030303030303030303030303030303030303030303030303030303030303" + }, + { + "cc_id": { + "source_chain": "mock-chain", + "message_id": "NotFoundOnSourceChain4" + }, + "source_address": "idc", + "destination_chain": "mock-chain-2", + "destination_address": "idc", + "payload_hash": "0404040404040404040404040404040404040404040404040404040404040404" + }, + { + "cc_id": { + "source_chain": "mock-chain", + "message_id": "NotFoundOnSourceChain5" + }, + "source_address": "idc", + "destination_chain": "mock-chain-2", + "destination_address": "idc", + "payload_hash": "0505050505050505050505050505050505050505050505050505050505050505" + }, + { + "cc_id": { + "source_chain": "mock-chain", + "message_id": "NotFoundOnSourceChain6" + }, + "source_address": "idc", + "destination_chain": "mock-chain-2", + "destination_address": "idc", + "payload_hash": "0606060606060606060606060606060606060606060606060606060606060606" + }, + { + "cc_id": { + "source_chain": "mock-chain", + "message_id": "NotFoundOnSourceChain7" + }, + "source_address": "idc", + "destination_chain": "mock-chain-2", + "destination_address": "idc", + "payload_hash": "0707070707070707070707070707070707070707070707070707070707070707" + }, + { + "cc_id": { + "source_chain": "mock-chain", + "message_id": "NotFoundOnSourceChain8" + }, + "source_address": "idc", + "destination_chain": "mock-chain-2", + "destination_address": "idc", + "payload_hash": "0808080808080808080808080808080808080808080808080808080808080808" + }, + { + "cc_id": { + "source_chain": "mock-chain", + "message_id": "NotFoundOnSourceChain9" + }, + "source_address": "idc", + "destination_chain": "mock-chain-2", + "destination_address": "idc", + "payload_hash": "0909090909090909090909090909090909090909090909090909090909090909" + }, + { + "cc_id": { + "source_chain": "mock-chain", + "message_id": "FailedToVerify0" + }, + "source_address": "idc", + "destination_chain": "mock-chain-2", + "destination_address": "idc", + "payload_hash": "0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "cc_id": { + "source_chain": "mock-chain", + "message_id": "FailedToVerify1" + }, + "source_address": "idc", + "destination_chain": "mock-chain-2", + "destination_address": "idc", + "payload_hash": "0101010101010101010101010101010101010101010101010101010101010101" + }, + { + "cc_id": { + "source_chain": "mock-chain", + "message_id": "FailedToVerify2" + }, + "source_address": "idc", + "destination_chain": "mock-chain-2", + "destination_address": "idc", + "payload_hash": "0202020202020202020202020202020202020202020202020202020202020202" + }, + { + "cc_id": { + "source_chain": "mock-chain", + "message_id": "FailedToVerify3" + }, + "source_address": "idc", + "destination_chain": "mock-chain-2", + "destination_address": "idc", + "payload_hash": "0303030303030303030303030303030303030303030303030303030303030303" + }, + { + "cc_id": { + "source_chain": "mock-chain", + "message_id": "FailedToVerify4" + }, + "source_address": "idc", + "destination_chain": "mock-chain-2", + "destination_address": "idc", + "payload_hash": "0404040404040404040404040404040404040404040404040404040404040404" + }, + { + "cc_id": { + "source_chain": "mock-chain", + "message_id": "FailedToVerify5" + }, + "source_address": "idc", + "destination_chain": "mock-chain-2", + "destination_address": "idc", + "payload_hash": "0505050505050505050505050505050505050505050505050505050505050505" + }, + { + "cc_id": { + "source_chain": "mock-chain", + "message_id": "FailedToVerify6" + }, + "source_address": "idc", + "destination_chain": "mock-chain-2", + "destination_address": "idc", + "payload_hash": "0606060606060606060606060606060606060606060606060606060606060606" + }, + { + "cc_id": { + "source_chain": "mock-chain", + "message_id": "FailedToVerify7" + }, + "source_address": "idc", + "destination_chain": "mock-chain-2", + "destination_address": "idc", + "payload_hash": "0707070707070707070707070707070707070707070707070707070707070707" + }, + { + "cc_id": { + "source_chain": "mock-chain", + "message_id": "FailedToVerify8" + }, + "source_address": "idc", + "destination_chain": "mock-chain-2", + "destination_address": "idc", + "payload_hash": "0808080808080808080808080808080808080808080808080808080808080808" + }, + { + "cc_id": { + "source_chain": "mock-chain", + "message_id": "FailedToVerify9" + }, + "source_address": "idc", + "destination_chain": "mock-chain-2", + "destination_address": "idc", + "payload_hash": "0909090909090909090909090909090909090909090909090909090909090909" + }, + { + "cc_id": { + "source_chain": "mock-chain", + "message_id": "Unknown0" + }, + "source_address": "idc", + "destination_chain": "mock-chain-2", + "destination_address": "idc", + "payload_hash": "0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "cc_id": { + "source_chain": "mock-chain", + "message_id": "Unknown1" + }, + "source_address": "idc", + "destination_chain": "mock-chain-2", + "destination_address": "idc", + "payload_hash": "0101010101010101010101010101010101010101010101010101010101010101" + }, + { + "cc_id": { + "source_chain": "mock-chain", + "message_id": "Unknown2" + }, + "source_address": "idc", + "destination_chain": "mock-chain-2", + "destination_address": "idc", + "payload_hash": "0202020202020202020202020202020202020202020202020202020202020202" + }, + { + "cc_id": { + "source_chain": "mock-chain", + "message_id": "Unknown3" + }, + "source_address": "idc", + "destination_chain": "mock-chain-2", + "destination_address": "idc", + "payload_hash": "0303030303030303030303030303030303030303030303030303030303030303" + }, + { + "cc_id": { + "source_chain": "mock-chain", + "message_id": "Unknown4" + }, + "source_address": "idc", + "destination_chain": "mock-chain-2", + "destination_address": "idc", + "payload_hash": "0404040404040404040404040404040404040404040404040404040404040404" + }, + { + "cc_id": { + "source_chain": "mock-chain", + "message_id": "Unknown5" + }, + "source_address": "idc", + "destination_chain": "mock-chain-2", + "destination_address": "idc", + "payload_hash": "0505050505050505050505050505050505050505050505050505050505050505" + }, + { + "cc_id": { + "source_chain": "mock-chain", + "message_id": "Unknown6" + }, + "source_address": "idc", + "destination_chain": "mock-chain-2", + "destination_address": "idc", + "payload_hash": "0606060606060606060606060606060606060606060606060606060606060606" + }, + { + "cc_id": { + "source_chain": "mock-chain", + "message_id": "Unknown7" + }, + "source_address": "idc", + "destination_chain": "mock-chain-2", + "destination_address": "idc", + "payload_hash": "0707070707070707070707070707070707070707070707070707070707070707" + }, + { + "cc_id": { + "source_chain": "mock-chain", + "message_id": "Unknown8" + }, + "source_address": "idc", + "destination_chain": "mock-chain-2", + "destination_address": "idc", + "payload_hash": "0808080808080808080808080808080808080808080808080808080808080808" + }, + { + "cc_id": { + "source_chain": "mock-chain", + "message_id": "Unknown9" + }, + "source_address": "idc", + "destination_chain": "mock-chain-2", + "destination_address": "idc", + "payload_hash": "0909090909090909090909090909090909090909090909090909090909090909" + } + ] + } +] \ No newline at end of file From 0827d36f5c08b03740b40c08076a6b0801f9ce7b Mon Sep 17 00:00:00 2001 From: Christian Gorenflo Date: Fri, 23 Aug 2024 01:06:37 -0400 Subject: [PATCH 11/23] lint --- contracts/axelarnet-gateway/tests/utils/messages.rs | 4 ++-- contracts/axelarnet-gateway/tests/utils/params.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/axelarnet-gateway/tests/utils/messages.rs b/contracts/axelarnet-gateway/tests/utils/messages.rs index d8905b262..ff3611020 100644 --- a/contracts/axelarnet-gateway/tests/utils/messages.rs +++ b/contracts/axelarnet-gateway/tests/utils/messages.rs @@ -33,12 +33,12 @@ where let msg = followup_messages.next().ok_or(())?.msg; - if let Some(_) = followup_messages.next() { + if followup_messages.next().is_some() { return Err(()); } match msg { - CosmosMsg::Wasm(WasmMsg::Execute { msg, .. }) => from_json(&msg).map_err(|_| ()), + CosmosMsg::Wasm(WasmMsg::Execute { msg, .. }) => from_json(msg).map_err(|_| ()), _ => Err(()), } } diff --git a/contracts/axelarnet-gateway/tests/utils/params.rs b/contracts/axelarnet-gateway/tests/utils/params.rs index 53c73f6e4..a7897336f 100644 --- a/contracts/axelarnet-gateway/tests/utils/params.rs +++ b/contracts/axelarnet-gateway/tests/utils/params.rs @@ -1,2 +1,2 @@ -pub const AXELARNET: &'static str = "axelarnet"; -pub const ROUTER: &'static str = "router"; +pub const AXELARNET: &str = "axelarnet"; +pub const ROUTER: &str = "router"; From 8eef118520feaa614003df653ee748fd26349d5c Mon Sep 17 00:00:00 2001 From: Christian Gorenflo Date: Fri, 23 Aug 2024 10:13:40 -0400 Subject: [PATCH 12/23] revert gateway changes --- Cargo.lock | 3 +- contracts/gateway/Cargo.toml | 3 +- contracts/gateway/src/contract/execute.rs | 23 +- contracts/gateway/src/contract/query.rs | 18 +- contracts/gateway/src/state.rs | 8 +- contracts/gateway/tests/contract.rs | 44 ++- ...licate_ids_should_ignore_duplicates.golden | 106 ------ ...licate_ids_should_ignore_duplicates.golden | 306 ------------------ 8 files changed, 63 insertions(+), 448 deletions(-) delete mode 100644 contracts/gateway/tests/testdata/route_duplicate_ids_should_ignore_duplicates.golden delete mode 100644 contracts/gateway/tests/testdata/verify_calls_with_duplicate_ids_should_ignore_duplicates.golden diff --git a/Cargo.lock b/Cargo.lock index af3ba2322..9db0763f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3229,12 +3229,11 @@ dependencies = [ "client", "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test 1.2.0", + "cw-multi-test 0.15.1", "cw-storage-plus 1.2.0", "cw2 1.1.2", "error-stack", "gateway-api", - "goldie", "itertools 0.11.0", "rand", "report", diff --git a/contracts/gateway/Cargo.toml b/contracts/gateway/Cargo.toml index 8fda2fbfe..dce577e75 100644 --- a/contracts/gateway/Cargo.toml +++ b/contracts/gateway/Cargo.toml @@ -52,8 +52,7 @@ thiserror = { workspace = true } voting-verifier = { workspace = true, features = ["library"] } [dev-dependencies] -cw-multi-test = { workspace = true } -goldie = { workspace = true } +cw-multi-test = "0.15.1" rand = "0.8.5" [lints] diff --git a/contracts/gateway/src/contract/execute.rs b/contracts/gateway/src/contract/execute.rs index e34ebb717..0369d96c9 100644 --- a/contracts/gateway/src/contract/execute.rs +++ b/contracts/gateway/src/contract/execute.rs @@ -34,7 +34,7 @@ pub fn route_outgoing_messages( store: &mut dyn Storage, verified: Vec, ) -> Result { - let msgs: Vec<_> = verified.into_iter().unique().collect(); + let msgs = check_for_duplicates(verified)?; for msg in msgs.iter() { state::save_outgoing_message(store, &msg.cc_id, msg) @@ -52,10 +52,8 @@ fn apply( msgs: Vec, action: impl Fn(Vec<(VerificationStatus, Vec)>) -> (Option, Vec), ) -> Result { - let unique_msgs = msgs.into_iter().unique().collect(); - - verifier - .messages_status(unique_msgs) + check_for_duplicates(msgs)? + .then(|msgs| verifier.messages_status(msgs)) .change_context(Error::MessageStatus)? .then(group_by_status) .then(action) @@ -63,6 +61,21 @@ fn apply( .then(Ok) } +fn check_for_duplicates(msgs: Vec) -> Result, Error> { + let duplicates: Vec<_> = msgs + .iter() + // the following two map instructions are separated on purpose + // so the duplicate check is done on the typed id instead of just a string + .map(|m| &m.cc_id) + .duplicates() + .map(|cc_id| cc_id.to_string()) + .collect(); + if !duplicates.is_empty() { + return Err(Error::DuplicateMessageIds).attach_printable(duplicates.iter().join(", ")); + } + Ok(msgs) +} + fn group_by_status( msgs_with_status: impl IntoIterator, ) -> Vec<(VerificationStatus, Vec)> { diff --git a/contracts/gateway/src/contract/query.rs b/contracts/gateway/src/contract/query.rs index 64d081a13..59cc4c4e5 100644 --- a/contracts/gateway/src/contract/query.rs +++ b/contracts/gateway/src/contract/query.rs @@ -1,7 +1,7 @@ -use axelar_wasm_std::error::accumulate_errs; +use axelar_wasm_std::error::extend_err; use cosmwasm_std::{to_json_binary, Binary, Storage}; use error_stack::Result; -use router_api::CrossChainId; +use router_api::{CrossChainId, Message}; use crate::state; @@ -16,6 +16,20 @@ pub fn outgoing_messages<'a>( Ok(to_json_binary(&msgs).map_err(state::Error::from)?) } +fn accumulate_errs( + acc: Result, state::Error>, + msg: std::result::Result, +) -> Result, state::Error> { + match (acc, msg) { + (Ok(mut msgs), Ok(msg)) => { + msgs.push(msg); + Ok(msgs) + } + (Err(report), Ok(_)) => Err(report), + (acc, Err(msg_err)) => extend_err(acc, msg_err.into()), + } +} + #[cfg(test)] mod test { use cosmwasm_std::from_json; diff --git a/contracts/gateway/src/state.rs b/contracts/gateway/src/state.rs index e6c80c6da..35f8f8c1b 100644 --- a/contracts/gateway/src/state.rs +++ b/contracts/gateway/src/state.rs @@ -26,7 +26,10 @@ pub enum Error { } pub fn load_config(storage: &dyn Storage) -> Result { - CONFIG.may_load(storage)?.ok_or(Error::MissingConfig) + CONFIG + .may_load(storage) + .map_err(Error::from)? + .ok_or(Error::MissingConfig) } pub fn save_config(storage: &mut dyn Storage, config: &Config) -> Result<(), Error> { @@ -38,7 +41,8 @@ pub fn load_outgoing_message( cc_id: &CrossChainId, ) -> Result { OUTGOING_MESSAGES - .may_load(storage, cc_id)? + .may_load(storage, cc_id) + .map_err(Error::from)? .ok_or_else(|| Error::MessageNotFound(cc_id.clone())) } diff --git a/contracts/gateway/tests/contract.rs b/contracts/gateway/tests/contract.rs index b5c6802a0..cb86390aa 100644 --- a/contracts/gateway/tests/contract.rs +++ b/contracts/gateway/tests/contract.rs @@ -9,8 +9,7 @@ use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info, MockQuerier} #[cfg(not(feature = "generate_golden_files"))] use cosmwasm_std::Response; use cosmwasm_std::{ - from_json, to_json_binary, Addr, ContractResult, CosmosMsg, DepsMut, QuerierResult, WasmMsg, - WasmQuery, + from_json, to_json_binary, Addr, ContractResult, DepsMut, QuerierResult, WasmQuery, }; use gateway::contract::*; use gateway::msg::InstantiateMsg; @@ -227,7 +226,7 @@ fn route_incoming_with_faulty_verifier_fails() { } #[test] -fn verify_calls_with_duplicate_ids_should_ignore_duplicates() { +fn calls_with_duplicate_ids_should_fail() { let (test_cases, handler) = test_cases_for_duplicate_msgs(); for msgs in test_cases { let mut deps = mock_dependencies(); @@ -242,14 +241,28 @@ fn verify_calls_with_duplicate_ids_should_ignore_duplicates() { mock_info("sender", &[]), ExecuteMsg::VerifyMessages(msgs.clone()), ); - assert!(response.is_ok()); - let routed_msgs: Vec = extract_messages_from_response(response.unwrap()); - goldie::assert_json!(routed_msgs); + assert!(response.is_err()); + + let response = execute( + deps.as_mut(), + mock_env(), + mock_info("sender", &[]), + ExecuteMsg::RouteMessages(msgs.clone()), + ); + assert!(response.is_err()); + + let response = execute( + deps.as_mut(), + mock_env(), + mock_info(router, &[]), + ExecuteMsg::RouteMessages(msgs), + ); + assert!(response.is_err()); } } #[test] -fn route_duplicate_ids_should_ignore_duplicates() { +fn route_duplicate_ids_should_fail() { let (test_cases, handler) = test_cases_for_duplicate_msgs(); for msgs in test_cases { let mut deps = mock_dependencies(); @@ -264,25 +277,10 @@ fn route_duplicate_ids_should_ignore_duplicates() { ExecuteMsg::RouteMessages(msgs), ); - assert!(response.is_ok()); - let msgs: Vec = extract_messages_from_response(response.unwrap()); - goldie::assert_json!(msgs) + assert!(response.is_err()); } } -fn extract_messages_from_response(response: Response) -> Vec { - response - .messages - .into_iter() - .map(|sub_msg| { - let CosmosMsg::Wasm(WasmMsg::Execute { msg, .. }) = sub_msg.msg else { - panic!("pattern must match") - }; - from_json(msg).unwrap() - }) - .collect() -} - #[test] fn reject_reroute_outgoing_message_with_different_contents() { let mut msgs = generate_msgs(VerificationStatus::SucceededOnSourceChain, 10); diff --git a/contracts/gateway/tests/testdata/route_duplicate_ids_should_ignore_duplicates.golden b/contracts/gateway/tests/testdata/route_duplicate_ids_should_ignore_duplicates.golden deleted file mode 100644 index 284c674c7..000000000 --- a/contracts/gateway/tests/testdata/route_duplicate_ids_should_ignore_duplicates.golden +++ /dev/null @@ -1,106 +0,0 @@ -[ - { - "route_messages": [ - { - "cc_id": { - "source_chain": "mock-chain", - "message_id": "SucceededOnSourceChain0" - }, - "source_address": "idc", - "destination_chain": "mock-chain-2", - "destination_address": "idc", - "payload_hash": "0000000000000000000000000000000000000000000000000000000000000000" - }, - { - "cc_id": { - "source_chain": "mock-chain", - "message_id": "SucceededOnSourceChain1" - }, - "source_address": "idc", - "destination_chain": "mock-chain-2", - "destination_address": "idc", - "payload_hash": "0101010101010101010101010101010101010101010101010101010101010101" - }, - { - "cc_id": { - "source_chain": "mock-chain", - "message_id": "SucceededOnSourceChain2" - }, - "source_address": "idc", - "destination_chain": "mock-chain-2", - "destination_address": "idc", - "payload_hash": "0202020202020202020202020202020202020202020202020202020202020202" - }, - { - "cc_id": { - "source_chain": "mock-chain", - "message_id": "SucceededOnSourceChain3" - }, - "source_address": "idc", - "destination_chain": "mock-chain-2", - "destination_address": "idc", - "payload_hash": "0303030303030303030303030303030303030303030303030303030303030303" - }, - { - "cc_id": { - "source_chain": "mock-chain", - "message_id": "SucceededOnSourceChain4" - }, - "source_address": "idc", - "destination_chain": "mock-chain-2", - "destination_address": "idc", - "payload_hash": "0404040404040404040404040404040404040404040404040404040404040404" - }, - { - "cc_id": { - "source_chain": "mock-chain", - "message_id": "SucceededOnSourceChain5" - }, - "source_address": "idc", - "destination_chain": "mock-chain-2", - "destination_address": "idc", - "payload_hash": "0505050505050505050505050505050505050505050505050505050505050505" - }, - { - "cc_id": { - "source_chain": "mock-chain", - "message_id": "SucceededOnSourceChain6" - }, - "source_address": "idc", - "destination_chain": "mock-chain-2", - "destination_address": "idc", - "payload_hash": "0606060606060606060606060606060606060606060606060606060606060606" - }, - { - "cc_id": { - "source_chain": "mock-chain", - "message_id": "SucceededOnSourceChain7" - }, - "source_address": "idc", - "destination_chain": "mock-chain-2", - "destination_address": "idc", - "payload_hash": "0707070707070707070707070707070707070707070707070707070707070707" - }, - { - "cc_id": { - "source_chain": "mock-chain", - "message_id": "SucceededOnSourceChain8" - }, - "source_address": "idc", - "destination_chain": "mock-chain-2", - "destination_address": "idc", - "payload_hash": "0808080808080808080808080808080808080808080808080808080808080808" - }, - { - "cc_id": { - "source_chain": "mock-chain", - "message_id": "SucceededOnSourceChain9" - }, - "source_address": "idc", - "destination_chain": "mock-chain-2", - "destination_address": "idc", - "payload_hash": "0909090909090909090909090909090909090909090909090909090909090909" - } - ] - } -] \ No newline at end of file diff --git a/contracts/gateway/tests/testdata/verify_calls_with_duplicate_ids_should_ignore_duplicates.golden b/contracts/gateway/tests/testdata/verify_calls_with_duplicate_ids_should_ignore_duplicates.golden deleted file mode 100644 index 5d32dfb12..000000000 --- a/contracts/gateway/tests/testdata/verify_calls_with_duplicate_ids_should_ignore_duplicates.golden +++ /dev/null @@ -1,306 +0,0 @@ -[ - { - "verify_messages": [ - { - "cc_id": { - "source_chain": "mock-chain", - "message_id": "NotFoundOnSourceChain0" - }, - "source_address": "idc", - "destination_chain": "mock-chain-2", - "destination_address": "idc", - "payload_hash": "0000000000000000000000000000000000000000000000000000000000000000" - }, - { - "cc_id": { - "source_chain": "mock-chain", - "message_id": "NotFoundOnSourceChain1" - }, - "source_address": "idc", - "destination_chain": "mock-chain-2", - "destination_address": "idc", - "payload_hash": "0101010101010101010101010101010101010101010101010101010101010101" - }, - { - "cc_id": { - "source_chain": "mock-chain", - "message_id": "NotFoundOnSourceChain2" - }, - "source_address": "idc", - "destination_chain": "mock-chain-2", - "destination_address": "idc", - "payload_hash": "0202020202020202020202020202020202020202020202020202020202020202" - }, - { - "cc_id": { - "source_chain": "mock-chain", - "message_id": "NotFoundOnSourceChain3" - }, - "source_address": "idc", - "destination_chain": "mock-chain-2", - "destination_address": "idc", - "payload_hash": "0303030303030303030303030303030303030303030303030303030303030303" - }, - { - "cc_id": { - "source_chain": "mock-chain", - "message_id": "NotFoundOnSourceChain4" - }, - "source_address": "idc", - "destination_chain": "mock-chain-2", - "destination_address": "idc", - "payload_hash": "0404040404040404040404040404040404040404040404040404040404040404" - }, - { - "cc_id": { - "source_chain": "mock-chain", - "message_id": "NotFoundOnSourceChain5" - }, - "source_address": "idc", - "destination_chain": "mock-chain-2", - "destination_address": "idc", - "payload_hash": "0505050505050505050505050505050505050505050505050505050505050505" - }, - { - "cc_id": { - "source_chain": "mock-chain", - "message_id": "NotFoundOnSourceChain6" - }, - "source_address": "idc", - "destination_chain": "mock-chain-2", - "destination_address": "idc", - "payload_hash": "0606060606060606060606060606060606060606060606060606060606060606" - }, - { - "cc_id": { - "source_chain": "mock-chain", - "message_id": "NotFoundOnSourceChain7" - }, - "source_address": "idc", - "destination_chain": "mock-chain-2", - "destination_address": "idc", - "payload_hash": "0707070707070707070707070707070707070707070707070707070707070707" - }, - { - "cc_id": { - "source_chain": "mock-chain", - "message_id": "NotFoundOnSourceChain8" - }, - "source_address": "idc", - "destination_chain": "mock-chain-2", - "destination_address": "idc", - "payload_hash": "0808080808080808080808080808080808080808080808080808080808080808" - }, - { - "cc_id": { - "source_chain": "mock-chain", - "message_id": "NotFoundOnSourceChain9" - }, - "source_address": "idc", - "destination_chain": "mock-chain-2", - "destination_address": "idc", - "payload_hash": "0909090909090909090909090909090909090909090909090909090909090909" - }, - { - "cc_id": { - "source_chain": "mock-chain", - "message_id": "FailedToVerify0" - }, - "source_address": "idc", - "destination_chain": "mock-chain-2", - "destination_address": "idc", - "payload_hash": "0000000000000000000000000000000000000000000000000000000000000000" - }, - { - "cc_id": { - "source_chain": "mock-chain", - "message_id": "FailedToVerify1" - }, - "source_address": "idc", - "destination_chain": "mock-chain-2", - "destination_address": "idc", - "payload_hash": "0101010101010101010101010101010101010101010101010101010101010101" - }, - { - "cc_id": { - "source_chain": "mock-chain", - "message_id": "FailedToVerify2" - }, - "source_address": "idc", - "destination_chain": "mock-chain-2", - "destination_address": "idc", - "payload_hash": "0202020202020202020202020202020202020202020202020202020202020202" - }, - { - "cc_id": { - "source_chain": "mock-chain", - "message_id": "FailedToVerify3" - }, - "source_address": "idc", - "destination_chain": "mock-chain-2", - "destination_address": "idc", - "payload_hash": "0303030303030303030303030303030303030303030303030303030303030303" - }, - { - "cc_id": { - "source_chain": "mock-chain", - "message_id": "FailedToVerify4" - }, - "source_address": "idc", - "destination_chain": "mock-chain-2", - "destination_address": "idc", - "payload_hash": "0404040404040404040404040404040404040404040404040404040404040404" - }, - { - "cc_id": { - "source_chain": "mock-chain", - "message_id": "FailedToVerify5" - }, - "source_address": "idc", - "destination_chain": "mock-chain-2", - "destination_address": "idc", - "payload_hash": "0505050505050505050505050505050505050505050505050505050505050505" - }, - { - "cc_id": { - "source_chain": "mock-chain", - "message_id": "FailedToVerify6" - }, - "source_address": "idc", - "destination_chain": "mock-chain-2", - "destination_address": "idc", - "payload_hash": "0606060606060606060606060606060606060606060606060606060606060606" - }, - { - "cc_id": { - "source_chain": "mock-chain", - "message_id": "FailedToVerify7" - }, - "source_address": "idc", - "destination_chain": "mock-chain-2", - "destination_address": "idc", - "payload_hash": "0707070707070707070707070707070707070707070707070707070707070707" - }, - { - "cc_id": { - "source_chain": "mock-chain", - "message_id": "FailedToVerify8" - }, - "source_address": "idc", - "destination_chain": "mock-chain-2", - "destination_address": "idc", - "payload_hash": "0808080808080808080808080808080808080808080808080808080808080808" - }, - { - "cc_id": { - "source_chain": "mock-chain", - "message_id": "FailedToVerify9" - }, - "source_address": "idc", - "destination_chain": "mock-chain-2", - "destination_address": "idc", - "payload_hash": "0909090909090909090909090909090909090909090909090909090909090909" - }, - { - "cc_id": { - "source_chain": "mock-chain", - "message_id": "Unknown0" - }, - "source_address": "idc", - "destination_chain": "mock-chain-2", - "destination_address": "idc", - "payload_hash": "0000000000000000000000000000000000000000000000000000000000000000" - }, - { - "cc_id": { - "source_chain": "mock-chain", - "message_id": "Unknown1" - }, - "source_address": "idc", - "destination_chain": "mock-chain-2", - "destination_address": "idc", - "payload_hash": "0101010101010101010101010101010101010101010101010101010101010101" - }, - { - "cc_id": { - "source_chain": "mock-chain", - "message_id": "Unknown2" - }, - "source_address": "idc", - "destination_chain": "mock-chain-2", - "destination_address": "idc", - "payload_hash": "0202020202020202020202020202020202020202020202020202020202020202" - }, - { - "cc_id": { - "source_chain": "mock-chain", - "message_id": "Unknown3" - }, - "source_address": "idc", - "destination_chain": "mock-chain-2", - "destination_address": "idc", - "payload_hash": "0303030303030303030303030303030303030303030303030303030303030303" - }, - { - "cc_id": { - "source_chain": "mock-chain", - "message_id": "Unknown4" - }, - "source_address": "idc", - "destination_chain": "mock-chain-2", - "destination_address": "idc", - "payload_hash": "0404040404040404040404040404040404040404040404040404040404040404" - }, - { - "cc_id": { - "source_chain": "mock-chain", - "message_id": "Unknown5" - }, - "source_address": "idc", - "destination_chain": "mock-chain-2", - "destination_address": "idc", - "payload_hash": "0505050505050505050505050505050505050505050505050505050505050505" - }, - { - "cc_id": { - "source_chain": "mock-chain", - "message_id": "Unknown6" - }, - "source_address": "idc", - "destination_chain": "mock-chain-2", - "destination_address": "idc", - "payload_hash": "0606060606060606060606060606060606060606060606060606060606060606" - }, - { - "cc_id": { - "source_chain": "mock-chain", - "message_id": "Unknown7" - }, - "source_address": "idc", - "destination_chain": "mock-chain-2", - "destination_address": "idc", - "payload_hash": "0707070707070707070707070707070707070707070707070707070707070707" - }, - { - "cc_id": { - "source_chain": "mock-chain", - "message_id": "Unknown8" - }, - "source_address": "idc", - "destination_chain": "mock-chain-2", - "destination_address": "idc", - "payload_hash": "0808080808080808080808080808080808080808080808080808080808080808" - }, - { - "cc_id": { - "source_chain": "mock-chain", - "message_id": "Unknown9" - }, - "source_address": "idc", - "destination_chain": "mock-chain-2", - "destination_address": "idc", - "payload_hash": "0909090909090909090909090909090909090909090909090909090909090909" - } - ] - } -] \ No newline at end of file From 31d21bbe606060e2a178a9dc295b2a98c21cdac8 Mon Sep 17 00:00:00 2001 From: Christian Gorenflo Date: Wed, 28 Aug 2024 23:38:54 -0400 Subject: [PATCH 13/23] fixes from comments --- Cargo.lock | 1 + .../axelarnet-gateway/src/clients/external.rs | 81 ++++++------------- .../axelarnet-gateway/src/clients/gateway.rs | 6 +- .../axelarnet-gateway/src/clients/mod.rs | 7 +- contracts/axelarnet-gateway/src/contract.rs | 11 +-- .../axelarnet-gateway/src/contract/execute.rs | 77 ++++++++++++------ .../axelarnet-gateway/src/contract/query.rs | 7 +- contracts/axelarnet-gateway/src/state.rs | 10 ++- contracts/axelarnet-gateway/tests/execute.rs | 4 +- .../axelarnet-gateway/tests/utils/execute.rs | 17 ++-- contracts/gateway/src/contract.rs | 2 +- contracts/voting-verifier/src/client.rs | 6 +- packages/axelar-wasm-std/Cargo.toml | 1 + packages/axelar-wasm-std/src/error.rs | 21 ----- packages/client/src/lib.rs | 4 +- 15 files changed, 118 insertions(+), 137 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 96b7aa119..0ff90de9b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -812,6 +812,7 @@ dependencies = [ "cw2 1.1.2", "error-stack", "flagset", + "goldie", "hex", "into-inner-derive", "itertools 0.11.0", diff --git a/contracts/axelarnet-gateway/src/clients/external.rs b/contracts/axelarnet-gateway/src/clients/external.rs index e414edadd..6015a7264 100644 --- a/contracts/axelarnet-gateway/src/clients/external.rs +++ b/contracts/axelarnet-gateway/src/clients/external.rs @@ -1,6 +1,5 @@ -use axelar_wasm_std::address; use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Deps, HexBinary, WasmMsg}; +use cosmwasm_std::{Addr, HexBinary, QuerierWrapper, WasmMsg}; use router_api::{Address, CrossChainId}; /// `AxelarExecutableMsg` is a struct containing the args used by the axelarnet gateway to execute a destination contract on Axelar. @@ -15,84 +14,56 @@ pub struct AxelarExecutableMsg { /// By convention, amplifier-compatible contracts must expose this `Execute` variant. /// Due to identical json serialization, we can imitate it here so the gateway can call it. #[cw_serde] -pub enum ExecuteMsg { +enum ExecuteMsg { /// Execute the message at the destination contract with the corresponding payload. Execute(AxelarExecutableMsg), } -pub struct CrossChainExecutor<'a> { +pub struct Client<'a> { client: client::Client<'a, ExecuteMsg, ()>, } -impl<'a> CrossChainExecutor<'a> { - pub fn new(deps: Deps<'a>, destination: &str) -> error_stack::Result { - let destination = address::validate_cosmwasm_address(deps.api, destination)?; - Ok(CrossChainExecutor { - client: client::Client::new(deps.querier, destination), - }) +impl<'a> Client<'a> { + pub fn new(querier: QuerierWrapper<'a>, destination: &'a Addr) -> Self { + Client { + client: client::Client::new(querier, destination), + } } - pub fn prepare_execute_msg( - &self, - cc_id: CrossChainId, - source_address: Address, - payload: HexBinary, - ) -> WasmMsg { - self.client - .execute(&ExecuteMsg::Execute(AxelarExecutableMsg { - cc_id, - source_address, - payload, - })) - } -} - -impl<'a> From> for CrossChainExecutor<'a> { - fn from(client: client::Client<'a, ExecuteMsg, ()>) -> Self { - CrossChainExecutor { client } + pub fn execute(&self, msg: AxelarExecutableMsg) -> WasmMsg { + self.client.execute(&ExecuteMsg::Execute(msg)) } } #[cfg(test)] mod test { - use cosmwasm_std::testing::MockQuerier; - use cosmwasm_std::{to_json_binary, Addr, QuerierWrapper}; + use cosmwasm_std::testing::mock_dependencies; + use cosmwasm_std::{to_json_binary, Addr, HexBinary, WasmMsg}; + use router_api::CrossChainId; - use super::*; + use crate::clients::external; #[test] fn execute_message() { - let (querier, addr) = setup(); - let client: CrossChainExecutor = - client::Client::new(QuerierWrapper::new(&querier), addr.clone()).into(); + let deps = mock_dependencies(); + + let destination_addr = Addr::unchecked("axelar-executable"); - let cc_id = CrossChainId::new("source-chain", "message-id").unwrap(); - let source_address: Address = "source-address".parse().unwrap(); - let payload = HexBinary::from(vec![1, 2, 3]); + let executable_msg = external::AxelarExecutableMsg { + source_address: "source-address".parse().unwrap(), + payload: HexBinary::from(vec![1, 2, 3]), + cc_id: CrossChainId::new("source-chain", "message-id").unwrap(), + }; - let msg = - client.prepare_execute_msg(cc_id.clone(), source_address.clone(), payload.clone()); + let client = external::Client::new(deps.as_ref().querier, &destination_addr); assert_eq!( - msg, + client.execute(executable_msg.clone()), WasmMsg::Execute { - contract_addr: addr.to_string(), - msg: to_json_binary(&ExecuteMsg::Execute(AxelarExecutableMsg { - cc_id, - source_address, - payload, - })) - .unwrap(), + contract_addr: destination_addr.to_string(), + msg: to_json_binary(&external::ExecuteMsg::Execute(executable_msg)).unwrap(), funds: vec![], } ); } - - fn setup() -> (MockQuerier, Addr) { - let addr = Addr::unchecked("axelar-executable"); - - let querier = MockQuerier::default(); - - (querier, addr) - } } diff --git a/contracts/axelarnet-gateway/src/clients/gateway.rs b/contracts/axelarnet-gateway/src/clients/gateway.rs index 29ea04f8e..bba544de7 100644 --- a/contracts/axelarnet-gateway/src/clients/gateway.rs +++ b/contracts/axelarnet-gateway/src/clients/gateway.rs @@ -77,8 +77,7 @@ mod test { #[test] fn call_contract() { let (querier, _, addr) = setup(); - let client: Client = - client::Client::new(QuerierWrapper::new(&querier), addr.clone()).into(); + let client: Client = client::Client::new(QuerierWrapper::new(&querier), &addr).into(); let destination_chain: ChainName = "destination-chain".parse().unwrap(); let destination_address: Address = "destination-address".parse().unwrap(); @@ -108,8 +107,7 @@ mod test { #[test] fn execute_message() { let (querier, _, addr) = setup(); - let client: Client = - client::Client::new(QuerierWrapper::new(&querier), addr.clone()).into(); + let client: Client = client::Client::new(QuerierWrapper::new(&querier), &addr).into(); let payload = HexBinary::from(vec![1, 2, 3]); let cc_id = CrossChainId::new("source-chain", "message-id").unwrap(); diff --git a/contracts/axelarnet-gateway/src/clients/mod.rs b/contracts/axelarnet-gateway/src/clients/mod.rs index ae910c897..c752583a4 100644 --- a/contracts/axelarnet-gateway/src/clients/mod.rs +++ b/contracts/axelarnet-gateway/src/clients/mod.rs @@ -1,5 +1,2 @@ -mod external; -mod gateway; - -pub use external::*; -pub use gateway::Client as GatewayClient; +pub mod external; +pub mod gateway; diff --git a/contracts/axelarnet-gateway/src/contract.rs b/contracts/axelarnet-gateway/src/contract.rs index 3e22aadd7..073b82637 100644 --- a/contracts/axelarnet-gateway/src/contract.rs +++ b/contracts/axelarnet-gateway/src/contract.rs @@ -22,8 +22,8 @@ pub enum Error { RouteMessages, #[error("failed to execute a cross-chain execution payload")] Execute, - #[error("failed to query cross-chain contract call messages")] - QueryContractCallMessages, + #[error("failed to query routable messages")] + QueryRoutableMessage, #[error("failed to query executable messages")] QueryExecutableMessages, } @@ -93,14 +93,15 @@ pub fn execute( pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result { match msg { QueryMsg::RoutableMessages { cc_ids } => to_json_binary( - &query::routable_messages(deps, cc_ids) - .change_context(Error::QueryContractCallMessages)?, + &query::routable_messages(deps, cc_ids).change_context(Error::QueryRoutableMessage)?, ), QueryMsg::ExecutableMessages { cc_ids } => to_json_binary( &query::executable_messages(deps, cc_ids) .change_context(Error::QueryExecutableMessages)?, ), - QueryMsg::ChainName => to_json_binary(&query::chain_name().change_context(Error::QueryChainName)?), + QueryMsg::ChainName => { + to_json_binary(&query::chain_name().change_context(Error::QueryChainName)?) + } }? .then(Ok) } diff --git a/contracts/axelarnet-gateway/src/contract/execute.rs b/contracts/axelarnet-gateway/src/contract/execute.rs index 989ec6109..cd6c9a608 100644 --- a/contracts/axelarnet-gateway/src/contract/execute.rs +++ b/contracts/axelarnet-gateway/src/contract/execute.rs @@ -1,7 +1,7 @@ use std::str::FromStr; use axelar_wasm_std::msg_id::HexTxHashAndEventIndex; -use axelar_wasm_std::{FnExt, IntoContractError}; +use axelar_wasm_std::{address, FnExt, IntoContractError}; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, DepsMut, HexBinary, Response, Storage, Uint256}; use error_stack::{bail, ensure, report, Result, ResultExt}; @@ -10,10 +10,10 @@ use router_api::client::Router; use router_api::{Address, ChainName, CrossChainId, Message}; use sha3::{Digest, Keccak256}; -use crate::clients::CrossChainExecutor; +use crate::clients::external; use crate::events::AxelarnetGatewayEvent; -use crate::state; use crate::state::Config; +use crate::{state, AxelarExecutableMsg}; #[derive(thiserror::Error, Debug, IntoContractError)] pub enum Error { @@ -38,14 +38,12 @@ pub enum Error { SaveRoutableMessage, #[error("invalid cross-chain id")] InvalidCrossChainId, - #[error("failed to create executor")] - CreateExecutor, - #[error("not allowed to execute payload")] - PayloadNotApproved, #[error("unable to generate event index")] EventIndex, #[error("invalid source address {0}")] InvalidSourceAddress(Addr), + #[error("invalid destination address {0}")] + InvalidDestinationAddress(String), } #[cw_serde] @@ -56,12 +54,12 @@ pub struct CallContractData { } impl CallContractData { - pub fn into_message(self, id: CrossChainId, source_address: Address) -> Message { + pub fn to_message(&self, id: CrossChainId, source_address: Address) -> Message { Message { cc_id: id, source_address, - destination_chain: self.destination_chain, - destination_address: self.destination_address, + destination_chain: self.destination_chain.clone(), + destination_address: self.destination_address.clone(), payload_hash: Keccak256::digest(self.payload.as_slice()).into(), } } @@ -73,8 +71,6 @@ pub fn call_contract( sender: Addr, call_contract: CallContractData, ) -> Result { - let payload = call_contract.payload.clone(); - let Config { router, chain_name } = state::load_config(storage).change_context(Error::ConfigAccess)?; @@ -82,15 +78,20 @@ pub fn call_contract( .change_context(Error::CrossChainIdGeneration)?; let source_address = Address::from_str(sender.as_str()) .change_context(Error::InvalidSourceAddress(sender.clone()))?; - let msg = call_contract.into_message(id, source_address); + let msg = call_contract.to_message(id, source_address); state::save_unique_routable_msg(storage, &msg.cc_id, &msg) .inspect_err(|err| panic_if_already_exists(err, &msg.cc_id)) .change_context(Error::SaveRoutableMessage)?; Ok( - route_to_router(storage, &Router { address: router }, vec![msg.clone()])? - .add_event(AxelarnetGatewayEvent::ContractCalled { msg, payload }.into()), + route_to_router(storage, &Router { address: router }, vec![msg.clone()])?.add_event( + AxelarnetGatewayEvent::ContractCalled { + msg, + payload: call_contract.payload, + } + .into(), + ), ) } @@ -113,22 +114,39 @@ pub fn route_messages( pub fn execute(deps: DepsMut, cc_id: CrossChainId, payload: HexBinary) -> Result { let payload_hash: [u8; 32] = Keccak256::digest(payload.as_slice()).into(); - let msg = state::update_as_executed(deps.storage, &cc_id, |msg| { - (payload_hash == msg.payload_hash) - .then_some(msg) - .ok_or(state::Error::PayloadHashMismatch) - }) - .change_context(Error::MarkExecuted(cc_id.clone()))?; + let msg = state::mark_as_executed(deps.storage, &cc_id, ensure_same_payload_hash(payload_hash)) + .change_context(Error::MarkExecuted(cc_id.clone()))?; - let executor = CrossChainExecutor::new(deps.as_ref(), &msg.destination_address) - .change_context(Error::CreateExecutor)?; + let execute_msg = + external_client(deps, &msg.destination_address)?.execute(AxelarExecutableMsg { + cc_id, + source_address: msg.source_address.clone(), + payload, + }); Response::new() - .add_message(executor.prepare_execute_msg(cc_id, msg.source_address.clone(), payload)) + .add_message(execute_msg) .add_event(AxelarnetGatewayEvent::MessageExecuted { msg }.into()) .then(Ok) } +fn external_client(deps: DepsMut, contract_addr: &str) -> Result { + let destination = address::validate_cosmwasm_address(deps.api, contract_addr) + .change_context(Error::InvalidDestinationAddress(contract_addr.to_string()))?; + + Ok(external::Client::new(deps.querier, &destination)) +} + +fn ensure_same_payload_hash( + payload_hash: [u8; 32], +) -> fn(Message) -> core::result::Result { + |msg| { + (payload_hash == msg.payload_hash) + .then_some(msg) + .ok_or(state::Error::PayloadHashMismatch) + } +} + fn generate_cross_chain_id( storage: &mut dyn Storage, block_height: u64, @@ -189,7 +207,11 @@ fn route_to_router( let msgs: Vec<_> = msgs .into_iter() .unique() - .filter_map(|msg| verify_message(store, msg).transpose()) + .map(|msg| try_load_executable_msg(store, msg)) + .filter_map_ok(|msg| match msg { + Some(msg) => Some(msg), + None => None, + }) .try_collect()?; Ok(Response::new() @@ -202,7 +224,10 @@ 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 verify_message(store: &mut dyn Storage, msg: Message) -> Result, Error> { +fn try_load_executable_msg( + store: &mut dyn Storage, + msg: Message, +) -> Result, Error> { let stored_msg = state::may_load_routable_msg(store, &msg.cc_id) .change_context(Error::ExecutableMessageAccess)?; diff --git a/contracts/axelarnet-gateway/src/contract/query.rs b/contracts/axelarnet-gateway/src/contract/query.rs index 90d575843..47f35ec1d 100644 --- a/contracts/axelarnet-gateway/src/contract/query.rs +++ b/contracts/axelarnet-gateway/src/contract/query.rs @@ -1,6 +1,5 @@ -use axelar_wasm_std::error::accumulate_errs; use cosmwasm_std::Deps; -use error_stack::Result; +use itertools::Itertools; use router_api::{CrossChainId, Message}; use crate::state::{self, ExecutableMessage}; @@ -12,7 +11,7 @@ pub fn routable_messages( cc_ids .into_iter() .map(|cc_id| state::load_routable_msg(deps.storage, &cc_id)) - .fold(Ok(vec![]), accumulate_errs) + .try_collect() } pub fn executable_messages( @@ -22,5 +21,5 @@ pub fn executable_messages( cc_ids .into_iter() .map(|cc_id| state::load_executable_msg(deps.storage, &cc_id)) - .fold(Ok(vec![]), accumulate_errs) + .try_collect() } diff --git a/contracts/axelarnet-gateway/src/state.rs b/contracts/axelarnet-gateway/src/state.rs index d50bc4bad..8b24eeb5a 100644 --- a/contracts/axelarnet-gateway/src/state.rs +++ b/contracts/axelarnet-gateway/src/state.rs @@ -39,8 +39,9 @@ pub struct Config { #[cw_serde] pub enum ExecutableMessage { - // sent by the router, but not executed yet + /// A message that has been sent by the router, but not executed yet. Approved(Message), + /// An approved message that has been executed. Executed(Message), } @@ -112,15 +113,16 @@ pub fn load_executable_msg( } /// Update the status of a message to executed if it is in approved status, error otherwise. -pub fn update_as_executed( +/// The validation function can define additional checks on the message. +pub fn mark_as_executed( storage: &mut dyn Storage, cc_id: &CrossChainId, - action: impl FnOnce(Message) -> Result, + validate: impl FnOnce(Message) -> Result, ) -> Result { let msg = match may_load_executable_msg(storage, cc_id)? { None => Err(Error::MessageNotApproved(cc_id.clone())), Some(ExecutableMessage::Executed(_)) => Err(Error::MessageAlreadyExecuted(cc_id.clone())), - Some(ExecutableMessage::Approved(msg)) => Ok(action(msg)?), + Some(ExecutableMessage::Approved(msg)) => Ok(validate(msg)?), }?; EXECUTABLE_MESSAGES.save(storage, cc_id, &ExecutableMessage::Executed(msg.clone()))?; diff --git a/contracts/axelarnet-gateway/tests/execute.rs b/contracts/axelarnet-gateway/tests/execute.rs index 9ba009422..de53e7d24 100644 --- a/contracts/axelarnet-gateway/tests/execute.rs +++ b/contracts/axelarnet-gateway/tests/execute.rs @@ -1,8 +1,8 @@ use std::str::FromStr; use axelar_wasm_std::error::ContractError; +use axelarnet_gateway::contract; use axelarnet_gateway::msg::ExecuteMsg; -use axelarnet_gateway::{contract, ExternalExecuteMsg}; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; use cosmwasm_std::{DepsMut, HexBinary, Response}; use router_api::msg::ExecuteMsg as RouterExecuteMsg; @@ -69,7 +69,7 @@ fn execute_approved_message_once_returns_correct_message() { let response = utils::execute_payload(deps.as_mut(), cc_id, payload); assert!(response.is_ok()); - let msg = inspect_response_msg::(response.unwrap()); + let msg = inspect_response_msg::(response.unwrap()); assert!(msg.is_ok()); goldie::assert_json!(msg.unwrap()) } diff --git a/contracts/axelarnet-gateway/tests/utils/execute.rs b/contracts/axelarnet-gateway/tests/utils/execute.rs index fe5d72248..25b2a74e1 100644 --- a/contracts/axelarnet-gateway/tests/utils/execute.rs +++ b/contracts/axelarnet-gateway/tests/utils/execute.rs @@ -1,12 +1,19 @@ use axelar_wasm_std::error::ContractError; -use axelarnet_gateway::contract; -use axelarnet_gateway::msg::ExecuteMsg; +use axelarnet_gateway::msg::ExecuteMsg as GatewayExecuteMsg; +use axelarnet_gateway::{contract, AxelarExecutableMsg}; +use cosmwasm_schema::cw_serde; use cosmwasm_std::testing::{mock_env, mock_info}; use cosmwasm_std::{DepsMut, HexBinary, Response}; use router_api::{Address, ChainName, CrossChainId, Message}; use crate::utils::params; +#[cw_serde] +/// simulating a contract's implementation of the `Execute` variant of `ExecuteMsg` from `axelarnet-gateway` +pub enum ExecuteMsg { + Execute(AxelarExecutableMsg), +} + pub fn call_contract( deps: DepsMut, destination_chain: ChainName, @@ -17,7 +24,7 @@ pub fn call_contract( deps, mock_env(), mock_info("sender", &[]), - ExecuteMsg::CallContract { + GatewayExecuteMsg::CallContract { destination_chain, destination_address, payload, @@ -30,7 +37,7 @@ pub fn route_from_router(deps: DepsMut, msgs: Vec) -> Result Result { let config = state::load_config(deps.storage).change_context(Error::Execute)?; - let verifier = client::Client::new(deps.querier, config.verifier).into(); + let verifier = client::Client::new(deps.querier, &config.verifier).into(); match msg.ensure_permissions(deps.storage, &info.sender)? { ExecuteMsg::VerifyMessages(msgs) => { diff --git a/contracts/voting-verifier/src/client.rs b/contracts/voting-verifier/src/client.rs index a8162ef4d..a5461b206 100644 --- a/contracts/voting-verifier/src/client.rs +++ b/contracts/voting-verifier/src/client.rs @@ -105,7 +105,7 @@ mod test { #[test] fn query_messages_status() { let (querier, _, addr) = setup(); - let client: Client = client::Client::new(QuerierWrapper::new(&querier), addr).into(); + let client: Client = client::Client::new(QuerierWrapper::new(&querier), &addr).into(); let msg_1 = Message { cc_id: CrossChainId::new( @@ -155,7 +155,7 @@ mod test { #[test] fn query_verifier_set_status() { let (querier, _, addr) = setup(); - let client: Client = client::Client::new(QuerierWrapper::new(&querier), addr).into(); + let client: Client = client::Client::new(QuerierWrapper::new(&querier), &addr).into(); assert_eq!( client @@ -172,7 +172,7 @@ mod test { #[test] fn query_current_threshold() { let (querier, instantiate_msg, addr) = setup(); - let client: Client = client::Client::new(QuerierWrapper::new(&querier), addr).into(); + let client: Client = client::Client::new(QuerierWrapper::new(&querier), &addr).into(); assert_eq!( client.current_threshold().unwrap(), diff --git a/packages/axelar-wasm-std/Cargo.toml b/packages/axelar-wasm-std/Cargo.toml index 05c32e53a..ca04d20df 100644 --- a/packages/axelar-wasm-std/Cargo.toml +++ b/packages/axelar-wasm-std/Cargo.toml @@ -56,6 +56,7 @@ valuable = { version = "0.1.0", features = ["derive"] } cw-multi-test = "0.15.1" hex = { version = "0.4.3", default-features = false } rand = "0.8.5" +goldie = { workspace = true } [lints] workspace = true diff --git a/packages/axelar-wasm-std/src/error.rs b/packages/axelar-wasm-std/src/error.rs index 05f5d214e..1e2922591 100644 --- a/packages/axelar-wasm-std/src/error.rs +++ b/packages/axelar-wasm-std/src/error.rs @@ -77,27 +77,6 @@ pub fn extend_err( } } -pub fn accumulate_errs( - acc: error_stack::Result, E>, - value: Result, -) -> error_stack::Result, E> { - accumulate_reports(acc, value.map_err(|err| err.into())) -} - -pub fn accumulate_reports( - acc: error_stack::Result, E>, - value: Result>, -) -> error_stack::Result, E> { - match (acc, value) { - (Ok(mut values), Ok(value)) => { - values.push(value); - Ok(values) - } - (Err(report), Ok(_)) => Err(report), - (acc, Err(err)) => extend_err(acc, err), - } -} - #[macro_export] macro_rules! err_contains { ($expression:expr, $error_type:ty, $pattern:pat $(if $guard:expr)? $(,)?) => { diff --git a/packages/client/src/lib.rs b/packages/client/src/lib.rs index 49864d1b8..92ffc1c71 100644 --- a/packages/client/src/lib.rs +++ b/packages/client/src/lib.rs @@ -19,7 +19,7 @@ where Q: Serialize, { pub querier: QuerierWrapper<'a>, - pub address: Addr, + pub address: &'a Addr, execute_msg_type: PhantomData, query_msg_type: PhantomData, } @@ -29,7 +29,7 @@ where M: Serialize, Q: Serialize, { - pub fn new(querier: QuerierWrapper<'a>, address: Addr) -> Self { + pub fn new(querier: QuerierWrapper<'a>, address: &'a Addr) -> Self { Client { querier, address, From b9650220c1f53103fd46ddfa9c07af056589a333 Mon Sep 17 00:00:00 2001 From: Christian Gorenflo Date: Wed, 28 Aug 2024 23:39:29 -0400 Subject: [PATCH 14/23] lint --- contracts/axelarnet-gateway/src/lib.rs | 3 ++- packages/axelar-wasm-std/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/contracts/axelarnet-gateway/src/lib.rs b/contracts/axelarnet-gateway/src/lib.rs index 92afd9664..d96da0a01 100644 --- a/contracts/axelarnet-gateway/src/lib.rs +++ b/contracts/axelarnet-gateway/src/lib.rs @@ -4,5 +4,6 @@ pub mod msg; mod state; mod clients; -pub use clients::{external::AxelarExecutableMsg, gateway::Client}; +pub use clients::external::AxelarExecutableMsg; +pub use clients::gateway::Client; pub use state::ExecutableMessage; diff --git a/packages/axelar-wasm-std/Cargo.toml b/packages/axelar-wasm-std/Cargo.toml index ca04d20df..827b33fac 100644 --- a/packages/axelar-wasm-std/Cargo.toml +++ b/packages/axelar-wasm-std/Cargo.toml @@ -54,9 +54,9 @@ valuable = { version = "0.1.0", features = ["derive"] } [dev-dependencies] cw-multi-test = "0.15.1" +goldie = { workspace = true } hex = { version = "0.4.3", default-features = false } rand = "0.8.5" -goldie = { workspace = true } [lints] workspace = true From b0867a7aec6e88881967df7b75a7b8213e45b81e Mon Sep 17 00:00:00 2001 From: Christian Gorenflo Date: Thu, 29 Aug 2024 00:11:16 -0400 Subject: [PATCH 15/23] clippy --- .../axelarnet-gateway/src/clients/gateway.rs | 3 +- contracts/axelarnet-gateway/src/contract.rs | 9 ++-- .../axelarnet-gateway/src/contract/execute.rs | 43 +++++++++---------- .../axelarnet-gateway/src/contract/query.rs | 16 ++++--- .../nexus-gateway/src/contract/execute.rs | 2 +- 5 files changed, 38 insertions(+), 35 deletions(-) diff --git a/contracts/axelarnet-gateway/src/clients/gateway.rs b/contracts/axelarnet-gateway/src/clients/gateway.rs index bba544de7..8c985e796 100644 --- a/contracts/axelarnet-gateway/src/clients/gateway.rs +++ b/contracts/axelarnet-gateway/src/clients/gateway.rs @@ -65,8 +65,7 @@ mod test { #[test] fn chain_name() { let (querier, _, addr) = setup(); - let client: Client = - client::Client::new(QuerierWrapper::new(&querier), addr.clone()).into(); + let client: Client = client::Client::new(QuerierWrapper::new(&querier), &addr).into(); assert_eq!( client.chain_name().unwrap(), diff --git a/contracts/axelarnet-gateway/src/contract.rs b/contracts/axelarnet-gateway/src/contract.rs index 073b82637..cc3e619f7 100644 --- a/contracts/axelarnet-gateway/src/contract.rs +++ b/contracts/axelarnet-gateway/src/contract.rs @@ -26,6 +26,8 @@ pub enum Error { QueryRoutableMessage, #[error("failed to query executable messages")] QueryExecutableMessages, + #[error("failed to query chain name")] + QueryChainName, } #[cfg_attr(not(feature = "library"), entry_point)] @@ -93,14 +95,15 @@ pub fn execute( pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result { match msg { QueryMsg::RoutableMessages { cc_ids } => to_json_binary( - &query::routable_messages(deps, cc_ids).change_context(Error::QueryRoutableMessage)?, + &query::routable_messages(deps.storage, cc_ids) + .change_context(Error::QueryRoutableMessage)?, ), QueryMsg::ExecutableMessages { cc_ids } => to_json_binary( - &query::executable_messages(deps, cc_ids) + &query::executable_messages(deps.storage, cc_ids) .change_context(Error::QueryExecutableMessages)?, ), QueryMsg::ChainName => { - to_json_binary(&query::chain_name().change_context(Error::QueryChainName)?) + to_json_binary(&query::chain_name(deps.storage).change_context(Error::QueryChainName)?) } }? .then(Ok) diff --git a/contracts/axelarnet-gateway/src/contract/execute.rs b/contracts/axelarnet-gateway/src/contract/execute.rs index cd6c9a608..65dac8943 100644 --- a/contracts/axelarnet-gateway/src/contract/execute.rs +++ b/contracts/axelarnet-gateway/src/contract/execute.rs @@ -114,34 +114,34 @@ pub fn route_messages( pub fn execute(deps: DepsMut, cc_id: CrossChainId, payload: HexBinary) -> Result { let payload_hash: [u8; 32] = Keccak256::digest(payload.as_slice()).into(); - let msg = state::mark_as_executed(deps.storage, &cc_id, ensure_same_payload_hash(payload_hash)) - .change_context(Error::MarkExecuted(cc_id.clone()))?; + let msg = state::mark_as_executed( + deps.storage, + &cc_id, + ensure_same_payload_hash(&payload_hash), + ) + .change_context(Error::MarkExecuted(cc_id.clone()))?; - let execute_msg = - external_client(deps, &msg.destination_address)?.execute(AxelarExecutableMsg { - cc_id, - source_address: msg.source_address.clone(), - payload, - }); + let executable_msg = AxelarExecutableMsg { + cc_id, + source_address: msg.source_address.clone(), + payload, + }; + let destination = address::validate_cosmwasm_address(deps.api, &msg.destination_address) + .change_context(Error::InvalidDestinationAddress( + msg.destination_address.to_string(), + ))?; Response::new() - .add_message(execute_msg) + .add_message(external::Client::new(deps.querier, &destination).execute(executable_msg)) .add_event(AxelarnetGatewayEvent::MessageExecuted { msg }.into()) .then(Ok) } -fn external_client(deps: DepsMut, contract_addr: &str) -> Result { - let destination = address::validate_cosmwasm_address(deps.api, contract_addr) - .change_context(Error::InvalidDestinationAddress(contract_addr.to_string()))?; - - Ok(external::Client::new(deps.querier, &destination)) -} - fn ensure_same_payload_hash( - payload_hash: [u8; 32], -) -> fn(Message) -> core::result::Result { + payload_hash: &[u8; 32], +) -> impl FnOnce(Message) -> core::result::Result + '_ { |msg| { - (payload_hash == msg.payload_hash) + (*payload_hash == msg.payload_hash) .then_some(msg) .ok_or(state::Error::PayloadHashMismatch) } @@ -208,10 +208,7 @@ fn route_to_router( .into_iter() .unique() .map(|msg| try_load_executable_msg(store, msg)) - .filter_map_ok(|msg| match msg { - Some(msg) => Some(msg), - None => None, - }) + .filter_map_ok(|msg| msg) .try_collect()?; Ok(Response::new() diff --git a/contracts/axelarnet-gateway/src/contract/query.rs b/contracts/axelarnet-gateway/src/contract/query.rs index 47f35ec1d..deeb4ac6d 100644 --- a/contracts/axelarnet-gateway/src/contract/query.rs +++ b/contracts/axelarnet-gateway/src/contract/query.rs @@ -1,25 +1,29 @@ -use cosmwasm_std::Deps; +use cosmwasm_std::Storage; use itertools::Itertools; -use router_api::{CrossChainId, Message}; +use router_api::{ChainName, CrossChainId, Message}; use crate::state::{self, ExecutableMessage}; pub fn routable_messages( - deps: Deps, + storage: &dyn Storage, cc_ids: Vec, ) -> Result, state::Error> { cc_ids .into_iter() - .map(|cc_id| state::load_routable_msg(deps.storage, &cc_id)) + .map(|cc_id| state::load_routable_msg(storage, &cc_id)) .try_collect() } pub fn executable_messages( - deps: Deps, + storage: &dyn Storage, cc_ids: Vec, ) -> Result, state::Error> { cc_ids .into_iter() - .map(|cc_id| state::load_executable_msg(deps.storage, &cc_id)) + .map(|cc_id| state::load_executable_msg(storage, &cc_id)) .try_collect() } + +pub fn chain_name(storage: &dyn Storage) -> Result { + state::load_config(storage).map(|config| config.chain_name) +} diff --git a/contracts/nexus-gateway/src/contract/execute.rs b/contracts/nexus-gateway/src/contract/execute.rs index 71374ca8a..9f5fc0d7d 100644 --- a/contracts/nexus-gateway/src/contract/execute.rs +++ b/contracts/nexus-gateway/src/contract/execute.rs @@ -21,7 +21,7 @@ pub fn call_contract_with_token( ) -> Result> { let config = load_config(storage)?; let axelarnet_gateway: axelarnet_gateway::Client = - client::Client::new(querier, config.axelar_gateway).into(); + client::Client::new(querier, &config.axelar_gateway).into(); let source_address: Address = info .sender .into_string() From 8b91e4c2dbfcd18e77752ed8feddf08a1fc824a4 Mon Sep 17 00:00:00 2001 From: Christian Gorenflo Date: Thu, 29 Aug 2024 00:12:14 -0400 Subject: [PATCH 16/23] remove goldie --- contracts/axelarnet-gateway/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/axelarnet-gateway/Cargo.toml b/contracts/axelarnet-gateway/Cargo.toml index db4eed7af..4c39c81ec 100644 --- a/contracts/axelarnet-gateway/Cargo.toml +++ b/contracts/axelarnet-gateway/Cargo.toml @@ -50,7 +50,6 @@ thiserror = { workspace = true } [dev-dependencies] cw-multi-test = { workspace = true } -goldie = { workspace = true } [lints] workspace = true From af5c11c583a7034d6054cf31adaefc505cb46f3b Mon Sep 17 00:00:00 2001 From: Christian Gorenflo Date: Thu, 29 Aug 2024 00:18:50 -0400 Subject: [PATCH 17/23] readd goldie --- contracts/axelarnet-gateway/Cargo.toml | 1 + contracts/axelarnet-gateway/src/contract.rs | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/contracts/axelarnet-gateway/Cargo.toml b/contracts/axelarnet-gateway/Cargo.toml index 4c39c81ec..db4eed7af 100644 --- a/contracts/axelarnet-gateway/Cargo.toml +++ b/contracts/axelarnet-gateway/Cargo.toml @@ -50,6 +50,7 @@ thiserror = { workspace = true } [dev-dependencies] cw-multi-test = { workspace = true } +goldie = { workspace = true } [lints] workspace = true diff --git a/contracts/axelarnet-gateway/src/contract.rs b/contracts/axelarnet-gateway/src/contract.rs index cc3e619f7..21de28afa 100644 --- a/contracts/axelarnet-gateway/src/contract.rs +++ b/contracts/axelarnet-gateway/src/contract.rs @@ -52,10 +52,12 @@ pub fn instantiate( ) -> Result { cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - let router = address::validate_cosmwasm_address(deps.api, &msg.router_address)?; - let chain_name = msg.chain_name; + let config = Config { + chain_name: msg.chain_name, + router: address::validate_cosmwasm_address(deps.api, &msg.router_address)?, + }; - state::save_config(deps.storage, &Config { chain_name, router })?; + state::save_config(deps.storage, &config)?; Ok(Response::new()) } From 101961a4609f5cb80fc270159c242af31151aa3f Mon Sep 17 00:00:00 2001 From: Christian Gorenflo Date: Thu, 29 Aug 2024 00:20:29 -0400 Subject: [PATCH 18/23] remove goldie --- packages/axelar-wasm-std/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/axelar-wasm-std/Cargo.toml b/packages/axelar-wasm-std/Cargo.toml index 827b33fac..05c32e53a 100644 --- a/packages/axelar-wasm-std/Cargo.toml +++ b/packages/axelar-wasm-std/Cargo.toml @@ -54,7 +54,6 @@ valuable = { version = "0.1.0", features = ["derive"] } [dev-dependencies] cw-multi-test = "0.15.1" -goldie = { workspace = true } hex = { version = "0.4.3", default-features = false } rand = "0.8.5" From 095cd76b4f5befe780b7689be51ffc8aeaf8a5b4 Mon Sep 17 00:00:00 2001 From: Christian Gorenflo Date: Thu, 29 Aug 2024 00:20:33 -0400 Subject: [PATCH 19/23] remove goldie --- Cargo.lock | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 0ff90de9b..96b7aa119 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -812,7 +812,6 @@ dependencies = [ "cw2 1.1.2", "error-stack", "flagset", - "goldie", "hex", "into-inner-derive", "itertools 0.11.0", From cfbd7a3ae459be98154392fc350fe0b7542f97b4 Mon Sep 17 00:00:00 2001 From: Christian Gorenflo Date: Thu, 29 Aug 2024 00:28:24 -0400 Subject: [PATCH 20/23] panic if config is missing --- .../axelarnet-gateway/src/contract/execute.rs | 8 ++------ contracts/axelarnet-gateway/src/state.rs | 6 ++++-- contracts/axelarnet-gateway/tests/query.rs | 18 +++++++++++++++++- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/contracts/axelarnet-gateway/src/contract/execute.rs b/contracts/axelarnet-gateway/src/contract/execute.rs index 65dac8943..601520aee 100644 --- a/contracts/axelarnet-gateway/src/contract/execute.rs +++ b/contracts/axelarnet-gateway/src/contract/execute.rs @@ -32,8 +32,6 @@ pub enum Error { }, #[error("failed to generate cross chain id")] CrossChainIdGeneration, - #[error("unable to load the contract config")] - ConfigAccess, #[error("unable to save the message before routing")] SaveRoutableMessage, #[error("invalid cross-chain id")] @@ -71,8 +69,7 @@ pub fn call_contract( sender: Addr, call_contract: CallContractData, ) -> Result { - let Config { router, chain_name } = - state::load_config(storage).change_context(Error::ConfigAccess)?; + let Config { router, chain_name } = state::load_config(storage); let id = generate_cross_chain_id(storage, block_height, chain_name) .change_context(Error::CrossChainIdGeneration)?; @@ -100,8 +97,7 @@ pub fn route_messages( sender: Addr, msgs: Vec, ) -> Result { - let Config { chain_name, router } = - state::load_config(storage).change_context(Error::ConfigAccess)?; + let Config { chain_name, router } = state::load_config(storage); let router = Router { address: router }; if sender == router.address { diff --git a/contracts/axelarnet-gateway/src/state.rs b/contracts/axelarnet-gateway/src/state.rs index 8b24eeb5a..fcba59fa7 100644 --- a/contracts/axelarnet-gateway/src/state.rs +++ b/contracts/axelarnet-gateway/src/state.rs @@ -57,8 +57,10 @@ pub fn save_config(storage: &mut dyn Storage, value: &Config) -> Result<(), Erro Ok(CONFIG.save(storage, value)?) } -pub fn load_config(storage: &dyn Storage) -> Result { - CONFIG.may_load(storage)?.ok_or(Error::MissingConfig) +pub fn load_config(storage: &dyn Storage) -> Config { + CONFIG + .load(storage) + .expect("gateway config must be set during instantiation") } pub fn save_unique_routable_msg( diff --git a/contracts/axelarnet-gateway/tests/query.rs b/contracts/axelarnet-gateway/tests/query.rs index cac56ab77..b866d6454 100644 --- a/contracts/axelarnet-gateway/tests/query.rs +++ b/contracts/axelarnet-gateway/tests/query.rs @@ -3,7 +3,7 @@ use axelarnet_gateway::{contract, ExecutableMessage}; use cosmwasm_std::testing::{mock_dependencies, mock_env, MockApi, MockQuerier, MockStorage}; use cosmwasm_std::{from_json, Deps, OwnedDeps}; use router_api::msg::ExecuteMsg as RouterExecuteMsg; -use router_api::{CrossChainId, Message}; +use router_api::{ChainName, CrossChainId, Message}; use sha3::{Digest, Keccak256}; use crate::utils::messages::inspect_response_msg; @@ -40,6 +40,17 @@ fn query_executable_messages_gets_expected_messages() { goldie::assert_json!(result.unwrap()); } +#[test] +fn query_chain_name_gets_expected_chain() { + let mut deps = mock_dependencies(); + + utils::instantiate_contract(deps.as_mut()).unwrap(); + + let result = query_chain_name(deps.as_ref()); + + assert_eq!(result.unwrap().as_ref(), params::AXELARNET); +} + fn query_routable_messages(deps: Deps, cc_ids: Vec) -> Result, ()> { from_json( contract::query(deps, mock_env(), QueryMsg::RoutableMessages { cc_ids }).map_err(|_| ())?, @@ -58,6 +69,11 @@ fn query_executable_messages( .map_err(|_| ()) } +fn query_chain_name(deps: Deps) -> Result { + from_json(contract::query(deps, mock_env(), QueryMsg::ChainName).map_err(|_| ())?) + .map_err(|_| ()) +} + fn populate_routable_messages( deps: &mut OwnedDeps, ) -> Vec { From c2f0ad96610371b410cb1203c16eae860d9cfa1a Mon Sep 17 00:00:00 2001 From: Christian Gorenflo Date: Thu, 29 Aug 2024 00:31:17 -0400 Subject: [PATCH 21/23] fix bug --- contracts/axelarnet-gateway/src/contract.rs | 6 +----- contracts/axelarnet-gateway/src/contract/query.rs | 4 ++-- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/contracts/axelarnet-gateway/src/contract.rs b/contracts/axelarnet-gateway/src/contract.rs index 21de28afa..bddf123ca 100644 --- a/contracts/axelarnet-gateway/src/contract.rs +++ b/contracts/axelarnet-gateway/src/contract.rs @@ -26,8 +26,6 @@ pub enum Error { QueryRoutableMessage, #[error("failed to query executable messages")] QueryExecutableMessages, - #[error("failed to query chain name")] - QueryChainName, } #[cfg_attr(not(feature = "library"), entry_point)] @@ -104,9 +102,7 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result { - to_json_binary(&query::chain_name(deps.storage).change_context(Error::QueryChainName)?) - } + QueryMsg::ChainName => to_json_binary(&query::chain_name(deps.storage)), }? .then(Ok) } diff --git a/contracts/axelarnet-gateway/src/contract/query.rs b/contracts/axelarnet-gateway/src/contract/query.rs index deeb4ac6d..bb10090f0 100644 --- a/contracts/axelarnet-gateway/src/contract/query.rs +++ b/contracts/axelarnet-gateway/src/contract/query.rs @@ -24,6 +24,6 @@ pub fn executable_messages( .try_collect() } -pub fn chain_name(storage: &dyn Storage) -> Result { - state::load_config(storage).map(|config| config.chain_name) +pub fn chain_name(storage: &dyn Storage) -> ChainName { + state::load_config(storage).chain_name } From 2880411bba2ebc96a41d3cf106c02933ba05dddf Mon Sep 17 00:00:00 2001 From: Christian Gorenflo Date: Thu, 29 Aug 2024 16:50:06 -0400 Subject: [PATCH 22/23] address validation function comment --- contracts/axelarnet-gateway/src/contract/execute.rs | 10 ++++++---- contracts/axelarnet-gateway/src/state.rs | 7 ++++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/contracts/axelarnet-gateway/src/contract/execute.rs b/contracts/axelarnet-gateway/src/contract/execute.rs index 601520aee..07c911417 100644 --- a/contracts/axelarnet-gateway/src/contract/execute.rs +++ b/contracts/axelarnet-gateway/src/contract/execute.rs @@ -135,11 +135,13 @@ pub fn execute(deps: DepsMut, cc_id: CrossChainId, payload: HexBinary) -> Result fn ensure_same_payload_hash( payload_hash: &[u8; 32], -) -> impl FnOnce(Message) -> core::result::Result + '_ { +) -> impl FnOnce(&Message) -> core::result::Result<(), state::Error> + '_ { |msg| { - (*payload_hash == msg.payload_hash) - .then_some(msg) - .ok_or(state::Error::PayloadHashMismatch) + if *payload_hash == msg.payload_hash { + return Err(state::Error::PayloadHashMismatch); + } + + Ok(()) } } diff --git a/contracts/axelarnet-gateway/src/state.rs b/contracts/axelarnet-gateway/src/state.rs index fcba59fa7..5b8e0cc63 100644 --- a/contracts/axelarnet-gateway/src/state.rs +++ b/contracts/axelarnet-gateway/src/state.rs @@ -1,5 +1,5 @@ use axelar_wasm_std::counter::Counter; -use axelar_wasm_std::IntoContractError; +use axelar_wasm_std::{FnExt, IntoContractError}; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, StdError, Storage}; use cw_storage_plus::{Item, Map}; @@ -119,12 +119,13 @@ pub fn load_executable_msg( pub fn mark_as_executed( storage: &mut dyn Storage, cc_id: &CrossChainId, - validate: impl FnOnce(Message) -> Result, + // this uses a reference to ensure the caller cannot mutate the message + validate: impl FnOnce(&Message) -> Result<(), Error>, ) -> Result { let msg = match may_load_executable_msg(storage, cc_id)? { None => Err(Error::MessageNotApproved(cc_id.clone())), Some(ExecutableMessage::Executed(_)) => Err(Error::MessageAlreadyExecuted(cc_id.clone())), - Some(ExecutableMessage::Approved(msg)) => Ok(validate(msg)?), + Some(ExecutableMessage::Approved(msg)) => validate(&msg)?.then(|_| Ok(msg)), }?; EXECUTABLE_MESSAGES.save(storage, cc_id, &ExecutableMessage::Executed(msg.clone()))?; From 1619e05131e562856753dbf98b2aabb899d0af44 Mon Sep 17 00:00:00 2001 From: Christian Gorenflo Date: Thu, 29 Aug 2024 17:03:04 -0400 Subject: [PATCH 23/23] fix bug --- contracts/axelarnet-gateway/src/contract/execute.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/axelarnet-gateway/src/contract/execute.rs b/contracts/axelarnet-gateway/src/contract/execute.rs index 07c911417..bc378be40 100644 --- a/contracts/axelarnet-gateway/src/contract/execute.rs +++ b/contracts/axelarnet-gateway/src/contract/execute.rs @@ -137,7 +137,7 @@ fn ensure_same_payload_hash( payload_hash: &[u8; 32], ) -> impl FnOnce(&Message) -> core::result::Result<(), state::Error> + '_ { |msg| { - if *payload_hash == msg.payload_hash { + if *payload_hash != msg.payload_hash { return Err(state::Error::PayloadHashMismatch); }