diff --git a/cosmwasm/contracts/crosschain-registry/src/contract.rs b/cosmwasm/contracts/crosschain-registry/src/contract.rs index 3931b08a8ff..a448c651e23 100644 --- a/cosmwasm/contracts/crosschain-registry/src/contract.rs +++ b/cosmwasm/contracts/crosschain-registry/src/contract.rs @@ -4,15 +4,21 @@ use cosmwasm_std::{to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, use cw2::set_contract_version; use crate::error::ContractError; -use crate::msg::{ExecuteMsg, GetAddressFromAliasResponse, InstantiateMsg, QueryMsg}; -use crate::state::{Config, CONFIG, CONTRACT_ALIAS_MAP}; -use crate::{execute, query}; +use crate::msg::{ + ExecuteMsg, GetAddressFromAliasResponse, IBCLifecycleComplete, InstantiateMsg, QueryMsg, + SudoMsg, +}; +use crate::state::{ChainPFM, Config, CHAIN_PFM_MAP, CONFIG, CONTRACT_ALIAS_MAP}; +use crate::{execute, ibc_lifecycle, query}; use registry::Registry; // version info for migration const CONTRACT_NAME: &str = "crates.io:crosschain-registry"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); +// The name of the chain on which this contract is instantiated +pub const CONTRACT_CHAIN: &str = "osmosis"; + #[cfg_attr(not(feature = "imported"), entry_point)] pub fn instantiate( deps: DepsMut, @@ -27,6 +33,15 @@ pub fn instantiate( let state = Config { owner }; CONFIG.save(deps.storage, &state)?; + CHAIN_PFM_MAP.save( + deps.storage, + CONTRACT_CHAIN, + &ChainPFM { + acknowledged: true, + validated: true, + }, + )?; + Ok(Response::new().add_attribute("method", "instantiate")) } @@ -73,12 +88,16 @@ pub fn execute( env.block.time, with_memo, None, + false, )?; deps.api.debug(&format!("transfer_msg: {transfer_msg:?}")); Ok(Response::new() .add_message(transfer_msg) .add_attribute("method", "unwrap_coin")) } + + ExecuteMsg::ProposePFM { chain } => execute::propose_pfm((deps, env, info), chain), + ExecuteMsg::ValidatePFM { chain } => execute::validate_pfm((deps, env, info), chain), } } @@ -120,6 +139,24 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { QueryMsg::GetChainNameFromBech32Prefix { prefix } => { to_binary(&query::query_chain_name_from_bech32_prefix(deps, prefix)?) } + QueryMsg::HasPacketForwarding { chain } => { + to_binary(&query::query_chain_has_pfm(deps, chain)) + } + } +} + +#[cfg_attr(not(feature = "imported"), entry_point)] +pub fn sudo(deps: DepsMut, _env: Env, msg: SudoMsg) -> Result { + match msg { + SudoMsg::IBCLifecycleComplete(IBCLifecycleComplete::IBCAck { + channel, + sequence, + ack, + success, + }) => ibc_lifecycle::receive_ack(deps, channel, sequence, ack, success), + SudoMsg::IBCLifecycleComplete(IBCLifecycleComplete::IBCTimeout { channel, sequence }) => { + ibc_lifecycle::receive_timeout(deps, channel, sequence) + } } } @@ -211,7 +248,7 @@ mod test { deps.as_ref(), mock_env(), QueryMsg::GetChannelFromChainPair { - source_chain: "osmosis".to_string(), + source_chain: CONTRACT_CHAIN.to_string(), destination_chain: "juno".to_string(), }, ) @@ -224,7 +261,7 @@ mod test { deps.as_ref(), mock_env(), QueryMsg::GetDestinationChainFromSourceChainViaChannel { - on_chain: "osmosis".to_string(), + on_chain: CONTRACT_CHAIN.to_string(), via_channel: "channel-42".to_string(), }, ) @@ -237,7 +274,7 @@ mod test { deps.as_ref(), mock_env(), QueryMsg::GetChannelFromChainPair { - source_chain: "osmosis".to_string(), + source_chain: CONTRACT_CHAIN.to_string(), destination_chain: "stargaze".to_string(), }, ) @@ -250,7 +287,7 @@ mod test { deps.as_ref(), mock_env(), QueryMsg::GetDestinationChainFromSourceChainViaChannel { - on_chain: "osmosis".to_string(), + on_chain: CONTRACT_CHAIN.to_string(), via_channel: "channel-75".to_string(), }, ) @@ -264,7 +301,7 @@ mod test { mock_env(), QueryMsg::GetChannelFromChainPair { source_chain: "stargaze".to_string(), - destination_chain: "osmosis".to_string(), + destination_chain: CONTRACT_CHAIN.to_string(), }, ) .unwrap(); @@ -282,14 +319,14 @@ mod test { ) .unwrap(); let destination_chain: String = from_binary(&destination_chain).unwrap(); - assert_eq!("osmosis", destination_chain); + assert_eq!(CONTRACT_CHAIN, destination_chain); // Attempt to retrieve a link that doesn't exist and check that we get an error let channel_binary = query( deps.as_ref(), mock_env(), QueryMsg::GetChannelFromChainPair { - source_chain: "osmosis".to_string(), + source_chain: CONTRACT_CHAIN.to_string(), destination_chain: "cerberus".to_string(), }, ); @@ -299,7 +336,7 @@ mod test { let msg = ExecuteMsg::ModifyChainChannelLinks { operations: vec![ConnectionInput { operation: execute::FullOperation::Disable, - source_chain: "OSMOSIS".to_string(), + source_chain: CONTRACT_CHAIN.to_string(), destination_chain: "JUNO".to_string(), channel_id: Some("CHANNEL-42".to_string()), new_source_chain: None, @@ -316,7 +353,7 @@ mod test { deps.as_ref(), mock_env(), QueryMsg::GetChannelFromChainPair { - source_chain: "osmosis".to_string(), + source_chain: CONTRACT_CHAIN.to_string(), destination_chain: "juno".to_string(), }, ); @@ -326,7 +363,7 @@ mod test { let msg = ExecuteMsg::ModifyChainChannelLinks { operations: vec![ConnectionInput { operation: execute::FullOperation::Enable, - source_chain: "OSMOSIS".to_string(), + source_chain: CONTRACT_CHAIN.to_string(), destination_chain: "JUNO".to_string(), channel_id: Some("CHANNEL-42".to_string()), new_source_chain: None, @@ -342,7 +379,7 @@ mod test { deps.as_ref(), mock_env(), QueryMsg::GetChannelFromChainPair { - source_chain: "osmosis".to_string(), + source_chain: CONTRACT_CHAIN.to_string(), destination_chain: "juno".to_string(), }, ) diff --git a/cosmwasm/contracts/crosschain-registry/src/error.rs b/cosmwasm/contracts/crosschain-registry/src/error.rs index 18ad07f9b56..e72f60f7dab 100644 --- a/cosmwasm/contracts/crosschain-registry/src/error.rs +++ b/cosmwasm/contracts/crosschain-registry/src/error.rs @@ -13,12 +13,34 @@ pub enum ContractError { #[error("{0}")] Payment(#[from] cw_utils::PaymentError), - #[error("Unauthorized")] + #[error("unauthorized")] Unauthorized {}, + #[error("chain validation not started for {chain}")] + ValidationNotFound { chain: String }, + + #[error("coin from invalid chain. It belongs to {supplied_chain} and should be from {expected_chain}")] + CoinFromInvalidChain { + supplied_chain: String, + expected_chain: String, + }, + + #[error( + "only messages initialized by the address of this contract in another chain are allowed. Expected {expected_sender} but got {actual_sender}" + )] + InvalidSender { + expected_sender: String, + actual_sender: String, + }, + #[error("contract alias already exists: {alias:?}")] AliasAlreadyExists { alias: String }, + #[error( + "PFM validation already in progress for {chain:?}. Wait for the ibc lifecycle to complete" + )] + PFMValidationAlreadyInProgress { chain: String }, + #[error("authorized address already exists for source chain: {source_chain:?}")] ChainAuthorizedAddressAlreadyExists { source_chain: String }, diff --git a/cosmwasm/contracts/crosschain-registry/src/execute.rs b/cosmwasm/contracts/crosschain-registry/src/execute.rs index 4d2ff7964ce..0779ed69527 100644 --- a/cosmwasm/contracts/crosschain-registry/src/execute.rs +++ b/cosmwasm/contracts/crosschain-registry/src/execute.rs @@ -1,13 +1,14 @@ use crate::helpers::*; use crate::state::{ - CHAIN_ADMIN_MAP, CHAIN_MAINTAINER_MAP, CHAIN_TO_BECH32_PREFIX_MAP, + ChainPFM, CHAIN_ADMIN_MAP, CHAIN_MAINTAINER_MAP, CHAIN_PFM_MAP, CHAIN_TO_BECH32_PREFIX_MAP, CHAIN_TO_BECH32_PREFIX_REVERSE_MAP, CHAIN_TO_CHAIN_CHANNEL_MAP, CHANNEL_ON_CHAIN_CHAIN_MAP, CONTRACT_ALIAS_MAP, GLOBAL_ADMIN_MAP, }; use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, DepsMut, Response}; +use cosmwasm_std::{Addr, DepsMut, Env, MessageInfo, Response}; use cw_storage_plus::Map; -use registry::RegistryError; +use registry::msg::Callback; +use registry::{Registry, RegistryError}; use crate::ContractError; @@ -40,6 +41,102 @@ pub struct ContractAliasInput { pub new_alias: Option, } +pub fn propose_pfm( + ctx: (DepsMut, Env, MessageInfo), + chain: String, +) -> Result { + let (deps, env, info) = ctx; + + // enforce lowercase + let chain = chain.to_lowercase(); + + // validation + let registry = Registry::default(deps.as_ref()); + let coin = cw_utils::one_coin(&info)?; + let native_chain = registry.get_native_chain(&coin.denom)?; + + if native_chain.as_ref() != chain { + return Err(ContractError::CoinFromInvalidChain { + supplied_chain: native_chain.as_ref().to_string(), + expected_chain: chain, + }); + } + + // check if the chain is already registered or is in progress + if let Some(chain_pfm) = CHAIN_PFM_MAP.may_load(deps.storage, &chain)? { + if chain_pfm.is_validated() { + // Only authorized addresses can ask for a validated PFM to be re-checked + // If sender is the contract governor, then they are authorized to do do this to any chain + // Otherwise, they must be authorized to do manage the chain they are attempting to modify + let user_permission = + check_is_authorized(deps.as_ref(), info.sender, Some(chain.clone()))?; + check_action_permission(FullOperation::Change, user_permission)?; + } else { + return Err(ContractError::PFMValidationAlreadyInProgress { + chain: chain.clone(), + }); + } + }; + + // Store the chain to validate + CHAIN_PFM_MAP.save(deps.storage, &chain, &ChainPFM::default())?; + + let own_addr = env.contract.address; + + // redeclaring (shadowing) registry to avoid issues with the borrow checker + let registry = Registry::default(deps.as_ref()); + let ibc_transfer = registry.unwrap_coin_into( + coin, + own_addr.to_string(), + None, + own_addr.to_string(), + env.block.time, + format!(r#"{{"ibc_callback":"{}"}}"#, own_addr), + Some(Callback { + contract: own_addr, + msg: format!(r#"{{"validate_pfm": {{"chain": "{}"}} }}"#, chain).try_into()?, + }), + true, + )?; + + Ok(Response::default().add_message(ibc_transfer)) +} + +pub fn validate_pfm( + ctx: (DepsMut, Env, MessageInfo), + chain: String, +) -> Result { + let (deps, _env, _info) = ctx; + + let chain = chain.to_lowercase(); + + // TODO: Uncomment this once all chains are on the latest PFM and we can properly verify the sender + // + // let registry = Registry::default(deps.as_ref()); + // let channel = registry.get_channel(&chain, CONTRACT_CHAIN)?; + // let own_addr = env.contract.address.as_str(); + // let original_sender = registry.encode_addr_for_chain(own_addr, &chain)?; + // let expected_sender = registry::derive_wasmhooks_sender(&channel, &original_sender, "osmo")?; + // if expected_sender != info.sender { + // return Err(ContractError::InvalidSender { + // expected_sender, + // actual_sender: info.sender.into_string(), + // }); + // } + + let mut chain_pfm = CHAIN_PFM_MAP.load(deps.storage, &chain).map_err(|_| { + ContractError::ValidationNotFound { + chain: chain.clone(), + } + })?; + + chain_pfm.validated = true; + + CHAIN_PFM_MAP.save(deps.storage, &chain, &chain_pfm)?; + + Ok(Response::default()) +} + // Set, change, or remove a contract alias to an address pub fn contract_alias_operations( deps: DepsMut, @@ -590,6 +687,7 @@ mod tests { static CHAIN_ADMIN: &str = "chain_admin"; static CHAIN_MAINTAINER: &str = "chain_maintainer"; static UNAUTHORIZED_ADDRESS: &str = "unauthorized_address"; + use crate::contract::CONTRACT_CHAIN; #[test] fn test_set_contract_alias() { @@ -854,7 +952,7 @@ mod tests { let msg = ExecuteMsg::ModifyChainChannelLinks { operations: vec![ConnectionInput { operation: FullOperation::Set, - source_chain: "OSMOSIS".to_string(), + source_chain: CONTRACT_CHAIN.to_string(), destination_chain: "COSMOS".to_string(), channel_id: Some("CHANNEL-0".to_string()), new_source_chain: None, @@ -867,7 +965,7 @@ mod tests { assert_eq!( CHAIN_TO_CHAIN_CHANNEL_MAP - .load(&deps.storage, ("osmosis", "cosmos")) + .load(&deps.storage, (CONTRACT_CHAIN, "cosmos")) .unwrap(), ("channel-0", true).into() ); @@ -875,7 +973,7 @@ mod tests { // Verify that channel-0 on osmosis is linked to cosmos assert_eq!( CHANNEL_ON_CHAIN_CHAIN_MAP - .load(&deps.storage, ("channel-0", "osmosis")) + .load(&deps.storage, ("channel-0", CONTRACT_CHAIN)) .unwrap(), ("cosmos", true).into() ); @@ -885,7 +983,7 @@ mod tests { let msg = ExecuteMsg::ModifyChainChannelLinks { operations: vec![ConnectionInput { operation: FullOperation::Set, - source_chain: "osmosis".to_string(), + source_chain: CONTRACT_CHAIN.to_string(), destination_chain: "cosmos".to_string(), channel_id: Some("channel-150".to_string()), new_source_chain: None, @@ -898,19 +996,19 @@ mod tests { assert!(result.is_err()); let expected_error = ContractError::ChainToChainChannelLinkAlreadyExists { - source_chain: "osmosis".to_string(), + source_chain: CONTRACT_CHAIN.to_string(), destination_chain: "cosmos".to_string(), }; assert_eq!(result.unwrap_err(), expected_error); assert_eq!( CHAIN_TO_CHAIN_CHANNEL_MAP - .load(&deps.storage, ("osmosis", "cosmos")) + .load(&deps.storage, (CONTRACT_CHAIN, "cosmos")) .unwrap(), ("channel-0", true).into() ); assert_eq!( CHANNEL_ON_CHAIN_CHAIN_MAP - .load(&deps.storage, ("channel-0", "osmosis")) + .load(&deps.storage, ("channel-0", CONTRACT_CHAIN)) .unwrap(), ("cosmos", true).into() ); @@ -920,7 +1018,7 @@ mod tests { operations: vec![ConnectionInput { operation: FullOperation::Set, source_chain: "mars".to_string(), - destination_chain: "osmosis".to_string(), + destination_chain: CONTRACT_CHAIN.to_string(), channel_id: Some("channel-1".to_string()), new_source_chain: None, new_destination_chain: None, @@ -933,14 +1031,14 @@ mod tests { let expected_error = ContractError::Unauthorized {}; assert_eq!(result.unwrap_err(), expected_error); - assert!(!CHAIN_TO_CHAIN_CHANNEL_MAP.has(&deps.storage, ("mars", "osmosis"))); + assert!(!CHAIN_TO_CHAIN_CHANNEL_MAP.has(&deps.storage, ("mars", CONTRACT_CHAIN))); // Set the canonical channel link between mars and osmosis to channel-1 with a mars chain admin address let chain_admin_info = mock_info(CHAIN_ADMIN, &[]); contract::execute(deps.as_mut(), mock_env(), chain_admin_info.clone(), msg).unwrap(); assert_eq!( CHAIN_TO_CHAIN_CHANNEL_MAP - .load(&deps.storage, ("mars", "osmosis")) + .load(&deps.storage, ("mars", CONTRACT_CHAIN)) .unwrap(), ("channel-1", true).into() ); @@ -948,7 +1046,7 @@ mod tests { CHANNEL_ON_CHAIN_CHAIN_MAP .load(&deps.storage, ("channel-1", "mars")) .unwrap(), - ("osmosis", true).into() + (CONTRACT_CHAIN, true).into() ); // Set the canonical channel link between juno and mars to channel-2 with a juno chain maintainer address @@ -1040,7 +1138,7 @@ mod tests { let msg = ExecuteMsg::ModifyChainChannelLinks { operations: vec![ConnectionInput { operation: FullOperation::Set, - source_chain: "OSMOSIS".to_string(), + source_chain: CONTRACT_CHAIN.to_string(), destination_chain: "COSMOS".to_string(), channel_id: Some("CHANNEL-0".to_string()), new_source_chain: None, @@ -1056,7 +1154,7 @@ mod tests { let msg = ExecuteMsg::ModifyChainChannelLinks { operations: vec![ConnectionInput { operation: FullOperation::Change, - source_chain: "osmosis".to_string(), + source_chain: CONTRACT_CHAIN.to_string(), destination_chain: "cosmos".to_string(), channel_id: None, new_source_chain: None, @@ -1070,7 +1168,7 @@ mod tests { // Verify that the channel between osmosis and cosmos has changed from channel-0 to channel-150 assert_eq!( CHAIN_TO_CHAIN_CHANNEL_MAP - .load(&deps.storage, ("osmosis", "cosmos")) + .load(&deps.storage, (CONTRACT_CHAIN, "cosmos")) .unwrap(), ("channel-150", true).into() ); @@ -1079,7 +1177,7 @@ mod tests { let msg = ExecuteMsg::ModifyChainChannelLinks { operations: vec![ConnectionInput { operation: FullOperation::Change, - source_chain: "osmosis".to_string(), + source_chain: CONTRACT_CHAIN.to_string(), destination_chain: "regen".to_string(), channel_id: None, new_source_chain: None, @@ -1091,7 +1189,7 @@ mod tests { assert!(result.is_err()); let expected_error = ContractError::from(RegistryError::ChainChannelLinkDoesNotExist { - source_chain: "osmosis".to_string(), + source_chain: CONTRACT_CHAIN.to_string(), destination_chain: "regen".to_string(), }); assert_eq!(result.unwrap_err(), expected_error); @@ -1100,7 +1198,7 @@ mod tests { let msg = ExecuteMsg::ModifyChainChannelLinks { operations: vec![ConnectionInput { operation: FullOperation::Change, - source_chain: "osmosis".to_string(), + source_chain: CONTRACT_CHAIN.to_string(), destination_chain: "cosmos".to_string(), channel_id: None, new_source_chain: None, @@ -1114,7 +1212,7 @@ mod tests { // Verify that channel-150 on osmosis is linked to regen assert_eq!( CHANNEL_ON_CHAIN_CHAIN_MAP - .load(&deps.storage, ("channel-150", "osmosis")) + .load(&deps.storage, ("channel-150", CONTRACT_CHAIN)) .unwrap(), ("regen", true).into() ); @@ -1123,7 +1221,7 @@ mod tests { let msg = ExecuteMsg::ModifyChainChannelLinks { operations: vec![ConnectionInput { operation: FullOperation::Change, - source_chain: "osmosis".to_string(), + source_chain: CONTRACT_CHAIN.to_string(), destination_chain: "regen".to_string(), channel_id: None, new_source_chain: None, @@ -1143,7 +1241,7 @@ mod tests { contract::execute(deps.as_mut(), mock_env(), info_chain_admin, msg).unwrap(); assert_eq!( CHAIN_TO_CHAIN_CHANNEL_MAP - .load(&deps.storage, ("osmosis", "regen")) + .load(&deps.storage, (CONTRACT_CHAIN, "regen")) .unwrap(), ("channel-2", true).into() ); @@ -1152,7 +1250,7 @@ mod tests { let msg = ExecuteMsg::ModifyChainChannelLinks { operations: vec![ConnectionInput { operation: FullOperation::Change, - source_chain: "osmosis".to_string(), + source_chain: CONTRACT_CHAIN.to_string(), destination_chain: "cosmos".to_string(), channel_id: None, new_source_chain: None, @@ -1164,7 +1262,7 @@ mod tests { let result = contract::execute(deps.as_mut(), mock_env(), info, msg); let expected_error = ContractError::from(RegistryError::ChainChannelLinkDoesNotExist { - source_chain: "osmosis".to_string(), + source_chain: CONTRACT_CHAIN.to_string(), destination_chain: "cosmos".to_string(), }); assert_eq!(result.unwrap_err(), expected_error); @@ -1175,7 +1273,7 @@ mod tests { let msg = ExecuteMsg::ModifyChainChannelLinks { operations: vec![ConnectionInput { operation: FullOperation::Change, - source_chain: "osmosis".to_string(), + source_chain: CONTRACT_CHAIN.to_string(), destination_chain: "regen".to_string(), channel_id: None, new_source_chain: None, @@ -1200,7 +1298,7 @@ mod tests { operations: vec![ ConnectionInput { operation: FullOperation::Set, - source_chain: "OSMOSIS".to_string(), + source_chain: CONTRACT_CHAIN.to_string(), destination_chain: "COSMOS".to_string(), channel_id: Some("CHANNEL-0".to_string()), new_source_chain: None, @@ -1209,7 +1307,7 @@ mod tests { }, ConnectionInput { operation: FullOperation::Set, - source_chain: "OSMOSIS".to_string(), + source_chain: CONTRACT_CHAIN.to_string(), destination_chain: "REGEN".to_string(), channel_id: Some("CHANNEL-1".to_string()), new_source_chain: None, @@ -1225,7 +1323,7 @@ mod tests { let msg = ExecuteMsg::ModifyChainChannelLinks { operations: vec![ConnectionInput { operation: FullOperation::Remove, - source_chain: "osmosis".to_string(), + source_chain: CONTRACT_CHAIN.to_string(), destination_chain: "cosmos".to_string(), channel_id: None, new_source_chain: None, @@ -1237,13 +1335,13 @@ mod tests { contract::execute(deps.as_mut(), mock_env(), info, msg.clone()).unwrap(); // Verify that the link no longer exists - assert!(!CHAIN_TO_CHAIN_CHANNEL_MAP.has(&deps.storage, ("osmosis", "cosmos"))); + assert!(!CHAIN_TO_CHAIN_CHANNEL_MAP.has(&deps.storage, (CONTRACT_CHAIN, "cosmos"))); let info = mock_info(CREATOR_ADDRESS, &[]); let result = contract::execute(deps.as_mut(), mock_env(), info, msg); let expected_error = ContractError::from(RegistryError::ChainChannelLinkDoesNotExist { - source_chain: "osmosis".to_string(), + source_chain: CONTRACT_CHAIN.to_string(), destination_chain: "cosmos".to_string(), }); assert_eq!(result.unwrap_err(), expected_error); @@ -1254,7 +1352,7 @@ mod tests { let msg = ExecuteMsg::ModifyChainChannelLinks { operations: vec![ConnectionInput { operation: FullOperation::Remove, - source_chain: "osmosis".to_string(), + source_chain: CONTRACT_CHAIN.to_string(), destination_chain: "regen".to_string(), channel_id: None, new_source_chain: None, @@ -1278,7 +1376,7 @@ mod tests { let msg = ExecuteMsg::ModifyBech32Prefixes { operations: vec![ChainToBech32PrefixInput { operation: FullOperation::Set, - chain_name: "OSMOSIS".to_string(), + chain_name: CONTRACT_CHAIN.to_string(), prefix: "OSMO".to_string(), new_prefix: None, }], @@ -1288,7 +1386,7 @@ mod tests { assert_eq!( CHAIN_TO_BECH32_PREFIX_MAP - .load(&deps.storage, "osmosis") + .load(&deps.storage, CONTRACT_CHAIN) .unwrap(), ("osmo", true).into() ); @@ -1296,7 +1394,7 @@ mod tests { CHAIN_TO_BECH32_PREFIX_REVERSE_MAP .load(&deps.storage, "osmo") .unwrap(), - vec!["osmosis"] + vec![CONTRACT_CHAIN] ); // Set another chain with the same prefix @@ -1320,14 +1418,14 @@ mod tests { CHAIN_TO_BECH32_PREFIX_REVERSE_MAP .load(&deps.storage, "osmo") .unwrap(), - vec!["osmosis", "ismisis"] + vec![CONTRACT_CHAIN, "ismisis"] ); // Set another chain with the same prefix let msg = ExecuteMsg::ModifyBech32Prefixes { operations: vec![ChainToBech32PrefixInput { operation: FullOperation::Disable, - chain_name: "OSMOSIS".to_string(), + chain_name: CONTRACT_CHAIN.to_string(), prefix: "OSMO".to_string(), new_prefix: None, }], @@ -1335,7 +1433,7 @@ mod tests { contract::execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); assert_eq!( CHAIN_TO_BECH32_PREFIX_MAP - .load(&deps.storage, "osmosis") + .load(&deps.storage, CONTRACT_CHAIN) .unwrap(), ("osmo", false).into() ); @@ -1350,7 +1448,7 @@ mod tests { let msg = ExecuteMsg::ModifyBech32Prefixes { operations: vec![ChainToBech32PrefixInput { operation: FullOperation::Enable, - chain_name: "OSMOSIS".to_string(), + chain_name: CONTRACT_CHAIN.to_string(), prefix: "OSMO".to_string(), new_prefix: None, }], @@ -1358,7 +1456,7 @@ mod tests { contract::execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); assert_eq!( CHAIN_TO_BECH32_PREFIX_MAP - .load(&deps.storage, "osmosis") + .load(&deps.storage, CONTRACT_CHAIN) .unwrap(), ("osmo", true).into() ); @@ -1366,14 +1464,14 @@ mod tests { CHAIN_TO_BECH32_PREFIX_REVERSE_MAP .load(&deps.storage, "osmo") .unwrap(), - vec!["ismisis", "osmosis"] + vec!["ismisis", CONTRACT_CHAIN] ); // Set another chain with the same prefix let msg = ExecuteMsg::ModifyBech32Prefixes { operations: vec![ChainToBech32PrefixInput { operation: FullOperation::Remove, - chain_name: "OSMOSIS".to_string(), + chain_name: CONTRACT_CHAIN.to_string(), prefix: "OSMO".to_string(), new_prefix: None, }], @@ -1393,7 +1491,7 @@ mod tests { ); CHAIN_TO_BECH32_PREFIX_MAP - .load(&deps.storage, "osmosis") + .load(&deps.storage, CONTRACT_CHAIN) .unwrap_err(); } } diff --git a/cosmwasm/contracts/crosschain-registry/src/ibc_lifecycle.rs b/cosmwasm/contracts/crosschain-registry/src/ibc_lifecycle.rs new file mode 100644 index 00000000000..23e4029e131 --- /dev/null +++ b/cosmwasm/contracts/crosschain-registry/src/ibc_lifecycle.rs @@ -0,0 +1,41 @@ +use cosmwasm_std::{DepsMut, Response}; +use registry::Registry; + +use crate::{contract::CONTRACT_CHAIN, state::CHAIN_PFM_MAP, ContractError}; + +pub fn receive_ack( + deps: DepsMut, + source_channel: String, + _sequence: u64, + _ack: String, + success: bool, +) -> Result { + let registry = Registry::default(deps.as_ref()); + let chain = registry.get_connected_chain(CONTRACT_CHAIN, source_channel.as_str())?; + let mut chain_pfm = CHAIN_PFM_MAP.load(deps.storage, &chain).map_err(|_| { + ContractError::ValidationNotFound { + chain: chain.clone(), + } + })?; + + if success { + chain_pfm.acknowledged = true; + CHAIN_PFM_MAP.save(deps.storage, &chain, &chain_pfm)?; + } else { + CHAIN_PFM_MAP.remove(deps.storage, &chain); + } + + Ok(Response::default()) +} + +pub fn receive_timeout( + deps: DepsMut, + source_channel: String, + _sequence: u64, +) -> Result { + let registry = Registry::default(deps.as_ref()); + let chain = registry.get_connected_chain(CONTRACT_CHAIN, source_channel.as_str())?; + CHAIN_PFM_MAP.remove(deps.storage, &chain); + + Ok(Response::default()) +} diff --git a/cosmwasm/contracts/crosschain-registry/src/lib.rs b/cosmwasm/contracts/crosschain-registry/src/lib.rs index ac94946c4d1..c00aae18bb8 100644 --- a/cosmwasm/contracts/crosschain-registry/src/lib.rs +++ b/cosmwasm/contracts/crosschain-registry/src/lib.rs @@ -3,6 +3,7 @@ mod error; pub mod execute; mod exports; pub mod helpers; +mod ibc_lifecycle; pub mod msg; pub mod query; pub mod state; diff --git a/cosmwasm/contracts/crosschain-registry/src/msg.rs b/cosmwasm/contracts/crosschain-registry/src/msg.rs index c7de423f2ee..2bf534b5803 100644 --- a/cosmwasm/contracts/crosschain-registry/src/msg.rs +++ b/cosmwasm/contracts/crosschain-registry/src/msg.rs @@ -29,6 +29,12 @@ pub enum ExecuteMsg { operations: Vec, }, + // Add PFM to the registry + #[serde(rename = "propose_pfm")] + ProposePFM { chain: String }, + #[serde(rename = "validate_pfm")] + ValidatePFM { chain: String }, + UnwrapCoin { receiver: String, into_chain: Option, @@ -44,3 +50,32 @@ pub use registry::msg::{ GetDestinationChainFromSourceChainViaChannelResponse, QueryGetBech32PrefixFromChainNameResponse, }; + +#[cw_serde] +pub enum IBCLifecycleComplete { + #[serde(rename = "ibc_ack")] + IBCAck { + /// The source channel (osmosis side) of the IBC packet + channel: String, + /// The sequence number that the packet was sent with + sequence: u64, + /// String encoded version of the ack as seen by OnAcknowledgementPacket(..) + ack: String, + /// Whether an ack is a success of failure according to the transfer spec + success: bool, + }, + #[serde(rename = "ibc_timeout")] + IBCTimeout { + /// The source channel (osmosis side) of the IBC packet + channel: String, + /// The sequence number that the packet was sent with + sequence: u64, + }, +} + +/// Message type for `sudo` entry_point +#[cw_serde] +pub enum SudoMsg { + #[serde(rename = "ibc_lifecycle_complete")] + IBCLifecycleComplete(IBCLifecycleComplete), +} diff --git a/cosmwasm/contracts/crosschain-registry/src/query.rs b/cosmwasm/contracts/crosschain-registry/src/query.rs index d507d62837c..aff0b77fbbf 100644 --- a/cosmwasm/contracts/crosschain-registry/src/query.rs +++ b/cosmwasm/contracts/crosschain-registry/src/query.rs @@ -1,6 +1,6 @@ use crate::state::{ - CHAIN_TO_BECH32_PREFIX_MAP, CHAIN_TO_BECH32_PREFIX_REVERSE_MAP, CHAIN_TO_CHAIN_CHANNEL_MAP, - CHANNEL_ON_CHAIN_CHAIN_MAP, + CHAIN_PFM_MAP, CHAIN_TO_BECH32_PREFIX_MAP, CHAIN_TO_BECH32_PREFIX_REVERSE_MAP, + CHAIN_TO_CHAIN_CHANNEL_MAP, CHANNEL_ON_CHAIN_CHAIN_MAP, }; use cosmwasm_std::{Deps, StdError}; @@ -89,3 +89,13 @@ pub fn query_chain_from_channel_chain_pair( Ok(chain.value) } + +pub fn query_chain_has_pfm(deps: Deps, chain: String) -> bool { + let chain = chain.to_lowercase(); + if let Ok(chain_pfm) = CHAIN_PFM_MAP.load(deps.storage, &chain) { + deps.api.debug(&format!("{chain_pfm:?}")); + chain_pfm.is_validated() + } else { + false + } +} diff --git a/cosmwasm/contracts/crosschain-registry/src/state.rs b/cosmwasm/contracts/crosschain-registry/src/state.rs index 1fafa24192c..0c6d0da7635 100644 --- a/cosmwasm/contracts/crosschain-registry/src/state.rs +++ b/cosmwasm/contracts/crosschain-registry/src/state.rs @@ -13,6 +13,7 @@ enum StorageKey { GlobalAdminMap, ChainAdminMap, ChainMaintainerMap, + HasPacketForwardMiddleware, } // Implement the `StorageKey` enum to a string conversion. @@ -28,6 +29,7 @@ impl StorageKey { StorageKey::GlobalAdminMap => "gam", StorageKey::ChainAdminMap => "cam", StorageKey::ChainMaintainerMap => "cmm", + StorageKey::HasPacketForwardMiddleware => "hpfm", } } } @@ -47,6 +49,27 @@ impl> From<(T, bool)> for RegistryValue { } } +/// ChainPFM stores the state of the packet forward middleware for a chain. Anyone can request +/// to enable the packet forward middleware for a chain, but the contract will verify that +/// packets can properly be forwarded by the chain +#[cw_serde] +#[derive(Default)] +pub struct ChainPFM { + /// The verification packet has been received by the chain, forwarded, and the ack has been received + pub acknowledged: bool, + /// The contract has validated that the received packet is as expected + pub validated: bool, +} + +impl ChainPFM { + /// Both acknowledged and validated must be true for the pfm to be enabled. This is to avoid + /// situations in which the chain calls the contract to set validated to true but that call is + /// not from the same packet that was forwarded by this contract. + pub fn is_validated(&self) -> bool { + self.acknowledged && self.validated + } +} + // CONTRACT_ALIAS_MAP is a map from a contract alias to a contract address pub const CONTRACT_ALIAS_MAP: Map<&str, String> = Map::new(StorageKey::ContractAliasMap.to_string()); @@ -88,6 +111,10 @@ pub const CHAIN_ADMIN_MAP: Map<&str, Addr> = Map::new(StorageKey::ChainAdminMap. pub const CHAIN_MAINTAINER_MAP: Map<&str, Addr> = Map::new(StorageKey::ChainMaintainerMap.to_string()); +// CHAIN_PFM_MAP stores whether a chain supports the Packet Forward Middleware interface for forwarding IBC packets +pub const CHAIN_PFM_MAP: Map<&str, ChainPFM> = + Map::new(StorageKey::HasPacketForwardMiddleware.to_string()); + #[cw_serde] pub struct Config { pub owner: Addr, diff --git a/cosmwasm/contracts/crosschain-swaps/src/execute.rs b/cosmwasm/contracts/crosschain-swaps/src/execute.rs index 8cd393d5ca2..eec1762095c 100644 --- a/cosmwasm/contracts/crosschain-swaps/src/execute.rs +++ b/cosmwasm/contracts/crosschain-swaps/src/execute.rs @@ -69,6 +69,7 @@ pub fn unwrap_or_swap_and_forward( })? .into(), }), + false, )?; return Ok(Response::new().add_message(ibc_transfer)); } @@ -127,6 +128,7 @@ pub fn swap_and_forward( env.block.time, memo, None, + false, )?; // Message to swap tokens in the underlying swaprouter contract @@ -200,6 +202,7 @@ pub fn handle_swap_reply( env.block.time, memo, None, + false, )?; deps.api.debug(&format!("IBC transfer: {ibc_transfer:?}")); diff --git a/cosmwasm/packages/registry/src/error.rs b/cosmwasm/packages/registry/src/error.rs index e926975766b..9c38d910a1d 100644 --- a/cosmwasm/packages/registry/src/error.rs +++ b/cosmwasm/packages/registry/src/error.rs @@ -15,6 +15,9 @@ pub enum RegistryError { #[error("{0}")] ValueSerialization(ValueSerError), + #[error("{0}")] + Bech32ErrorRaw(#[from] bech32::Error), + // Validation errors #[error("Invalid channel id: {0}")] InvalidChannelId(String), @@ -30,6 +33,9 @@ pub enum RegistryError { #[error("serialization error: {error}")] SerialiaztionError { error: String }, + #[error("registry improperly configured")] + ImproperlyConfigured {}, + #[error("denom {denom:?} is not an IBC denom")] InvalidIBCDenom { denom: String }, @@ -92,6 +98,9 @@ pub enum RegistryError { #[error("bech32 prefix does not exist for chain: {chain}")] Bech32PrefixDoesNotExist { chain: String }, + + #[error("Chain {chain} does not support forwarding")] + ForwardingUnsopported { chain: String }, } impl From for StdError { diff --git a/cosmwasm/packages/registry/src/lib.rs b/cosmwasm/packages/registry/src/lib.rs index 35d6aba584c..0568bb6d0e7 100644 --- a/cosmwasm/packages/registry/src/lib.rs +++ b/cosmwasm/packages/registry/src/lib.rs @@ -1,7 +1,9 @@ mod error; mod registry; +pub use crate::registry::derive_wasmhooks_sender; pub use crate::registry::Registry; + pub use error::RegistryError; pub mod msg; diff --git a/cosmwasm/packages/registry/src/msg.rs b/cosmwasm/packages/registry/src/msg.rs index 5d71a48d58b..c831d4d1fba 100644 --- a/cosmwasm/packages/registry/src/msg.rs +++ b/cosmwasm/packages/registry/src/msg.rs @@ -1,6 +1,7 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::Addr; use schemars::JsonSchema; +use serde_json_wasm::from_str; use crate::RegistryError; @@ -30,6 +31,9 @@ pub enum QueryMsg { #[returns(crate::proto::QueryDenomTraceResponse)] GetDenomTrace { ibc_denom: String }, + + #[returns(bool)] + HasPacketForwarding { chain: String }, } // Response for GetAddressFromAlias query @@ -96,6 +100,14 @@ impl From for SerializableJson { } } +impl TryFrom for SerializableJson { + type Error = RegistryError; + + fn try_from(value: String) -> Result { + Ok(Self(from_str(&value)?)) + } +} + /// Information about which contract to call when the crosschain swap finishes #[cw_serde] pub struct Callback { diff --git a/cosmwasm/packages/registry/src/registry.rs b/cosmwasm/packages/registry/src/registry.rs index 6ac40eae331..f0b6d35be3a 100644 --- a/cosmwasm/packages/registry/src/registry.rs +++ b/cosmwasm/packages/registry/src/registry.rs @@ -19,6 +19,44 @@ pub fn hash_denom_trace(unwrapped: &str) -> String { format!("ibc/{}", hash.to_uppercase()) } +// When a contract is called using ibc callbacks, the addres is a combination of +// the channel and the original sender. This function lets us compute that. +pub fn derive_wasmhooks_sender( + channel: &str, + original_sender: &str, + bech32_prefix: &str, +) -> Result { + let sender = format!("{}/{}", channel, original_sender); + + let mut hasher0 = Sha256::new(); + hasher0.update("ibc-wasm-hook-intermediary".as_bytes()); + let th = hasher0.finalize(); + + let mut hasher = Sha256::new(); + hasher.reset(); + hasher.update(th.as_slice()); + hasher.update(sender.as_bytes()); + + let result = hasher.finalize(); + + // The bech32 crate requires a Vec as input, so we need to convert the bytes. + let result_u5 = bech32::convert_bits(result.as_slice(), 8, 5, true)?; + // result_u5 contains the bytes as a u5 but in an u8 type, so we need to explicitly + // do the type conversion + let result_u5: Vec = result_u5 + .iter() + .filter_map(|&x| bech32::u5::try_from_u8(x).ok()) + .collect(); + + bech32::encode(bech32_prefix, result_u5, bech32::Variant::Bech32).map_err(|e| { + RegistryError::Bech32Error { + action: "encoding".into(), + addr: original_sender.into(), + source: e, + } + }) +} + // IBC transfer port const TRANSFER_PORT: &str = "transfer"; // IBC timeout @@ -101,6 +139,9 @@ pub struct MultiHopDenom { pub via: Option, // This is optional because native tokens have no channel } +// The name of the chain on which the contract using this lib is instantiated +pub const CONTRACT_CHAIN: &str = "osmosis"; + pub struct Registry<'a> { pub deps: Deps<'a>, pub registry_contract: String, @@ -115,7 +156,6 @@ impl<'a> Registry<'a> { }) } - #[allow(dead_code)] pub fn default(deps: Deps<'a>) -> Self { Self { deps, @@ -188,6 +228,20 @@ impl<'a> Registry<'a> { }) } + /// Returns a boolean specifying if a chain supports forwarding + /// Example: supports_forwarding("gaia") -> true + pub fn supports_forwarding(&self, chain: &str) -> Result { + self.deps + .querier + .query_wasm_smart( + &self.registry_contract, + &QueryMsg::HasPacketForwarding { + chain: chain.to_string(), + }, + ) + .map_err(|_e| RegistryError::ImproperlyConfigured {}) + } + /// Re-encodes the bech32 address for the receiving chain /// Example: encode_addr_for_chain("osmo1...", "juno") -> "juno1..." pub fn encode_addr_for_chain(&self, addr: &str, chain: &str) -> Result { @@ -262,7 +316,7 @@ impl<'a> Registry<'a> { pub fn unwrap_denom_path(&self, denom: &str) -> Result, RegistryError> { self.debug(format!("Unwrapping denom {denom}")); - let mut current_chain = "osmosis".to_string(); // The initial chain is always osmosis + let mut current_chain = CONTRACT_CHAIN.to_string(); // The initial chain is always the contract chain // Check that the denom is an IBC denom if !denom.starts_with("ibc/") { @@ -289,9 +343,7 @@ impl<'a> Registry<'a> { }), }?; - self.deps - .api - .debug(&format!("procesing denom trace {path}")); + self.debug(format!("procesing denom trace {path}")); // Let's iterate over the parts of the denom trace and extract the // chain/channels into a more useful structure: MultiHopDenom let mut hops: Vec = vec![]; @@ -331,6 +383,21 @@ impl<'a> Registry<'a> { Ok(hops) } + pub fn get_native_chain(&self, denom: &str) -> Result { + let hops = self.unwrap_denom_path(denom)?; + self.debug(format!("hops: {:?}", hops)); + // verify that the last hop is native + let last_hop = hops.last().ok_or(RegistryError::NoDenomTrace { + denom: denom.into(), + })?; + if last_hop.via.is_some() { + return Err(RegistryError::InvalidDenomTrace { + error: format!("Path {hops:?} is not properly formatted"), + }); + } + Ok(hops.last().unwrap().on.clone()) + } + /// Returns an IBC MsgTransfer that with a packet forward middleware memo /// that will send the coin back to its original chain and then to the /// receiver in `into_chain`. @@ -354,6 +421,7 @@ impl<'a> Registry<'a> { block_time: Timestamp, first_transfer_memo: String, receiver_callback: Option, + skip_forwarding_check: bool, ) -> Result { // Calculate the path that this coin took to get to the current chain. // Each element in the path is an IBC hop. @@ -470,6 +538,17 @@ impl<'a> Registry<'a> { None => ChannelId(self.get_channel(prev_chain, hop.on.as_ref())?), }; + self.debug(format!( + "checking that: {:?} supports pfm (into {:?})", + hop.on.as_ref(), + prev_chain + )); + if !skip_forwarding_check && !self.supports_forwarding(hop.on.as_ref())? { + return Err(RegistryError::ForwardingUnsopported { + chain: hop.on.as_ref().into(), + }); + } + // The next memo wraps the previous one next = Some(Box::new(Memo { forward: Some(ForwardingMemo { @@ -577,4 +656,39 @@ mod test { r#"{"forward":{"receiver":"receiver","port":"port","channel":"channel-0","next":{"forward":{"receiver":"receiver2","port":"port2","channel":"channel-1"}}}}"# ) } + + #[test] + fn test_derive_wasmhooks_sender() { + let test_cases = vec![ + ( + "channel-0", + "cosmos1tfejvgp5yzd8ypvn9t0e2uv2kcjf2laa8upya8", + "osmo", + "osmo1sguz3gtyl2tjsdulwxmtprd68xtd43yyep6g5c554utz642sr8rqcgw0q6", + ), + ( + "channel-1", + "cosmos1tfejvgp5yzd8ypvn9t0e2uv2kcjf2laa8upya8", + "osmo", + "osmo1svnare87kluww5hnltv24m4dg72hst0qqwm5xslsvnwd22gftcussaz5l7", + ), + ( + "channel-0", + "osmo12smx2wdlyttvyzvzg54y2vnqwq2qjateuf7thj", + "osmo", + "osmo1vz8evs4ek3vnz4f8wy86nw9ayzn67y28vtxzjgxv6achc4pa8gesqldfz0", + ), + ( + "channel-0", + "osmo12smx2wdlyttvyzvzg54y2vnqwq2qjateuf7thj", + "cosmos", + "cosmos1vz8evs4ek3vnz4f8wy86nw9ayzn67y28vtxzjgxv6achc4pa8ges4z434f", + ), + ]; + + for tc in test_cases { + assert!(derive_wasmhooks_sender(tc.0, tc.1, tc.2).is_ok()); + assert_eq!(derive_wasmhooks_sender(tc.0, tc.1, tc.2).unwrap(), tc.3); + } + } } diff --git a/tests/ibc-hooks/bytecode/crosschain_registry.wasm b/tests/ibc-hooks/bytecode/crosschain_registry.wasm index 4248452bd5a..3433152088e 100644 Binary files a/tests/ibc-hooks/bytecode/crosschain_registry.wasm and b/tests/ibc-hooks/bytecode/crosschain_registry.wasm differ diff --git a/tests/ibc-hooks/bytecode/crosschain_swaps.wasm b/tests/ibc-hooks/bytecode/crosschain_swaps.wasm index 910814081e3..1a87f6548c4 100644 Binary files a/tests/ibc-hooks/bytecode/crosschain_swaps.wasm and b/tests/ibc-hooks/bytecode/crosschain_swaps.wasm differ diff --git a/tests/ibc-hooks/bytecode/outpost.wasm b/tests/ibc-hooks/bytecode/outpost.wasm index 0b1f86d5cb3..25a95ad62f9 100644 Binary files a/tests/ibc-hooks/bytecode/outpost.wasm and b/tests/ibc-hooks/bytecode/outpost.wasm differ diff --git a/tests/ibc-hooks/ibc_middleware_test.go b/tests/ibc-hooks/ibc_middleware_test.go index 630b7f49bf3..d1990be95f1 100644 --- a/tests/ibc-hooks/ibc_middleware_test.go +++ b/tests/ibc-hooks/ibc_middleware_test.go @@ -70,6 +70,7 @@ func (suite *HooksTestSuite) SetupTest() { suite.SkipIfWSL() // TODO: This needs to get removed. Waiting on https://github.com/cosmos/ibc-go/issues/3123 txfeetypes.ConsensusMinFee = sdk.ZeroDec() + suite.Setup() ibctesting.DefaultTestingAppInit = osmosisibctesting.SetupTestingApp suite.coordinator = ibctesting.NewCoordinator(suite.T(), 3) @@ -202,6 +203,50 @@ func (suite *HooksTestSuite) GetReceiverChannel(chainA, chainB Chain) string { return receiver.ChannelID } +func (suite *HooksTestSuite) TestDeriveIntermediateSender() { + + testCases := []struct { + channel string + originalSender string + bech32Prefix string + expectedAddress string + }{ + { + channel: "channel-0", + originalSender: "cosmos1tfejvgp5yzd8ypvn9t0e2uv2kcjf2laa8upya8", + bech32Prefix: "osmo", + expectedAddress: "osmo1sguz3gtyl2tjsdulwxmtprd68xtd43yyep6g5c554utz642sr8rqcgw0q6", + }, + { + channel: "channel-1", + originalSender: "cosmos1tfejvgp5yzd8ypvn9t0e2uv2kcjf2laa8upya8", + bech32Prefix: "osmo", + expectedAddress: "osmo1svnare87kluww5hnltv24m4dg72hst0qqwm5xslsvnwd22gftcussaz5l7", + }, + { + channel: "channel-0", + originalSender: "osmo12smx2wdlyttvyzvzg54y2vnqwq2qjateuf7thj", + bech32Prefix: "osmo", + expectedAddress: "osmo1vz8evs4ek3vnz4f8wy86nw9ayzn67y28vtxzjgxv6achc4pa8gesqldfz0", + }, + { + channel: "channel-0", + originalSender: "osmo12smx2wdlyttvyzvzg54y2vnqwq2qjateuf7thj", + bech32Prefix: "cosmos", + expectedAddress: "cosmos1vz8evs4ek3vnz4f8wy86nw9ayzn67y28vtxzjgxv6achc4pa8ges4z434f", + }, + } + + for _, tc := range testCases { + suite.Run(fmt.Sprintf("Test failed for case (channel=%s, originalSender=%s, bech32Prefix=%s).", + tc.channel, tc.originalSender, tc.bech32Prefix), func() { + actualAddress, err := ibchookskeeper.DeriveIntermediateSender(tc.channel, tc.originalSender, tc.bech32Prefix) + suite.Require().NoError(err) + suite.Require().Equal(tc.expectedAddress, actualAddress) + }) + } +} + func (suite *HooksTestSuite) TestOnRecvPacketHooks() { var ( trace transfertypes.DenomTrace @@ -697,13 +742,16 @@ func (suite *HooksTestSuite) SetupPools(chainName Chain, multipliers []sdk.Dec) return pools } -func (suite *HooksTestSuite) SetupCrosschainSwaps(chainName Chain) (sdk.AccAddress, sdk.AccAddress) { +func (suite *HooksTestSuite) SetupCrosschainSwaps(chainName Chain, setupForwarding bool) (sdk.AccAddress, sdk.AccAddress) { chain := suite.GetChain(chainName) owner := chain.SenderAccount.GetAddress() registryAddr, _, _, _ := suite.SetupCrosschainRegistry(chainName) suite.setChainChannelLinks(registryAddr, chainName) - fmt.Println("registryAddr", registryAddr) + suite.setAllPrefixesToOsmo(registryAddr, chainName) + if setupForwarding { + suite.setForwardingOnAllChains(registryAddr) + } // Fund the account with some uosmo and some stake bankKeeper := chain.GetOsmosisApp().BankKeeper @@ -731,23 +779,7 @@ func (suite *HooksTestSuite) SetupCrosschainSwaps(chainName Chain) (sdk.AccAddre contractKeeper := wasmkeeper.NewDefaultPermissionKeeper(osmosisApp.WasmKeeper) ctx := chain.GetContext() - // Configuring two prefixes for the same channel here. This is so that we can test bad acks when the receiver can't handle the receiving addr - msg := `{ - "modify_bech32_prefixes": { - "operations": [ - {"operation": "set", "chain_name": "osmosis", "prefix": "osmo"}, - {"operation": "set", "chain_name": "chainB", "prefix": "osmo"}, - {"operation": "set", "chain_name": "chainB-cw20", "prefix": "osmo"}, - {"operation": "set", "chain_name": "chainC", "prefix": "osmo"} - ] - } - } - ` - _, err = contractKeeper.Execute(ctx, registryAddr, owner, []byte(msg), sdk.NewCoins()) - suite.Require().NoError(err) - - // ctx sdk.Context, contractAddress sdk.AccAddress, caller sdk.AccAddress, msg []byte, coins sdk.Coins - msg = fmt.Sprintf(`{"set_route":{"input_denom":"token0","output_denom":"token1","pool_route":[{"pool_id":"1","token_out_denom":"%s"},{"pool_id":"2","token_out_denom":"token1"}]}}`, sdk.DefaultBondDenom) + msg := `{"set_route":{"input_denom":"token0","output_denom":"token1","pool_route":[{"pool_id":"1","token_out_denom":"stake"},{"pool_id":"2","token_out_denom":"token1"}]}}` _, err = contractKeeper.Execute(ctx, swaprouterAddr, owner, []byte(msg), sdk.NewCoins()) suite.Require().NoError(err) @@ -856,6 +888,35 @@ func (suite *HooksTestSuite) setChainChannelLinks(registryAddr sdk.AccAddress, c ` _, err := contractKeeper.Execute(ctx, registryAddr, owner, []byte(msg), sdk.NewCoins()) suite.Require().NoError(err) + +} + +func (suite *HooksTestSuite) setAllPrefixesToOsmo(registryAddr sdk.AccAddress, chainName Chain) { + chain := suite.GetChain(chainName) + ctx := chain.GetContext() + owner := chain.SenderAccount.GetAddress() + osmosisApp := chain.GetOsmosisApp() + contractKeeper := wasmkeeper.NewDefaultPermissionKeeper(osmosisApp.WasmKeeper) + + // Add all chain channel links in a single message + msg := fmt.Sprintf(`{ + "modify_bech32_prefixes": { + "operations": [ + {"operation": "set", "chain_name": "osmosis", "prefix": "osmo"}, + {"operation": "set", "chain_name": "chainB", "prefix": "osmo"}, + {"operation": "set", "chain_name": "chainB-cw20", "prefix": "osmo"}, + {"operation": "set", "chain_name": "chainC", "prefix": "osmo"} + ] + } + } + `) + _, err := contractKeeper.Execute(ctx, registryAddr, owner, []byte(msg), sdk.NewCoins()) + suite.Require().NoError(err) +} + +func (suite *HooksTestSuite) setForwardingOnAllChains(registryAddr sdk.AccAddress) { + suite.SetupAndTestPFM(ChainB, "chainB", registryAddr) + suite.SetupAndTestPFM(ChainC, "chainC", registryAddr) } // modifyChainChannelLinks modifies the chain channel links in the crosschain registry utilizing set, remove, and change operations @@ -949,29 +1010,14 @@ func (suite *HooksTestSuite) TestUnwrapToken() { // Instantiate contract and set up three chains with funds sent between each registryAddr, _, token0CBA, _ := suite.SetupCrosschainRegistry(ChainA) suite.setChainChannelLinks(registryAddr, ChainA) + suite.setAllPrefixesToOsmo(registryAddr, ChainA) + suite.setForwardingOnAllChains(registryAddr) chain := suite.GetChain(ChainA) - ctx := chain.GetContext() owner := chain.SenderAccount.GetAddress() receiver := chain.SenderAccounts[1].SenderAccount.GetAddress() osmosisApp := chain.GetOsmosisApp() - contractKeeper := wasmkeeper.NewDefaultPermissionKeeper(osmosisApp.WasmKeeper) - - msg := `{ - "modify_bech32_prefixes": { - "operations": [ - {"operation": "set", "chain_name": "osmosis", "prefix": "osmo"}, - {"operation": "set", "chain_name": "chainA", "prefix": "osmo"}, - {"operation": "set", "chain_name": "chainB", "prefix": "osmo"}, - {"operation": "set", "chain_name": "chainC", "prefix": "osmo"} - ] - } - } - ` - _, err := contractKeeper.Execute(ctx, registryAddr, owner, []byte(msg), sdk.NewCoins()) - suite.Require().NoError(err) - // Check that the balances are correct: token0CB should be >100, token0CBA should be 0 denomTrace0CA := transfertypes.ParseDenomTrace(transfertypes.GetPrefixedDenom("transfer", suite.pathAC.EndpointA.ChannelID, "token0")) token0CA := denomTrace0CA.IBCDenom() @@ -1004,7 +1050,7 @@ func (suite *HooksTestSuite) TestUnwrapToken() { initialReceiverBalance := receiverApp.BankKeeper.GetBalance(receiverChain.GetContext(), receiver, tc.receivedToken) suite.Require().Equal(sdk.NewInt(0), initialReceiverBalance.Amount) - msg = fmt.Sprintf(`{ + msg := fmt.Sprintf(`{ "unwrap_coin": { "receiver": "%s", "into_chain": "%s" @@ -1036,7 +1082,7 @@ func (suite *HooksTestSuite) TestUnwrapToken() { func (suite *HooksTestSuite) TestCrosschainSwaps() { owner := suite.chainA.SenderAccount.GetAddress() - _, crosschainAddr := suite.SetupCrosschainSwaps(ChainA) + _, crosschainAddr := suite.SetupCrosschainSwaps(ChainA, true) osmosisApp := suite.chainA.GetOsmosisApp() contractKeeper := wasmkeeper.NewDefaultPermissionKeeper(osmosisApp.WasmKeeper) @@ -1070,7 +1116,7 @@ func (suite *HooksTestSuite) TestCrosschainSwaps() { packetSequence, ok := responseJson["packet_sequence"].(float64) suite.Require().True(ok) - suite.Require().Equal(packetSequence, 1.0) + suite.Require().Equal(packetSequence, 2.0) balanceSender2 := osmosisApp.BankKeeper.GetBalance(suite.chainA.GetContext(), owner, "token0") suite.Require().Equal(int64(1000), balanceSender.Amount.Sub(balanceSender2.Amount).Int64()) @@ -1078,7 +1124,7 @@ func (suite *HooksTestSuite) TestCrosschainSwaps() { func (suite *HooksTestSuite) TestCrosschainSwapsViaIBCTest() { initializer := suite.chainB.SenderAccount.GetAddress() - _, crosschainAddr := suite.SetupCrosschainSwaps(ChainA) + _, crosschainAddr := suite.SetupCrosschainSwaps(ChainA, true) // Send some token0 tokens to B so that there are ibc tokens to send to A and crosschain-swap transferMsg := NewMsgTransfer(sdk.NewCoin("token0", sdk.NewInt(2000)), suite.chainA.SenderAccount.GetAddress().String(), initializer.String(), "channel-0", "") _, _, _, err := suite.FullSend(transferMsg, AtoB) @@ -1129,7 +1175,7 @@ func (suite *HooksTestSuite) TestCrosschainSwapsViaIBCTest() { // exist on chain B func (suite *HooksTestSuite) TestCrosschainSwapsViaIBCBadAck() { initializer := suite.chainB.SenderAccount.GetAddress() - _, crosschainAddr := suite.SetupCrosschainSwaps(ChainA) + _, crosschainAddr := suite.SetupCrosschainSwaps(ChainA, true) // Send some token0 tokens to B so that there are ibc tokens to send to A and crosschain-swap transferMsg := NewMsgTransfer(sdk.NewCoin("token0", sdk.NewInt(2000)), suite.chainA.SenderAccount.GetAddress().String(), initializer.String(), "channel-0", "") _, _, _, err := suite.FullSend(transferMsg, AtoB) @@ -1205,7 +1251,7 @@ func (suite *HooksTestSuite) TestCrosschainSwapsViaIBCBadAck() { &suite.Suite, crosschainAddr, []byte(fmt.Sprintf(`{"recoverable": {"addr": "%s"}}`, recoverAddr))) suite.Require().Contains(state, "token1") - suite.Require().Contains(state, `"sequence":2`) + suite.Require().Contains(state, `"sequence":3`) // Recover the stuck amount recoverMsg := `{"recover": {}}` @@ -1225,7 +1271,7 @@ func (suite *HooksTestSuite) TestCrosschainSwapsViaIBCBadAck() { // This is very similar to the two tests above, but the swap is done incorrectly func (suite *HooksTestSuite) TestCrosschainSwapsViaIBCBadSwap() { initializer := suite.chainB.SenderAccount.GetAddress() - _, crosschainAddr := suite.SetupCrosschainSwaps(ChainA) + _, crosschainAddr := suite.SetupCrosschainSwaps(ChainA, true) // Send some token0 tokens to B so that there are ibc tokens to send to A and crosschain-swap transferMsg := NewMsgTransfer(sdk.NewCoin("token0", sdk.NewInt(2000)), suite.chainA.SenderAccount.GetAddress().String(), initializer.String(), "channel-0", "") _, _, _, err := suite.FullSend(transferMsg, AtoB) @@ -1268,7 +1314,7 @@ func (suite *HooksTestSuite) TestCrosschainSwapsViaIBCBadSwap() { func (suite *HooksTestSuite) TestBadCrosschainSwapsNextMemoMessages() { initializer := suite.chainB.SenderAccount.GetAddress() - _, crosschainAddr := suite.SetupCrosschainSwaps(ChainA) + _, crosschainAddr := suite.SetupCrosschainSwaps(ChainA, true) // Send some token0 tokens to B so that there are ibc tokens to send to A and crosschain-swap transferMsg := NewMsgTransfer(sdk.NewCoin("token0", sdk.NewInt(20000)), suite.chainA.SenderAccount.GetAddress().String(), initializer.String(), "channel-0", "") _, _, _, err := suite.FullSend(transferMsg, AtoB) @@ -1417,8 +1463,8 @@ func (suite *HooksTestSuite) TestCrosschainForwardWithMemo() { initializer := suite.chainB.SenderAccount.GetAddress() receiver := suite.chainA.SenderAccounts[5].SenderAccount.GetAddress() - _, crosschainAddrA := suite.SetupCrosschainSwaps(ChainA) - swaprouterAddrB, crosschainAddrB := suite.SetupCrosschainSwaps(ChainB) + _, crosschainAddrA := suite.SetupCrosschainSwaps(ChainA, true) + swaprouterAddrB, crosschainAddrB := suite.SetupCrosschainSwaps(ChainB, false) // Send some token0 and token1 tokens to B so that there are ibc token0 to send to A and crosschain-swap, and token1 to create the pool transferMsg := NewMsgTransfer(sdk.NewCoin("token0", sdk.NewInt(500000)), suite.chainA.SenderAccount.GetAddress().String(), initializer.String(), "channel-0", "") _, _, _, err := suite.FullSend(transferMsg, AtoB) @@ -1489,7 +1535,7 @@ func (suite *HooksTestSuite) TestCrosschainSwapsViaIBCMultiHop() { accountB := suite.chainB.SenderAccount.GetAddress() accountC := suite.chainC.SenderAccount.GetAddress() - _, crosschainAddr := suite.SetupCrosschainSwaps(ChainA) + _, crosschainAddr := suite.SetupCrosschainSwaps(ChainA, true) // Send A's token0 all the way to B (A->C->B) transferMsg := NewMsgTransfer( @@ -1614,7 +1660,7 @@ type ChainActorDefinition struct { func (suite *HooksTestSuite) TestMultiHopXCS() { accountB := suite.chainB.SenderAccount.GetAddress() - swapRouterAddr, crosschainAddr := suite.SetupCrosschainSwaps(ChainA) + swapRouterAddr, crosschainAddr := suite.SetupCrosschainSwaps(ChainA, true) sendAmount := sdk.NewInt(100) @@ -1815,7 +1861,7 @@ func (suite *HooksTestSuite) TestMultiHopXCS() { func (suite *HooksTestSuite) ExecuteOutpostSwap(initializer, receiverAddr sdk.AccAddress, receiver string) { // Setup - _, crosschainAddr := suite.SetupCrosschainSwaps(ChainA) + _, crosschainAddr := suite.SetupCrosschainSwaps(ChainA, true) // Store and instantiate the outpost on chainB suite.chainB.StoreContractCode(&suite.Suite, "./bytecode/outpost.wasm") outpostAddr := suite.chainB.InstantiateContract(&suite.Suite, diff --git a/tests/ibc-hooks/path_validation_test.go b/tests/ibc-hooks/path_validation_test.go new file mode 100644 index 00000000000..5c96f8fa2b7 --- /dev/null +++ b/tests/ibc-hooks/path_validation_test.go @@ -0,0 +1,101 @@ +package ibc_hooks_test + +import ( + "fmt" + wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" + sdk "github.com/cosmos/cosmos-sdk/types" + ibctesting "github.com/cosmos/ibc-go/v4/testing" +) + +// This sets up PFM on chainB and tests that it works as expected. We assume ChainA is osmosis +func (suite *HooksTestSuite) SetupAndTestPFM(chainBId Chain, chainBName string, registryAddr sdk.AccAddress) { + targetChain := suite.GetChain(chainBId) + sendFrom := targetChain.SenderAccount.GetAddress() + direction := suite.GetDirection(ChainA, chainBId) + reverseDirection := suite.GetDirection(chainBId, ChainA) + sender, receiver := suite.GetEndpoints(suite.GetDirection(ChainA, chainBId)) + + osmosisApp := suite.chainA.GetOsmosisApp() + contractKeeper := wasmkeeper.NewDefaultPermissionKeeper(osmosisApp.WasmKeeper) + + pfm_msg := fmt.Sprintf(`{"has_packet_forwarding": {"chain": "%s"}}`, chainBName) + forwarding := suite.chainA.QueryContractJson(&suite.Suite, registryAddr, []byte(pfm_msg)) + suite.Require().False(forwarding.Bool()) + + transferMsg := NewMsgTransfer(sdk.NewCoin("token0", sdk.NewInt(2000)), targetChain.SenderAccount.GetAddress().String(), sendFrom.String(), suite.GetSenderChannel(chainBId, ChainA), "") + suite.FullSend(transferMsg, reverseDirection) + tokenBA := suite.GetIBCDenom(chainBId, ChainA, "token0") + + ctx := suite.chainA.GetContext() + + msg := fmt.Sprintf(`{"propose_pfm":{"chain": "%s"}}`, chainBName) + _, err := contractKeeper.Execute(ctx, registryAddr, sendFrom, []byte(msg), sdk.NewCoins(sdk.NewCoin(tokenBA, sdk.NewInt(1)))) + suite.Require().NoError(err) + + forwarding = suite.chainA.QueryContractJson(&suite.Suite, registryAddr, []byte(pfm_msg)) + suite.Require().False(forwarding.Bool()) + + // Move forward one block + suite.chainA.NextBlock() + suite.chainA.Coordinator.IncrementTime() + + // Update both clients + err = receiver.UpdateClient() + suite.Require().NoError(err) + err = sender.UpdateClient() + suite.Require().NoError(err) + + events := ctx.EventManager().Events() + packet0, err := ibctesting.ParsePacketFromEvents(events) + suite.Require().NoError(err) + result := suite.RelayPacketNoAck(packet0, direction) // No ack because it's a forward + + forwarding = suite.chainA.QueryContractJson(&suite.Suite, registryAddr, []byte(pfm_msg)) + suite.Require().False(forwarding.Bool()) + + packet1, err := ibctesting.ParsePacketFromEvents(result.GetEvents()) + suite.Require().NoError(err) + receiveResult, _ := suite.RelayPacket(packet1, reverseDirection) + + forwarding = suite.chainA.QueryContractJson(&suite.Suite, registryAddr, []byte(pfm_msg)) + suite.Require().False(forwarding.Bool()) + + err = sender.UpdateClient() + suite.Require().NoError(err) + err = receiver.UpdateClient() + suite.Require().NoError(err) + + ack, err := ibctesting.ParseAckFromEvents(receiveResult.GetEvents()) + suite.Require().NoError(err) + + err = sender.AcknowledgePacket(packet0, ack) + suite.Require().NoError(err) + + // After the ack fully travels back to the initial chain, we consider PFM to be properly set + forwarding = suite.chainA.QueryContractJson(&suite.Suite, registryAddr, []byte(pfm_msg)) + suite.Require().True(forwarding.Bool()) +} + +func (suite *HooksTestSuite) TestPathValidation() { + owner := suite.chainA.SenderAccount.GetAddress() + registryAddr, _, _, _ := suite.SetupCrosschainRegistry(ChainA) + suite.setChainChannelLinks(registryAddr, ChainA) + + osmosisApp := suite.chainA.GetOsmosisApp() + contractKeeper := wasmkeeper.NewDefaultPermissionKeeper(osmosisApp.WasmKeeper) + + msg := fmt.Sprintf(`{ + "modify_bech32_prefixes": { + "operations": [ + {"operation": "set", "chain_name": "osmosis", "prefix": "osmo"}, + {"operation": "set", "chain_name": "chainA", "prefix": "osmo"}, + {"operation": "set", "chain_name": "chainB", "prefix": "osmo"}, + {"operation": "set", "chain_name": "chainC", "prefix": "osmo"} + ] + } + } + `) + _, err := contractKeeper.Execute(suite.chainA.GetContext(), registryAddr, owner, []byte(msg), sdk.NewCoins()) + suite.Require().NoError(err) + suite.SetupAndTestPFM(ChainB, "chainB", registryAddr) +} diff --git a/tests/ibc-hooks/xcs_cw20_test.go b/tests/ibc-hooks/xcs_cw20_test.go index 38d85c66dcc..62da7cdd1d5 100644 --- a/tests/ibc-hooks/xcs_cw20_test.go +++ b/tests/ibc-hooks/xcs_cw20_test.go @@ -104,7 +104,7 @@ func (suite *HooksTestSuite) TestCW20ICS20() { cw20IbcDenom := "ibc/134A49086C1164C78313D57E69E5A8656D8AE8CF6BB45B52F2DBFEFAE6EE30B8" cw20Addr, cw20ics20Addr := suite.SetupCW20(ChainB) - swaprouterAddr, crosschainAddr := suite.SetupCrosschainSwaps(ChainA) + swaprouterAddr, crosschainAddr := suite.SetupCrosschainSwaps(ChainA, true) chainA := suite.GetChain(ChainA) chainB := suite.GetChain(ChainB)