From ab3f01d343c82710fb414533b7ffc4997c83739e Mon Sep 17 00:00:00 2001 From: Milap Sheth Date: Sat, 2 Nov 2024 17:05:00 -0400 Subject: [PATCH 1/2] feat(major-interchain-token-service): apply token balance invariants (#671) Co-authored-by: Christian Gorenflo --- Cargo.lock | 1 + contracts/interchain-token-service/Cargo.toml | 1 + contracts/interchain-token-service/README.md | 26 +- contracts/interchain-token-service/src/abi.rs | 105 ++-- .../interchain-token-service/src/contract.rs | 18 +- .../src/contract/execute.rs | 441 ++++++++++++--- .../src/contract/query.rs | 17 +- .../interchain-token-service/src/events.rs | 58 +- contracts/interchain-token-service/src/lib.rs | 1 + contracts/interchain-token-service/src/msg.rs | 14 +- .../src/primitives.rs | 106 ++-- .../interchain-token-service/src/state.rs | 122 +++- .../interchain-token-service/tests/execute.rs | 527 +++++++++++++++--- .../interchain-token-service/tests/query.rs | 13 + .../execute_hub_message_succeeds.golden | 99 +++- .../tests/utils/execute.rs | 58 +- .../tests/utils/messages.rs | 14 +- .../tests/utils/query.rs | 22 + packages/router-api/src/primitives.rs | 7 + 19 files changed, 1346 insertions(+), 304 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6830e3192..a6fb746e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4089,6 +4089,7 @@ dependencies = [ "error-stack", "goldie", "hex", + "itertools 0.11.0", "msgs-derive", "report", "router-api", diff --git a/contracts/interchain-token-service/Cargo.toml b/contracts/interchain-token-service/Cargo.toml index fdf4422a8..7829e06cb 100644 --- a/contracts/interchain-token-service/Cargo.toml +++ b/contracts/interchain-token-service/Cargo.toml @@ -43,6 +43,7 @@ cw-storage-plus = { workspace = true } cw2 = { workspace = true } error-stack = { workspace = true } hex = { workspace = true } +itertools = "0.11.0" msgs-derive = { workspace = true } report = { workspace = true } router-api = { workspace = true } diff --git a/contracts/interchain-token-service/README.md b/contracts/interchain-token-service/README.md index 260766bf4..285686a4e 100644 --- a/contracts/interchain-token-service/README.md +++ b/contracts/interchain-token-service/README.md @@ -2,14 +2,30 @@ ## 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)). +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 tracks the token supply 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. +2. **Token Supply Tracking**: Ensures accurate token supply is maintained during cross-chain operations. +3. **ITS Address Registry**: Tracks the trusted ITS address for each chain for routing. This avoids each chain's ITS + contract from having to know about all other ITS contract addresses. -### Cross-chain messaging +## 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. +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. + +## Token Supply Invariant + +ITS Hub maintains the token supply for native interchain tokens for every chain they're deployed to. This helps isolate +the security risk between chains. A compromised chain can only affect the total token supply that was sent to that chain +in the worst case. For example, if USDC was deployed from Ethereum to Solana via the ITS Hub, and 10M USDC was moved to +Solana (in total). If there's a compromise on Solana (potentially the chain itself, or the Solana ITS contract, or +another related contract), an attacker can only withdraw at most 10M USDC back to Ethereum or another chain (and not all +the bridged USDC locked on the Ethereum ITS contract). ITS Hub will prevent all USDC transfers from Solana once 10M USDC +has been moved back out from it. diff --git a/contracts/interchain-token-service/src/abi.rs b/contracts/interchain-token-service/src/abi.rs index 4bbffd0be..dabec3048 100644 --- a/contracts/interchain-token-service/src/abi.rs +++ b/contracts/interchain-token-service/src/abi.rs @@ -6,7 +6,7 @@ use error_stack::{bail, ensure, report, Report, ResultExt}; use router_api::ChainNameRaw; use crate::primitives::{HubMessage, Message}; -use crate::{TokenId, TokenManagerType}; +use crate::{primitives, TokenId, TokenManagerType}; // ITS Message payload types // Reference: https://github.com/axelarnetwork/interchain-token-service/blob/v1.2.4/DESIGN.md#interchain-communication-spec @@ -80,13 +80,13 @@ pub enum Error { impl Message { pub fn abi_encode(self) -> HexBinary { match self { - Message::InterchainTransfer { + Message::InterchainTransfer(primitives::InterchainTransfer { token_id, source_address, destination_address, amount, data, - } => InterchainTransfer { + }) => InterchainTransfer { messageType: MessageType::InterchainTransfer.into(), tokenId: FixedBytes::<32>::new(token_id.into()), sourceAddress: Vec::::from(source_address).into(), @@ -95,13 +95,13 @@ impl Message { data: into_vec(data).into(), } .abi_encode_params(), - Message::DeployInterchainToken { + Message::DeployInterchainToken(primitives::DeployInterchainToken { token_id, name, symbol, decimals, minter, - } => DeployInterchainToken { + }) => DeployInterchainToken { messageType: MessageType::DeployInterchainToken.into(), tokenId: FixedBytes::<32>::new(token_id.into()), name: name.into(), @@ -110,11 +110,11 @@ impl Message { minter: into_vec(minter).into(), } .abi_encode_params(), - Message::DeployTokenManager { + Message::DeployTokenManager(primitives::DeployTokenManager { token_id, token_manager_type, params, - } => DeployTokenManager { + }) => DeployTokenManager { messageType: MessageType::DeployTokenManager.into(), tokenId: FixedBytes::<32>::new(token_id.into()), tokenManagerType: token_manager_type.into(), @@ -136,7 +136,7 @@ impl Message { let decoded = InterchainTransfer::abi_decode_params(payload, true) .map_err(Error::AbiDecodeFailed)?; - Message::InterchainTransfer { + primitives::InterchainTransfer { token_id: TokenId::new(decoded.tokenId.into()), source_address: Vec::::from(decoded.sourceAddress) .try_into() @@ -149,18 +149,20 @@ impl Message { .map_err(Error::NonEmpty)?, data: from_vec(decoded.data.into())?, } + .into() } MessageType::DeployInterchainToken => { let decoded = DeployInterchainToken::abi_decode_params(payload, true) .map_err(Error::AbiDecodeFailed)?; - Message::DeployInterchainToken { + primitives::DeployInterchainToken { token_id: TokenId::new(decoded.tokenId.into()), name: decoded.name.try_into().map_err(Error::NonEmpty)?, symbol: decoded.symbol.try_into().map_err(Error::NonEmpty)?, decimals: decoded.decimals, minter: from_vec(decoded.minter.into())?, } + .into() } MessageType::DeployTokenManager => { let decoded = DeployTokenManager::abi_decode_params(payload, true) @@ -171,13 +173,14 @@ impl Message { .then(TokenManagerType::from_repr) .ok_or_else(|| Error::InvalidTokenManagerType)?; - Message::DeployTokenManager { + primitives::DeployTokenManager { token_id: TokenId::new(decoded.tokenId.into()), token_manager_type, params: Vec::::from(decoded.params) .try_into() .map_err(Error::NonEmpty)?, } + .into() } _ => bail!(Error::InvalidMessageType), }; @@ -284,7 +287,7 @@ mod tests { use super::{DeployInterchainToken, InterchainTransfer}; use crate::abi::{DeployTokenManager, Error, MessageType, SendToHub}; - use crate::{HubMessage, Message, TokenManagerType}; + use crate::{primitives, HubMessage, TokenManagerType}; fn from_hex(hex: &str) -> nonempty::HexBinary { HexBinary::from_hex(hex).unwrap().try_into().unwrap() @@ -297,43 +300,47 @@ mod tests { let cases = vec![ HubMessage::SendToHub { destination_chain: remote_chain.clone(), - message: Message::InterchainTransfer { + message: primitives::InterchainTransfer { token_id: [0u8; 32].into(), source_address: from_hex("00"), destination_address: from_hex("00"), amount: 1u64.try_into().unwrap(), data: None, - }, + } + .into(), }, HubMessage::SendToHub { destination_chain: remote_chain.clone(), - message: Message::InterchainTransfer { + message: primitives::InterchainTransfer { token_id: [255u8; 32].into(), source_address: from_hex("4F4495243837681061C4743b74B3eEdf548D56A5"), destination_address: from_hex("4F4495243837681061C4743b74B3eEdf548D56A5"), amount: Uint256::MAX.try_into().unwrap(), data: Some(from_hex("abcd")), - }, + } + .into(), }, HubMessage::ReceiveFromHub { source_chain: remote_chain.clone(), - message: Message::InterchainTransfer { + message: primitives::InterchainTransfer { token_id: [0u8; 32].into(), source_address: from_hex("00"), destination_address: from_hex("00"), amount: 1u64.try_into().unwrap(), data: None, - }, + } + .into(), }, HubMessage::ReceiveFromHub { source_chain: remote_chain.clone(), - message: Message::InterchainTransfer { + message: primitives::InterchainTransfer { token_id: [255u8; 32].into(), source_address: from_hex("4F4495243837681061C4743b74B3eEdf548D56A5"), destination_address: from_hex("4F4495243837681061C4743b74B3eEdf548D56A5"), amount: Uint256::MAX.try_into().unwrap(), data: Some(from_hex("abcd")), - }, + } + .into(), }, ]; @@ -428,63 +435,69 @@ mod tests { let cases = vec![ HubMessage::SendToHub { destination_chain: remote_chain.clone(), - message: Message::DeployInterchainToken { + message: primitives::DeployInterchainToken { token_id: [0u8; 32].into(), name: "t".try_into().unwrap(), symbol: "T".try_into().unwrap(), decimals: 0, minter: None, - }, + } + .into(), }, HubMessage::SendToHub { destination_chain: remote_chain.clone(), - message: Message::DeployInterchainToken { + message: primitives::DeployInterchainToken { token_id: [1u8; 32].into(), name: "Test Token".try_into().unwrap(), symbol: "TST".try_into().unwrap(), decimals: 18, minter: Some(from_hex("1234")), - }, + } + .into(), }, HubMessage::SendToHub { destination_chain: remote_chain.clone(), - message: Message::DeployInterchainToken { + message: primitives::DeployInterchainToken { token_id: [0u8; 32].into(), name: "Unicode Token 🪙".try_into().unwrap(), symbol: "UNI🔣".try_into().unwrap(), decimals: 255, minter: Some(from_hex("abcd")), - }, + } + .into(), }, HubMessage::ReceiveFromHub { source_chain: remote_chain.clone(), - message: Message::DeployInterchainToken { + message: primitives::DeployInterchainToken { token_id: [0u8; 32].into(), name: "t".try_into().unwrap(), symbol: "T".try_into().unwrap(), decimals: 0, minter: None, - }, + } + .into(), }, HubMessage::ReceiveFromHub { source_chain: remote_chain.clone(), - message: Message::DeployInterchainToken { + message: primitives::DeployInterchainToken { token_id: [1u8; 32].into(), name: "Test Token".try_into().unwrap(), symbol: "TST".try_into().unwrap(), decimals: 18, minter: Some(from_hex("1234")), - }, + } + .into(), }, HubMessage::ReceiveFromHub { source_chain: remote_chain.clone(), - message: Message::DeployInterchainToken { + message: primitives::DeployInterchainToken { token_id: [0u8; 32].into(), name: "Unicode Token 🪙".try_into().unwrap(), symbol: "UNI🔣".try_into().unwrap(), decimals: 255, minter: Some(from_hex("abcd")), - }, + } + .into(), }, ]; @@ -509,35 +522,39 @@ mod tests { let cases = vec![ HubMessage::SendToHub { destination_chain: remote_chain.clone(), - message: Message::DeployTokenManager { + message: primitives::DeployTokenManager { token_id: [0u8; 32].into(), token_manager_type: TokenManagerType::NativeInterchainToken, params: from_hex("00"), - }, + } + .into(), }, HubMessage::SendToHub { destination_chain: remote_chain.clone(), - message: Message::DeployTokenManager { + message: primitives::DeployTokenManager { token_id: [1u8; 32].into(), token_manager_type: TokenManagerType::Gateway, params: from_hex("1234"), - }, + } + .into(), }, HubMessage::ReceiveFromHub { source_chain: remote_chain.clone(), - message: Message::DeployTokenManager { + message: primitives::DeployTokenManager { token_id: [0u8; 32].into(), token_manager_type: TokenManagerType::NativeInterchainToken, params: from_hex("00"), - }, + } + .into(), }, HubMessage::ReceiveFromHub { source_chain: remote_chain.clone(), - message: Message::DeployTokenManager { + message: primitives::DeployTokenManager { token_id: [1u8; 32].into(), token_manager_type: TokenManagerType::Gateway, params: from_hex("1234"), - }, + } + .into(), }, ]; @@ -646,13 +663,14 @@ mod tests { let large_data = vec![0u8; 1024 * 1024]; // 1MB of data let original = HubMessage::SendToHub { destination_chain: ChainNameRaw::from_str("large-data-chain").unwrap(), - message: Message::InterchainTransfer { + message: primitives::InterchainTransfer { token_id: [0u8; 32].into(), source_address: from_hex("1234"), destination_address: from_hex("5678"), amount: Uint256::from(1u128).try_into().unwrap(), data: Some(large_data.try_into().unwrap()), - }, + } + .into(), }; let encoded = original.clone().abi_encode(); @@ -664,13 +682,14 @@ mod tests { fn encode_decode_unicode_strings() { let original = HubMessage::SendToHub { destination_chain: ChainNameRaw::from_str("chain").unwrap(), - message: Message::DeployInterchainToken { + message: primitives::DeployInterchainToken { token_id: [0u8; 32].into(), name: "Unicode Token 🪙".try_into().unwrap(), symbol: "UNI🔣".try_into().unwrap(), decimals: 18, minter: Some(from_hex("abcd")), - }, + } + .into(), }; let encoded = original.clone().abi_encode(); diff --git a/contracts/interchain-token-service/src/contract.rs b/contracts/interchain-token-service/src/contract.rs index eaa92f2ba..e7d3aaf7a 100644 --- a/contracts/interchain-token-service/src/contract.rs +++ b/contracts/interchain-token-service/src/contract.rs @@ -36,14 +36,18 @@ pub enum Error { UnfreezeChain, #[error("failed to set chain config")] SetChainConfig, - #[error("failed to query its address")] - QueryItsContract, - #[error("failed to query all its addresses")] - QueryAllItsContracts, #[error("failed to disable execution")] DisableExecution, #[error("failed to enable execution")] EnableExecution, + #[error("failed to query its address")] + QueryItsContract, + #[error("failed to query all its addresses")] + QueryAllItsContracts, + #[error("failed to query a specific token instance")] + QueryTokenInstance, + #[error("failed to query the token config")] + QueryTokenConfig, } #[cfg_attr(not(feature = "library"), entry_point)] @@ -145,6 +149,12 @@ pub fn query(deps: Deps, _: Env, msg: QueryMsg) -> Result QueryMsg::AllItsContracts => { query::all_its_contracts(deps).change_context(Error::QueryAllItsContracts) } + QueryMsg::TokenInstance { chain, token_id } => { + query::token_instance(deps, chain, token_id).change_context(Error::QueryTokenInstance) + } + QueryMsg::TokenConfig { token_id } => { + query::token_config(deps, token_id).change_context(Error::QueryTokenConfig) + } }? .then(Ok) } diff --git a/contracts/interchain-token-service/src/contract/execute.rs b/contracts/interchain-token-service/src/contract/execute.rs index 6444b3d7a..a5dcd6a46 100644 --- a/contracts/interchain-token-service/src/contract/execute.rs +++ b/contracts/interchain-token-service/src/contract/execute.rs @@ -1,11 +1,15 @@ use axelar_wasm_std::{killswitch, nonempty, FnExt, 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 router_api::{Address, ChainNameRaw, CrossChainId}; use crate::events::Event; use crate::primitives::HubMessage; -use crate::state::{self, is_chain_frozen, load_config, load_its_contract}; +use crate::state::{self, is_chain_frozen, load_config, load_its_contract, TokenDeploymentType}; +use crate::{ + DeployInterchainToken, DeployTokenManager, InterchainTransfer, Message, TokenConfig, TokenId, + TokenInstance, +}; #[derive(thiserror::Error, Debug, IntoContractError)] pub enum Error { @@ -25,22 +29,39 @@ pub enum Error { FailedExecuteMessage, #[error("execution is currently disabled")] ExecutionDisabled, - #[error("chain config for {0} already set")] - ChainConfigAlreadySet(ChainNameRaw), - #[error("failed to load chain config for chain {0}")] - LoadChainConfig(ChainNameRaw), - #[error("failed to save chain config for chain {0}")] - SaveChainConfig(ChainNameRaw), #[error("chain {0} is frozen")] ChainFrozen(ChainNameRaw), #[error("state error")] State, + #[error("chain config for {0} already set")] + ChainConfigAlreadySet(ChainNameRaw), + #[error("token {token_id} not deployed on chain {chain}")] + TokenNotDeployed { + token_id: TokenId, + chain: ChainNameRaw, + }, + #[error("token {token_id} already deployed on chain {chain}")] + TokenAlreadyDeployed { + token_id: TokenId, + chain: ChainNameRaw, + }, + #[error("token {token_id} can only be deployed from its origin chain {origin_chain} and not from {chain}")] + TokenDeployedFromNonOriginChain { + token_id: TokenId, + origin_chain: ChainNameRaw, + chain: ChainNameRaw, + }, + #[error("token supply invariant violated for token {token_id} on chain {chain}")] + TokenSupplyInvariantViolated { + token_id: TokenId, + chain: 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, +/// its sources. It verifies the source address, decodes the message, applies various checks and transformations, /// and forwards the message to the destination chain. pub fn execute_message( deps: DepsMut, @@ -52,63 +73,103 @@ pub fn execute_message( killswitch::is_contract_active(deps.storage), Error::ExecutionDisabled ); - ensure_its_source_address(deps.storage, &cc_id.source_chain, &source_address)?; + ensure_is_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()))?; - - verify_chains_not_frozen(deps.storage, &cc_id.source_chain, &destination_chain)?; - - 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(), - )) - } + } => execute_message_on_hub(deps, cc_id, destination_chain, message), _ => bail!(Error::InvalidMessageType), } } -fn verify_chains_not_frozen( - storage: &dyn Storage, - source_chain: &ChainNameRaw, - destination_chain: &ChainNameRaw, +fn execute_message_on_hub( + deps: DepsMut, + cc_id: CrossChainId, + destination_chain: ChainNameRaw, + message: Message, +) -> Result { + let destination_address = load_its_contract(deps.storage, &destination_chain) + .change_context_lazy(|| Error::UnknownChain(destination_chain.clone()))?; + + apply_to_hub( + deps.storage, + cc_id.source_chain.clone(), + destination_chain.clone(), + &message, + )?; + + 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, + destination_address, + destination_payload, + )? + .add_event( + Event::MessageReceived { + cc_id, + destination_chain, + message, + } + .into(), + )) +} + +fn apply_to_hub( + storage: &mut dyn Storage, + source_chain: ChainNameRaw, + destination_chain: ChainNameRaw, + message: &Message, ) -> Result<(), Error> { - for chain in [source_chain, destination_chain] { - if is_chain_frozen(storage, chain).change_context(Error::State)? { - return Err(report!(Error::ChainFrozen(chain.to_owned()))); + ensure_chain_not_frozen(storage, &source_chain)?; + ensure_chain_not_frozen(storage, &destination_chain)?; + + match message { + Message::InterchainTransfer(transfer) => { + apply_transfer(storage, source_chain, destination_chain, transfer)?; + } + Message::DeployInterchainToken(deploy_token) => { + apply_token_deployment( + storage, + source_chain, + destination_chain, + deploy_token.token_id.clone(), + &deploy_token.deployment_type(), + )?; + } + Message::DeployTokenManager(deploy_manager) => { + apply_token_deployment( + storage, + source_chain, + destination_chain, + deploy_manager.token_id.clone(), + &deploy_manager.deployment_type(), + )?; } } + Ok(()) } -fn normalize(chain: &ChainNameRaw) -> ChainName { - ChainName::try_from(chain.as_ref()).expect("invalid chain name") +fn ensure_chain_not_frozen(storage: &dyn Storage, chain: &ChainNameRaw) -> Result<(), Error> { + ensure!( + !is_chain_frozen(storage, chain).change_context(Error::State)?, + Error::ChainFrozen(chain.to_owned()) + ); + + Ok(()) } /// Ensures that the source address of the cross-chain message is the registered ITS contract for the source chain. -fn ensure_its_source_address( +fn ensure_is_its_source_address( storage: &dyn Storage, source_chain: &ChainNameRaw, source_address: &Address, @@ -127,7 +188,7 @@ fn ensure_its_source_address( fn send_to_destination( storage: &dyn Storage, querier: QuerierWrapper, - destination_chain: ChainNameRaw, + destination_chain: &ChainNameRaw, destination_address: Address, payload: HexBinary, ) -> Result { @@ -137,7 +198,7 @@ fn send_to_destination( client::ContractClient::new(querier, &config.axelarnet_gateway).into(); let call_contract_msg = - gateway.call_contract(normalize(&destination_chain), destination_address, payload); + gateway.call_contract(destination_chain.normalize(), destination_address, payload); Ok(Response::new().add_message(call_contract_msg)) } @@ -186,16 +247,189 @@ pub fn set_chain_config( max_uint: nonempty::Uint256, max_target_decimals: u8, ) -> Result { - match state::may_load_chain_config(deps.storage, &chain) - .change_context_lazy(|| Error::LoadChainConfig(chain.clone()))? - { + match state::may_load_chain_config(deps.storage, &chain).change_context(Error::State)? { Some(_) => bail!(Error::ChainConfigAlreadySet(chain)), None => state::save_chain_config(deps.storage, &chain, max_uint, max_target_decimals) - .change_context_lazy(|| Error::SaveChainConfig(chain))? + .change_context(Error::State)? .then(|_| Ok(Response::new())), } } +fn apply_transfer( + storage: &mut dyn Storage, + source_chain: ChainNameRaw, + destination_chain: ChainNameRaw, + transfer: &InterchainTransfer, +) -> Result<(), Error> { + subtract_amount_from_source(storage, source_chain, transfer)?; + add_amount_to_destination(storage, destination_chain, transfer) +} + +fn subtract_amount_from_source( + storage: &mut dyn Storage, + source_chain: ChainNameRaw, + transfer: &InterchainTransfer, +) -> Result<(), Error> { + let mut source_instance = + try_load_token_instance(storage, source_chain.clone(), transfer.token_id.clone())?; + + source_instance.supply = source_instance + .supply + .checked_sub(transfer.amount) + .change_context_lazy(|| Error::TokenSupplyInvariantViolated { + token_id: transfer.token_id.clone(), + chain: source_chain.clone(), + })?; + + state::save_token_instance( + storage, + source_chain, + transfer.token_id.clone(), + &source_instance, + ) + .change_context(Error::State) +} + +fn add_amount_to_destination( + storage: &mut dyn Storage, + destination_chain: ChainNameRaw, + transfer: &InterchainTransfer, +) -> Result<(), Error> { + let mut destination_instance = try_load_token_instance( + storage, + destination_chain.clone(), + transfer.token_id.clone(), + )?; + + destination_instance.supply = destination_instance + .supply + .checked_add(transfer.amount) + .change_context_lazy(|| Error::TokenSupplyInvariantViolated { + token_id: transfer.token_id.clone(), + chain: destination_chain.clone(), + })?; + + state::save_token_instance( + storage, + destination_chain, + transfer.token_id.clone(), + &destination_instance, + ) + .change_context(Error::State) +} + +fn apply_token_deployment( + storage: &mut dyn Storage, + source_chain: ChainNameRaw, + destination_chain: ChainNameRaw, + token_id: TokenId, + deployment_type: &TokenDeploymentType, +) -> Result<(), Error> { + ensure_token_not_deployed_on_destination(storage, token_id.clone(), destination_chain.clone())?; + + let token_config = + state::may_load_token_config(storage, &token_id).change_context(Error::State)?; + + if let Some(TokenConfig { origin_chain, .. }) = token_config { + ensure_origin_matches_source_chain(source_chain, origin_chain, token_id.clone())?; + } else { + initialize_token_on_origin(storage, source_chain, token_id.clone())?; + } + + let destination_instance = TokenInstance::new(deployment_type); + + state::save_token_instance(storage, destination_chain, token_id, &destination_instance) + .change_context(Error::State) +} + +fn try_load_token_instance( + storage: &mut dyn Storage, + chain: ChainNameRaw, + token_id: TokenId, +) -> Result { + state::may_load_token_instance(storage, chain.clone(), token_id.clone()) + .change_context(Error::State)? + .ok_or(report!(Error::TokenNotDeployed { + token_id: token_id.clone(), + chain, + })) +} + +fn ensure_origin_matches_source_chain( + source_chain: ChainNameRaw, + origin_chain: ChainNameRaw, + token_id: TokenId, +) -> Result<(), Error> { + ensure!( + origin_chain == source_chain, + Error::TokenDeployedFromNonOriginChain { + token_id, + origin_chain, + chain: source_chain.clone(), + } + ); + + Ok(()) +} + +fn initialize_token_on_origin( + storage: &mut dyn Storage, + source_chain: ChainNameRaw, + token_id: TokenId, +) -> Result<(), Error> { + // Token is being deployed for the first time + let token_config = TokenConfig { + origin_chain: source_chain.clone(), + }; + let instance = TokenInstance::new_on_origin(); + + state::save_token_config(storage, &token_id, &token_config) + .and_then(|_| state::save_token_instance(storage, source_chain, token_id, &instance)) + .change_context(Error::State)?; + Ok(()) +} + +/// Ensures that the token is not being redeployed to the same destination chain. +fn ensure_token_not_deployed_on_destination( + storage: &dyn Storage, + token_id: TokenId, + destination_chain: ChainNameRaw, +) -> Result<(), Error> { + let token_instance = + state::may_load_token_instance(storage, destination_chain.clone(), token_id.clone()) + .change_context(Error::State)?; + + ensure!( + token_instance.is_none(), + Error::TokenAlreadyDeployed { + token_id, + chain: destination_chain, + } + ); + + Ok(()) +} + +trait DeploymentType { + fn deployment_type(&self) -> TokenDeploymentType; +} + +impl DeploymentType for DeployInterchainToken { + fn deployment_type(&self) -> TokenDeploymentType { + if self.minter.is_some() { + TokenDeploymentType::CustomMinter + } else { + TokenDeploymentType::Trustless + } + } +} + +impl DeploymentType for DeployTokenManager { + fn deployment_type(&self) -> TokenDeploymentType { + TokenDeploymentType::CustomMinter + } +} + #[cfg(test)] mod tests { use assert_ok::assert_ok; @@ -210,7 +444,7 @@ mod tests { set_chain_config, unfreeze_chain, Error, }; use crate::state::{self, Config}; - use crate::{HubMessage, Message}; + use crate::{DeployInterchainToken, HubMessage, InterchainTransfer}; const SOLANA: &str = "solana"; const ETHEREUM: &str = "ethereum"; @@ -234,27 +468,46 @@ mod tests { let mut deps = mock_dependencies(); init(&mut deps); + let msg = HubMessage::SendToHub { + destination_chain: ChainNameRaw::try_from(SOLANA).unwrap(), + message: DeployInterchainToken { + token_id: [7u8; 32].into(), + name: "Test".parse().unwrap(), + symbol: "TEST".parse().unwrap(), + decimals: 18, + minter: None, + } + .into(), + }; + let cc_id = CrossChainId { + source_chain: ChainNameRaw::try_from(ETHEREUM).unwrap(), + message_id: HexTxHashAndEventIndex::new([1u8; 32], 0u32).into(), + }; + + assert_ok!(execute_message( + deps.as_mut(), + cc_id.clone(), + ITS_ADDRESS.to_string().try_into().unwrap(), + msg.clone().abi_encode(), + )); + assert_ok!(disable_execution(deps.as_mut())); let msg = HubMessage::SendToHub { destination_chain: ChainNameRaw::try_from(SOLANA).unwrap(), - message: Message::InterchainTransfer { + message: InterchainTransfer { token_id: [7u8; 32].into(), + amount: Uint256::one().try_into().unwrap(), source_address: its_address(), destination_address: its_address(), - amount: Uint256::one().try_into().unwrap(), data: None, - }, + } + .into(), }; + let res = execute_message( deps.as_mut(), - CrossChainId { - source_chain: ChainNameRaw::try_from(SOLANA).unwrap(), - message_id: HexTxHashAndEventIndex::new([1u8; 32], 0u32) - .to_string() - .try_into() - .unwrap(), - }, + cc_id.clone(), ITS_ADDRESS.to_string().try_into().unwrap(), msg.clone().abi_encode(), ); @@ -262,15 +515,20 @@ mod tests { assert_ok!(enable_execution(deps.as_mut())); + let msg = HubMessage::SendToHub { + destination_chain: ChainNameRaw::try_from(SOLANA).unwrap(), + message: DeployInterchainToken { + token_id: [1u8; 32].into(), + name: "Test".parse().unwrap(), + symbol: "TEST".parse().unwrap(), + decimals: 18, + minter: None, + } + .into(), + }; assert_ok!(execute_message( deps.as_mut(), - CrossChainId { - source_chain: ChainNameRaw::try_from(SOLANA).unwrap(), - message_id: HexTxHashAndEventIndex::new([1u8; 32], 0u32) - .to_string() - .try_into() - .unwrap(), - }, + cc_id.clone(), ITS_ADDRESS.to_string().try_into().unwrap(), msg.abi_encode(), )); @@ -288,13 +546,14 @@ mod tests { let msg = HubMessage::SendToHub { destination_chain, - message: Message::InterchainTransfer { + message: DeployInterchainToken { token_id: [7u8; 32].into(), - source_address: its_address(), - destination_address: its_address(), - amount: Uint256::one().try_into().unwrap(), - data: None, - }, + name: "Test".parse().unwrap(), + symbol: "TEST".parse().unwrap(), + decimals: 18, + minter: None, + } + .into(), }; let res = execute_message( deps.as_mut(), @@ -339,13 +598,14 @@ mod tests { let msg = HubMessage::SendToHub { destination_chain: destination_chain.clone(), - message: Message::InterchainTransfer { + message: DeployInterchainToken { token_id: [7u8; 32].into(), - source_address: its_address(), - destination_address: its_address(), - amount: Uint256::one().try_into().unwrap(), - data: None, - }, + name: "Test".parse().unwrap(), + symbol: "TEST".parse().unwrap(), + decimals: 18, + minter: None, + } + .into(), }; let cc_id = CrossChainId { source_chain, @@ -386,13 +646,14 @@ mod tests { let msg = HubMessage::SendToHub { destination_chain: destination_chain.clone(), - message: Message::InterchainTransfer { + message: DeployInterchainToken { token_id: [7u8; 32].into(), - source_address: its_address(), - destination_address: its_address(), - amount: Uint256::one().try_into().unwrap(), - data: None, - }, + name: "Test".parse().unwrap(), + symbol: "TEST".parse().unwrap(), + decimals: 18, + minter: None, + } + .into(), }; let cc_id = CrossChainId { source_chain, diff --git a/contracts/interchain-token-service/src/contract/query.rs b/contracts/interchain-token-service/src/contract/query.rs index 1ae2e043b..cb9da38b2 100644 --- a/contracts/interchain-token-service/src/contract/query.rs +++ b/contracts/interchain-token-service/src/contract/query.rs @@ -3,7 +3,10 @@ use cosmwasm_std::{to_json_binary, Binary, Deps}; use error_stack::{Result, ResultExt}; use router_api::ChainNameRaw; -use crate::state::{load_all_its_contracts, may_load_its_contract}; +use crate::state::{ + load_all_its_contracts, may_load_its_contract, may_load_token_config, may_load_token_instance, +}; +use crate::TokenId; #[derive(thiserror::Error, Debug, IntoContractError)] pub enum Error { @@ -23,3 +26,15 @@ pub fn all_its_contracts(deps: Deps) -> Result { let contract_addresses = load_all_its_contracts(deps.storage).change_context(Error::State)?; to_json_binary(&contract_addresses).change_context(Error::JsonSerialization) } + +pub fn token_instance(deps: Deps, chain: ChainNameRaw, token_id: TokenId) -> Result { + let token_instance = + may_load_token_instance(deps.storage, chain, token_id).change_context(Error::State)?; + to_json_binary(&token_instance).change_context(Error::JsonSerialization) +} + +pub fn token_config(deps: Deps, token_id: TokenId) -> Result { + let token_config = + may_load_token_config(deps.storage, &token_id).change_context(Error::State)?; + to_json_binary(&token_config).change_context(Error::JsonSerialization) +} diff --git a/contracts/interchain-token-service/src/events.rs b/contracts/interchain-token-service/src/events.rs index 6d98c9597..b13634dfc 100644 --- a/contracts/interchain-token-service/src/events.rs +++ b/contracts/interchain-token-service/src/events.rs @@ -2,6 +2,7 @@ use axelar_wasm_std::event::EventExt; use router_api::{Address, ChainNameRaw, CrossChainId}; use crate::primitives::Message; +use crate::{DeployInterchainToken, DeployTokenManager, InterchainTransfer}; pub enum Event { MessageReceived { @@ -55,35 +56,35 @@ fn make_message_event( .add_attribute("message_type", msg.as_ref().to_string()); match msg { - Message::InterchainTransfer { + Message::InterchainTransfer(InterchainTransfer { token_id, source_address, destination_address, amount, data, - } => event + }) => event .add_attribute("token_id", token_id.to_string()) .add_attribute("source_address", source_address.to_string()) .add_attribute("destination_address", destination_address.to_string()) .add_attribute("amount", amount.to_string()) .add_attribute_if_some("data", data.map(|data| data.to_string())), - Message::DeployInterchainToken { + Message::DeployInterchainToken(DeployInterchainToken { token_id, name, symbol, decimals, minter, - } => event + }) => event .add_attribute("token_id", token_id.to_string()) .add_attribute("name", name) .add_attribute("symbol", symbol) .add_attribute("decimals", decimals.to_string()) .add_attribute_if_some("minter", minter.map(|minter| minter.to_string())), - Message::DeployTokenManager { + Message::DeployTokenManager(DeployTokenManager { token_id, token_manager_type, params, - } => event + }) => event .add_attribute("token_id", token_id.to_string()) .add_attribute( "token_manager_type", @@ -99,30 +100,36 @@ mod test { use router_api::CrossChainId; use crate::events::Event; - use crate::{Message, TokenId, TokenManagerType}; + use crate::{ + DeployInterchainToken, DeployTokenManager, InterchainTransfer, Message, TokenId, + TokenManagerType, + }; #[test] fn message_received_with_all_attributes() { let test_cases: Vec = vec![ - Message::InterchainTransfer { + InterchainTransfer { token_id: TokenId::new([1; 32]), source_address: HexBinary::from([1; 32]).try_into().unwrap(), destination_address: HexBinary::from([1, 2, 3, 4]).try_into().unwrap(), amount: 1u64.try_into().unwrap(), data: Some(HexBinary::from([1, 2, 3, 4]).try_into().unwrap()), - }, - Message::DeployInterchainToken { + } + .into(), + DeployInterchainToken { token_id: TokenId::new([1; 32]), name: "Test".try_into().unwrap(), symbol: "TST".try_into().unwrap(), decimals: 18, minter: Some(HexBinary::from([1; 32]).try_into().unwrap()), - }, - Message::DeployTokenManager { + } + .into(), + DeployTokenManager { token_id: TokenId::new([1; 32]), token_manager_type: TokenManagerType::MintBurn, params: HexBinary::from([1, 2, 3, 4]).try_into().unwrap(), - }, + } + .into(), ]; let events: Vec<_> = test_cases @@ -144,39 +151,44 @@ mod test { #[test] fn message_received_with_empty_attributes() { let test_cases: Vec = vec![ - Message::InterchainTransfer { + InterchainTransfer { token_id: TokenId::new([1; 32]), source_address: HexBinary::from([1; 32]).try_into().unwrap(), destination_address: HexBinary::from([1, 2, 3, 4]).try_into().unwrap(), amount: 1u64.try_into().unwrap(), data: None, - }, - Message::InterchainTransfer { + } + .into(), + InterchainTransfer { token_id: TokenId::new([1; 32]), source_address: HexBinary::from([0u8]).try_into().unwrap(), destination_address: HexBinary::from([0u8]).try_into().unwrap(), amount: 1u64.try_into().unwrap(), data: None, - }, - Message::DeployInterchainToken { + } + .into(), + DeployInterchainToken { token_id: TokenId::new([1; 32]), name: "Test".try_into().unwrap(), symbol: "TST".try_into().unwrap(), decimals: 18, minter: None, - }, - Message::DeployInterchainToken { + } + .into(), + DeployInterchainToken { token_id: TokenId::new([1; 32]), name: "t".try_into().unwrap(), symbol: "T".try_into().unwrap(), decimals: 0, minter: None, - }, - Message::DeployTokenManager { + } + .into(), + DeployTokenManager { token_id: TokenId::new([1; 32]), token_manager_type: TokenManagerType::MintBurn, params: HexBinary::from([0u8]).try_into().unwrap(), - }, + } + .into(), ]; let events: Vec<_> = test_cases diff --git a/contracts/interchain-token-service/src/lib.rs b/contracts/interchain-token-service/src/lib.rs index 45a424182..bbdc33d88 100644 --- a/contracts/interchain-token-service/src/lib.rs +++ b/contracts/interchain-token-service/src/lib.rs @@ -6,3 +6,4 @@ pub mod contract; pub mod events; pub mod msg; mod state; +pub use state::{TokenConfig, TokenInstance, TokenSupply}; diff --git a/contracts/interchain-token-service/src/msg.rs b/contracts/interchain-token-service/src/msg.rs index 76d8c09ca..b38732379 100644 --- a/contracts/interchain-token-service/src/msg.rs +++ b/contracts/interchain-token-service/src/msg.rs @@ -6,6 +6,9 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use msgs_derive::EnsurePermissions; use router_api::{Address, ChainNameRaw}; +use crate::state::{TokenConfig, TokenInstance}; +use crate::TokenId; + #[cw_serde] pub struct InstantiateMsg { pub governance_address: String, @@ -63,7 +66,16 @@ pub enum QueryMsg { /// Query the ITS contract address registered for a chain #[returns(Option
)] ItsContract { chain: ChainNameRaw }, - /// Query all registererd ITS contract addresses + /// Query all registered ITS contract addresses #[returns(HashMap)] AllItsContracts, + /// Query a token instance on a specific chain + #[returns(Option)] + TokenInstance { + chain: ChainNameRaw, + token_id: TokenId, + }, + /// Query the configuration parameters for a token + #[returns(Option)] + TokenConfig { token_id: TokenId }, } diff --git a/contracts/interchain-token-service/src/primitives.rs b/contracts/interchain-token-service/src/primitives.rs index 55c8d3e7f..4e5dcdeb3 100644 --- a/contracts/interchain-token-service/src/primitives.rs +++ b/contracts/interchain-token-service/src/primitives.rs @@ -40,41 +40,71 @@ pub enum TokenManagerType { #[derive(Eq, strum::AsRefStr)] 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: nonempty::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: nonempty::HexBinary, - /// The amount of tokens to transfer - amount: nonempty::Uint256, - /// An optional payload to be provided to the destination address, if `data` is not empty - data: Option, - }, + InterchainTransfer(InterchainTransfer), /// 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: nonempty::String, - /// The symbol of the token - symbol: nonempty::String, - /// The number of decimal places the token supports - decimals: u8, - /// An additional minter of the token (optional). ITS on the external chain is always a minter. - minter: Option, - }, + DeployInterchainToken(DeployInterchainToken), /// 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: nonempty::HexBinary, - }, + DeployTokenManager(DeployTokenManager), +} + +#[cw_serde] +#[derive(Eq)] +pub struct InterchainTransfer { + /// The unique identifier of the token being transferred + pub token_id: TokenId, + /// The address that called the ITS contract on the source chain + pub source_address: nonempty::HexBinary, + /// The address that the token will be sent to on the destination chain + /// If data is not empty, this address will give the token and executed as a contract on the destination chain + pub destination_address: nonempty::HexBinary, + /// The amount of tokens to transfer + pub amount: nonempty::Uint256, + /// An optional payload to be provided to the destination address, if `data` is not empty + pub data: Option, +} + +impl From for Message { + fn from(value: InterchainTransfer) -> Self { + Message::InterchainTransfer(value) + } +} + +#[cw_serde] +#[derive(Eq)] +pub struct DeployInterchainToken { + /// The unique identifier of the token to be deployed + pub token_id: TokenId, + /// The name of the token + pub name: nonempty::String, + /// The symbol of the token + pub symbol: nonempty::String, + /// The number of decimal places the token supports + pub decimals: u8, + /// An additional minter of the token (optional). ITS on the external chain is always a minter. + pub minter: Option, +} + +impl From for Message { + fn from(value: DeployInterchainToken) -> Self { + Message::DeployInterchainToken(value) + } +} + +#[cw_serde] +#[derive(Eq)] +pub struct DeployTokenManager { + /// The unique identifier of the token that the token manager will manage + pub token_id: TokenId, + /// The type of token manager to deploy + pub token_manager_type: TokenManagerType, + /// The parameters to be provided to the token manager contract + pub params: nonempty::HexBinary, +} + +impl From for Message { + fn from(value: DeployTokenManager) -> Self { + Message::DeployTokenManager(value) + } } /// A message sent between ITS edge contracts and the ITS hub contract (defined in this crate). @@ -103,14 +133,18 @@ impl HubMessage { HubMessage::ReceiveFromHub { message, .. } => message, } } + + pub fn token_id(&self) -> TokenId { + self.message().token_id() + } } impl Message { pub fn token_id(&self) -> TokenId { match self { - Message::InterchainTransfer { token_id, .. } - | Message::DeployInterchainToken { token_id, .. } - | Message::DeployTokenManager { token_id, .. } => token_id.clone(), + Message::InterchainTransfer(InterchainTransfer { token_id, .. }) + | Message::DeployInterchainToken(DeployInterchainToken { token_id, .. }) + | Message::DeployTokenManager(DeployTokenManager { token_id, .. }) => token_id.clone(), } } } diff --git a/contracts/interchain-token-service/src/state.rs b/contracts/interchain-token-service/src/state.rs index 053aacfc1..96a3eb1e6 100644 --- a/contracts/interchain-token-service/src/state.rs +++ b/contracts/interchain-token-service/src/state.rs @@ -1,12 +1,14 @@ use std::collections::HashMap; -use axelar_wasm_std::{nonempty, IntoContractError}; +use axelar_wasm_std::{nonempty, FnExt, IntoContractError}; use cosmwasm_schema::cw_serde; -use cosmwasm_std::{ensure, Addr, StdError, Storage}; +use cosmwasm_std::{ensure, Addr, OverflowError, StdError, Storage, Uint256}; use cw_storage_plus::{Item, Map}; use error_stack::{report, Result, ResultExt}; use router_api::{Address, ChainNameRaw}; +use crate::TokenId; + #[derive(thiserror::Error, Debug, IntoContractError)] pub enum Error { #[error("ITS contract got into an invalid state, its config is missing")] @@ -35,9 +37,77 @@ pub struct ChainConfig { frozen: bool, } +#[cw_serde] +pub enum TokenSupply { + /// The total token supply bridged to this chain. + /// ITS Hub will not allow bridging back more than this amount of the token from the corresponding chain. + Tracked(Uint256), + /// The token supply bridged to this chain is not tracked. + Untracked, +} + +impl TokenSupply { + pub fn checked_add(self, amount: nonempty::Uint256) -> Result { + match self { + TokenSupply::Untracked => TokenSupply::Untracked, + TokenSupply::Tracked(supply) => { + TokenSupply::Tracked(supply.checked_add(amount.into())?) + } + } + .then(Ok) + } + pub fn checked_sub(self, amount: nonempty::Uint256) -> Result { + match self { + TokenSupply::Untracked => TokenSupply::Untracked, + TokenSupply::Tracked(supply) => { + TokenSupply::Tracked(supply.checked_sub(amount.into())?) + } + } + .then(Ok) + } +} + +/// Information about a token on a specific chain. +#[cw_serde] +pub struct TokenInstance { + pub supply: TokenSupply, +} + +impl TokenInstance { + pub fn new_on_origin() -> Self { + Self { + supply: TokenSupply::Untracked, + } + } + + pub fn new(deployment_type: &TokenDeploymentType) -> Self { + let supply = match deployment_type { + TokenDeploymentType::Trustless => TokenSupply::Tracked(Uint256::zero()), + _ => TokenSupply::Untracked, + }; + + Self { supply } + } +} + +/// The deployment type of the token. +pub enum TokenDeploymentType { + /// The token is trustless, i.e. only owned by ITS. + Trustless, + /// The token has a custom minter. + CustomMinter, +} + +#[cw_serde] +pub struct TokenConfig { + pub origin_chain: ChainNameRaw, +} + const CONFIG: Item = Item::new("config"); const ITS_CONTRACTS: Map<&ChainNameRaw, Address> = Map::new("its_contracts"); const CHAIN_CONFIGS: Map<&ChainNameRaw, ChainConfig> = Map::new("chain_configs"); +const TOKEN_INSTANCE: Map<&(ChainNameRaw, TokenId), TokenInstance> = Map::new("token_instance"); +const TOKEN_CONFIGS: Map<&TokenId, TokenConfig> = Map::new("token_configs"); pub fn load_config(storage: &dyn Storage) -> Config { CONFIG @@ -138,9 +208,7 @@ pub fn freeze_chain(storage: &mut dyn Storage, chain: &ChainNameRaw) -> Result Ok(ChainConfig { frozen: true, ..x }), - None => Err(StdError::NotFound { - kind: "chain not found".to_string(), - }), + None => Err(StdError::not_found("chain not found".to_string())), }) .change_context(Error::ChainNotFound(chain.to_owned())) } @@ -152,13 +220,51 @@ pub fn unfreeze_chain( CHAIN_CONFIGS .update(storage, chain, |elt| match elt { Some(x) => Ok(ChainConfig { frozen: false, ..x }), - None => Err(StdError::NotFound { - kind: "chain not found".to_string(), - }), + None => Err(StdError::not_found("chain not found".to_string())), }) .change_context(Error::ChainNotFound(chain.to_owned())) } +pub fn save_token_instance( + storage: &mut dyn Storage, + chain: ChainNameRaw, + token_id: TokenId, + token_instance: &TokenInstance, +) -> Result<(), Error> { + TOKEN_INSTANCE + .save(storage, &(chain, token_id), token_instance) + .change_context(Error::Storage) +} + +pub fn may_load_token_instance( + storage: &dyn Storage, + chain: ChainNameRaw, + token_id: TokenId, +) -> Result, Error> { + TOKEN_INSTANCE + .may_load(storage, &(chain, token_id)) + .change_context(Error::Storage) +} + +pub fn may_load_token_config( + storage: &dyn Storage, + token_id: &TokenId, +) -> Result, Error> { + TOKEN_CONFIGS + .may_load(storage, token_id) + .change_context(Error::Storage) +} + +pub fn save_token_config( + storage: &mut dyn Storage, + token_id: &TokenId, + token_config: &TokenConfig, +) -> Result<(), Error> { + TOKEN_CONFIGS + .save(storage, token_id, token_config) + .change_context(Error::Storage) +} + #[cfg(test)] mod tests { use assert_ok::assert_ok; diff --git a/contracts/interchain-token-service/tests/execute.rs b/contracts/interchain-token-service/tests/execute.rs index cfaf94d9f..cbd202c81 100644 --- a/contracts/interchain-token-service/tests/execute.rs +++ b/contracts/interchain-token-service/tests/execute.rs @@ -2,16 +2,19 @@ use std::str::FromStr; use assert_ok::assert_ok; use axelar_wasm_std::response::inspect_response_msg; -use axelar_wasm_std::{assert_err_contains, permission_control}; +use axelar_wasm_std::{assert_err_contains, nonempty, permission_control}; use axelarnet_gateway::msg::ExecuteMsg as AxelarnetGatewayExecuteMsg; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; use cosmwasm_std::{HexBinary, Uint256}; 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 interchain_token_service::{ + DeployInterchainToken, DeployTokenManager, HubMessage, InterchainTransfer, TokenId, + TokenManagerType, TokenSupply, +}; use router_api::{Address, ChainName, ChainNameRaw, CrossChainId}; -use utils::{make_deps, params, TestMessage}; +use utils::{params, TestMessage}; mod utils; @@ -80,72 +83,49 @@ fn deregistering_unknown_chain_fails() { #[test] fn execute_hub_message_succeeds() { - let mut deps = make_deps(); - 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 max_uint = Uint256::from_str("120000000000000000000000000") - .unwrap() - .try_into() - .unwrap(); - let decimals = 18; - - assert_ok!(utils::set_chain_config( - deps.as_mut(), - source_its_chain.clone(), - max_uint, - decimals - )); - - assert_ok!(utils::set_chain_config( - deps.as_mut(), - destination_its_chain.clone(), - max_uint, - decimals - )); + let ( + mut deps, + TestMessage { + router_message, + source_its_chain, + source_its_contract, + destination_its_chain, + destination_its_contract, + .. + }, + ) = utils::setup(); - let token_id = TokenId::new([1; 32]); let test_messages = vec![ - Message::InterchainTransfer { - token_id: token_id.clone(), - source_address: HexBinary::from([1; 32]).try_into().unwrap(), - destination_address: HexBinary::from([2; 32]).try_into().unwrap(), - amount: 1u64.try_into().unwrap(), - data: Some(HexBinary::from([1, 2, 3, 4]).try_into().unwrap()), - }, - Message::DeployInterchainToken { - token_id: token_id.clone(), + DeployInterchainToken { + token_id: TokenId::new([1; 32]), name: "Test".try_into().unwrap(), symbol: "TST".try_into().unwrap(), decimals: 18, minter: Some(HexBinary::from([1; 32]).try_into().unwrap()), - }, - Message::DeployTokenManager { - token_id: token_id.clone(), + } + .into(), + InterchainTransfer { + token_id: TokenId::new([1; 32]), + source_address: HexBinary::from([1; 32]).try_into().unwrap(), + destination_address: HexBinary::from([2; 32]).try_into().unwrap(), + amount: 1u64.try_into().unwrap(), + data: Some(HexBinary::from([1, 2, 3, 4]).try_into().unwrap()), + } + .into(), + DeployTokenManager { + token_id: TokenId::new([2; 32]), token_manager_type: TokenManagerType::MintBurn, params: HexBinary::from([1, 2, 3, 4]).try_into().unwrap(), - }, + } + .into(), + InterchainTransfer { + token_id: TokenId::new([2; 32]), + source_address: HexBinary::from([1; 32]).try_into().unwrap(), + destination_address: HexBinary::from([2; 32]).try_into().unwrap(), + amount: 1u64.try_into().unwrap(), + data: Some(HexBinary::from([1, 2, 3, 4]).try_into().unwrap()), + } + .into(), ]; let responses: Vec<_> = test_messages @@ -155,18 +135,17 @@ fn execute_hub_message_succeeds() { 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( + let response = assert_ok!(utils::execute_hub_message( deps.as_mut(), router_message.cc_id.clone(), source_its_contract.clone(), - payload, + hub_message.clone(), )); let msg: AxelarnetGatewayExecuteMsg = assert_ok!(inspect_response_msg(response.clone())); @@ -573,3 +552,423 @@ fn set_chain_config_should_fail_if_chain_config_is_already_set() { ExecuteError::ChainConfigAlreadySet(_) ) } + +#[test] +fn deploy_interchain_token_tracks_supply() { + let ( + mut deps, + TestMessage { + router_message, + source_its_chain, + source_its_contract, + destination_its_chain, + destination_its_contract, + hub_message, + }, + ) = utils::setup(); + + let token_id = hub_message.token_id(); + let amount = nonempty::Uint256::try_from(400u64).unwrap(); + + assert_ok!(utils::execute_hub_message( + deps.as_mut(), + router_message.cc_id.clone(), + source_its_contract.clone(), + hub_message, + )); + + let msg = HubMessage::SendToHub { + destination_chain: destination_its_chain.clone(), + message: InterchainTransfer { + token_id: token_id.clone(), + source_address: HexBinary::from([1; 32]).try_into().unwrap(), + destination_address: HexBinary::from([2; 32]).try_into().unwrap(), + amount, + data: None, + } + .into(), + }; + assert_ok!(utils::execute_hub_message( + deps.as_mut(), + router_message.cc_id.clone(), + source_its_contract.clone(), + msg, + )); + + assert_eq!( + assert_ok!(utils::query_token_instance( + deps.as_ref(), + source_its_chain.clone(), + token_id.clone() + )) + .unwrap() + .supply, + TokenSupply::Untracked, + ); + assert_eq!( + assert_ok!(utils::query_token_instance( + deps.as_ref(), + destination_its_chain.clone(), + token_id.clone() + )) + .unwrap() + .supply, + TokenSupply::Tracked(amount.into()) + ); + + // Send the same amount back + let msg = HubMessage::SendToHub { + destination_chain: source_its_chain.clone(), + message: InterchainTransfer { + token_id: token_id.clone(), + source_address: HexBinary::from([1; 32]).try_into().unwrap(), + destination_address: HexBinary::from([2; 32]).try_into().unwrap(), + amount, + data: None, + } + .into(), + }; + assert_ok!(utils::execute_hub_message( + deps.as_mut(), + CrossChainId::new( + destination_its_chain.clone(), + router_message.cc_id.message_id.clone() + ) + .unwrap(), + destination_its_contract, + msg, + )); + + assert_eq!( + assert_ok!(utils::query_token_instance( + deps.as_ref(), + source_its_chain.clone(), + token_id.clone() + )) + .unwrap() + .supply, + TokenSupply::Untracked + ); + assert_eq!( + assert_ok!(utils::query_token_instance( + deps.as_ref(), + destination_its_chain, + token_id.clone() + )) + .unwrap() + .supply, + TokenSupply::Tracked(Uint256::zero()) + ); +} + +#[test] +fn deploy_interchain_token_with_minter_does_not_track_supply() { + let ( + mut deps, + TestMessage { + router_message, + source_its_chain, + source_its_contract, + destination_its_chain, + destination_its_contract, + .. + }, + ) = utils::setup(); + + let token_id = TokenId::new([1u8; 32]); + let amount = nonempty::Uint256::try_from(400u64).unwrap(); + + let msg = HubMessage::SendToHub { + destination_chain: destination_its_chain.clone(), + message: DeployInterchainToken { + token_id: token_id.clone(), + name: "Test".try_into().unwrap(), + symbol: "TST".try_into().unwrap(), + decimals: 18, + minter: Some(HexBinary::from([1; 32]).try_into().unwrap()), + } + .into(), + }; + assert_ok!(utils::execute_hub_message( + deps.as_mut(), + router_message.cc_id.clone(), + source_its_contract.clone(), + msg, + )); + for chain in [source_its_chain.clone(), destination_its_chain.clone()] { + assert_eq!( + assert_ok!(utils::query_token_instance( + deps.as_ref(), + chain.clone(), + token_id.clone() + )) + .unwrap() + .supply, + TokenSupply::Untracked, + ); + } + + let msg = HubMessage::SendToHub { + destination_chain: destination_its_chain.clone(), + message: InterchainTransfer { + token_id: token_id.clone(), + source_address: HexBinary::from([1; 32]).try_into().unwrap(), + destination_address: HexBinary::from([2; 32]).try_into().unwrap(), + amount, + data: None, + } + .into(), + }; + assert_ok!(utils::execute_hub_message( + deps.as_mut(), + router_message.cc_id.clone(), + source_its_contract.clone(), + msg, + )); + + // Send a larger amount back + let msg = HubMessage::SendToHub { + destination_chain: source_its_chain.clone(), + message: InterchainTransfer { + token_id: token_id.clone(), + source_address: HexBinary::from([1; 32]).try_into().unwrap(), + destination_address: HexBinary::from([2; 32]).try_into().unwrap(), + amount: amount.strict_add(Uint256::one()).try_into().unwrap(), + data: None, + } + .into(), + }; + assert_ok!(utils::execute_hub_message( + deps.as_mut(), + CrossChainId::new( + destination_its_chain.clone(), + router_message.cc_id.message_id.clone() + ) + .unwrap(), + destination_its_contract, + msg, + )); + + for chain in [source_its_chain.clone(), destination_its_chain.clone()] { + assert_eq!( + assert_ok!(utils::query_token_instance( + deps.as_ref(), + chain.clone(), + token_id.clone() + )) + .unwrap() + .supply, + TokenSupply::Untracked, + ); + } +} + +#[test] +fn interchain_transfer_exceeds_supply_fails() { + let ( + mut deps, + TestMessage { + router_message, + source_its_chain, + source_its_contract, + destination_its_chain, + destination_its_contract, + hub_message: msg, + }, + ) = utils::setup(); + + let token_id = msg.token_id(); + let amount = nonempty::Uint256::try_from(400u64).unwrap(); + + assert_ok!(utils::execute_hub_message( + deps.as_mut(), + router_message.cc_id.clone(), + source_its_contract.clone(), + msg, + )); + + let msg = HubMessage::SendToHub { + destination_chain: source_its_chain.clone(), + message: InterchainTransfer { + token_id: token_id.clone(), + source_address: HexBinary::from([1; 32]).try_into().unwrap(), + destination_address: HexBinary::from([2; 32]).try_into().unwrap(), + amount: 1u64.try_into().unwrap(), + data: None, + } + .into(), + }; + assert_err_contains!( + utils::execute_hub_message( + deps.as_mut(), + CrossChainId::new( + destination_its_chain.clone(), + router_message.cc_id.message_id.clone() + ) + .unwrap(), + destination_its_contract.clone(), + msg, + ), + ExecuteError, + ExecuteError::TokenSupplyInvariantViolated { .. } + ); + + let msg = HubMessage::SendToHub { + destination_chain: destination_its_chain.clone(), + message: InterchainTransfer { + token_id: token_id.clone(), + source_address: HexBinary::from([1; 32]).try_into().unwrap(), + destination_address: HexBinary::from([2; 32]).try_into().unwrap(), + amount, + data: None, + } + .into(), + }; + assert_ok!(utils::execute_hub_message( + deps.as_mut(), + router_message.cc_id.clone(), + source_its_contract.clone(), + msg, + )); + + let msg = HubMessage::SendToHub { + destination_chain: source_its_chain.clone(), + message: InterchainTransfer { + token_id: token_id.clone(), + source_address: HexBinary::from([1; 32]).try_into().unwrap(), + destination_address: HexBinary::from([2; 32]).try_into().unwrap(), + amount: amount.strict_add(Uint256::one()).try_into().unwrap(), + data: None, + } + .into(), + }; + assert_err_contains!( + utils::execute_hub_message( + deps.as_mut(), + CrossChainId::new(destination_its_chain, router_message.cc_id.message_id).unwrap(), + destination_its_contract, + msg, + ), + ExecuteError, + ExecuteError::TokenSupplyInvariantViolated { .. } + ); +} + +#[test] +fn deploy_interchain_token_submitted_twice_fails() { + let ( + mut deps, + TestMessage { + router_message, + source_its_contract, + hub_message: msg, + .. + }, + ) = utils::setup(); + + assert_ok!(utils::execute_hub_message( + deps.as_mut(), + router_message.cc_id.clone(), + source_its_contract.clone(), + msg.clone(), + )); + + assert_err_contains!( + utils::execute_hub_message( + deps.as_mut(), + router_message.cc_id.clone(), + source_its_contract.clone(), + msg, + ), + ExecuteError, + ExecuteError::TokenAlreadyDeployed { .. } + ); +} + +#[test] +fn deploy_interchain_token_from_non_origin_chain_fails() { + let ( + mut deps, + TestMessage { + router_message, + source_its_contract, + hub_message: msg, + .. + }, + ) = utils::setup(); + + assert_ok!(utils::execute_hub_message( + deps.as_mut(), + router_message.cc_id.clone(), + source_its_contract.clone(), + msg.clone(), + )); + + // Deploy the same token from a different origin chain to a different destination chain now + let another_source_chain: ChainNameRaw = "another-source-chain".parse().unwrap(); + utils::register_chain( + &mut deps, + another_source_chain.clone(), + source_its_contract.clone(), + ); + let another_destination_chain: ChainNameRaw = "another-dest-chain".parse().unwrap(); + utils::register_chain( + &mut deps, + another_destination_chain.clone(), + source_its_contract.clone(), + ); + + let new_destination_msg = HubMessage::SendToHub { + destination_chain: another_source_chain.clone(), + message: msg.message().clone(), + }; + + assert_err_contains!( + utils::execute_hub_message( + deps.as_mut(), + CrossChainId::new(another_source_chain, router_message.cc_id.message_id).unwrap(), + source_its_contract, + new_destination_msg, + ), + ExecuteError, + ExecuteError::TokenDeployedFromNonOriginChain { .. } + ); +} + +#[test] +fn deploy_interchain_token_to_multiple_destination_succeeds() { + let ( + mut deps, + TestMessage { + router_message, + source_its_contract, + hub_message: msg, + .. + }, + ) = utils::setup(); + + assert_ok!(utils::execute_hub_message( + deps.as_mut(), + router_message.cc_id.clone(), + source_its_contract.clone(), + msg.clone(), + )); + + let another_chain: ChainNameRaw = "another-chain".parse().unwrap(); + utils::register_chain( + &mut deps, + another_chain.clone(), + source_its_contract.clone(), + ); + + let msg = HubMessage::SendToHub { + destination_chain: another_chain, + message: msg.message().clone(), + }; + assert_ok!(utils::execute_hub_message( + deps.as_mut(), + router_message.cc_id.clone(), + source_its_contract.clone(), + msg.clone(), + )); +} diff --git a/contracts/interchain-token-service/tests/query.rs b/contracts/interchain-token-service/tests/query.rs index 93f96af01..c50c996f0 100644 --- a/contracts/interchain-token-service/tests/query.rs +++ b/contracts/interchain-token-service/tests/query.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use assert_ok::assert_ok; use cosmwasm_std::testing::mock_dependencies; +use interchain_token_service::TokenId; use router_api::{Address, ChainNameRaw}; mod utils; @@ -67,3 +68,15 @@ fn query_all_its_contracts() { let queried_addresses = assert_ok!(utils::query_all_its_contracts(deps.as_ref())); assert_eq!(queried_addresses, its_contracts); } + +#[test] +fn query_token_chain_config() { + let mut deps = mock_dependencies(); + utils::instantiate_contract(deps.as_mut()).unwrap(); + + let chain: ChainNameRaw = "ethereum".parse().unwrap(); + let token_id: TokenId = TokenId::new([1; 32]); + + let config = utils::query_token_instance(deps.as_ref(), chain, token_id).unwrap(); + assert_eq!(config, None); +} diff --git a/contracts/interchain-token-service/tests/testdata/execute_hub_message_succeeds.golden b/contracts/interchain-token-service/tests/testdata/execute_hub_message_succeeds.golden index 0985ec368..69e88351f 100644 --- a/contracts/interchain-token-service/tests/testdata/execute_hub_message_succeeds.golden +++ b/contracts/interchain-token-service/tests/testdata/execute_hub_message_succeeds.golden @@ -1,4 +1,63 @@ [ + { + "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": [ { @@ -66,7 +125,7 @@ "wasm": { "execute": { "contract_addr": "gateway", - "msg": "eyJjYWxsX2NvbnRyYWN0Ijp7ImRlc3RpbmF0aW9uX2NoYWluIjoiZGVzdC1pdHMtY2hhaW4iLCJkZXN0aW5hdGlvbl9hZGRyZXNzIjoiZGVzdC1pdHMtY29udHJhY3QiLCJwYXlsb2FkIjoiMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwNDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwNjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMGEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAxMDczNmY3NTcyNjM2NTJkNjk3NDczMmQ2MzY4NjE2OTZlMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMTgwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMGMwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMTIwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMTQwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwNDU0NjU3Mzc0MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAzNTQ1MzU0MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMjAwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxIn19", + "msg": "eyJjYWxsX2NvbnRyYWN0Ijp7ImRlc3RpbmF0aW9uX2NoYWluIjoiZGVzdC1pdHMtY2hhaW4iLCJkZXN0aW5hdGlvbl9hZGRyZXNzIjoiZGVzdC1pdHMtY29udHJhY3QiLCJwYXlsb2FkIjoiMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwNDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwNjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMGEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAxMDczNmY3NTcyNjM2NTJkNjk3NDczMmQ2MzY4NjE2OTZlMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMGMwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMjAyMDIwMjAyMDIwMjAyMDIwMjAyMDIwMjAyMDIwMjAyMDIwMjAyMDIwMjAyMDIwMjAyMDIwMjAyMDIwMjAyMDIwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA0MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA4MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDQwMTAyMDMwNDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwIn19", "funds": [] } } @@ -90,27 +149,19 @@ }, { "key": "message_type", - "value": "DeployInterchainToken" + "value": "DeployTokenManager" }, { "key": "token_id", - "value": "0101010101010101010101010101010101010101010101010101010101010101" - }, - { - "key": "name", - "value": "Test" - }, - { - "key": "symbol", - "value": "TST" + "value": "0202020202020202020202020202020202020202020202020202020202020202" }, { - "key": "decimals", - "value": "18" + "key": "token_manager_type", + "value": "MintBurn" }, { - "key": "minter", - "value": "0101010101010101010101010101010101010101010101010101010101010101" + "key": "params", + "value": "01020304" } ] } @@ -125,7 +176,7 @@ "wasm": { "execute": { "contract_addr": "gateway", - "msg": "eyJjYWxsX2NvbnRyYWN0Ijp7ImRlc3RpbmF0aW9uX2NoYWluIjoiZGVzdC1pdHMtY2hhaW4iLCJkZXN0aW5hdGlvbl9hZGRyZXNzIjoiZGVzdC1pdHMtY29udHJhY3QiLCJwYXlsb2FkIjoiMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwNDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwNjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMGEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAxMDczNmY3NTcyNjM2NTJkNjk3NDczMmQ2MzY4NjE2OTZlMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMGMwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMjAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA0MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA4MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDQwMTAyMDMwNDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwIn19", + "msg": "eyJjYWxsX2NvbnRyYWN0Ijp7ImRlc3RpbmF0aW9uX2NoYWluIjoiZGVzdC1pdHMtY2hhaW4iLCJkZXN0aW5hdGlvbl9hZGRyZXNzIjoiZGVzdC1pdHMtY29udHJhY3QiLCJwYXlsb2FkIjoiMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwNDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwNjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMGEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAxMDczNmY3NTcyNjM2NTJkNjk3NDczMmQ2MzY4NjE2OTZlMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMTgwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAyMDIwMjAyMDIwMjAyMDIwMjAyMDIwMjAyMDIwMjAyMDIwMjAyMDIwMjAyMDIwMjAyMDIwMjAyMDIwMjAyMDIwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMGMwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMTQwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAyMDAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDIwMDIwMjAyMDIwMjAyMDIwMjAyMDIwMjAyMDIwMjAyMDIwMjAyMDIwMjAyMDIwMjAyMDIwMjAyMDIwMjAyMDIwMjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDQwMTAyMDMwNDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwIn19", "funds": [] } } @@ -149,18 +200,26 @@ }, { "key": "message_type", - "value": "DeployTokenManager" + "value": "InterchainTransfer" }, { "key": "token_id", + "value": "0202020202020202020202020202020202020202020202020202020202020202" + }, + { + "key": "source_address", "value": "0101010101010101010101010101010101010101010101010101010101010101" }, { - "key": "token_manager_type", - "value": "MintBurn" + "key": "destination_address", + "value": "0202020202020202020202020202020202020202020202020202020202020202" }, { - "key": "params", + "key": "amount", + "value": "1" + }, + { + "key": "data", "value": "01020304" } ] diff --git a/contracts/interchain-token-service/tests/utils/execute.rs b/contracts/interchain-token-service/tests/utils/execute.rs index 77d1ba538..5bcca0cd8 100644 --- a/contracts/interchain-token-service/tests/utils/execute.rs +++ b/contracts/interchain-token-service/tests/utils/execute.rs @@ -8,12 +8,13 @@ use axelar_wasm_std::nonempty; use cosmwasm_std::testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage}; use cosmwasm_std::{ from_json, to_json_binary, Addr, DepsMut, HexBinary, MemoryStorage, OwnedDeps, Response, - WasmQuery, + Uint256, WasmQuery, }; -use interchain_token_service::contract; use interchain_token_service::msg::ExecuteMsg; +use interchain_token_service::{contract, HubMessage}; use router_api::{Address, ChainName, ChainNameRaw, CrossChainId}; +use super::{instantiate_contract, TestMessage}; use crate::utils::params; pub fn execute( @@ -34,6 +35,15 @@ pub fn execute( ) } +pub fn execute_hub_message( + deps: DepsMut, + cc_id: CrossChainId, + source_address: Address, + message: HubMessage, +) -> Result { + execute(deps, cc_id, source_address, message.abi_encode()) +} + pub fn register_its_contract( deps: DepsMut, chain: ChainNameRaw, @@ -115,3 +125,47 @@ pub fn make_deps() -> OwnedDeps>, + chain: ChainNameRaw, + its_contract: Address, +) { + register_its_contract(deps.as_mut(), chain.clone(), its_contract).unwrap(); + set_chain_config( + deps.as_mut(), + chain, + Uint256::MAX.try_into().unwrap(), + u8::MAX, + ) + .unwrap(); +} + +pub fn setup() -> ( + OwnedDeps>, + TestMessage, +) { + let mut deps = make_deps(); + instantiate_contract(deps.as_mut()).unwrap(); + + let TestMessage { + source_its_chain, + source_its_contract, + destination_its_chain, + destination_its_contract, + .. + } = TestMessage::dummy(); + + register_chain( + &mut deps, + source_its_chain.clone(), + source_its_contract.clone(), + ); + register_chain( + &mut deps, + destination_its_chain.clone(), + destination_its_contract.clone(), + ); + + (deps, TestMessage::dummy()) +} diff --git a/contracts/interchain-token-service/tests/utils/messages.rs b/contracts/interchain-token-service/tests/utils/messages.rs index 041cfbb01..48e1fba1d 100644 --- a/contracts/interchain-token-service/tests/utils/messages.rs +++ b/contracts/interchain-token-service/tests/utils/messages.rs @@ -1,15 +1,15 @@ -use cosmwasm_std::HexBinary; -use interchain_token_service::{HubMessage, Message, TokenId}; +use interchain_token_service::{DeployInterchainToken, HubMessage, Message, TokenId}; use router_api::{Address, ChainNameRaw, CrossChainId}; pub fn dummy_message() -> Message { - Message::InterchainTransfer { + DeployInterchainToken { token_id: TokenId::new([2; 32]), - source_address: HexBinary::from_hex("1234").unwrap().try_into().unwrap(), - destination_address: HexBinary::from_hex("5678").unwrap().try_into().unwrap(), - amount: 1000u64.try_into().unwrap(), - data: Some(HexBinary::from_hex("abcd").unwrap().try_into().unwrap()), + name: "Test".try_into().unwrap(), + symbol: "TST".try_into().unwrap(), + decimals: 18, + minter: None, } + .into() } pub struct TestMessage { diff --git a/contracts/interchain-token-service/tests/utils/query.rs b/contracts/interchain-token-service/tests/utils/query.rs index 19739a010..1ea3e1465 100644 --- a/contracts/interchain-token-service/tests/utils/query.rs +++ b/contracts/interchain-token-service/tests/utils/query.rs @@ -5,6 +5,7 @@ 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 interchain_token_service::{TokenConfig, TokenId, TokenInstance}; use router_api::{Address, ChainNameRaw}; pub fn query_its_contract( @@ -21,3 +22,24 @@ pub fn query_all_its_contracts( let bin = query(deps, mock_env(), QueryMsg::AllItsContracts)?; Ok(from_json(bin)?) } + +pub fn query_token_instance( + deps: Deps, + chain: ChainNameRaw, + token_id: TokenId, +) -> Result, ContractError> { + let bin = query( + deps, + mock_env(), + QueryMsg::TokenInstance { chain, token_id }, + )?; + Ok(from_json(bin)?) +} + +pub fn query_token_config( + deps: Deps, + token_id: TokenId, +) -> Result, ContractError> { + let bin = query(deps, mock_env(), QueryMsg::TokenConfig { token_id })?; + Ok(from_json(bin)?) +} diff --git a/packages/router-api/src/primitives.rs b/packages/router-api/src/primitives.rs index 315450baa..57712bf48 100644 --- a/packages/router-api/src/primitives.rs +++ b/packages/router-api/src/primitives.rs @@ -314,6 +314,13 @@ impl ChainNameRaw { /// Maximum length of a chain name (in bytes). /// This MUST NOT be changed without a corresponding change to the ChainName validation in axelar-core. const MAX_LEN: usize = 20; + + /// Special care must be taken when using this function. Normalization means a loss of information + /// and can lead to the chain not being found in the database. This function should only be used if absolutely necessary. + pub fn normalize(&self) -> ChainName { + // assert: if ChainNameRaw is valid, ChainName is also valid, just lower-cased + ChainName::try_from(self.as_ref()).expect("invalid chain name") + } } impl FromStr for ChainNameRaw { From 08bb9c3c41bb622facc307a546ba0d57a04bcfc4 Mon Sep 17 00:00:00 2001 From: Sammy Date: Sun, 3 Nov 2024 03:11:22 -0500 Subject: [PATCH 2/2] add tests --- .../interchain-token-service/tests/execute.rs | 160 +++++++++++++++++- ...cimals_when_max_uints_are_different.golden | 67 ++++++++ ...ecimals_when_max_uints_are_the_same.golden | 67 ++++++++ 3 files changed, 293 insertions(+), 1 deletion(-) create mode 100644 contracts/interchain-token-service/tests/testdata/execute_message_deploy_interchain_token_should_translate_decimals_when_max_uints_are_different.golden create mode 100644 contracts/interchain-token-service/tests/testdata/execute_message_deploy_interchain_token_should_translate_decimals_when_max_uints_are_the_same.golden diff --git a/contracts/interchain-token-service/tests/execute.rs b/contracts/interchain-token-service/tests/execute.rs index d7e20e90d..3fd9ff549 100644 --- a/contracts/interchain-token-service/tests/execute.rs +++ b/contracts/interchain-token-service/tests/execute.rs @@ -14,7 +14,8 @@ use interchain_token_service::{ TokenManagerType, TokenSupply, }; use router_api::{Address, ChainName, ChainNameRaw, CrossChainId}; -use utils::{params, TestMessage}; +use serde_json::json; +use utils::{instantiate_contract, make_deps, params, TestMessage}; mod utils; @@ -173,6 +174,163 @@ fn execute_hub_message_succeeds() { goldie::assert_json!(responses); } +#[test] +fn execute_message_deploy_interchain_token_should_translate_decimals_when_max_uints_are_different() +{ + let mut deps = make_deps(); + 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(); + utils::set_chain_config( + deps.as_mut(), + source_its_chain.clone(), + Uint256::MAX.try_into().unwrap(), + 6, + ) + .unwrap(); + utils::set_chain_config( + deps.as_mut(), + destination_its_chain.clone(), + Uint256::from_u128(2) + .pow(64) + .strict_sub(Uint256::one()) + .try_into() + .unwrap(), + 6, + ) + .unwrap(); + + let token_id = TokenId::new([1; 32]); + let message = DeployInterchainToken { + token_id, + name: "Test".try_into().unwrap(), + symbol: "TST".try_into().unwrap(), + decimals: 18, + minter: None, + } + .into(); + let hub_message = HubMessage::SendToHub { + destination_chain: destination_its_chain.clone(), + message, + }; + let response = assert_ok!(utils::execute_hub_message( + deps.as_mut(), + router_message.cc_id.clone(), + source_its_contract.clone(), + hub_message.clone(), + )); + let source_token_instance = assert_ok!(utils::query_token_instance( + deps.as_ref(), + source_its_chain.clone(), + token_id + )); + let destination_token_instance = assert_ok!(utils::query_token_instance( + deps.as_ref(), + destination_its_chain.clone(), + token_id + )); + + goldie::assert_json!( + json!({ "response": response, "source_token_instance": source_token_instance, "destination_token_instance": destination_token_instance }) + ); +} + +#[test] +fn execute_message_deploy_interchain_token_should_translate_decimals_when_max_uints_are_the_same() { + let mut deps = make_deps(); + 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(); + utils::set_chain_config( + deps.as_mut(), + source_its_chain.clone(), + Uint256::MAX.try_into().unwrap(), + 6, + ) + .unwrap(); + utils::set_chain_config( + deps.as_mut(), + destination_its_chain.clone(), + Uint256::MAX.try_into().unwrap(), + 6, + ) + .unwrap(); + + let token_id = TokenId::new([1; 32]); + let message = DeployInterchainToken { + token_id, + name: "Test".try_into().unwrap(), + symbol: "TST".try_into().unwrap(), + decimals: 18, + minter: None, + } + .into(); + let hub_message = HubMessage::SendToHub { + destination_chain: destination_its_chain.clone(), + message, + }; + let response = assert_ok!(utils::execute_hub_message( + deps.as_mut(), + router_message.cc_id.clone(), + source_its_contract.clone(), + hub_message.clone(), + )); + let source_token_instance = assert_ok!(utils::query_token_instance( + deps.as_ref(), + source_its_chain.clone(), + token_id + )); + let destination_token_instance = assert_ok!(utils::query_token_instance( + deps.as_ref(), + destination_its_chain.clone(), + token_id + )); + + goldie::assert_json!( + json!({ "response": response, "source_token_instance": source_token_instance, "destination_token_instance": destination_token_instance }) + ); +} + #[test] fn execute_its_when_not_gateway_sender_fails() { let mut deps = mock_dependencies(); diff --git a/contracts/interchain-token-service/tests/testdata/execute_message_deploy_interchain_token_should_translate_decimals_when_max_uints_are_different.golden b/contracts/interchain-token-service/tests/testdata/execute_message_deploy_interchain_token_should_translate_decimals_when_max_uints_are_different.golden new file mode 100644 index 000000000..530997071 --- /dev/null +++ b/contracts/interchain-token-service/tests/testdata/execute_message_deploy_interchain_token_should_translate_decimals_when_max_uints_are_different.golden @@ -0,0 +1,67 @@ +{ + "destination_token_instance": { + "decimals": 6, + "supply": { + "tracked": "0" + } + }, + "response": { + "attributes": [], + "data": null, + "events": [ + { + "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": "6" + } + ], + "type": "message_received" + } + ], + "messages": [ + { + "gas_limit": null, + "id": 0, + "msg": { + "wasm": { + "execute": { + "contract_addr": "gateway", + "funds": [], + "msg": "eyJjYWxsX2NvbnRyYWN0Ijp7ImRlc3RpbmF0aW9uX2NoYWluIjoiZGVzdC1pdHMtY2hhaW4iLCJkZXN0aW5hdGlvbl9hZGRyZXNzIjoiZGVzdC1pdHMtY29udHJhY3QiLCJwYXlsb2FkIjoiMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwNDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwNjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMGEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAxMDczNmY3NTcyNjM2NTJkNjk3NDczMmQ2MzY4NjE2OTZlMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMTYwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMGMwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDYwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMTQwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwNDU0NjU3Mzc0MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAzNTQ1MzU0MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAifX0=" + } + } + }, + "reply_on": "never" + } + ] + }, + "source_token_instance": { + "decimals": 18, + "supply": "untracked" + } +} \ No newline at end of file diff --git a/contracts/interchain-token-service/tests/testdata/execute_message_deploy_interchain_token_should_translate_decimals_when_max_uints_are_the_same.golden b/contracts/interchain-token-service/tests/testdata/execute_message_deploy_interchain_token_should_translate_decimals_when_max_uints_are_the_same.golden new file mode 100644 index 000000000..43c0ceafa --- /dev/null +++ b/contracts/interchain-token-service/tests/testdata/execute_message_deploy_interchain_token_should_translate_decimals_when_max_uints_are_the_same.golden @@ -0,0 +1,67 @@ +{ + "destination_token_instance": { + "decimals": 18, + "supply": { + "tracked": "0" + } + }, + "response": { + "attributes": [], + "data": null, + "events": [ + { + "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" + } + ], + "type": "message_received" + } + ], + "messages": [ + { + "gas_limit": null, + "id": 0, + "msg": { + "wasm": { + "execute": { + "contract_addr": "gateway", + "funds": [], + "msg": "eyJjYWxsX2NvbnRyYWN0Ijp7ImRlc3RpbmF0aW9uX2NoYWluIjoiZGVzdC1pdHMtY2hhaW4iLCJkZXN0aW5hdGlvbl9hZGRyZXNzIjoiZGVzdC1pdHMtY29udHJhY3QiLCJwYXlsb2FkIjoiMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwNDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwNjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMGEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAxMDczNmY3NTcyNjM2NTJkNjk3NDczMmQ2MzY4NjE2OTZlMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMTYwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMTAxMDEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMGMwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMTIwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMTQwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwNDU0NjU3Mzc0MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAzNTQ1MzU0MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAifX0=" + } + } + }, + "reply_on": "never" + } + ] + }, + "source_token_instance": { + "decimals": 18, + "supply": "untracked" + } +} \ No newline at end of file