diff --git a/contracts/interchain-token-service/src/contract/execute/interceptors.rs b/contracts/interchain-token-service/src/contract/execute/interceptors.rs index c0f70726d..323008d35 100644 --- a/contracts/interchain-token-service/src/contract/execute/interceptors.rs +++ b/contracts/interchain-token-service/src/contract/execute/interceptors.rs @@ -3,52 +3,58 @@ use cosmwasm_std::{Storage, Uint256}; use error_stack::{bail, ensure, report, Result, ResultExt}; use router_api::ChainNameRaw; -use super::Error; +use super::{ChainSpecifier, Error}; use crate::state::{self, TokenDeploymentType}; use crate::{DeployInterchainToken, InterchainTransfer, TokenConfig, TokenId, TokenInstance}; -pub fn subtract_supply_amount( +pub fn subtract_supply_amount_if_amplifier_chain( storage: &mut dyn Storage, - chain: &ChainNameRaw, + chain: &ChainSpecifier, transfer: &InterchainTransfer, ) -> Result<(), Error> { - let mut token = try_load_token_instance(storage, chain.clone(), transfer.token_id)?; + if !chain.is_amplifier_chain { + return Ok(()); + } + let mut token = try_load_token_instance(storage, chain.name.clone(), transfer.token_id)?; token.supply = token .supply .checked_sub(transfer.amount) .change_context_lazy(|| Error::TokenSupplyInvariantViolated { token_id: transfer.token_id, - chain: chain.clone(), + chain: chain.name.clone(), })?; - state::save_token_instance(storage, chain.clone(), transfer.token_id, &token) + state::save_token_instance(storage, chain.name.clone(), transfer.token_id, &token) .change_context(Error::State) } -pub fn add_supply_amount( +pub fn add_supply_amount_if_amplifier_chain( storage: &mut dyn Storage, - chain: &ChainNameRaw, + chain: &ChainSpecifier, transfer: &InterchainTransfer, ) -> Result<(), Error> { - let mut token = try_load_token_instance(storage, chain.clone(), transfer.token_id)?; + if !chain.is_amplifier_chain { + return Ok(()); + } + let mut token = try_load_token_instance(storage, chain.name.clone(), transfer.token_id)?; token.supply = token .supply .checked_add(transfer.amount) .change_context_lazy(|| Error::TokenSupplyInvariantViolated { token_id: transfer.token_id, - chain: chain.clone(), + chain: chain.name.clone(), })?; - state::save_token_instance(storage, chain.clone(), transfer.token_id, &token) + state::save_token_instance(storage, chain.name.clone(), transfer.token_id, &token) .change_context(Error::State) } pub fn apply_scaling_factor_to_amount( storage: &dyn Storage, - source_chain: &ChainNameRaw, - destination_chain: &ChainNameRaw, + source_chain: &ChainSpecifier, + destination_chain: &ChainSpecifier, mut transfer: InterchainTransfer, ) -> Result { transfer.amount = destination_amount( @@ -186,6 +192,35 @@ fn try_load_token_instance( .ok_or(report!(Error::TokenNotDeployed { token_id, chain })) } +fn try_load_token_instance_or_default( + storage: &dyn Storage, + chain: &ChainSpecifier, + token_id: TokenId, +) -> Result { + let res = state::may_load_token_instance(storage, chain.name.clone(), token_id) + .change_context(Error::State)? + .ok_or(report!(Error::TokenNotDeployed { + token_id, + chain: chain.name.clone(), + })); + if res.is_err() && !chain.is_amplifier_chain { + return try_load_origin_chain_token_instance(storage, token_id); + } + res +} + +fn try_load_origin_chain_token_instance( + storage: &dyn Storage, + token_id: TokenId, +) -> Result { + state::may_load_token_config(storage, &token_id) + .change_context(Error::State)? + .ok_or(report!(Error::TokenConfigNotFound(token_id))) + .and_then(|token_config| { + try_load_token_instance(storage, token_config.origin_chain, token_id) + }) +} + /// Calculates the destination token decimals. /// /// The destination chain's token decimals are calculated and saved as following: @@ -229,13 +264,14 @@ fn destination_token_decimals( /// 4) If new_amount is zero, the translation fails. fn destination_amount( storage: &dyn Storage, - source_chain: &ChainNameRaw, - destination_chain: &ChainNameRaw, + source_chain: &ChainSpecifier, + destination_chain: &ChainSpecifier, token_id: TokenId, source_amount: nonempty::Uint256, ) -> Result { - let source_token = try_load_token_instance(storage, source_chain.clone(), token_id)?; - let destination_token = try_load_token_instance(storage, destination_chain.clone(), token_id)?; + let source_token = try_load_token_instance_or_default(storage, source_chain, token_id)?; + let destination_token = + try_load_token_instance_or_default(storage, destination_chain, token_id)?; let (source_decimals, destination_decimals) = (source_token.decimals, destination_token.decimals); @@ -244,7 +280,7 @@ fn destination_amount( return Ok(source_amount); } - let destination_max_uint = state::load_chain_config(storage, destination_chain) + let destination_max_uint = state::load_chain_config(storage, &destination_chain.name) .change_context(Error::State)? .truncation .max_uint; @@ -255,8 +291,8 @@ fn destination_amount( let scaling_factor = Uint256::from_u128(10) .checked_pow(source_decimals.abs_diff(destination_decimals).into()) .change_context_lazy(|| Error::InvalidTransferAmount { - source_chain: source_chain.to_owned(), - destination_chain: destination_chain.to_owned(), + source_chain: source_chain.name.to_owned(), + destination_chain: destination_chain.name.to_owned(), amount: source_amount, })?; let destination_amount = if source_decimals > destination_decimals { @@ -267,24 +303,24 @@ fn destination_amount( source_amount .checked_mul(scaling_factor) .change_context_lazy(|| Error::InvalidTransferAmount { - source_chain: source_chain.to_owned(), - destination_chain: destination_chain.to_owned(), + source_chain: source_chain.name.to_owned(), + destination_chain: destination_chain.name.to_owned(), amount: source_amount, })? }; if destination_amount.gt(&destination_max_uint) { bail!(Error::InvalidTransferAmount { - source_chain: source_chain.to_owned(), - destination_chain: destination_chain.to_owned(), + source_chain: source_chain.name.to_owned(), + destination_chain: destination_chain.name.to_owned(), amount: source_amount, }) } nonempty::Uint256::try_from(destination_amount).change_context_lazy(|| { Error::InvalidTransferAmount { - source_chain: source_chain.to_owned(), - destination_chain: destination_chain.to_owned(), + source_chain: source_chain.name.to_owned(), + destination_chain: destination_chain.name.to_owned(), amount: source_amount, } }) @@ -313,7 +349,7 @@ mod test { use router_api::ChainNameRaw; use super::Error; - use crate::contract::execute::interceptors; + use crate::contract::execute::{interceptors, ChainSpecifier}; use crate::msg::TruncationConfig; use crate::state::{self, TokenDeploymentType}; use crate::{msg, DeployInterchainToken, InterchainTransfer, TokenInstance}; @@ -361,8 +397,14 @@ mod test { let transfer = assert_ok!(interceptors::apply_scaling_factor_to_amount( &storage, - &source_chain, - &destination_chain, + &crate::contract::execute::ChainSpecifier { + name: source_chain, + is_amplifier_chain: true + }, + &crate::contract::execute::ChainSpecifier { + name: destination_chain, + is_amplifier_chain: true + }, transfer, )); assert_eq!( @@ -414,8 +456,14 @@ mod test { let transfer = assert_ok!(interceptors::apply_scaling_factor_to_amount( &storage, - &source_chain, - &destination_chain, + &ChainSpecifier { + name: source_chain, + is_amplifier_chain: true + }, + &ChainSpecifier { + name: destination_chain, + is_amplifier_chain: true + }, transfer, )); assert_eq!( @@ -467,8 +515,14 @@ mod test { let transfer = assert_ok!(interceptors::apply_scaling_factor_to_amount( &storage, - &source_chain, - &destination_chain, + &ChainSpecifier { + name: source_chain, + is_amplifier_chain: true + }, + &ChainSpecifier { + name: destination_chain, + is_amplifier_chain: true + }, transfer, )); assert_eq!( @@ -521,8 +575,14 @@ mod test { assert_err_contains!( interceptors::apply_scaling_factor_to_amount( &storage, - &source_chain, - &destination_chain, + &ChainSpecifier { + name: source_chain, + is_amplifier_chain: true + }, + &ChainSpecifier { + name: destination_chain, + is_amplifier_chain: true + }, transfer, ), Error, @@ -574,8 +634,14 @@ mod test { assert_err_contains!( interceptors::apply_scaling_factor_to_amount( &storage, - &source_chain, - &destination_chain, + &ChainSpecifier { + name: source_chain, + is_amplifier_chain: true + }, + &ChainSpecifier { + name: destination_chain, + is_amplifier_chain: true + }, transfer, ), Error, diff --git a/contracts/interchain-token-service/src/contract/execute/mod.rs b/contracts/interchain-token-service/src/contract/execute/mod.rs index 3bbb4b925..3c8c3a790 100644 --- a/contracts/interchain-token-service/src/contract/execute/mod.rs +++ b/contracts/interchain-token-service/src/contract/execute/mod.rs @@ -1,3 +1,4 @@ +use axelar_core_std::nexus; use axelar_wasm_std::{killswitch, nonempty, FnExt, IntoContractError}; use cosmwasm_std::{DepsMut, HexBinary, QuerierWrapper, Response, Storage}; use error_stack::{bail, ensure, report, Result, ResultExt}; @@ -42,6 +43,9 @@ pub enum Error { State, #[error("chain {0} already registered")] ChainAlreadyRegistered(ChainNameRaw), + + #[error("token {0} config not found")] + TokenConfigNotFound(TokenId), #[error("token {token_id} not deployed on chain {chain}")] TokenNotDeployed { token_id: TokenId, @@ -72,6 +76,8 @@ pub enum Error { token_id: TokenId, chain: ChainNameRaw, }, + #[error("error querying nexus module for chain registration for chain {0}")] + Nexus(ChainNameRaw), } /// Executes an incoming ITS message. @@ -111,6 +117,7 @@ fn execute_message_on_hub( let message = apply_to_hub( deps.storage, + deps.querier, cc_id.source_chain.clone(), destination_chain.clone(), message, @@ -141,6 +148,7 @@ fn execute_message_on_hub( fn apply_to_hub( storage: &mut dyn Storage, + querier: QuerierWrapper, source_chain: ChainNameRaw, destination_chain: ChainNameRaw, message: Message, @@ -150,7 +158,7 @@ fn apply_to_hub( match message { Message::InterchainTransfer(transfer) => { - apply_to_transfer(storage, source_chain, destination_chain, transfer) + apply_to_transfer(storage, querier, source_chain, destination_chain, transfer) .map(Message::InterchainTransfer)? } Message::DeployInterchainToken(deploy_token) => { @@ -161,20 +169,43 @@ fn apply_to_hub( .then(Result::Ok) } +pub struct ChainSpecifier { + pub name: ChainNameRaw, + pub is_amplifier_chain: bool, +} + fn apply_to_transfer( storage: &mut dyn Storage, + querier: QuerierWrapper, source_chain: ChainNameRaw, destination_chain: ChainNameRaw, transfer: InterchainTransfer, ) -> Result { - interceptors::subtract_supply_amount(storage, &source_chain, &transfer)?; + // we only do balance tracking for amplifier chains + let client: nexus::Client = client::CosmosClient::new(querier).into(); + let source_chain = ChainSpecifier { + name: source_chain.clone(), + is_amplifier_chain: !client + .is_chain_registered(&source_chain.normalize()) + .change_context(Error::Nexus(source_chain.clone()))?, + }; + let destination_chain = ChainSpecifier { + name: destination_chain.clone(), + is_amplifier_chain: !client + .is_chain_registered(&destination_chain.normalize()) + .change_context(Error::Nexus(destination_chain.clone()))?, + }; + + interceptors::subtract_supply_amount_if_amplifier_chain(storage, &source_chain, &transfer)?; + let transfer = interceptors::apply_scaling_factor_to_amount( storage, &source_chain, &destination_chain, transfer, )?; - interceptors::add_supply_amount(storage, &destination_chain, &transfer)?; + + interceptors::add_supply_amount_if_amplifier_chain(storage, &destination_chain, &transfer)?; Ok(transfer) } diff --git a/contracts/interchain-token-service/tests/execute.rs b/contracts/interchain-token-service/tests/execute.rs index 2ddec0568..1cb9de46d 100644 --- a/contracts/interchain-token-service/tests/execute.rs +++ b/contracts/interchain-token-service/tests/execute.rs @@ -14,6 +14,7 @@ use interchain_token_service::{ }; use router_api::{Address, ChainName, ChainNameRaw, CrossChainId}; use serde_json::json; +use utils::params::{CORE_CHAIN, CORE_ITS_CONTRACT}; use utils::{params, register_chains, TestMessage}; mod utils; @@ -950,6 +951,64 @@ fn deploy_interchain_token_tracks_supply() { ); } +#[test] +fn transfer_interchain_token_from_core_does_not_check_balance_or_deployment() { + 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(); + + // this deploys the token from source_its_chain to destination_its_chain + 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, + 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 { + source_chain: CORE_CHAIN.parse().unwrap(), + message_id: router_message.cc_id.message_id.clone() + }, + CORE_ITS_CONTRACT.parse().unwrap(), + msg, + )); + + assert_eq!( + assert_ok!(utils::query_token_instance( + deps.as_ref(), + destination_its_chain.clone(), + token_id + )) + .unwrap() + .supply, + TokenSupply::Tracked(amount.into()) + ); +} + #[test] fn deploy_interchain_token_with_minter_does_not_track_supply() { let ( diff --git a/contracts/interchain-token-service/tests/utils/execute.rs b/contracts/interchain-token-service/tests/utils/execute.rs index 7885341c6..3f51d4cb2 100644 --- a/contracts/interchain-token-service/tests/utils/execute.rs +++ b/contracts/interchain-token-service/tests/utils/execute.rs @@ -14,6 +14,7 @@ use interchain_token_service::msg::{self, ExecuteMsg, TruncationConfig}; use interchain_token_service::{contract, HubMessage}; use router_api::{Address, ChainName, ChainNameRaw, CrossChainId}; +use super::params::{CORE_CHAIN, CORE_ITS_CONTRACT}; use super::{instantiate_contract, TestMessage}; use crate::utils::params; @@ -70,7 +71,7 @@ pub fn make_deps() -> OwnedDeps { Ok(to_json_binary( &(IsChainRegisteredResponse { - is_registered: chain == "ethereum", + is_registered: chain == CORE_CHAIN, }), ) .into()) @@ -204,5 +205,14 @@ pub fn setup() -> ( ) .unwrap(); + register_chain( + deps.as_mut(), + CORE_CHAIN.parse().unwrap(), + CORE_ITS_CONTRACT.parse().unwrap(), + Uint256::MAX.try_into().unwrap(), + u8::MAX, + ) + .unwrap(); + (deps, TestMessage::dummy()) } diff --git a/contracts/interchain-token-service/tests/utils/params.rs b/contracts/interchain-token-service/tests/utils/params.rs index 4bc425d60..fb3095d66 100644 --- a/contracts/interchain-token-service/tests/utils/params.rs +++ b/contracts/interchain-token-service/tests/utils/params.rs @@ -3,3 +3,5 @@ pub const ROUTER: &str = "router"; pub const GATEWAY: &str = "gateway"; pub const GOVERNANCE: &str = "governance"; pub const ADMIN: &str = "admin"; +pub const CORE_CHAIN: &str = "core-ethereum"; +pub const CORE_ITS_CONTRACT: &str = "core-its-contract";