diff --git a/.github/workflows/basic.yaml b/.github/workflows/basic.yaml index 8c5721efe..aa8391a17 100644 --- a/.github/workflows/basic.yaml +++ b/.github/workflows/basic.yaml @@ -93,7 +93,7 @@ jobs: - name: Build ITS release working-directory: ./interchain-token-service - run: cargo build --release --target wasm32-unknown-unknown --locked + run: cargo build --release --lib --target wasm32-unknown-unknown --locked # cosmwasm-check v1.3.x is used to check for compatibility with wasmvm v1.3.x used by Axelar # Older rust toolchain is required to install cosmwasm-check v1.3.x diff --git a/Cargo.lock b/Cargo.lock index 4d8d6a688..edf3f894d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4042,16 +4042,24 @@ version = "0.1.0" dependencies = [ "alloy-primitives", "alloy-sol-types", + "assert_ok", "axelar-wasm-std", + "axelarnet-gateway", + "client", "cosmwasm-schema", "cosmwasm-std", + "cw-storage-plus 1.2.0", + "cw2 1.1.2", "error-stack", "goldie", + "hex", + "msgs-derive", "report", "router-api", "schemars", "serde", "serde_json", + "sha3", "strum 0.25.0", "thiserror", ] diff --git a/contracts/axelarnet-gateway/tests/execute.rs b/contracts/axelarnet-gateway/tests/execute.rs index e65b83ec2..9869816c5 100644 --- a/contracts/axelarnet-gateway/tests/execute.rs +++ b/contracts/axelarnet-gateway/tests/execute.rs @@ -3,6 +3,7 @@ use std::str::FromStr; use assert_ok::assert_ok; use axelar_wasm_std::assert_err_contains; use axelar_wasm_std::error::ContractError; +use axelar_wasm_std::response::inspect_response_msg; use axelarnet_gateway::contract::{self, ExecuteError}; use axelarnet_gateway::msg::ExecuteMsg; use axelarnet_gateway::StateError; @@ -12,7 +13,6 @@ use router_api::msg::ExecuteMsg as RouterExecuteMsg; use router_api::{Address, ChainName, CrossChainId, Message}; use crate::utils::messages; -use crate::utils::messages::inspect_response_msg; mod utils; diff --git a/contracts/axelarnet-gateway/tests/query.rs b/contracts/axelarnet-gateway/tests/query.rs index 58cf70b71..ed5d4d876 100644 --- a/contracts/axelarnet-gateway/tests/query.rs +++ b/contracts/axelarnet-gateway/tests/query.rs @@ -1,4 +1,5 @@ use assert_ok::assert_ok; +use axelar_wasm_std::response::inspect_response_msg; use axelarnet_gateway::msg::QueryMsg; use axelarnet_gateway::{contract, ExecutableMessage}; use cosmwasm_std::testing::{mock_dependencies, mock_env, MockApi, MockQuerier, MockStorage}; @@ -7,7 +8,6 @@ use router_api::msg::ExecuteMsg as RouterExecuteMsg; use router_api::{ChainName, CrossChainId, Message}; use sha3::{Digest, Keccak256}; -use crate::utils::messages::inspect_response_msg; use crate::utils::params; mod utils; diff --git a/contracts/axelarnet-gateway/tests/utils/messages.rs b/contracts/axelarnet-gateway/tests/utils/messages.rs index ff3611020..386ff094f 100644 --- a/contracts/axelarnet-gateway/tests/utils/messages.rs +++ b/contracts/axelarnet-gateway/tests/utils/messages.rs @@ -1,6 +1,4 @@ -use cosmwasm_std::{from_json, CosmosMsg, Response, WasmMsg}; use router_api::{CrossChainId, Message}; -use serde::de::DeserializeOwned; use sha3::Digest; use crate::utils::params; @@ -24,21 +22,3 @@ pub fn dummy_to_router(payload: &impl AsRef<[u8]>) -> Message { 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 followup_messages.next().is_some() { - return Err(()); - } - - match msg { - CosmosMsg::Wasm(WasmMsg::Execute { msg, .. }) => from_json(msg).map_err(|_| ()), - _ => Err(()), - } -} diff --git a/interchain-token-service/Cargo.toml b/interchain-token-service/Cargo.toml index 738c27a49..5f863c729 100644 --- a/interchain-token-service/Cargo.toml +++ b/interchain-token-service/Cargo.toml @@ -4,22 +4,56 @@ version = "0.1.0" rust-version = { workspace = true } edition = { workspace = true } +exclude = [ + # Those files are rust-optimizer artifacts. You might want to commit them for convenience, but they should not be part of the source code publication. + "contract.wasm", + "hash.txt", +] + +[lib] +crate-type = ["cdylib", "rlib"] + +[[bin]] +name = "interchain-token-service-schema" +path = "src/bin/schema.rs" + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[package.metadata.scripts] +optimize = """docker run --rm -v "$(pwd)":/code \ + --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \ + --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ + cosmwasm/optimizer:0.16.0 +""" + [dependencies] alloy-primitives = { workspace = true } alloy-sol-types = { workspace = true } axelar-wasm-std = { workspace = true, features = ["derive"] } +axelarnet-gateway = { workspace = true, features = ["library"] } +client = { workspace = true } cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } +cw-storage-plus = { workspace = true } +cw2 = { workspace = true } error-stack = { workspace = true } +hex = { workspace = true } +msgs-derive = { workspace = true } report = { workspace = true } router-api = { workspace = true } schemars = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +sha3 = { workspace = true } strum = { workspace = true } thiserror = { workspace = true } [dev-dependencies] +assert_ok = { workspace = true } goldie = { workspace = true } [lints] diff --git a/interchain-token-service/README.md b/interchain-token-service/README.md new file mode 100644 index 000000000..260766bf4 --- /dev/null +++ b/interchain-token-service/README.md @@ -0,0 +1,15 @@ +# Interchain Token Service Hub + +## Overview + +The Interchain Token Service (ITS) Hub contract is a crucial component of a cross-chain ITS protocol. It facilitates the transfer of tokens between different blockchains, manages token deployments, and maintains balance integrity across chains. It connects to ITS edge contracts on different chains (e.g. EVM ITS [contract](https://github.com/axelarnetwork/interchain-token-service)). + +## Key Components + +1. **ITS Message Processing**: Processes incoming ITS messages from trusted sources. +2. **Balance Tracking**: Ensures accurate token balances are maintained during cross-chain operations. +3. **ITS Address Registry**: Tracks the trusted ITS address for each chain for routing. + +### Cross-chain messaging + +The ITS Hub makes use of the Axelarnet gateway [contract](../contracts/axelarnet-gateway/) to facilitate sending or receiving cross-chain messages. Messages are sent via `CallContract`, and received when the Axelarnet gateway is executed (by a relayer / user) through `Execute`, which in turn executes ITS Hub's `Execute` method. diff --git a/interchain-token-service/src/abi.rs b/interchain-token-service/src/abi.rs index 0c9f6f6dc..18ec31929 100644 --- a/interchain-token-service/src/abi.rs +++ b/interchain-token-service/src/abi.rs @@ -1,12 +1,11 @@ use alloy_primitives::{FixedBytes, U256}; use alloy_sol_types::{sol, SolValue}; -use axelar_wasm_std::FnExt; +use axelar_wasm_std::{FnExt, IntoContractError}; use cosmwasm_std::{HexBinary, Uint256}; -use error_stack::{Report, ResultExt}; -use router_api::ChainName; +use error_stack::{bail, ensure, report, Report, ResultExt}; +use router_api::ChainNameRaw; -use crate::error::Error; -use crate::primitives::{ItsHubMessage, ItsMessage}; +use crate::primitives::{HubMessage, Message}; use crate::{TokenId, TokenManagerType}; // ITS Message payload types @@ -62,10 +61,22 @@ sol! { } } -impl ItsMessage { +#[derive(thiserror::Error, Debug, IntoContractError)] +pub enum Error { + #[error("failed to decode ITS message")] + MessageDecodeFailed, + #[error("invalid message type")] + InvalidMessageType, + #[error("invalid chain name")] + InvalidChainName, + #[error("invalid token manager type")] + InvalidTokenManagerType, +} + +impl Message { pub fn abi_encode(self) -> HexBinary { match self { - ItsMessage::InterchainTransfer { + Message::InterchainTransfer { token_id, source_address, destination_address, @@ -80,7 +91,7 @@ impl ItsMessage { data: Vec::::from(data).into(), } .abi_encode_params(), - ItsMessage::DeployInterchainToken { + Message::DeployInterchainToken { token_id, name, symbol, @@ -95,7 +106,7 @@ impl ItsMessage { minter: Vec::::from(minter).into(), } .abi_encode_params(), - ItsMessage::DeployTokenManager { + Message::DeployTokenManager { token_id, token_manager_type, params, @@ -111,9 +122,7 @@ impl ItsMessage { } pub fn abi_decode(payload: &[u8]) -> Result> { - if payload.len() < 32 { - return Err(Report::new(Error::InvalidMessage)); - } + ensure!(payload.len() >= 32, Error::MessageDecodeFailed); let message_type = MessageType::abi_decode(&payload[0..32], true) .change_context(Error::InvalidMessageType)?; @@ -121,54 +130,54 @@ impl ItsMessage { let message = match message_type { MessageType::InterchainTransfer => { let decoded = InterchainTransfer::abi_decode_params(payload, true) - .change_context(Error::InvalidMessage)?; + .change_context(Error::MessageDecodeFailed)?; - Ok(ItsMessage::InterchainTransfer { + Message::InterchainTransfer { token_id: TokenId::new(decoded.tokenId.into()), source_address: HexBinary::from(decoded.sourceAddress.to_vec()), destination_address: HexBinary::from(decoded.destinationAddress.as_ref()), amount: Uint256::from_le_bytes(decoded.amount.to_le_bytes()), data: HexBinary::from(decoded.data.as_ref()), - }) + } } MessageType::DeployInterchainToken => { let decoded = DeployInterchainToken::abi_decode_params(payload, true) - .change_context(Error::InvalidMessage)?; + .change_context(Error::MessageDecodeFailed)?; - Ok(ItsMessage::DeployInterchainToken { + Message::DeployInterchainToken { token_id: TokenId::new(decoded.tokenId.into()), name: decoded.name, symbol: decoded.symbol, decimals: decoded.decimals, minter: HexBinary::from(decoded.minter.as_ref()), - }) + } } MessageType::DeployTokenManager => { let decoded = DeployTokenManager::abi_decode_params(payload, true) - .change_context(Error::InvalidMessage)?; + .change_context(Error::MessageDecodeFailed)?; let token_manager_type = u8::try_from(decoded.tokenManagerType) .change_context(Error::InvalidTokenManagerType)? .then(TokenManagerType::from_repr) - .ok_or_else(|| Report::new(Error::InvalidTokenManagerType))?; + .ok_or_else(|| report!(Error::InvalidTokenManagerType))?; - Ok(ItsMessage::DeployTokenManager { + Message::DeployTokenManager { token_id: TokenId::new(decoded.tokenId.into()), token_manager_type, params: HexBinary::from(decoded.params.as_ref()), - }) + } } - _ => Err(Report::new(Error::InvalidMessageType)), - }?; + _ => bail!(Error::InvalidMessageType), + }; Ok(message) } } -impl ItsHubMessage { +impl HubMessage { pub fn abi_encode(self) -> HexBinary { match self { - ItsHubMessage::SendToHub { + HubMessage::SendToHub { destination_chain, message, } => SendToHub { @@ -178,7 +187,7 @@ impl ItsHubMessage { } .abi_encode_params() .into(), - ItsHubMessage::ReceiveFromHub { + HubMessage::ReceiveFromHub { source_chain, message, } => ReceiveFromHub { @@ -192,9 +201,7 @@ impl ItsHubMessage { } pub fn abi_decode(payload: &[u8]) -> Result> { - if payload.len() < 32 { - return Err(Report::new(Error::InvalidMessage)); - } + ensure!(payload.len() >= 32, Error::MessageDecodeFailed); let message_type = MessageType::abi_decode(&payload[0..32], true) .change_context(Error::InvalidMessageType)?; @@ -202,25 +209,25 @@ impl ItsHubMessage { let hub_message = match message_type { MessageType::SendToHub => { let decoded = SendToHub::abi_decode_params(payload, true) - .change_context(Error::InvalidMessage)?; + .change_context(Error::MessageDecodeFailed)?; - ItsHubMessage::SendToHub { - destination_chain: ChainName::try_from(decoded.destination_chain) + HubMessage::SendToHub { + destination_chain: ChainNameRaw::try_from(decoded.destination_chain) .change_context(Error::InvalidChainName)?, - message: ItsMessage::abi_decode(&decoded.message)?, + message: Message::abi_decode(&decoded.message)?, } } MessageType::ReceiveFromHub => { let decoded = ReceiveFromHub::abi_decode_params(payload, true) - .change_context(Error::InvalidMessage)?; + .change_context(Error::MessageDecodeFailed)?; - ItsHubMessage::ReceiveFromHub { - source_chain: ChainName::try_from(decoded.source_chain) + HubMessage::ReceiveFromHub { + source_chain: ChainNameRaw::try_from(decoded.source_chain) .change_context(Error::InvalidChainName)?, - message: ItsMessage::abi_decode(&decoded.message)?, + message: Message::abi_decode(&decoded.message)?, } } - _ => return Err(Report::new(Error::InvalidMessageType)), + _ => bail!(Error::InvalidMessageType), }; Ok(hub_message) @@ -245,21 +252,22 @@ mod tests { use alloy_primitives::{FixedBytes, U256}; use alloy_sol_types::SolValue; + use assert_ok::assert_ok; + use axelar_wasm_std::assert_err_contains; use cosmwasm_std::{HexBinary, Uint256}; - use router_api::ChainName; + use router_api::ChainNameRaw; - use crate::abi::{DeployTokenManager, MessageType, SendToHub}; - use crate::error::Error; - use crate::{ItsHubMessage, ItsMessage, TokenManagerType}; + use crate::abi::{DeployTokenManager, Error, MessageType, SendToHub}; + use crate::{HubMessage, Message, TokenManagerType}; #[test] fn interchain_transfer_encode_decode() { - let remote_chain = ChainName::from_str("chain").unwrap(); + let remote_chain = ChainNameRaw::from_str("chain").unwrap(); let cases = vec![ - ItsHubMessage::SendToHub { + HubMessage::SendToHub { destination_chain: remote_chain.clone(), - message: ItsMessage::InterchainTransfer { + message: Message::InterchainTransfer { token_id: [0u8; 32].into(), source_address: HexBinary::from_hex("").unwrap(), destination_address: HexBinary::from_hex("").unwrap(), @@ -267,9 +275,9 @@ mod tests { data: HexBinary::from_hex("").unwrap(), }, }, - ItsHubMessage::SendToHub { + HubMessage::SendToHub { destination_chain: remote_chain.clone(), - message: ItsMessage::InterchainTransfer { + message: Message::InterchainTransfer { token_id: [255u8; 32].into(), source_address: HexBinary::from_hex("4F4495243837681061C4743b74B3eEdf548D56A5") .unwrap(), @@ -281,9 +289,9 @@ mod tests { data: HexBinary::from_hex("abcd").unwrap(), }, }, - ItsHubMessage::ReceiveFromHub { + HubMessage::ReceiveFromHub { source_chain: remote_chain.clone(), - message: ItsMessage::InterchainTransfer { + message: Message::InterchainTransfer { token_id: [0u8; 32].into(), source_address: HexBinary::from_hex("").unwrap(), destination_address: HexBinary::from_hex("").unwrap(), @@ -291,9 +299,9 @@ mod tests { data: HexBinary::from_hex("").unwrap(), }, }, - ItsHubMessage::ReceiveFromHub { + HubMessage::ReceiveFromHub { source_chain: remote_chain.clone(), - message: ItsMessage::InterchainTransfer { + message: Message::InterchainTransfer { token_id: [255u8; 32].into(), source_address: HexBinary::from_hex("4F4495243837681061C4743b74B3eEdf548D56A5") .unwrap(), @@ -316,19 +324,19 @@ mod tests { for original in cases { let encoded = original.clone().abi_encode(); - let decoded = ItsHubMessage::abi_decode(&encoded).unwrap(); + let decoded = assert_ok!(HubMessage::abi_decode(&encoded)); assert_eq!(original, decoded); } } #[test] fn deploy_interchain_token_encode_decode() { - let remote_chain = ChainName::from_str("chain").unwrap(); + let remote_chain = ChainNameRaw::from_str("chain").unwrap(); let cases = vec![ - ItsHubMessage::SendToHub { + HubMessage::SendToHub { destination_chain: remote_chain.clone(), - message: ItsMessage::DeployInterchainToken { + message: Message::DeployInterchainToken { token_id: [0u8; 32].into(), name: "".into(), symbol: "".into(), @@ -336,9 +344,9 @@ mod tests { minter: HexBinary::from_hex("").unwrap(), }, }, - ItsHubMessage::SendToHub { + HubMessage::SendToHub { destination_chain: remote_chain.clone(), - message: ItsMessage::DeployInterchainToken { + message: Message::DeployInterchainToken { token_id: [1u8; 32].into(), name: "Test Token".into(), symbol: "TST".into(), @@ -346,9 +354,9 @@ mod tests { minter: HexBinary::from_hex("1234").unwrap(), }, }, - ItsHubMessage::SendToHub { + HubMessage::SendToHub { destination_chain: remote_chain.clone(), - message: ItsMessage::DeployInterchainToken { + message: Message::DeployInterchainToken { token_id: [0u8; 32].into(), name: "Unicode Token 🪙".into(), symbol: "UNI🔣".into(), @@ -356,9 +364,9 @@ mod tests { minter: HexBinary::from_hex("abcd").unwrap(), }, }, - ItsHubMessage::ReceiveFromHub { + HubMessage::ReceiveFromHub { source_chain: remote_chain.clone(), - message: ItsMessage::DeployInterchainToken { + message: Message::DeployInterchainToken { token_id: [0u8; 32].into(), name: "".into(), symbol: "".into(), @@ -366,9 +374,9 @@ mod tests { minter: HexBinary::from_hex("").unwrap(), }, }, - ItsHubMessage::ReceiveFromHub { + HubMessage::ReceiveFromHub { source_chain: remote_chain.clone(), - message: ItsMessage::DeployInterchainToken { + message: Message::DeployInterchainToken { token_id: [1u8; 32].into(), name: "Test Token".into(), symbol: "TST".into(), @@ -376,9 +384,9 @@ mod tests { minter: HexBinary::from_hex("1234").unwrap(), }, }, - ItsHubMessage::ReceiveFromHub { + HubMessage::ReceiveFromHub { source_chain: remote_chain.clone(), - message: ItsMessage::DeployInterchainToken { + message: Message::DeployInterchainToken { token_id: [0u8; 32].into(), name: "Unicode Token 🪙".into(), symbol: "UNI🔣".into(), @@ -397,43 +405,43 @@ mod tests { for original in cases { let encoded = original.clone().abi_encode(); - let decoded = ItsHubMessage::abi_decode(&encoded).unwrap(); + let decoded = assert_ok!(HubMessage::abi_decode(&encoded)); assert_eq!(original, decoded); } } #[test] fn deploy_token_manager_encode_decode() { - let remote_chain = ChainName::from_str("chain").unwrap(); + let remote_chain = ChainNameRaw::from_str("chain").unwrap(); let cases = vec![ - ItsHubMessage::SendToHub { + HubMessage::SendToHub { destination_chain: remote_chain.clone(), - message: ItsMessage::DeployTokenManager { + message: Message::DeployTokenManager { token_id: [0u8; 32].into(), token_manager_type: TokenManagerType::NativeInterchainToken, params: HexBinary::default(), }, }, - ItsHubMessage::SendToHub { + HubMessage::SendToHub { destination_chain: remote_chain.clone(), - message: ItsMessage::DeployTokenManager { + message: Message::DeployTokenManager { token_id: [1u8; 32].into(), token_manager_type: TokenManagerType::Gateway, params: HexBinary::from_hex("1234").unwrap(), }, }, - ItsHubMessage::ReceiveFromHub { + HubMessage::ReceiveFromHub { source_chain: remote_chain.clone(), - message: ItsMessage::DeployTokenManager { + message: Message::DeployTokenManager { token_id: [0u8; 32].into(), token_manager_type: TokenManagerType::NativeInterchainToken, params: HexBinary::default(), }, }, - ItsHubMessage::ReceiveFromHub { + HubMessage::ReceiveFromHub { source_chain: remote_chain.clone(), - message: ItsMessage::DeployTokenManager { + message: Message::DeployTokenManager { token_id: [1u8; 32].into(), token_manager_type: TokenManagerType::Gateway, params: HexBinary::from_hex("1234").unwrap(), @@ -450,46 +458,55 @@ mod tests { for original in cases { let encoded = original.clone().abi_encode(); - let decoded = ItsHubMessage::abi_decode(&encoded).unwrap(); + let decoded = assert_ok!(HubMessage::abi_decode(&encoded)); assert_eq!(original, decoded); } } #[test] - fn invalid_its_hub_message_type() { - let invalid_payload = SendToHub { - messageType: U256::from(MessageType::ReceiveFromHub as u8 + 1), - destination_chain: "remote-chain".into(), - message: vec![].into(), - } - .abi_encode_params(); + fn invalid_hub_message_type() { + let invalid_message_types = vec![ + u8::MIN, + MessageType::InterchainTransfer as u8, + MessageType::DeployInterchainToken as u8, + MessageType::DeployTokenManager as u8, + MessageType::ReceiveFromHub as u8 + 1, + u8::MAX, + ]; - let result = ItsHubMessage::abi_decode(&invalid_payload); - assert!(result.is_err()); - assert_eq!( - result.unwrap_err().current_context().to_string(), - Error::InvalidMessageType.to_string() - ); + for message_type in invalid_message_types { + let invalid_payload = SendToHub { + messageType: U256::from(message_type), + destination_chain: "remote-chain".into(), + message: vec![].into(), + } + .abi_encode_params(); + + let result = HubMessage::abi_decode(&invalid_payload); + assert_err_contains!(result, Error, Error::InvalidMessageType); + } } #[test] - fn invalid_its_message_type() { - let mut message = MessageType::DeployTokenManager.abi_encode(); - message[31] = 3; + fn invalid_message_type() { + let invalid_message_types = vec![ + MessageType::SendToHub as u8, + MessageType::ReceiveFromHub as u8, + MessageType::DeployTokenManager as u8 + 1, + u8::MAX, + ]; - let invalid_payload = SendToHub { - messageType: MessageType::SendToHub.into(), - destination_chain: "remote-chain".into(), - message: message.into(), - } - .abi_encode_params(); + for message_type in invalid_message_types { + let invalid_payload = SendToHub { + messageType: MessageType::SendToHub.into(), + destination_chain: "remote-chain".into(), + message: U256::from(message_type).abi_encode().into(), + } + .abi_encode_params(); - let result = ItsHubMessage::abi_decode(&invalid_payload); - assert!(result.is_err()); - assert_eq!( - result.unwrap_err().current_context().to_string(), - Error::InvalidMessageType.to_string() - ); + let result = HubMessage::abi_decode(&invalid_payload); + assert_err_contains!(result, Error, Error::InvalidMessageType); + } } #[test] @@ -508,12 +525,8 @@ mod tests { } .abi_encode_params(); - let result = ItsHubMessage::abi_decode(&payload); - assert!(result.is_err()); - assert_eq!( - result.unwrap_err().current_context().to_string(), - Error::InvalidChainName.to_string() - ); + let result = HubMessage::abi_decode(&payload); + assert_err_contains!(result, Error, Error::InvalidChainName); } #[test] @@ -532,20 +545,16 @@ mod tests { } .abi_encode_params(); - let result = ItsHubMessage::abi_decode(&payload); - assert!(result.is_err()); - assert_eq!( - result.unwrap_err().current_context().to_string(), - Error::InvalidTokenManagerType.to_string() - ); + let result = HubMessage::abi_decode(&payload); + assert_err_contains!(result, Error, Error::InvalidTokenManagerType); } #[test] fn encode_decode_large_data() { let large_data = vec![0u8; 1024 * 1024]; // 1MB of data - let original = ItsHubMessage::SendToHub { - destination_chain: ChainName::from_str("large-data-chain").unwrap(), - message: ItsMessage::InterchainTransfer { + let original = HubMessage::SendToHub { + destination_chain: ChainNameRaw::from_str("large-data-chain").unwrap(), + message: Message::InterchainTransfer { token_id: [0u8; 32].into(), source_address: HexBinary::from_hex("1234").unwrap(), destination_address: HexBinary::from_hex("5678").unwrap(), @@ -555,15 +564,15 @@ mod tests { }; let encoded = original.clone().abi_encode(); - let decoded = ItsHubMessage::abi_decode(&encoded).unwrap(); + let decoded = assert_ok!(HubMessage::abi_decode(&encoded)); assert_eq!(original, decoded); } #[test] fn encode_decode_unicode_strings() { - let original = ItsHubMessage::SendToHub { - destination_chain: ChainName::from_str("chain").unwrap(), - message: ItsMessage::DeployInterchainToken { + let original = HubMessage::SendToHub { + destination_chain: ChainNameRaw::from_str("chain").unwrap(), + message: Message::DeployInterchainToken { token_id: [0u8; 32].into(), name: "Unicode Token 🪙".into(), symbol: "UNI🔣".into(), @@ -573,7 +582,7 @@ mod tests { }; let encoded = original.clone().abi_encode(); - let decoded = ItsHubMessage::abi_decode(&encoded).unwrap(); + let decoded = assert_ok!(HubMessage::abi_decode(&encoded)); assert_eq!(original, decoded); } } diff --git a/interchain-token-service/src/bin/schema.rs b/interchain-token-service/src/bin/schema.rs new file mode 100644 index 000000000..52b5ea4f5 --- /dev/null +++ b/interchain-token-service/src/bin/schema.rs @@ -0,0 +1,10 @@ +use cosmwasm_schema::write_api; +use interchain_token_service::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + execute: ExecuteMsg, + query: QueryMsg, + } +} diff --git a/interchain-token-service/src/contract.rs b/interchain-token-service/src/contract.rs new file mode 100644 index 000000000..ed3fdc31e --- /dev/null +++ b/interchain-token-service/src/contract.rs @@ -0,0 +1,119 @@ +use std::fmt::Debug; + +use axelar_wasm_std::error::ContractError; +use axelar_wasm_std::{address, permission_control, FnExt, IntoContractError}; +use axelarnet_gateway::AxelarExecutableMsg; +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{Addr, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, Storage}; +use error_stack::{Report, ResultExt}; + +use crate::events::Event; +use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use crate::state; +use crate::state::Config; + +mod execute; +mod query; + +pub use execute::Error as ExecuteError; + +const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[derive(thiserror::Error, Debug, IntoContractError)] +pub enum Error { + #[error("failed to execute a cross-chain message")] + Execute, + #[error("failed to register an its edge contract")] + RegisterItsContract, + #[error("failed to deregsiter an its edge contract")] + DeregisterItsContract, + #[error("failed to query its address")] + QueryItsContract, + #[error("failed to query all its addresses")] + QueryAllItsContracts, +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut, _env: Env, _msg: Empty) -> Result { + // Implement migration logic if needed + + cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + Ok(Response::default()) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _: Env, + _: MessageInfo, + msg: InstantiateMsg, +) -> Result { + cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + let admin = address::validate_cosmwasm_address(deps.api, &msg.admin_address)?; + let governance = address::validate_cosmwasm_address(deps.api, &msg.governance_address)?; + + permission_control::set_admin(deps.storage, &admin)?; + permission_control::set_governance(deps.storage, &governance)?; + + let axelarnet_gateway = + address::validate_cosmwasm_address(deps.api, &msg.axelarnet_gateway_address)?; + + state::save_config(deps.storage, &Config { axelarnet_gateway })?; + + for (chain, address) in msg.its_contracts.iter() { + state::save_its_contract(deps.storage, chain, address)?; + } + + Ok(Response::new().add_events( + msg.its_contracts + .into_iter() + .map(|(chain, address)| Event::ItsContractRegistered { chain, address }.into()), + )) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + _: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg.ensure_permissions(deps.storage, &info.sender, match_gateway)? { + ExecuteMsg::Execute(AxelarExecutableMsg { + cc_id, + source_address, + payload, + }) => execute::execute_message(deps, cc_id, source_address, payload) + .change_context(Error::Execute), + ExecuteMsg::RegisterItsContract { chain, address } => { + execute::register_its_contract(deps, chain, address) + .change_context(Error::RegisterItsContract) + } + ExecuteMsg::DeregisterItsContract { chain } => { + execute::deregister_its_contract(deps, chain) + .change_context(Error::DeregisterItsContract) + } + }? + .then(Ok) +} + +fn match_gateway(storage: &dyn Storage, _: &ExecuteMsg) -> Result> { + Ok(state::load_config(storage).axelarnet_gateway) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _: Env, msg: QueryMsg) -> Result { + match msg { + QueryMsg::ItsContract { chain } => { + query::its_contracts(deps, chain).change_context(Error::QueryItsContract) + } + QueryMsg::AllItsContracts => { + query::all_its_contracts(deps).change_context(Error::QueryAllItsContracts) + } + }? + .then(Ok) +} diff --git a/interchain-token-service/src/contract/execute.rs b/interchain-token-service/src/contract/execute.rs new file mode 100644 index 000000000..f27aecf7e --- /dev/null +++ b/interchain-token-service/src/contract/execute.rs @@ -0,0 +1,128 @@ +use axelar_wasm_std::IntoContractError; +use cosmwasm_std::{DepsMut, HexBinary, QuerierWrapper, Response, Storage}; +use error_stack::{bail, ensure, report, Result, ResultExt}; +use router_api::{Address, ChainName, ChainNameRaw, CrossChainId}; + +use crate::events::Event; +use crate::primitives::HubMessage; +use crate::state::{self, load_config, load_its_contract}; + +#[derive(thiserror::Error, Debug, IntoContractError)] +pub enum Error { + #[error("unknown chain {0}")] + UnknownChain(ChainNameRaw), + #[error("unknown its address {0}")] + UnknownItsContract(Address), + #[error("failed to decode payload")] + InvalidPayload, + #[error("invalid message type")] + InvalidMessageType, + #[error("failed to register its contract for chain {0}")] + FailedItsContractRegistration(ChainNameRaw), + #[error("failed to deregister its contract for chain {0}")] + FailedItsContractDeregistration(ChainNameRaw), +} + +/// Executes an incoming ITS message. +/// +/// This function handles the execution of ITS (Interchain Token Service) messages received from +/// its sources. It verifies the source address, decodes the message, applies balance tracking, +/// and forwards the message to the destination chain. +pub fn execute_message( + deps: DepsMut, + cc_id: CrossChainId, + source_address: Address, + payload: HexBinary, +) -> Result { + ensure_its_source_address(deps.storage, &cc_id.source_chain, &source_address)?; + + match HubMessage::abi_decode(&payload).change_context(Error::InvalidPayload)? { + HubMessage::SendToHub { + destination_chain, + message, + } => { + let destination_address = load_its_contract(deps.storage, &destination_chain) + .change_context_lazy(|| Error::UnknownChain(destination_chain.clone()))?; + + let destination_payload = HubMessage::ReceiveFromHub { + source_chain: cc_id.source_chain.clone(), + message: message.clone(), + } + .abi_encode(); + + Ok(send_to_destination( + deps.storage, + deps.querier, + destination_chain.clone(), + destination_address, + destination_payload, + )? + .add_event( + Event::MessageReceived { + cc_id, + destination_chain, + message, + } + .into(), + )) + } + _ => bail!(Error::InvalidMessageType), + } +} + +fn normalize(chain: &ChainNameRaw) -> ChainName { + ChainName::try_from(chain.as_ref()).expect("invalid chain name") +} + +/// Ensures that the source address of the cross-chain message is the registered ITS contract for the source chain. +fn ensure_its_source_address( + storage: &dyn Storage, + source_chain: &ChainNameRaw, + source_address: &Address, +) -> Result<(), Error> { + let source_its_contract = load_its_contract(storage, source_chain) + .change_context_lazy(|| Error::UnknownChain(source_chain.clone()))?; + + ensure!( + source_address == &source_its_contract, + Error::UnknownItsContract(source_address.clone()) + ); + + Ok(()) +} + +fn send_to_destination( + storage: &dyn Storage, + querier: QuerierWrapper, + destination_chain: ChainNameRaw, + destination_address: Address, + payload: HexBinary, +) -> Result { + let config = load_config(storage); + + let gateway: axelarnet_gateway::Client = + client::Client::new(querier, &config.axelarnet_gateway).into(); + + let call_contract_msg = + gateway.call_contract(normalize(&destination_chain), destination_address, payload); + + Ok(Response::new().add_message(call_contract_msg)) +} + +pub fn register_its_contract( + deps: DepsMut, + chain: ChainNameRaw, + address: Address, +) -> Result { + state::save_its_contract(deps.storage, &chain, &address) + .change_context_lazy(|| Error::FailedItsContractRegistration(chain.clone()))?; + + Ok(Response::new().add_event(Event::ItsContractRegistered { chain, address }.into())) +} + +pub fn deregister_its_contract(deps: DepsMut, chain: ChainNameRaw) -> Result { + state::remove_its_contract(deps.storage, &chain) + .change_context_lazy(|| Error::FailedItsContractDeregistration(chain.clone()))?; + + Ok(Response::new().add_event(Event::ItsContractDeregistered { chain }.into())) +} diff --git a/interchain-token-service/src/contract/query.rs b/interchain-token-service/src/contract/query.rs new file mode 100644 index 000000000..f9aeeea0e --- /dev/null +++ b/interchain-token-service/src/contract/query.rs @@ -0,0 +1,14 @@ +use cosmwasm_std::{to_json_binary, Binary, Deps}; +use router_api::ChainNameRaw; + +use crate::state; + +pub fn its_contracts(deps: Deps, chain: ChainNameRaw) -> Result { + let contract_address = state::may_load_its_contract(deps.storage, &chain)?; + Ok(to_json_binary(&contract_address)?) +} + +pub fn all_its_contracts(deps: Deps) -> Result { + let contract_addresses = state::load_all_its_contracts(deps.storage)?; + Ok(to_json_binary(&contract_addresses)?) +} diff --git a/interchain-token-service/src/error.rs b/interchain-token-service/src/error.rs deleted file mode 100644 index e7f8be5c5..000000000 --- a/interchain-token-service/src/error.rs +++ /dev/null @@ -1,14 +0,0 @@ -use axelar_wasm_std::IntoContractError; -use thiserror::Error; - -#[derive(Error, Debug, PartialEq, IntoContractError)] -pub enum Error { - #[error("failed to decode ITS message")] - InvalidMessage, - #[error("invalid message type")] - InvalidMessageType, - #[error("invalid chain name")] - InvalidChainName, - #[error("invalid token manager type")] - InvalidTokenManagerType, -} diff --git a/interchain-token-service/src/events.rs b/interchain-token-service/src/events.rs new file mode 100644 index 000000000..4edd29472 --- /dev/null +++ b/interchain-token-service/src/events.rs @@ -0,0 +1,100 @@ +use cosmwasm_std::Attribute; +use router_api::{Address, ChainNameRaw, CrossChainId}; + +use crate::primitives::Message; + +pub enum Event { + MessageReceived { + cc_id: CrossChainId, + destination_chain: ChainNameRaw, + message: Message, + }, + ItsContractRegistered { + chain: ChainNameRaw, + address: Address, + }, + ItsContractDeregistered { + chain: ChainNameRaw, + }, +} + +impl From for cosmwasm_std::Event { + fn from(event: Event) -> Self { + match event { + Event::MessageReceived { + cc_id, + destination_chain, + message, + } => make_message_event("message_received", cc_id, destination_chain, message), + Event::ItsContractRegistered { chain, address } => { + cosmwasm_std::Event::new("its_contract_registered") + .add_attribute("chain", chain.to_string()) + .add_attribute("address", address.to_string()) + } + Event::ItsContractDeregistered { chain } => { + cosmwasm_std::Event::new("its_contract_deregistered") + .add_attribute("chain", chain.to_string()) + } + } + } +} + +fn make_message_event( + event_name: &str, + cc_id: CrossChainId, + destination_chain: ChainNameRaw, + msg: Message, +) -> cosmwasm_std::Event { + let message_type: &'static str = (&msg).into(); + let mut attrs = vec![ + Attribute::new("cc_id", cc_id.to_string()), + Attribute::new("destination_chain", destination_chain.to_string()), + Attribute::new("message_type", message_type.to_string()), + ]; + + match msg { + Message::InterchainTransfer { + token_id, + source_address, + destination_address, + amount, + data, + } => { + attrs.extend(vec![ + Attribute::new("token_id", token_id.to_string()), + Attribute::new("source_address", source_address.to_string()), + Attribute::new("destination_address", destination_address.to_string()), + Attribute::new("amount", amount.to_string()), + Attribute::new("data", data.to_string()), + ]); + } + Message::DeployInterchainToken { + token_id, + name, + symbol, + decimals, + minter, + } => { + attrs.extend(vec![ + Attribute::new("token_id", token_id.to_string()), + Attribute::new("name", name), + Attribute::new("symbol", symbol), + Attribute::new("decimals", decimals.to_string()), + Attribute::new("minter", minter.to_string()), + ]); + } + Message::DeployTokenManager { + token_id, + token_manager_type, + params, + } => { + attrs.extend(vec![ + Attribute::new("token_id", token_id.to_string()), + Attribute::new("token_manager_type", format!("{:?}", token_manager_type)), + Attribute::new("params", params.to_string()), + ]); + } + } + + cosmwasm_std::Event::new(event_name).add_attributes(attrs) +} diff --git a/interchain-token-service/src/lib.rs b/interchain-token-service/src/lib.rs index d4f5ac941..45a424182 100644 --- a/interchain-token-service/src/lib.rs +++ b/interchain-token-service/src/lib.rs @@ -1,5 +1,8 @@ mod primitives; - -pub mod error; pub use primitives::*; -pub mod abi; + +mod abi; +pub mod contract; +pub mod events; +pub mod msg; +mod state; diff --git a/interchain-token-service/src/msg.rs b/interchain-token-service/src/msg.rs new file mode 100644 index 000000000..c9ff32b55 --- /dev/null +++ b/interchain-token-service/src/msg.rs @@ -0,0 +1,47 @@ +use std::collections::HashMap; + +use axelarnet_gateway::AxelarExecutableMsg; +use cosmwasm_schema::{cw_serde, QueryResponses}; +use msgs_derive::EnsurePermissions; +use router_api::{Address, ChainNameRaw}; + +#[cw_serde] +pub struct InstantiateMsg { + pub governance_address: String, + pub admin_address: String, + /// The address of the axelarnet-gateway contract on Amplifier + pub axelarnet_gateway_address: String, + /// Addresses of the ITS edge contracts on connected chains + pub its_contracts: HashMap, +} + +#[cw_serde] +#[derive(EnsurePermissions)] +pub enum ExecuteMsg { + /// Execute a cross-chain message received by the axelarnet-gateway from another chain + #[permission(Specific(gateway))] + Execute(AxelarExecutableMsg), + /// Register the ITS contract address of another chain. Each chain's ITS contract has to be whitelisted before + /// ITS Hub can send cross-chain messages to it, or receive messages from it. + /// If an ITS contract is already set for the chain, an error is returned. + #[permission(Governance)] + RegisterItsContract { + chain: ChainNameRaw, + address: Address, + }, + /// Deregister the ITS contract address for the given chain. + /// The admin is allowed to remove the ITS address of a chain for emergencies. + #[permission(Elevated)] + DeregisterItsContract { chain: ChainNameRaw }, +} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + /// Query the ITS contract address registered for a chain + #[returns(Option
)] + ItsContract { chain: ChainNameRaw }, + /// Query all registererd ITS contract addresses + #[returns(HashMap)] + AllItsContracts, +} diff --git a/interchain-token-service/src/primitives.rs b/interchain-token-service/src/primitives.rs index 4970d294e..f86aa3e96 100644 --- a/interchain-token-service/src/primitives.rs +++ b/interchain-token-service/src/primitives.rs @@ -1,8 +1,11 @@ +use std::fmt::Display; + use cosmwasm_schema::cw_serde; use cosmwasm_std::{HexBinary, Uint256}; -use router_api::ChainName; +use router_api::ChainNameRaw; use strum::FromRepr; +/// A unique 32-byte identifier for linked cross-chain tokens across ITS contracts. #[cw_serde] #[derive(Eq)] pub struct TokenId( @@ -11,6 +14,13 @@ pub struct TokenId( [u8; 32], ); +impl Display for TokenId { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", hex::encode(self.0)) + } +} + +/// The supported types of token managers that can be deployed by ITS contracts. #[cw_serde] #[derive(Eq, Copy, FromRepr)] #[repr(u8)] @@ -23,50 +33,77 @@ pub enum TokenManagerType { Gateway, } -/// ITS message type that can be sent between ITS contracts for transfers/token deployments -/// `ItsMessage` that are routed via the ITS hub get wrapped inside `ItsHubMessage` +/// A message sent between ITS contracts to facilitate interchain transfers, token deployments, or token manager deployments. +/// `Message` routed via the ITS hub get wrapped inside a [`HubMessage`] #[cw_serde] -#[derive(Eq)] -pub enum ItsMessage { +#[derive(Eq, strum::IntoStaticStr)] +pub enum Message { + /// Transfer ITS tokens between different chains InterchainTransfer { + /// The unique identifier of the token being transferred token_id: TokenId, + /// The address that called the ITS contract on the source chain source_address: HexBinary, + /// The address that the token will be sent to on the destination chain + /// If data is not empty, this address will given the token and executed as a contract on the destination chain destination_address: HexBinary, + /// The amount of tokens to transfer amount: Uint256, + /// An optional payload to be provided to the destination address, if `data` is not empty data: HexBinary, }, + /// Deploy a new interchain token on the destination chain DeployInterchainToken { + /// The unique identifier of the token to be deployed token_id: TokenId, + /// The name of the token name: String, + /// The symbol of the token symbol: String, + /// The number of decimal places the token supports decimals: u8, + /// The address that will be the initial minter of the token (in addition to the ITS contract) minter: HexBinary, }, + /// Deploy a new token manager on the destination chain DeployTokenManager { + /// The unique identifier of the token that the token manager will manage token_id: TokenId, + /// The type of token manager to deploy token_manager_type: TokenManagerType, + /// The parameters to be provided to the token manager contract params: HexBinary, }, } -/// ITS message type that can be sent between ITS edge contracts and the ITS Hub +/// A message sent between ITS edge contracts and the ITS hub contract (defined in this crate). +/// `HubMessage` is used to route an ITS [`Message`] between ITS edge contracts on different chains via the ITS Hub. #[cw_serde] #[derive(Eq)] -pub enum ItsHubMessage { +pub enum HubMessage { /// ITS edge source contract -> ITS Hub SendToHub { /// True destination chain of the ITS message - destination_chain: ChainName, - message: ItsMessage, + destination_chain: ChainNameRaw, + message: Message, }, /// ITS Hub -> ITS edge destination contract ReceiveFromHub { /// True source chain of the ITS message - source_chain: ChainName, - message: ItsMessage, + source_chain: ChainNameRaw, + message: Message, }, } +impl HubMessage { + pub fn message(&self) -> &Message { + match self { + HubMessage::SendToHub { message, .. } => message, + HubMessage::ReceiveFromHub { message, .. } => message, + } + } +} + impl TokenId { #[inline(always)] pub fn new(id: [u8; 32]) -> Self { diff --git a/interchain-token-service/src/state.rs b/interchain-token-service/src/state.rs new file mode 100644 index 000000000..dcdfb3e31 --- /dev/null +++ b/interchain-token-service/src/state.rs @@ -0,0 +1,147 @@ +use std::collections::HashMap; + +use axelar_wasm_std::IntoContractError; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{ensure, Addr, StdError, Storage}; +use cw_storage_plus::{Item, Map}; +use router_api::{Address, ChainNameRaw}; + +#[derive(thiserror::Error, Debug, IntoContractError)] +pub enum Error { + #[error(transparent)] + Std(#[from] StdError), + #[error("ITS contract got into an invalid state, its config is missing")] + MissingConfig, + #[error("its address for chain {0} not found")] + ItsContractNotFound(ChainNameRaw), + #[error("its address for chain {0} already registered")] + ItsContractAlreadyRegistered(ChainNameRaw), +} + +#[cw_serde] +pub struct Config { + pub axelarnet_gateway: Addr, +} + +const CONFIG: Item = Item::new("config"); +const ITS_CONTRACTS: Map<&ChainNameRaw, Address> = Map::new("its_contracts"); + +pub fn load_config(storage: &dyn Storage) -> Config { + CONFIG + .load(storage) + .expect("config must be set during instantiation") +} + +pub fn save_config(storage: &mut dyn Storage, config: &Config) -> Result<(), Error> { + Ok(CONFIG.save(storage, config)?) +} + +pub fn may_load_its_contract( + storage: &dyn Storage, + chain: &ChainNameRaw, +) -> Result, Error> { + Ok(ITS_CONTRACTS.may_load(storage, chain)?) +} + +pub fn load_its_contract(storage: &dyn Storage, chain: &ChainNameRaw) -> Result { + may_load_its_contract(storage, chain)?.ok_or_else(|| Error::ItsContractNotFound(chain.clone())) +} + +pub fn save_its_contract( + storage: &mut dyn Storage, + chain: &ChainNameRaw, + address: &Address, +) -> Result<(), Error> { + ensure!( + may_load_its_contract(storage, chain)?.is_none(), + Error::ItsContractAlreadyRegistered(chain.clone()) + ); + + Ok(ITS_CONTRACTS.save(storage, chain, address)?) +} + +pub fn remove_its_contract(storage: &mut dyn Storage, chain: &ChainNameRaw) -> Result<(), Error> { + ensure!( + may_load_its_contract(storage, chain)?.is_some(), + Error::ItsContractNotFound(chain.clone()) + ); + + ITS_CONTRACTS.remove(storage, chain); + + Ok(()) +} + +pub fn load_all_its_contracts( + storage: &dyn Storage, +) -> Result, Error> { + Ok(ITS_CONTRACTS + .range(storage, None, None, cosmwasm_std::Order::Ascending) + .collect::, _>>()?) +} + +#[cfg(test)] +mod tests { + use assert_ok::assert_ok; + use axelar_wasm_std::assert_err_contains; + use cosmwasm_std::testing::mock_dependencies; + + use super::*; + + #[test] + fn save_and_load_config_succeeds() { + let mut deps = mock_dependencies(); + + let config = Config { + axelarnet_gateway: Addr::unchecked("gateway-address"), + }; + + assert_ok!(save_config(deps.as_mut().storage, &config)); + assert_eq!(load_config(deps.as_ref().storage), config); + } + + #[test] + #[should_panic(expected = "config must be set during instantiation")] + fn load_missing_config_fails() { + let deps = mock_dependencies(); + load_config(deps.as_ref().storage); + } + + #[test] + fn save_and_load_its_contract_succeeds() { + let mut deps = mock_dependencies(); + + let chain1 = "chain1".parse().unwrap(); + let chain2: ChainNameRaw = "chain2".parse().unwrap(); + let address1: Address = "address1".parse().unwrap(); + let address2: Address = "address2".parse().unwrap(); + + assert_err_contains!( + load_its_contract(deps.as_ref().storage, &chain1), + Error, + Error::ItsContractNotFound(its_chain) if its_chain == &chain1 + ); + assert_eq!( + assert_ok!(load_all_its_contracts(deps.as_ref().storage)), + HashMap::new() + ); + + assert_ok!(save_its_contract(deps.as_mut().storage, &chain1, &address1)); + assert_ok!(save_its_contract(deps.as_mut().storage, &chain2, &address2)); + assert_eq!( + assert_ok!(load_its_contract(deps.as_ref().storage, &chain1)), + address1 + ); + assert_eq!( + assert_ok!(load_its_contract(deps.as_ref().storage, &chain2)), + address2 + ); + + let all_addresses = assert_ok!(load_all_its_contracts(deps.as_ref().storage)); + assert_eq!( + all_addresses, + [(chain1, address1), (chain2, address2)] + .into_iter() + .collect::>() + ); + } +} diff --git a/interchain-token-service/tests/execute.rs b/interchain-token-service/tests/execute.rs new file mode 100644 index 000000000..291f9e0a7 --- /dev/null +++ b/interchain-token-service/tests/execute.rs @@ -0,0 +1,316 @@ +use assert_ok::assert_ok; +use axelar_wasm_std::response::inspect_response_msg; +use axelar_wasm_std::{assert_err_contains, permission_control}; +use axelarnet_gateway::msg::ExecuteMsg as AxelarnetGatewayExecuteMsg; +use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; +use cosmwasm_std::HexBinary; +use interchain_token_service::contract::{self, ExecuteError}; +use interchain_token_service::events::Event; +use interchain_token_service::msg::ExecuteMsg; +use interchain_token_service::{HubMessage, Message, TokenId, TokenManagerType}; +use router_api::{Address, ChainName, ChainNameRaw, CrossChainId}; +use utils::TestMessage; + +mod utils; + +#[test] +fn register_deregister_its_contract_succeeds() { + let mut deps = mock_dependencies(); + utils::instantiate_contract(deps.as_mut()).unwrap(); + + let chain: ChainNameRaw = "ethereum".parse().unwrap(); + let address: Address = "0x1234567890123456789012345678901234567890" + .parse() + .unwrap(); + + let register_response = assert_ok!(utils::register_its_contract( + deps.as_mut(), + chain.clone(), + address.clone() + )); + let res = assert_ok!(utils::query_its_contract(deps.as_ref(), chain.clone())); + assert_eq!(res, Some(address)); + + let deregister_response = + assert_ok!(utils::deregister_its_contract(deps.as_mut(), chain.clone())); + let res = assert_ok!(utils::query_its_contract(deps.as_ref(), chain.clone())); + assert_eq!(res, None); + + goldie::assert_json!([register_response, deregister_response]); +} + +#[test] +fn reregistering_its_contract_fails() { + let mut deps = mock_dependencies(); + utils::instantiate_contract(deps.as_mut()).unwrap(); + + let chain: ChainNameRaw = "ethereum".parse().unwrap(); + let address: Address = "0x1234567890123456789012345678901234567890" + .parse() + .unwrap(); + + assert_ok!(utils::register_its_contract( + deps.as_mut(), + chain.clone(), + address.clone() + )); + + assert_err_contains!( + utils::register_its_contract(deps.as_mut(), chain, address), + ExecuteError, + ExecuteError::FailedItsContractRegistration(..) + ); +} + +#[test] +fn deregistering_unknown_chain_fails() { + let mut deps = mock_dependencies(); + utils::instantiate_contract(deps.as_mut()).unwrap(); + + let chain: ChainNameRaw = "ethereum".parse().unwrap(); + + assert_err_contains!( + utils::deregister_its_contract(deps.as_mut(), chain), + ExecuteError, + ExecuteError::FailedItsContractDeregistration(..) + ); +} + +#[test] +fn execute_hub_message_succeeds() { + let mut deps = mock_dependencies(); + utils::instantiate_contract(deps.as_mut()).unwrap(); + + let TestMessage { + router_message, + source_its_chain, + source_its_contract, + destination_its_chain, + destination_its_contract, + .. + } = TestMessage::dummy(); + + utils::register_its_contract( + deps.as_mut(), + source_its_chain.clone(), + source_its_contract.clone(), + ) + .unwrap(); + utils::register_its_contract( + deps.as_mut(), + destination_its_chain.clone(), + destination_its_contract.clone(), + ) + .unwrap(); + + let token_id = TokenId::new([1; 32]); + let test_messages = vec![ + Message::InterchainTransfer { + token_id: token_id.clone(), + source_address: HexBinary::from([1; 32]), + destination_address: HexBinary::from([2; 32]), + amount: 1u64.into(), + data: HexBinary::from([1, 2, 3, 4]), + }, + Message::DeployInterchainToken { + token_id: token_id.clone(), + name: "Test".into(), + symbol: "TST".into(), + decimals: 18, + minter: HexBinary::from([1; 32]), + }, + Message::DeployTokenManager { + token_id: token_id.clone(), + token_manager_type: TokenManagerType::MintBurn, + params: HexBinary::from([1, 2, 3, 4]), + }, + ]; + + let responses: Vec<_> = test_messages + .into_iter() + .map(|message| { + let hub_message = HubMessage::SendToHub { + destination_chain: destination_its_chain.clone(), + message, + }; + let payload = hub_message.clone().abi_encode(); + let receive_payload = HubMessage::ReceiveFromHub { + source_chain: source_its_chain.clone(), + message: hub_message.message().clone(), + } + .abi_encode(); + + let response = assert_ok!(utils::execute( + deps.as_mut(), + router_message.cc_id.clone(), + source_its_contract.clone(), + payload, + )); + let msg: AxelarnetGatewayExecuteMsg = + assert_ok!(inspect_response_msg(response.clone())); + let expected_msg = AxelarnetGatewayExecuteMsg::CallContract { + destination_chain: ChainName::try_from(destination_its_chain.to_string()).unwrap(), + destination_address: destination_its_contract.clone(), + payload: receive_payload, + }; + assert_eq!(msg, expected_msg); + + let expected_event = Event::MessageReceived { + cc_id: router_message.cc_id.clone(), + destination_chain: destination_its_chain.clone(), + message: hub_message.message().clone(), + }; + assert_eq!( + response.events, + vec![cosmwasm_std::Event::from(expected_event)] + ); + + response + }) + .collect(); + + goldie::assert_json!(responses); +} + +#[test] +fn execute_its_when_not_gateway_sender_fails() { + let mut deps = mock_dependencies(); + utils::instantiate_contract(deps.as_mut()).unwrap(); + + let result = contract::execute( + deps.as_mut(), + mock_env(), + mock_info("not-gateway", &[]), + ExecuteMsg::Execute(axelarnet_gateway::AxelarExecutableMsg { + cc_id: CrossChainId::new("source", "hash").unwrap(), + source_address: "source".parse().unwrap(), + payload: HexBinary::from([]), + }), + ); + assert_err_contains!( + result, + permission_control::Error, + permission_control::Error::AddressNotWhitelisted { .. } + ); +} + +#[test] +fn execute_message_when_unknown_source_address_fails() { + let mut deps = mock_dependencies(); + utils::instantiate_contract(deps.as_mut()).unwrap(); + + let TestMessage { + hub_message, + router_message, + source_its_chain, + source_its_contract, + .. + } = TestMessage::dummy(); + + utils::register_its_contract(deps.as_mut(), source_its_chain, source_its_contract).unwrap(); + + let unknown_address: Address = "unknown-address".parse().unwrap(); + let result = utils::execute( + deps.as_mut(), + router_message.cc_id.clone(), + unknown_address, + hub_message.abi_encode(), + ); + assert_err_contains!( + result, + ExecuteError, + ExecuteError::UnknownItsContract { .. } + ); +} + +#[test] +fn execute_message_when_invalid_payload_fails() { + let mut deps = mock_dependencies(); + utils::instantiate_contract(deps.as_mut()).unwrap(); + + let TestMessage { + router_message, + source_its_chain, + source_its_contract, + .. + } = TestMessage::dummy(); + + utils::register_its_contract(deps.as_mut(), source_its_chain, source_its_contract.clone()) + .unwrap(); + + let invalid_payload = HexBinary::from_hex("1234").unwrap(); + let result = utils::execute( + deps.as_mut(), + router_message.cc_id.clone(), + source_its_contract, + invalid_payload, + ); + assert_err_contains!(result, ExecuteError, ExecuteError::InvalidPayload); +} + +#[test] +fn execute_message_when_unknown_chain_fails() { + let mut deps = mock_dependencies(); + utils::instantiate_contract(deps.as_mut()).unwrap(); + + let TestMessage { + hub_message, + router_message, + source_its_chain, + source_its_contract, + destination_its_chain, + .. + } = TestMessage::dummy(); + + let result = utils::execute( + deps.as_mut(), + router_message.cc_id.clone(), + source_its_contract.clone(), + hub_message.clone().abi_encode(), + ); + assert_err_contains!(result, ExecuteError, ExecuteError::UnknownChain(chain) if chain == &source_its_chain); + + utils::register_its_contract(deps.as_mut(), source_its_chain, source_its_contract.clone()) + .unwrap(); + + let result = utils::execute( + deps.as_mut(), + router_message.cc_id, + source_its_contract, + hub_message.abi_encode(), + ); + assert_err_contains!(result, ExecuteError, ExecuteError::UnknownChain(chain) if chain == &destination_its_chain); +} + +#[test] +fn execute_message_when_invalid_message_type_fails() { + let mut deps = mock_dependencies(); + utils::instantiate_contract(deps.as_mut()).unwrap(); + + let TestMessage { + hub_message, + router_message, + source_its_chain, + source_its_contract, + .. + } = TestMessage::dummy(); + + utils::register_its_contract( + deps.as_mut(), + source_its_chain.clone(), + source_its_contract.clone(), + ) + .unwrap(); + + let invalid_hub_message = HubMessage::ReceiveFromHub { + source_chain: source_its_chain, + message: hub_message.message().clone(), + }; + let result = utils::execute( + deps.as_mut(), + router_message.cc_id, + source_its_contract, + invalid_hub_message.abi_encode(), + ); + assert_err_contains!(result, ExecuteError, ExecuteError::InvalidMessageType); +} diff --git a/interchain-token-service/tests/instantiate.rs b/interchain-token-service/tests/instantiate.rs new file mode 100644 index 000000000..de04b96aa --- /dev/null +++ b/interchain-token-service/tests/instantiate.rs @@ -0,0 +1,79 @@ +use std::collections::HashMap; + +use assert_ok::assert_ok; +use axelar_wasm_std::permission_control::Permission; +use axelar_wasm_std::{assert_err_contains, permission_control}; +use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; +use cosmwasm_std::Addr; +use interchain_token_service::contract; +use interchain_token_service::msg::InstantiateMsg; +use utils::params; + +mod utils; + +#[test] +fn instantiate_succeeds() { + let mut deps = mock_dependencies(); + let response = assert_ok!(utils::instantiate_contract(deps.as_mut())); + goldie::assert_json!(response); +} + +#[test] +fn instantiate_with_args_succeeds() { + let mut deps = mock_dependencies(); + + let its_contracts = vec![ + ("ethereum".parse().unwrap(), "eth-address".parse().unwrap()), + ("optimism".parse().unwrap(), "op-address".parse().unwrap()), + ] + .into_iter() + .collect::>(); + + let response = assert_ok!(contract::instantiate( + deps.as_mut(), + mock_env(), + mock_info("sender", &[]), + InstantiateMsg { + governance_address: params::GOVERNANCE.to_string(), + admin_address: params::ADMIN.to_string(), + axelarnet_gateway_address: params::GATEWAY.to_string(), + its_contracts: its_contracts.clone(), + }, + )); + assert_eq!(0, response.messages.len()); + goldie::assert_json!(response); + + assert_eq!( + assert_ok!(permission_control::sender_role( + deps.as_ref().storage, + &Addr::unchecked(params::ADMIN) + )), + Permission::Admin.into() + ); + assert_eq!( + assert_ok!(permission_control::sender_role( + deps.as_ref().storage, + &Addr::unchecked(params::GOVERNANCE) + )), + Permission::Governance.into() + ); + + let stored_its_contracts = assert_ok!(utils::query_all_its_contracts(deps.as_ref())); + assert_eq!(stored_its_contracts, its_contracts); +} + +#[test] +fn invalid_gateway_address() { + let mut deps = mock_dependencies(); + let msg = InstantiateMsg { + governance_address: utils::params::GOVERNANCE.to_string(), + admin_address: utils::params::ADMIN.to_string(), + axelarnet_gateway_address: "".to_string(), + its_contracts: Default::default(), + }; + assert_err_contains!( + contract::instantiate(deps.as_mut(), mock_env(), mock_info("sender", &[]), msg), + axelar_wasm_std::address::Error, + axelar_wasm_std::address::Error::InvalidAddress(..) + ); +} diff --git a/interchain-token-service/tests/query.rs b/interchain-token-service/tests/query.rs new file mode 100644 index 000000000..6351f7309 --- /dev/null +++ b/interchain-token-service/tests/query.rs @@ -0,0 +1,69 @@ +use std::collections::HashMap; + +use assert_ok::assert_ok; +use cosmwasm_std::testing::mock_dependencies; +use router_api::{Address, ChainNameRaw}; + +mod utils; + +#[test] +fn query_its_contract() { + let mut deps = mock_dependencies(); + utils::instantiate_contract(deps.as_mut()).unwrap(); + + let chain: ChainNameRaw = "Ethereum".parse().unwrap(); + let address: Address = "0x1234567890123456789012345678901234567890" + .parse() + .unwrap(); + + utils::register_its_contract(deps.as_mut(), chain.clone(), address.clone()).unwrap(); + + let queried_address = assert_ok!(utils::query_its_contract(deps.as_ref(), chain.clone())); + assert_eq!(queried_address, Some(address)); + + // case sensitive query + let queried_address = assert_ok!(utils::query_its_contract( + deps.as_ref(), + "ethereum".parse().unwrap() + )); + assert_eq!(queried_address, None); + + assert_ok!(utils::deregister_its_contract(deps.as_mut(), chain.clone())); + + let queried_address = assert_ok!(utils::query_its_contract(deps.as_ref(), chain.clone())); + assert_eq!(queried_address, None); + + let non_existent_chain: ChainNameRaw = "non-existent-chain".parse().unwrap(); + let queried_address = assert_ok!(utils::query_its_contract(deps.as_ref(), non_existent_chain)); + assert_eq!(queried_address, None); +} + +#[test] +fn query_all_its_contractes() { + let mut deps = mock_dependencies(); + utils::instantiate_contract(deps.as_mut()).unwrap(); + + let its_contractes = vec![ + ( + "ethereum".parse::().unwrap(), + "0x1234567890123456789012345678901234567890" + .parse::
() + .unwrap(), + ), + ( + "Optimism".parse().unwrap(), + "0x0987654321098765432109876543210987654321" + .parse() + .unwrap(), + ), + ] + .into_iter() + .collect::>(); + + for (chain, address) in its_contractes.iter() { + utils::register_its_contract(deps.as_mut(), chain.clone(), address.clone()).unwrap(); + } + + let queried_addresses = assert_ok!(utils::query_all_its_contracts(deps.as_ref())); + assert_eq!(queried_addresses, its_contractes); +} diff --git a/interchain-token-service/tests/testdata/execute_hub_message_succeeds.golden b/interchain-token-service/tests/testdata/execute_hub_message_succeeds.golden new file mode 100644 index 000000000..0985ec368 --- /dev/null +++ b/interchain-token-service/tests/testdata/execute_hub_message_succeeds.golden @@ -0,0 +1,171 @@ +[ + { + "messages": [ + { + "id": 0, + "msg": { + "wasm": { + "execute": { + "contract_addr": "gateway", + "msg": "eyJjYWxsX2NvbnRyYWN0Ijp7ImRlc3RpbmF0aW9uX2NoYWluIjoiZGVzdC1pdHMtY2hhaW4iLCJkZXN0aW5hdGlvbl9hZGRyZXNzIjoiZGVzdC1pdHMtY29udHJhY3QiLCJwYXlsb2FkIjoiMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwNDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwNjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMGEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAxMDczNmY3NTcyNjM2NTJkNjk3NDczMmQ2MzY4NjE2OTZlMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMTgwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMGMwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMTQwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAyMDAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDIwMDIwMjAyMDIwMjAyMDIwMjAyMDIwMjAyMDIwMjAyMDIwMjAyMDIwMjAyMDIwMjAyMDIwMjAyMDIwMjAyMDIwMjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDQwMTAyMDMwNDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwIn19", + "funds": [] + } + } + }, + "gas_limit": null, + "reply_on": "never" + } + ], + "attributes": [], + "events": [ + { + "type": "message_received", + "attributes": [ + { + "key": "cc_id", + "value": "source-its-chain_message-id" + }, + { + "key": "destination_chain", + "value": "dest-its-chain" + }, + { + "key": "message_type", + "value": "InterchainTransfer" + }, + { + "key": "token_id", + "value": "0101010101010101010101010101010101010101010101010101010101010101" + }, + { + "key": "source_address", + "value": "0101010101010101010101010101010101010101010101010101010101010101" + }, + { + "key": "destination_address", + "value": "0202020202020202020202020202020202020202020202020202020202020202" + }, + { + "key": "amount", + "value": "1" + }, + { + "key": "data", + "value": "01020304" + } + ] + } + ], + "data": null + }, + { + "messages": [ + { + "id": 0, + "msg": { + "wasm": { + "execute": { + "contract_addr": "gateway", + "msg": "eyJjYWxsX2NvbnRyYWN0Ijp7ImRlc3RpbmF0aW9uX2NoYWluIjoiZGVzdC1pdHMtY2hhaW4iLCJkZXN0aW5hdGlvbl9hZGRyZXNzIjoiZGVzdC1pdHMtY29udHJhY3QiLCJwYXlsb2FkIjoiMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwNDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwNjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMGEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAxMDczNmY3NTcyNjM2NTJkNjk3NDczMmQ2MzY4NjE2OTZlMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMTgwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMGMwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMTIwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMTQwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwNDU0NjU3Mzc0MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAzNTQ1MzU0MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMjAwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxIn19", + "funds": [] + } + } + }, + "gas_limit": null, + "reply_on": "never" + } + ], + "attributes": [], + "events": [ + { + "type": "message_received", + "attributes": [ + { + "key": "cc_id", + "value": "source-its-chain_message-id" + }, + { + "key": "destination_chain", + "value": "dest-its-chain" + }, + { + "key": "message_type", + "value": "DeployInterchainToken" + }, + { + "key": "token_id", + "value": "0101010101010101010101010101010101010101010101010101010101010101" + }, + { + "key": "name", + "value": "Test" + }, + { + "key": "symbol", + "value": "TST" + }, + { + "key": "decimals", + "value": "18" + }, + { + "key": "minter", + "value": "0101010101010101010101010101010101010101010101010101010101010101" + } + ] + } + ], + "data": null + }, + { + "messages": [ + { + "id": 0, + "msg": { + "wasm": { + "execute": { + "contract_addr": "gateway", + "msg": "eyJjYWxsX2NvbnRyYWN0Ijp7ImRlc3RpbmF0aW9uX2NoYWluIjoiZGVzdC1pdHMtY2hhaW4iLCJkZXN0aW5hdGlvbl9hZGRyZXNzIjoiZGVzdC1pdHMtY29udHJhY3QiLCJwYXlsb2FkIjoiMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwNDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwNjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMGEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAxMDczNmY3NTcyNjM2NTJkNjk3NDczMmQ2MzY4NjE2OTZlMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMGMwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMjAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA0MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA4MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDQwMTAyMDMwNDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwIn19", + "funds": [] + } + } + }, + "gas_limit": null, + "reply_on": "never" + } + ], + "attributes": [], + "events": [ + { + "type": "message_received", + "attributes": [ + { + "key": "cc_id", + "value": "source-its-chain_message-id" + }, + { + "key": "destination_chain", + "value": "dest-its-chain" + }, + { + "key": "message_type", + "value": "DeployTokenManager" + }, + { + "key": "token_id", + "value": "0101010101010101010101010101010101010101010101010101010101010101" + }, + { + "key": "token_manager_type", + "value": "MintBurn" + }, + { + "key": "params", + "value": "01020304" + } + ] + } + ], + "data": null + } +] \ No newline at end of file diff --git a/interchain-token-service/tests/testdata/instantiate_succeeds.golden b/interchain-token-service/tests/testdata/instantiate_succeeds.golden new file mode 100644 index 000000000..c9a213425 --- /dev/null +++ b/interchain-token-service/tests/testdata/instantiate_succeeds.golden @@ -0,0 +1,6 @@ +{ + "messages": [], + "attributes": [], + "events": [], + "data": null +} \ No newline at end of file diff --git a/interchain-token-service/tests/testdata/instantiate_with_args_succeeds.golden b/interchain-token-service/tests/testdata/instantiate_with_args_succeeds.golden new file mode 100644 index 000000000..bd7a2a6f1 --- /dev/null +++ b/interchain-token-service/tests/testdata/instantiate_with_args_succeeds.golden @@ -0,0 +1,33 @@ +{ + "messages": [], + "attributes": [], + "events": [ + { + "type": "its_contract_registered", + "attributes": [ + { + "key": "chain", + "value": "optimism" + }, + { + "key": "address", + "value": "op-address" + } + ] + }, + { + "type": "its_contract_registered", + "attributes": [ + { + "key": "chain", + "value": "ethereum" + }, + { + "key": "address", + "value": "eth-address" + } + ] + } + ], + "data": null +} \ No newline at end of file diff --git a/interchain-token-service/tests/testdata/register_deregister_its_contract_succeeds.golden b/interchain-token-service/tests/testdata/register_deregister_its_contract_succeeds.golden new file mode 100644 index 000000000..0b81874d1 --- /dev/null +++ b/interchain-token-service/tests/testdata/register_deregister_its_contract_succeeds.golden @@ -0,0 +1,38 @@ +[ + { + "messages": [], + "attributes": [], + "events": [ + { + "type": "its_contract_registered", + "attributes": [ + { + "key": "chain", + "value": "ethereum" + }, + { + "key": "address", + "value": "0x1234567890123456789012345678901234567890" + } + ] + } + ], + "data": null + }, + { + "messages": [], + "attributes": [], + "events": [ + { + "type": "its_contract_deregistered", + "attributes": [ + { + "key": "chain", + "value": "ethereum" + } + ] + } + ], + "data": null + } +] \ No newline at end of file diff --git a/interchain-token-service/tests/utils/execute.rs b/interchain-token-service/tests/utils/execute.rs new file mode 100644 index 000000000..c43538039 --- /dev/null +++ b/interchain-token-service/tests/utils/execute.rs @@ -0,0 +1,51 @@ +use axelar_wasm_std::error::ContractError; +use cosmwasm_std::testing::{mock_env, mock_info}; +use cosmwasm_std::{DepsMut, HexBinary, Response}; +use interchain_token_service::contract; +use interchain_token_service::msg::ExecuteMsg; +use router_api::{Address, ChainNameRaw, CrossChainId}; + +use crate::utils::params; + +pub fn execute( + deps: DepsMut, + cc_id: CrossChainId, + source_address: Address, + payload: HexBinary, +) -> Result { + contract::execute( + deps, + mock_env(), + mock_info(params::GATEWAY, &[]), + ExecuteMsg::Execute(axelarnet_gateway::AxelarExecutableMsg { + cc_id, + source_address, + payload, + }), + ) +} + +pub fn register_its_contract( + deps: DepsMut, + chain: ChainNameRaw, + address: Address, +) -> Result { + contract::execute( + deps, + mock_env(), + mock_info(params::GOVERNANCE, &[]), + ExecuteMsg::RegisterItsContract { chain, address }, + ) +} + +pub fn deregister_its_contract( + deps: DepsMut, + chain: ChainNameRaw, +) -> Result { + contract::execute( + deps, + mock_env(), + mock_info(params::ADMIN, &[]), + ExecuteMsg::DeregisterItsContract { chain }, + ) +} diff --git a/interchain-token-service/tests/utils/instantiate.rs b/interchain-token-service/tests/utils/instantiate.rs new file mode 100644 index 000000000..e409dc675 --- /dev/null +++ b/interchain-token-service/tests/utils/instantiate.rs @@ -0,0 +1,21 @@ +use axelar_wasm_std::error::ContractError; +use cosmwasm_std::testing::{mock_env, mock_info}; +use cosmwasm_std::{DepsMut, Response}; +use interchain_token_service::contract; +use interchain_token_service::msg::InstantiateMsg; + +use crate::utils::params; + +pub fn instantiate_contract(deps: DepsMut) -> Result { + contract::instantiate( + deps, + mock_env(), + mock_info("sender", &[]), + InstantiateMsg { + governance_address: params::GOVERNANCE.to_string(), + admin_address: params::ADMIN.to_string(), + axelarnet_gateway_address: params::GATEWAY.to_string(), + its_contracts: Default::default(), + }, + ) +} diff --git a/interchain-token-service/tests/utils/messages.rs b/interchain-token-service/tests/utils/messages.rs new file mode 100644 index 000000000..cc843b590 --- /dev/null +++ b/interchain-token-service/tests/utils/messages.rs @@ -0,0 +1,52 @@ +use cosmwasm_std::{HexBinary, Uint256}; +use interchain_token_service::{HubMessage, Message, TokenId}; +use router_api::{Address, ChainNameRaw, CrossChainId}; + +pub fn dummy_message() -> Message { + Message::InterchainTransfer { + token_id: TokenId::new([2; 32]), + source_address: HexBinary::from_hex("1234").unwrap(), + destination_address: HexBinary::from_hex("5678").unwrap(), + amount: Uint256::from(1000u64), + data: HexBinary::from_hex("abcd").unwrap(), + } +} + +pub struct TestMessage { + pub hub_message: HubMessage, + pub router_message: router_api::Message, + pub source_its_chain: ChainNameRaw, + pub source_its_contract: Address, + pub destination_its_chain: ChainNameRaw, + pub destination_its_contract: Address, +} + +impl TestMessage { + pub fn dummy() -> Self { + let source_its_chain: ChainNameRaw = "source-its-chain".parse().unwrap(); + let source_its_contract: Address = "source-its-contract".parse().unwrap(); + let destination_its_chain: ChainNameRaw = "dest-its-chain".parse().unwrap(); + let destination_its_contract: Address = "dest-its-contract".parse().unwrap(); + + let hub_message = HubMessage::SendToHub { + destination_chain: destination_its_chain.clone(), + message: dummy_message(), + }; + let router_message = router_api::Message { + cc_id: CrossChainId::new(source_its_chain.clone(), "message-id").unwrap(), + source_address: source_its_contract.clone(), + destination_chain: "its-hub-chain".parse().unwrap(), + destination_address: "its-hub-contract".parse().unwrap(), + payload_hash: [1; 32], + }; + + TestMessage { + hub_message, + router_message, + source_its_chain, + source_its_contract, + destination_its_chain, + destination_its_contract, + } + } +} diff --git a/interchain-token-service/tests/utils/mod.rs b/interchain-token-service/tests/utils/mod.rs new file mode 100644 index 000000000..91fcddba4 --- /dev/null +++ b/interchain-token-service/tests/utils/mod.rs @@ -0,0 +1,17 @@ +// 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::*; +#[allow(unused_imports)] +pub use messages::*; +#[allow(unused_imports)] +pub use query::*; + +mod execute; +mod instantiate; +mod messages; +pub mod params; +mod query; diff --git a/interchain-token-service/tests/utils/params.rs b/interchain-token-service/tests/utils/params.rs new file mode 100644 index 000000000..4bc425d60 --- /dev/null +++ b/interchain-token-service/tests/utils/params.rs @@ -0,0 +1,5 @@ +pub const AXELARNET: &str = "axelarnet"; +pub const ROUTER: &str = "router"; +pub const GATEWAY: &str = "gateway"; +pub const GOVERNANCE: &str = "governance"; +pub const ADMIN: &str = "admin"; diff --git a/interchain-token-service/tests/utils/query.rs b/interchain-token-service/tests/utils/query.rs new file mode 100644 index 000000000..19739a010 --- /dev/null +++ b/interchain-token-service/tests/utils/query.rs @@ -0,0 +1,23 @@ +use std::collections::HashMap; + +use axelar_wasm_std::error::ContractError; +use cosmwasm_std::testing::mock_env; +use cosmwasm_std::{from_json, Deps}; +use interchain_token_service::contract::query; +use interchain_token_service::msg::QueryMsg; +use router_api::{Address, ChainNameRaw}; + +pub fn query_its_contract( + deps: Deps, + chain: ChainNameRaw, +) -> Result, ContractError> { + let bin = query(deps, mock_env(), QueryMsg::ItsContract { chain })?; + Ok(from_json(bin)?) +} + +pub fn query_all_its_contracts( + deps: Deps, +) -> Result, ContractError> { + let bin = query(deps, mock_env(), QueryMsg::AllItsContracts)?; + Ok(from_json(bin)?) +} diff --git a/packages/axelar-wasm-std/src/lib.rs b/packages/axelar-wasm-std/src/lib.rs index 43c1f0c77..86b2dfe70 100644 --- a/packages/axelar-wasm-std/src/lib.rs +++ b/packages/axelar-wasm-std/src/lib.rs @@ -14,6 +14,7 @@ pub mod killswitch; pub mod msg_id; pub mod nonempty; pub mod permission_control; +pub mod response; pub mod snapshot; pub mod threshold; pub mod utils; diff --git a/packages/axelar-wasm-std/src/response.rs b/packages/axelar-wasm-std/src/response.rs new file mode 100644 index 000000000..fd2e6ef92 --- /dev/null +++ b/packages/axelar-wasm-std/src/response.rs @@ -0,0 +1,32 @@ +use cosmwasm_std::{from_json, CosmosMsg, Response, StdError, WasmMsg}; +use serde::de::DeserializeOwned; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Std(#[from] StdError), + #[error("no wasm msg found")] + NotFound, + #[error("multiple msgs found in the response")] + MultipleMsgsFound, +} + +/// Get a msg wrapped inside a `WasmMsg::Execute` from a `Response`. +/// If there are no wasm messages or more than one message in the response, this returns an error. +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(Error::NotFound)?.msg; + + if followup_messages.next().is_some() { + return Err(Error::MultipleMsgsFound); + } + + match msg { + CosmosMsg::Wasm(WasmMsg::Execute { msg, .. }) => Ok(from_json(msg)?), + _ => Err(Error::NotFound), + } +} diff --git a/packages/router-api/src/primitives.rs b/packages/router-api/src/primitives.rs index 8bc5638b1..220f43272 100644 --- a/packages/router-api/src/primitives.rs +++ b/packages/router-api/src/primitives.rs @@ -113,6 +113,12 @@ impl TryFrom for Address { } } +impl std::fmt::Display for Address { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", *self.0) + } +} + #[cw_serde] #[derive(Eq, Hash)] pub struct CrossChainId {