From 8b0df43f6524552fcb237957bcffa89187e2c622 Mon Sep 17 00:00:00 2001 From: CJ Cobb <46455409+cjcobb23@users.noreply.github.com> Date: Wed, 9 Oct 2024 12:58:52 -0400 Subject: [PATCH] feat(minor-interchain-token-service): lock and unlock gateway tokens (#650) --- Cargo.lock | 1 + .../axelarnet-gateway/src/clients/gateway.rs | 19 +- contracts/interchain-token-service/Cargo.toml | 1 + .../interchain-token-service/src/contract.rs | 202 +++++- .../src/contract/execute.rs | 661 +++++++++++++++++- .../src/primitives.rs | 25 +- .../interchain-token-service/src/state.rs | 7 + .../interchain-token-service/tests/execute.rs | 4 +- .../tests/utils/execute.rs | 53 +- packages/client/src/lib.rs | 12 +- 10 files changed, 948 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 615185d5c..df4220ff7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4078,6 +4078,7 @@ dependencies = [ "alloy-primitives", "alloy-sol-types", "assert_ok", + "axelar-core-std", "axelar-wasm-std", "axelarnet-gateway", "client", diff --git a/contracts/axelarnet-gateway/src/clients/gateway.rs b/contracts/axelarnet-gateway/src/clients/gateway.rs index 3efe78794..cb2bfff5c 100644 --- a/contracts/axelarnet-gateway/src/clients/gateway.rs +++ b/contracts/axelarnet-gateway/src/clients/gateway.rs @@ -1,5 +1,5 @@ use axelar_wasm_std::vec::VecExt; -use cosmwasm_std::{Addr, CosmosMsg, HexBinary}; +use cosmwasm_std::{Addr, Coin, CosmosMsg, HexBinary}; use error_stack::{Result, ResultExt}; use router_api::{Address, ChainName, CrossChainId, Message}; @@ -35,6 +35,23 @@ impl<'a> Client<'a> { }) } + pub fn call_contract_with_token( + &self, + destination_chain: ChainName, + destination_address: Address, + payload: HexBinary, + coin: Coin, + ) -> CosmosMsg { + self.client.execute_with_funds( + &ExecuteMsg::CallContract { + destination_chain, + destination_address, + payload, + }, + coin, + ) + } + pub fn execute(&self, cc_id: CrossChainId, payload: HexBinary) -> CosmosMsg { self.client.execute(&ExecuteMsg::Execute { cc_id, payload }) } diff --git a/contracts/interchain-token-service/Cargo.toml b/contracts/interchain-token-service/Cargo.toml index 5f863c729..fdf4422a8 100644 --- a/contracts/interchain-token-service/Cargo.toml +++ b/contracts/interchain-token-service/Cargo.toml @@ -33,6 +33,7 @@ optimize = """docker run --rm -v "$(pwd)":/code \ [dependencies] alloy-primitives = { workspace = true } alloy-sol-types = { workspace = true } +axelar-core-std = { workspace = true } axelar-wasm-std = { workspace = true, features = ["derive"] } axelarnet-gateway = { workspace = true, features = ["library"] } client = { workspace = true } diff --git a/contracts/interchain-token-service/src/contract.rs b/contracts/interchain-token-service/src/contract.rs index d35a460e1..8e13f06b3 100644 --- a/contracts/interchain-token-service/src/contract.rs +++ b/contracts/interchain-token-service/src/contract.rs @@ -1,6 +1,7 @@ use std::fmt::Debug; use axelar_wasm_std::error::ContractError; +use axelar_wasm_std::token::GetToken; use axelar_wasm_std::{address, permission_control, FnExt, IntoContractError}; use axelarnet_gateway::AxelarExecutableMsg; #[cfg(not(feature = "library"))] @@ -31,6 +32,8 @@ pub enum Error { DeregisterItsContract, #[error("failed to register gateway token")] RegisterGatewayToken, + #[error("too many coins attached. Execute accepts zero or one coins")] + TooManyCoins, #[error("failed to query its address")] QueryItsContract, #[error("failed to query all its addresses")] @@ -91,8 +94,11 @@ pub fn execute( cc_id, source_address, payload, - }) => execute::execute_message(deps, cc_id, source_address, payload) - .change_context(Error::Execute), + }) => { + let coin = info.single_token()?; + execute::execute_message(deps, cc_id, source_address, payload, coin) + .change_context(Error::Execute) + } ExecuteMsg::RegisterItsContract { chain, address } => { execute::register_its_contract(deps, chain, address) .change_context(Error::RegisterItsContract) @@ -133,22 +139,34 @@ pub fn query(deps: Deps, _: Env, msg: QueryMsg) -> Result #[cfg(test)] mod tests { use std::collections::HashMap; + use std::marker::PhantomData; + use assert_ok::assert_ok; + use axelar_core_std::nexus; + use axelar_core_std::nexus::query::IsChainRegisteredResponse; + use axelar_core_std::query::AxelarQueryMsg; + use axelar_wasm_std::msg_id::HexTxHashAndEventIndex; use axelar_wasm_std::nonempty; - use cosmwasm_std::testing::{ - mock_dependencies, mock_env, mock_info, MockApi, MockQuerier, MockStorage, + use axelar_wasm_std::response::inspect_response_msg; + use axelarnet_gateway::AxelarExecutableMsg; + use cosmwasm_std::testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage}; + use cosmwasm_std::{ + from_json, to_json_binary, Coin, CosmosMsg, HexBinary, MemoryStorage, OwnedDeps, Uint128, + WasmMsg, WasmQuery, }; - use cosmwasm_std::{from_json, to_json_binary, OwnedDeps, WasmQuery}; - use router_api::{ChainName, ChainNameRaw}; + use router_api::{ChainName, ChainNameRaw, CrossChainId}; use super::{execute, instantiate}; use crate::contract::execute::gateway_token_id; use crate::contract::query; use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; - use crate::TokenId; + use crate::{HubMessage, Message, TokenId}; const GOVERNANCE_ADDRESS: &str = "governance"; const ADMIN_ADDRESS: &str = "admin"; const AXELARNET_GATEWAY_ADDRESS: &str = "axelarnet-gateway"; + const CORE_CHAIN: &str = "ethereum"; + const AMPLIFIER_CHAIN: &str = "solana"; + const AXELAR_CHAIN_NAME: &str = "axelar"; #[test] fn register_gateway_token_should_register_denom_and_token_id() { @@ -177,8 +195,165 @@ mod tests { ); } - fn setup() -> OwnedDeps { - let mut deps = mock_dependencies(); + /// Tests that a token can be attached to an ITS message, escrowed in the contract, and then subsequently + /// unlocked and sent back at a later time + #[test] + fn send_token_from_core_and_back() { + let mut deps = setup(); + let denom = "eth"; + let source_chain = ChainNameRaw::try_from(CORE_CHAIN).unwrap(); + let destination_chain = ChainNameRaw::try_from(AMPLIFIER_CHAIN).unwrap(); + + let its_address = "68d30f47F19c07bCCEf4Ac7FAE2Dc12FCa3e0dC9"; + let source_address = + HexBinary::from_hex("4838B106FCe9647Bdf1E7877BF73cE8B0BAD5f97").unwrap(); + + assert_ok!(execute( + deps.as_mut(), + mock_env(), + mock_info(GOVERNANCE_ADDRESS, &[]), + ExecuteMsg::RegisterGatewayToken { + denom: denom.try_into().unwrap(), + source_chain: source_chain.clone(), + }, + )); + + assert_ok!(execute( + deps.as_mut(), + mock_env(), + mock_info(GOVERNANCE_ADDRESS, &[]), + ExecuteMsg::RegisterItsContract { + chain: source_chain.clone(), + address: its_address.to_string().try_into().unwrap() + } + )); + + assert_ok!(execute( + deps.as_mut(), + mock_env(), + mock_info(GOVERNANCE_ADDRESS, &[]), + ExecuteMsg::RegisterItsContract { + chain: destination_chain.clone(), + address: its_address.to_string().try_into().unwrap() + } + )); + + let coin = Coin { + denom: denom.to_string(), + amount: Uint128::new(100u128), + }; + + let token_id = gateway_token_id(&deps.as_mut(), denom).unwrap(); + let msg = HubMessage::SendToHub { + destination_chain: destination_chain.clone(), + message: Message::InterchainTransfer { + token_id: token_id.clone(), + source_address: source_address.clone(), + destination_address: HexBinary::from_hex(its_address).unwrap(), + amount: coin.amount.into(), + data: HexBinary::from_hex("").unwrap(), + }, + }; + + assert_ok!(execute( + deps.as_mut(), + mock_env(), + mock_info(AXELARNET_GATEWAY_ADDRESS, &[coin.clone()]), + ExecuteMsg::Execute(AxelarExecutableMsg { + cc_id: CrossChainId { + source_chain: source_chain.clone(), + message_id: HexTxHashAndEventIndex::new([1u8; 32], 0u32) + .to_string() + .try_into() + .unwrap(), + }, + source_address: its_address.to_string().try_into().unwrap(), + payload: msg.abi_encode(), + }) + )); + + let msg = HubMessage::SendToHub { + destination_chain: source_chain.clone(), + message: Message::InterchainTransfer { + token_id: token_id.clone(), + source_address: source_address.clone(), + destination_address: HexBinary::from_hex(its_address).unwrap(), + amount: coin.amount.into(), + data: HexBinary::from_hex("").unwrap(), + }, + }; + + let res = assert_ok!(execute( + deps.as_mut(), + mock_env(), + mock_info(AXELARNET_GATEWAY_ADDRESS, &[]), + ExecuteMsg::Execute(AxelarExecutableMsg { + cc_id: CrossChainId { + source_chain: destination_chain.clone(), + message_id: HexTxHashAndEventIndex::new([1u8; 32], 0u32) + .to_string() + .try_into() + .unwrap(), + }, + source_address: its_address.to_string().try_into().unwrap(), + payload: msg.abi_encode(), + }) + )); + let _msg: axelarnet_gateway::msg::ExecuteMsg = + assert_ok!(inspect_response_msg(res.clone())); + + match &res.messages.first().unwrap().msg { + CosmosMsg::Wasm(WasmMsg::Execute { funds, .. }) => { + assert_eq!(funds.len(), 1); + assert_eq!(funds.first().unwrap(), &coin); + } + _ => panic!("incorrect msg type"), + }; + } + + fn make_deps() -> OwnedDeps> { + let mut deps = OwnedDeps { + storage: MockStorage::default(), + api: MockApi::default(), + querier: MockQuerier::::new(&[]), + custom_query_type: PhantomData, + }; + + let mut querier = MockQuerier::::new(&[]); + querier.update_wasm(move |msg| match msg { + WasmQuery::Smart { contract_addr, msg } + if contract_addr == AXELARNET_GATEWAY_ADDRESS => + { + let msg = from_json::(msg).unwrap(); + match msg { + axelarnet_gateway::msg::QueryMsg::ChainName {} => { + Ok(to_json_binary(&ChainName::try_from(AXELAR_CHAIN_NAME).unwrap()).into()) + .into() + } + _ => panic!("unsupported query"), + } + } + _ => panic!("unexpected query: {:?}", msg), + }); + querier = querier.with_custom_handler(|msg| match msg { + AxelarQueryMsg::Nexus(nexus::query::QueryMsg::IsChainRegistered { chain }) => { + Ok(to_json_binary( + &(IsChainRegisteredResponse { + is_registered: chain == CORE_CHAIN, + }), + ) + .into()) + .into() + } + _ => panic!("unsupported query"), + }); + + deps.querier = querier; + deps + } + + fn setup() -> OwnedDeps> { + let mut deps = make_deps(); instantiate( deps.as_mut(), @@ -193,15 +368,6 @@ mod tests { ) .unwrap(); - deps.querier.update_wasm(move |wq| match wq { - WasmQuery::Smart { contract_addr, .. } - if contract_addr == AXELARNET_GATEWAY_ADDRESS => - { - Ok(to_json_binary(&ChainName::try_from("axelar").unwrap()).into()).into() - } - _ => panic!("no mock for this query"), - }); - deps } } diff --git a/contracts/interchain-token-service/src/contract/execute.rs b/contracts/interchain-token-service/src/contract/execute.rs index 87b611513..36f57d9d5 100644 --- a/contracts/interchain-token-service/src/contract/execute.rs +++ b/contracts/interchain-token-service/src/contract/execute.rs @@ -1,5 +1,6 @@ +use axelar_core_std::nexus; use axelar_wasm_std::{nonempty, FnExt, IntoContractError}; -use cosmwasm_std::{DepsMut, HexBinary, QuerierWrapper, Response, Storage}; +use cosmwasm_std::{Coin, DepsMut, HexBinary, QuerierWrapper, Response, Storage, Uint128}; use error_stack::{bail, ensure, report, Result, ResultExt}; use router_api::{Address, ChainName, ChainNameRaw, CrossChainId}; use sha3::{Digest, Keccak256}; @@ -7,7 +8,7 @@ use sha3::{Digest, Keccak256}; use crate::events::Event; use crate::primitives::HubMessage; use crate::state::{self, load_config, load_its_contract}; -use crate::TokenId; +use crate::{Message, TokenId}; // this is just keccak256("its-interchain-token-id-gateway") const GATEWAY_TOKEN_PREFIX: [u8; 32] = [ @@ -31,8 +32,21 @@ pub enum Error { FailedItsContractDeregistration(ChainNameRaw), #[error("failed to register gateway token")] FailedGatewayTokenRegistration, + #[error("failed to execute message")] + FailedExecuteMessage, #[error("failed to generate token id")] FailedTokenIdGeneration, + #[error("transfer amount exceeds Uint128 max")] + TransferAmountOverflow, + #[error("attached coin {attached:?} does not match expected coin {expected:?}")] + IncorrectAttachedCoin { + attached: Option, + expected: Option, + }, + #[error("failed to query nexus")] + NexusQueryError, + #[error("storage error")] + StorageError, } /// Executes an incoming ITS message. @@ -45,6 +59,7 @@ pub fn execute_message( cc_id: CrossChainId, source_address: Address, payload: HexBinary, + coin: Option, ) -> Result { ensure_its_source_address(deps.storage, &cc_id.source_chain, &source_address)?; @@ -62,12 +77,15 @@ pub fn execute_message( } .abi_encode(); + verify_coin(&deps, &coin, &message, &cc_id.source_chain)?; + Ok(send_to_destination( deps.storage, deps.querier, destination_chain.clone(), destination_address, destination_payload, + gateway_token_transfer(&deps, &destination_chain, &message)?, )? .add_event( Event::MessageReceived { @@ -103,20 +121,71 @@ fn ensure_its_source_address( Ok(()) } +fn verify_coin( + deps: &DepsMut, + coin: &Option, + message: &Message, + source_chain: &ChainNameRaw, +) -> Result<(), Error> { + let expected_coin = gateway_token_transfer(deps, source_chain, message)?; + ensure!( + &expected_coin == coin, + Error::IncorrectAttachedCoin { + attached: coin.clone(), + expected: expected_coin + } + ); + Ok(()) +} + +fn gateway_token_transfer( + deps: &DepsMut, + chain: &ChainNameRaw, + message: &Message, +) -> Result, Error> { + let client: nexus::Client = client::CosmosClient::new(deps.querier).into(); + let is_core_chain = client + .is_chain_registered(&normalize(chain)) + .change_context(Error::NexusQueryError)?; + + if !is_core_chain { + return Ok(None); + } + + let token_id = message.token_id(); + let gateway_denom = state::may_load_gateway_denom(deps.storage, token_id) + .change_context(Error::StorageError)?; + match (gateway_denom, message) { + (Some(denom), Message::InterchainTransfer { amount, .. }) => Ok(Some(Coin { + denom: denom.to_string(), + amount: Uint128::try_from(*amount).change_context(Error::TransferAmountOverflow)?, + })), + _ => Ok(None), + } +} + fn send_to_destination( storage: &dyn Storage, querier: QuerierWrapper, destination_chain: ChainNameRaw, destination_address: Address, payload: HexBinary, + coin: Option, ) -> Result { let config = load_config(storage); let gateway: axelarnet_gateway::Client = client::ContractClient::new(querier, &config.axelarnet_gateway).into(); - let call_contract_msg = - gateway.call_contract(normalize(&destination_chain), destination_address, payload); + let call_contract_msg = match coin { + Some(coin) => gateway.call_contract_with_token( + normalize(&destination_chain), + destination_address, + payload, + coin, + ), + None => gateway.call_contract(normalize(&destination_chain), destination_address, payload), + }; Ok(Response::new().add_message(call_contract_msg)) } @@ -167,15 +236,31 @@ pub fn gateway_token_id(deps: &DepsMut, denom: &str) -> Result { #[cfg(test)] mod tests { + use std::marker::PhantomData; + use assert_ok::assert_ok; + use axelar_core_std::nexus; + use axelar_core_std::nexus::query::IsChainRegisteredResponse; + use axelar_core_std::query::AxelarQueryMsg; use axelar_wasm_std::assert_err_contains; + use axelar_wasm_std::msg_id::HexTxHashAndEventIndex; use axelarnet_gateway::msg::QueryMsg; - use cosmwasm_std::testing::{mock_dependencies, MockApi, MockQuerier}; - use cosmwasm_std::{from_json, to_json_binary, Addr, MemoryStorage, OwnedDeps, WasmQuery}; - use router_api::{ChainName, ChainNameRaw}; + use cosmwasm_std::testing::{MockApi, MockQuerier, MockStorage}; + use cosmwasm_std::{ + from_json, to_json_binary, Addr, Coin, CosmosMsg, HexBinary, MemoryStorage, OwnedDeps, + Uint128, Uint256, WasmMsg, WasmQuery, + }; + use router_api::{ChainName, ChainNameRaw, CrossChainId}; use super::{gateway_token_id, register_gateway_token, Error}; + use crate::contract::execute::{execute_message, register_its_contract}; use crate::state::{self, Config}; + use crate::{HubMessage, Message}; + + const CORE_CHAIN: &str = "ethereum"; + const AMPLIFIER_CHAIN: &str = "solana"; + const GATEWAY_TOKEN_DENOM: &str = "eth"; + const ITS_ADDRESS: &str = "68d30f47F19c07bCCEf4Ac7FAE2Dc12FCa3e0dC9"; #[test] fn gateway_token_id_should_be_idempotent() { @@ -222,9 +307,553 @@ mod tests { ); } - fn init() -> OwnedDeps { + #[test] + fn should_lock_and_unlock_gateway_token() { + let mut deps = init(); + register_token_and_its_contracts(&mut deps); + + let destination_chain = ChainNameRaw::try_from(AMPLIFIER_CHAIN).unwrap(); + let source_chain = ChainNameRaw::try_from(CORE_CHAIN).unwrap(); + let source_address = + HexBinary::from_hex("4838B106FCe9647Bdf1E7877BF73cE8B0BAD5f97").unwrap(); + + let token_id = gateway_token_id(&deps.as_mut(), GATEWAY_TOKEN_DENOM).unwrap(); + + let coin = Coin { + denom: GATEWAY_TOKEN_DENOM.to_string(), + amount: Uint128::from(1500u128), + }; + + let msg = HubMessage::SendToHub { + destination_chain: destination_chain.clone(), + message: Message::InterchainTransfer { + token_id: token_id.clone(), + source_address: source_address.clone(), + destination_address: HexBinary::from_hex(ITS_ADDRESS).unwrap(), + amount: coin.amount.into(), + data: HexBinary::from_hex("").unwrap(), + }, + }; + assert_ok!(execute_message( + deps.as_mut(), + CrossChainId { + source_chain: source_chain.clone(), + message_id: HexTxHashAndEventIndex::new([1u8; 32], 0u32) + .to_string() + .try_into() + .unwrap(), + }, + ITS_ADDRESS.to_string().try_into().unwrap(), + msg.abi_encode(), + Some(coin.clone()), + )); + + let msg = HubMessage::SendToHub { + destination_chain: source_chain.clone(), + message: Message::InterchainTransfer { + token_id, + source_address: source_address.clone(), + destination_address: HexBinary::from_hex(ITS_ADDRESS).unwrap(), + amount: coin.amount.into(), + data: HexBinary::from_hex("").unwrap(), + }, + }; + + let res = assert_ok!(execute_message( + deps.as_mut(), + CrossChainId { + source_chain: destination_chain.clone(), + message_id: HexTxHashAndEventIndex::new([1u8; 32], 0u32) + .to_string() + .try_into() + .unwrap(), + }, + ITS_ADDRESS.to_string().try_into().unwrap(), + msg.abi_encode(), + None, + )); + + match &res.messages[0].msg { + CosmosMsg::Wasm(WasmMsg::Execute { funds, .. }) => { + assert_eq!(funds.len(), 1); + assert_eq!(funds.first().unwrap(), &coin); + } + _ => panic!("incorrect msg type"), + }; + } + + #[test] + fn should_reject_transfer_if_token_id_does_not_match() { + let mut deps = init(); + register_token_and_its_contracts(&mut deps); + + let destination_chain = ChainNameRaw::try_from(AMPLIFIER_CHAIN).unwrap(); + let source_chain = ChainNameRaw::try_from(CORE_CHAIN).unwrap(); + let source_address = + HexBinary::from_hex("4838B106FCe9647Bdf1E7877BF73cE8B0BAD5f97").unwrap(); + + let coin = Coin { + denom: GATEWAY_TOKEN_DENOM.to_string(), + amount: Uint128::from(1500u128), + }; + + let msg = HubMessage::SendToHub { + destination_chain: destination_chain.clone(), + message: Message::InterchainTransfer { + token_id: [0u8; 32].into(), + source_address: source_address.clone(), + destination_address: HexBinary::from_hex(ITS_ADDRESS).unwrap(), + amount: coin.amount.into(), + data: HexBinary::from_hex("").unwrap(), + }, + }; + let res = execute_message( + deps.as_mut(), + CrossChainId { + source_chain: source_chain.clone(), + message_id: HexTxHashAndEventIndex::new([1u8; 32], 0u32) + .to_string() + .try_into() + .unwrap(), + }, + ITS_ADDRESS.to_string().try_into().unwrap(), + msg.abi_encode(), + Some(coin.clone()), + ); + assert!(res.is_err()); + assert_eq!( + res.unwrap_err().to_string(), + Error::IncorrectAttachedCoin { + attached: Some(coin), + expected: None + } + .to_string() + ); + } + + #[test] + fn should_reject_transfer_if_amount_does_not_match_attached_token() { + let mut deps = init(); + register_token_and_its_contracts(&mut deps); + + let destination_chain = ChainNameRaw::try_from(AMPLIFIER_CHAIN).unwrap(); + let source_chain = ChainNameRaw::try_from(CORE_CHAIN).unwrap(); + let source_address = + HexBinary::from_hex("4838B106FCe9647Bdf1E7877BF73cE8B0BAD5f97").unwrap(); + + let token_id = gateway_token_id(&deps.as_mut(), GATEWAY_TOKEN_DENOM).unwrap(); + + let coin = Coin { + denom: GATEWAY_TOKEN_DENOM.to_string(), + amount: Uint128::from(1500u128), + }; + + let amount_in_msg = coin.amount.strict_sub(Uint128::one()); + + let msg = HubMessage::SendToHub { + destination_chain: destination_chain.clone(), + message: Message::InterchainTransfer { + token_id: token_id.clone(), + source_address: source_address.clone(), + destination_address: HexBinary::from_hex(ITS_ADDRESS).unwrap(), + amount: amount_in_msg.into(), + data: HexBinary::from_hex("").unwrap(), + }, + }; + let res = execute_message( + deps.as_mut(), + CrossChainId { + source_chain: source_chain.clone(), + message_id: HexTxHashAndEventIndex::new([1u8; 32], 0u32) + .to_string() + .try_into() + .unwrap(), + }, + ITS_ADDRESS.to_string().try_into().unwrap(), + msg.abi_encode(), + Some(coin.clone()), + ); + assert!(res.is_err()); + assert_eq!( + res.unwrap_err().to_string(), + Error::IncorrectAttachedCoin { + attached: Some(coin.clone()), + expected: Some(Coin { + denom: coin.denom, + amount: amount_in_msg + }) + } + .to_string() + ); + } + + #[test] + fn should_reject_transfer_with_token_if_source_chain_is_not_core() { + let mut deps = init(); + register_token_and_its_contracts(&mut deps); + + let destination_chain = ChainNameRaw::try_from(CORE_CHAIN).unwrap(); + let source_chain = ChainNameRaw::try_from(AMPLIFIER_CHAIN).unwrap(); + let source_address = + HexBinary::from_hex("4838B106FCe9647Bdf1E7877BF73cE8B0BAD5f97").unwrap(); + + let token_id = gateway_token_id(&deps.as_mut(), GATEWAY_TOKEN_DENOM).unwrap(); + + let coin = Coin { + denom: GATEWAY_TOKEN_DENOM.to_string(), + amount: Uint128::from(1500u128), + }; + + let msg = HubMessage::SendToHub { + destination_chain: destination_chain.clone(), + message: Message::InterchainTransfer { + token_id: token_id.clone(), + source_address: source_address.clone(), + destination_address: HexBinary::from_hex(ITS_ADDRESS).unwrap(), + amount: coin.amount.into(), + data: HexBinary::from_hex("").unwrap(), + }, + }; + let res = execute_message( + deps.as_mut(), + CrossChainId { + source_chain: source_chain.clone(), + message_id: HexTxHashAndEventIndex::new([1u8; 32], 0u32) + .to_string() + .try_into() + .unwrap(), + }, + ITS_ADDRESS.to_string().try_into().unwrap(), + msg.abi_encode(), + Some(coin.clone()), + ); + assert!(res.is_err()); + assert_eq!( + res.unwrap_err().to_string(), + Error::IncorrectAttachedCoin { + attached: Some(coin), + expected: None + } + .to_string() + ); + } + + #[test] + fn should_reject_transfer_if_attached_token_is_not_registered() { + let mut deps = init(); + register_token_and_its_contracts(&mut deps); + + let destination_chain = ChainNameRaw::try_from(AMPLIFIER_CHAIN).unwrap(); + let source_chain = ChainNameRaw::try_from(CORE_CHAIN).unwrap(); + let source_address = + HexBinary::from_hex("4838B106FCe9647Bdf1E7877BF73cE8B0BAD5f97").unwrap(); + + let denom = "foobar"; + let token_id = gateway_token_id(&deps.as_mut(), denom).unwrap(); + + let coin = Coin { + denom: denom.to_string(), + amount: Uint128::from(1500u128), + }; + + let msg = HubMessage::SendToHub { + destination_chain: destination_chain.clone(), + message: Message::InterchainTransfer { + token_id: token_id.clone(), + source_address: source_address.clone(), + destination_address: HexBinary::from_hex(ITS_ADDRESS).unwrap(), + amount: coin.amount.into(), + data: HexBinary::from_hex("").unwrap(), + }, + }; + let res = execute_message( + deps.as_mut(), + CrossChainId { + source_chain: source_chain.clone(), + message_id: HexTxHashAndEventIndex::new([1u8; 32], 0u32) + .to_string() + .try_into() + .unwrap(), + }, + ITS_ADDRESS.to_string().try_into().unwrap(), + msg.abi_encode(), + Some(coin.clone()), + ); + assert!(res.is_err()); + assert_eq!( + res.unwrap_err().to_string(), + Error::IncorrectAttachedCoin { + attached: Some(coin), + expected: None + } + .to_string() + ); + } + + #[test] + fn should_not_attach_coin_if_destination_is_not_core() { + let mut deps = init(); + register_token_and_its_contracts(&mut deps); + + let destination_chain = ChainNameRaw::try_from(AMPLIFIER_CHAIN).unwrap(); + let source_chain = ChainNameRaw::try_from(CORE_CHAIN).unwrap(); + let source_address = + HexBinary::from_hex("4838B106FCe9647Bdf1E7877BF73cE8B0BAD5f97").unwrap(); + + let token_id = gateway_token_id(&deps.as_mut(), GATEWAY_TOKEN_DENOM).unwrap(); + + let coin = Coin { + denom: GATEWAY_TOKEN_DENOM.to_string(), + amount: Uint128::from(1500u128), + }; + + // send the token from core to an amplifier chain, should be escrowed + let msg = HubMessage::SendToHub { + destination_chain: destination_chain.clone(), + message: Message::InterchainTransfer { + token_id: token_id.clone(), + source_address: source_address.clone(), + destination_address: HexBinary::from_hex(ITS_ADDRESS).unwrap(), + amount: coin.amount.into(), + data: HexBinary::from_hex("").unwrap(), + }, + }; + let res = assert_ok!(execute_message( + deps.as_mut(), + CrossChainId { + source_chain: source_chain.clone(), + message_id: HexTxHashAndEventIndex::new([1u8; 32], 0u32) + .to_string() + .try_into() + .unwrap(), + }, + ITS_ADDRESS.to_string().try_into().unwrap(), + msg.abi_encode(), + Some(coin.clone()), + )); + + // no tokens attached, token is encoded purely as GMP + assert_eq!(res.messages.len(), 1); + match &res.messages[0].msg { + CosmosMsg::Wasm(WasmMsg::Execute { funds, .. }) => assert_eq!(funds.len(), 0), + _ => panic!("incorrect msg type"), + }; + + // now send from amplifier chain to another amplifier chain + let second_destination_chain = ChainNameRaw::try_from("xrpl").unwrap(); + assert_ok!(register_its_contract( + deps.as_mut(), + second_destination_chain.clone(), + ITS_ADDRESS.to_string().try_into().unwrap(), + )); + + let msg = HubMessage::SendToHub { + destination_chain: second_destination_chain.clone(), + message: Message::InterchainTransfer { + token_id, + source_address: source_address.clone(), + destination_address: HexBinary::from_hex(ITS_ADDRESS).unwrap(), + amount: coin.amount.into(), + data: HexBinary::from_hex("").unwrap(), + }, + }; + + let res = assert_ok!(execute_message( + deps.as_mut(), + CrossChainId { + source_chain: destination_chain.clone(), + message_id: HexTxHashAndEventIndex::new([1u8; 32], 0u32) + .to_string() + .try_into() + .unwrap(), + }, + ITS_ADDRESS.to_string().try_into().unwrap(), + msg.abi_encode(), + None, + )); + + // no tokens should be attached + assert_eq!(res.messages.len(), 1); + match &res.messages[0].msg { + CosmosMsg::Wasm(WasmMsg::Execute { funds, .. }) => assert_eq!(funds.len(), 0), + _ => panic!("incorrect msg type"), + }; + } + + #[test] + fn can_send_pure_gmp_from_core() { + let mut deps = init(); + register_token_and_its_contracts(&mut deps); + + let destination_chain = ChainNameRaw::try_from(AMPLIFIER_CHAIN).unwrap(); + let source_chain = ChainNameRaw::try_from(CORE_CHAIN).unwrap(); + let source_address = + HexBinary::from_hex("4838B106FCe9647Bdf1E7877BF73cE8B0BAD5f97").unwrap(); + + let denom = "wBTC"; + + let token_id = gateway_token_id(&deps.as_mut(), denom).unwrap(); + + // send the token from core to an amplifier chain, should be escrowed + let msg = HubMessage::SendToHub { + destination_chain: destination_chain.clone(), + message: Message::InterchainTransfer { + token_id: token_id.clone(), + source_address: source_address.clone(), + destination_address: HexBinary::from_hex(ITS_ADDRESS).unwrap(), + amount: Uint256::one(), + data: HexBinary::from_hex("").unwrap(), + }, + }; + + assert_ok!(execute_message( + deps.as_mut(), + CrossChainId { + source_chain: source_chain.clone(), + message_id: HexTxHashAndEventIndex::new([1u8; 32], 0u32) + .to_string() + .try_into() + .unwrap(), + }, + ITS_ADDRESS.to_string().try_into().unwrap(), + msg.abi_encode(), + None, + )); + } + + #[test] + fn should_reject_transfer_from_core_if_gateway_token_is_not_attached() { + let mut deps = init(); + register_token_and_its_contracts(&mut deps); + + let destination_chain = ChainNameRaw::try_from(AMPLIFIER_CHAIN).unwrap(); + let source_chain = ChainNameRaw::try_from(CORE_CHAIN).unwrap(); + let source_address = + HexBinary::from_hex("4838B106FCe9647Bdf1E7877BF73cE8B0BAD5f97").unwrap(); + + let token_id = gateway_token_id(&deps.as_mut(), GATEWAY_TOKEN_DENOM).unwrap(); + + let coin = Coin { + denom: GATEWAY_TOKEN_DENOM.to_string(), + amount: Uint128::from(1500u128), + }; + + let msg = HubMessage::SendToHub { + destination_chain: destination_chain.clone(), + message: Message::InterchainTransfer { + token_id: token_id.clone(), + source_address: source_address.clone(), + destination_address: HexBinary::from_hex(ITS_ADDRESS).unwrap(), + amount: coin.amount.into(), + data: HexBinary::from_hex("").unwrap(), + }, + }; + let res = execute_message( + deps.as_mut(), + CrossChainId { + source_chain: source_chain.clone(), + message_id: HexTxHashAndEventIndex::new([1u8; 32], 0u32) + .to_string() + .try_into() + .unwrap(), + }, + ITS_ADDRESS.to_string().try_into().unwrap(), + msg.abi_encode(), + None, + ); + assert!(res.is_err()); + assert_eq!( + res.unwrap_err().to_string(), + Error::IncorrectAttachedCoin { + attached: None, + expected: Some(coin) + } + .to_string() + ); + } + + #[test] + fn should_reject_message_if_coin_attached_but_not_interchain_transfer() { + let mut deps = init(); + register_token_and_its_contracts(&mut deps); + + let destination_chain = ChainNameRaw::try_from(AMPLIFIER_CHAIN).unwrap(); + let source_chain = ChainNameRaw::try_from(CORE_CHAIN).unwrap(); + + let token_id = gateway_token_id(&deps.as_mut(), GATEWAY_TOKEN_DENOM).unwrap(); + + let coin = Coin { + denom: GATEWAY_TOKEN_DENOM.to_string(), + amount: Uint128::from(1500u128), + }; + + let msg = HubMessage::SendToHub { + destination_chain: destination_chain.clone(), + message: Message::DeployInterchainToken { + token_id: token_id.clone(), + name: "foobar".to_string(), + symbol: "FOO".to_string(), + decimals: 10u8, + minter: HexBinary::from([0u8; 32]), + }, + }; + let res = execute_message( + deps.as_mut(), + CrossChainId { + source_chain: source_chain.clone(), + message_id: HexTxHashAndEventIndex::new([1u8; 32], 0u32) + .to_string() + .try_into() + .unwrap(), + }, + ITS_ADDRESS.to_string().try_into().unwrap(), + msg.abi_encode(), + Some(coin.clone()), + ); + assert!(res.is_err()); + assert_eq!( + res.unwrap_err().to_string(), + Error::IncorrectAttachedCoin { + expected: None, + attached: Some(coin) + } + .to_string() + ); + } + + fn register_token_and_its_contracts( + deps: &mut OwnedDeps>, + ) { + let amplifier_chain = ChainNameRaw::try_from(AMPLIFIER_CHAIN).unwrap(); + let core_chain = ChainNameRaw::try_from(CORE_CHAIN).unwrap(); + + assert_ok!(register_gateway_token( + deps.as_mut(), + GATEWAY_TOKEN_DENOM.try_into().unwrap(), + core_chain.clone() + )); + + assert_ok!(register_its_contract( + deps.as_mut(), + core_chain.clone(), + ITS_ADDRESS.to_string().try_into().unwrap(), + )); + + assert_ok!(register_its_contract( + deps.as_mut(), + amplifier_chain.clone(), + ITS_ADDRESS.to_string().try_into().unwrap(), + )); + } + + fn init() -> OwnedDeps> { let addr = Addr::unchecked("axelar-gateway"); - let mut deps = mock_dependencies(); + let mut deps = OwnedDeps { + storage: MockStorage::default(), + api: MockApi::default(), + querier: MockQuerier::::new(&[]), + custom_query_type: PhantomData, + }; state::save_config( deps.as_mut().storage, &Config { @@ -233,7 +862,7 @@ mod tests { ) .unwrap(); - let mut querier = MockQuerier::default(); + let mut querier = MockQuerier::::new(&[]); querier.update_wasm(move |msg| match msg { WasmQuery::Smart { contract_addr, msg } if contract_addr == &addr.to_string() => { let msg = from_json::(msg).unwrap(); @@ -246,6 +875,18 @@ mod tests { } _ => panic!("unexpected query: {:?}", msg), }); + querier = querier.with_custom_handler(|msg| match msg { + AxelarQueryMsg::Nexus(nexus::query::QueryMsg::IsChainRegistered { chain }) => { + Ok(to_json_binary( + &(IsChainRegisteredResponse { + is_registered: chain == CORE_CHAIN, + }), + ) + .into()) + .into() + } + _ => panic!("unsupported query"), + }); deps.querier = querier; deps diff --git a/contracts/interchain-token-service/src/primitives.rs b/contracts/interchain-token-service/src/primitives.rs index 2a645a39b..205cd4cce 100644 --- a/contracts/interchain-token-service/src/primitives.rs +++ b/contracts/interchain-token-service/src/primitives.rs @@ -2,7 +2,7 @@ use std::fmt::Display; use cosmwasm_schema::cw_serde; use cosmwasm_std::{HexBinary, Uint256}; -use cw_storage_plus::{Key, KeyDeserialize, PrimaryKey}; +use cw_storage_plus::{Key, KeyDeserialize, Prefixer, PrimaryKey}; use router_api::ChainNameRaw; use strum::FromRepr; @@ -105,6 +105,23 @@ impl HubMessage { } } +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(), + } + } + + pub fn transfer_amount(&self) -> Option { + match self { + Message::InterchainTransfer { amount, .. } => Some(amount.to_owned()), + _ => None, + } + } +} + impl TokenId { #[inline(always)] pub fn new(id: [u8; 32]) -> Self { @@ -144,3 +161,9 @@ impl KeyDeserialize for TokenId { Ok(TokenId(inner)) } } + +impl<'a> Prefixer<'a> for TokenId { + fn prefix(&self) -> Vec { + self.key() + } +} diff --git a/contracts/interchain-token-service/src/state.rs b/contracts/interchain-token-service/src/state.rs index f98147223..5928ab467 100644 --- a/contracts/interchain-token-service/src/state.rs +++ b/contracts/interchain-token-service/src/state.rs @@ -96,6 +96,13 @@ pub fn save_gateway_token_denom( Ok(()) } +pub fn may_load_gateway_denom( + storage: &dyn Storage, + token_id: TokenId, +) -> Result, Error> { + Ok(GATEWAY_TOKEN_DENOMS.may_load(storage, token_id)?) +} + pub fn load_all_gateway_tokens( storage: &dyn Storage, ) -> Result, Error> { diff --git a/contracts/interchain-token-service/tests/execute.rs b/contracts/interchain-token-service/tests/execute.rs index 291f9e0a7..16e2b8957 100644 --- a/contracts/interchain-token-service/tests/execute.rs +++ b/contracts/interchain-token-service/tests/execute.rs @@ -9,7 +9,7 @@ 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; +use utils::{make_deps, TestMessage}; mod utils; @@ -78,7 +78,7 @@ fn deregistering_unknown_chain_fails() { #[test] fn execute_hub_message_succeeds() { - let mut deps = mock_dependencies(); + let mut deps = make_deps(); utils::instantiate_contract(deps.as_mut()).unwrap(); let TestMessage { diff --git a/contracts/interchain-token-service/tests/utils/execute.rs b/contracts/interchain-token-service/tests/utils/execute.rs index c43538039..8dda5fbd5 100644 --- a/contracts/interchain-token-service/tests/utils/execute.rs +++ b/contracts/interchain-token-service/tests/utils/execute.rs @@ -1,9 +1,17 @@ +use std::marker::PhantomData; + +use axelar_core_std::nexus; +use axelar_core_std::nexus::query::IsChainRegisteredResponse; +use axelar_core_std::query::AxelarQueryMsg; use axelar_wasm_std::error::ContractError; -use cosmwasm_std::testing::{mock_env, mock_info}; -use cosmwasm_std::{DepsMut, HexBinary, Response}; +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, +}; use interchain_token_service::contract; use interchain_token_service::msg::ExecuteMsg; -use router_api::{Address, ChainNameRaw, CrossChainId}; +use router_api::{Address, ChainName, ChainNameRaw, CrossChainId}; use crate::utils::params; @@ -49,3 +57,42 @@ pub fn deregister_its_contract( ExecuteMsg::DeregisterItsContract { chain }, ) } + +pub fn make_deps() -> OwnedDeps> { + let addr = Addr::unchecked(params::GATEWAY); + let mut deps = OwnedDeps { + storage: MockStorage::default(), + api: MockApi::default(), + querier: MockQuerier::::new(&[]), + custom_query_type: PhantomData, + }; + + let mut querier = MockQuerier::::new(&[]); + querier.update_wasm(move |msg| match msg { + WasmQuery::Smart { contract_addr, msg } if contract_addr == &addr.to_string() => { + let msg = from_json::(msg).unwrap(); + match msg { + axelarnet_gateway::msg::QueryMsg::ChainName {} => { + Ok(to_json_binary(&ChainName::try_from("axelar").unwrap()).into()).into() + } + _ => panic!("unsupported query"), + } + } + _ => panic!("unexpected query: {:?}", msg), + }); + querier = querier.with_custom_handler(|msg| match msg { + AxelarQueryMsg::Nexus(nexus::query::QueryMsg::IsChainRegistered { chain }) => { + Ok(to_json_binary( + &(IsChainRegisteredResponse { + is_registered: chain == "ethereum", + }), + ) + .into()) + .into() + } + _ => panic!("unsupported query"), + }); + + deps.querier = querier; + deps +} diff --git a/packages/client/src/lib.rs b/packages/client/src/lib.rs index 80fc75a0b..28aaf7da8 100644 --- a/packages/client/src/lib.rs +++ b/packages/client/src/lib.rs @@ -2,8 +2,8 @@ use std::marker::PhantomData; use std::ops::Deref; use cosmwasm_std::{ - to_json_binary, Addr, CosmosMsg, CustomQuery, Empty, QuerierWrapper, QueryRequest, StdError, - WasmMsg, WasmQuery, + to_json_binary, Addr, Coin, CosmosMsg, CustomQuery, Empty, QuerierWrapper, QueryRequest, + StdError, WasmMsg, WasmQuery, }; use error_stack::{Report, Result}; use serde::de::DeserializeOwned; @@ -81,6 +81,14 @@ where }) } + pub fn execute_with_funds(&self, msg: &M, coin: Coin) -> CosmosMsg { + self.inner.execute(WasmMsg::Execute { + contract_addr: self.address.to_string(), + msg: to_json_binary(msg).expect("msg should always be serializable"), + funds: vec![coin], + }) + } + pub fn query(&self, msg: &Q) -> Result where R: DeserializeOwned,