diff --git a/Cargo.lock b/Cargo.lock index 53034388f4..94e1a6c596 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6174,9 +6174,11 @@ dependencies = [ "cw-storage-plus", "cw2", "ethabi", + "hex", "schemars", "semver", "serde", + "sha2 0.10.8", "thiserror", "token-factory-api", "ucs01-relay-api", diff --git a/cosmwasm/ucs01-relay-api/src/types.rs b/cosmwasm/ucs01-relay-api/src/types.rs index 3b96959aed..827aee7ee9 100644 --- a/cosmwasm/ucs01-relay-api/src/types.rs +++ b/cosmwasm/ucs01-relay-api/src/types.rs @@ -28,7 +28,7 @@ pub struct TransferToken { impl TransferToken { // If a denom originated from a remote network, it will be in the form: - // `factory/{contract_address}/{port_id}/{channel_id}/denom` + // `factory/{contract_address}/{hash}` // In order for the remote module to consider this denom as local, we must // strip the `factory/{contract_address}/` prefix before sending the tokens. pub fn normalize_for_ibc_transfer( diff --git a/cosmwasm/ucs01-relay/Cargo.toml b/cosmwasm/ucs01-relay/Cargo.toml index d1a5d77ad9..50fd1cfb19 100644 --- a/cosmwasm/ucs01-relay/Cargo.toml +++ b/cosmwasm/ucs01-relay/Cargo.toml @@ -22,5 +22,7 @@ schemars = "0.8" semver = "1" serde = { version = "1.0", default-features = false, features = ["derive"] } thiserror = { version = "1.0" } +sha2 = { version = "0.10", default-features = false } +hex = { version = "0.4", default-features = false } token-factory-api = { workspace = true } ucs01-relay-api = { workspace = true } diff --git a/cosmwasm/ucs01-relay/src/contract.rs b/cosmwasm/ucs01-relay/src/contract.rs index 245352b700..d2499d36d5 100644 --- a/cosmwasm/ucs01-relay/src/contract.rs +++ b/cosmwasm/ucs01-relay/src/contract.rs @@ -19,7 +19,10 @@ use crate::{ PortResponse, QueryMsg, TransferMsg, }, protocol::{Ics20Protocol, ProtocolCommon, Ucs01Protocol}, - state::{ChannelInfo, Config, ADMIN, CHANNEL_INFO, CHANNEL_STATE, CONFIG}, + state::{ + ChannelInfo, Config, ADMIN, CHANNEL_INFO, CHANNEL_STATE, CONFIG, FOREIGN_DENOM_TO_HASH, + HASH_TO_FOREIGN_DENOM, + }, }; const CONTRACT_NAME: &str = "crates.io:ucs01-relay"; @@ -71,6 +74,16 @@ pub fn execute( let admin = deps.api.addr_validate(&admin)?; Ok(ADMIN.execute_update_admin(deps, info, Some(admin))?) } + ExecuteMsg::RegisterDenom { denom, hash } => { + if info.sender != env.contract.address { + Err(ContractError::Unauthorized) + } else { + let normalized_hash = hash.0.try_into().expect("impossible"); + FOREIGN_DENOM_TO_HASH.save(deps.storage, denom.clone().into(), &normalized_hash)?; + HASH_TO_FOREIGN_DENOM.save(deps.storage, normalized_hash, &denom.to_string())?; + Ok(Response::default()) + } + } } } diff --git a/cosmwasm/ucs01-relay/src/error.rs b/cosmwasm/ucs01-relay/src/error.rs index a836e318fd..bd8e965a74 100644 --- a/cosmwasm/ucs01-relay/src/error.rs +++ b/cosmwasm/ucs01-relay/src/error.rs @@ -52,6 +52,9 @@ pub enum ContractError { channel_id: String, protocol_version: String, }, + + #[error("Only myself is able to trigger this message")] + Unauthorized, } impl From for ContractError { diff --git a/cosmwasm/ucs01-relay/src/msg.rs b/cosmwasm/ucs01-relay/src/msg.rs index e1de9a2b1e..3d453bff50 100644 --- a/cosmwasm/ucs01-relay/src/msg.rs +++ b/cosmwasm/ucs01-relay/src/msg.rs @@ -1,5 +1,5 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{IbcChannel, Uint512}; +use cosmwasm_std::{Binary, IbcChannel, Uint512}; use crate::state::ChannelInfo; @@ -20,6 +20,8 @@ pub struct MigrateMsg {} pub enum ExecuteMsg { /// This allows us to transfer native tokens Transfer(TransferMsg), + /// Register a denom, this message exist only to create sub-transaction from the top-level IBC call. + RegisterDenom { denom: String, hash: Binary }, /// Change the admin (must be called by current admin) UpdateAdmin { admin: String }, } diff --git a/cosmwasm/ucs01-relay/src/protocol.rs b/cosmwasm/ucs01-relay/src/protocol.rs index a88af6d8fc..dcf60b53b0 100644 --- a/cosmwasm/ucs01-relay/src/protocol.rs +++ b/cosmwasm/ucs01-relay/src/protocol.rs @@ -1,7 +1,8 @@ use cosmwasm_std::{ - Addr, BankMsg, Coin, CosmosMsg, DepsMut, Env, IbcEndpoint, IbcOrder, MessageInfo, Uint128, - Uint512, + wasm_execute, Addr, BankMsg, Coin, CosmosMsg, DepsMut, Env, IbcEndpoint, IbcOrder, MessageInfo, + StdError, Uint128, Uint512, }; +use sha2::{Digest, Sha256}; use token_factory_api::TokenFactoryMsg; use ucs01_relay_api::{ protocol::TransferProtocol, @@ -13,9 +14,20 @@ use ucs01_relay_api::{ use crate::{ error::ContractError, - state::{ChannelInfo, CHANNEL_STATE, FOREIGN_TOKEN_CREATED}, + msg::ExecuteMsg, + state::{ + ChannelInfo, Hash, CHANNEL_STATE, FOREIGN_DENOM_TO_HASH, HASH_LENGTH, HASH_TO_FOREIGN_DENOM, + }, }; +pub fn hash_denom(denom: &str) -> Hash { + let mut hasher = Sha256::new(); + hasher.update(denom); + hasher.finalize()[..HASH_LENGTH] + .try_into() + .expect("impossible") +} + pub fn protocol_ordering(version: &str) -> Option { match version { Ics20Protocol::VERSION => Some(Ics20Protocol::ORDERING), @@ -73,7 +85,11 @@ fn decrease_outstanding( } trait OnReceive { - fn foreign_toggle(&mut self, denom: &str) -> Result; + fn foreign_toggle( + &mut self, + contract_address: &Addr, + denom: &str, + ) -> Result<(bool, Hash, CosmosMsg), ContractError>; fn local_unescrow( &mut self, @@ -110,29 +126,28 @@ trait OnReceive { } DenomOrigin::Remote { denom } => { let foreign_denom = make_foreign_denom(counterparty_endpoint, denom); + let (exists, hashed_foreign_denom, register_msg) = + self.foreign_toggle(contract_address, &foreign_denom)?; + let normalized_foreign_denom = + format!("0x{}", hex::encode(hashed_foreign_denom)); let factory_denom = - format!("factory/{}/{}", contract_address, foreign_denom); - let exists = self.foreign_toggle(&factory_denom)?; + format!("factory/{}/{}", contract_address, normalized_foreign_denom); + let mint = TokenFactoryMsg::MintTokens { + denom: factory_denom, + amount, + mint_to_address: receiver.to_string(), + }; Ok(if exists { - vec![TokenFactoryMsg::MintTokens { - denom: factory_denom, - amount, - mint_to_address: receiver.to_string(), - } - .into()] + vec![mint.into()] } else { vec![ + register_msg, TokenFactoryMsg::CreateDenom { - subdenom: foreign_denom.clone(), + subdenom: normalized_foreign_denom.clone(), metadata: None, } .into(), - TokenFactoryMsg::MintTokens { - denom: factory_denom, - amount, - mint_to_address: receiver.to_string(), - } - .into(), + mint.into(), ] }) } @@ -147,10 +162,26 @@ pub struct StatefulOnReceive<'a> { deps: DepsMut<'a>, } impl<'a> OnReceive for StatefulOnReceive<'a> { - fn foreign_toggle(&mut self, denom: &str) -> Result { - let exists = FOREIGN_TOKEN_CREATED.has(self.deps.storage, denom); - FOREIGN_TOKEN_CREATED.save(self.deps.storage, denom, &())?; - Ok(exists) + fn foreign_toggle( + &mut self, + contract_address: &Addr, + denom: &str, + ) -> Result<(bool, Hash, CosmosMsg), ContractError> { + let exists = FOREIGN_DENOM_TO_HASH.has(self.deps.storage, denom.into()); + let hash = hash_denom(denom); + Ok(( + exists, + hash, + wasm_execute( + contract_address, + &ExecuteMsg::RegisterDenom { + denom: denom.to_string(), + hash: hash.into(), + }, + Default::default(), + )? + .into(), + )) } fn local_unescrow( @@ -165,6 +196,8 @@ impl<'a> OnReceive for StatefulOnReceive<'a> { } trait ForTokens { + fn hash_to_denom(&mut self, denom_hash: Hash) -> Result; + fn on_local( &mut self, channel_id: &str, @@ -191,12 +224,25 @@ trait ForTokens { let amount = amount .try_into() .expect("CosmWasm require transferred amount to be Uint128..."); + // We know at this point that the factory/{contract_address} have + // been stripped if the denom has been created by the contract. If + // the denom is a hexadecimal string, it's a foreign one. + let normalized_denom = match denom.strip_prefix("0x") { + Some(foreign_denom_hash) => self.hash_to_denom( + hex::decode(foreign_denom_hash) + .map_err(|e| { + StdError::generic_err(format!("Failed to decode denom hash {:?}", e)) + })? + .try_into() + .expect("impossible"), + )?, + None => denom.to_string(), + }; // This is the origin from the counterparty POV - match DenomOrigin::from((denom.as_str(), counterparty_endpoint)) { - DenomOrigin::Local { denom } => { - // the denom has been previously normalized (factory/{}/ prefix removed), we must reconstruct to burn - let foreign_denom = make_foreign_denom(counterparty_endpoint, denom); - let factory_denom = format!("factory/{}/{}", contract_address, foreign_denom); + match DenomOrigin::from((normalized_denom.as_ref(), counterparty_endpoint)) { + DenomOrigin::Local { .. } => { + // the denom has been previously normalized (factory/{contract}/ prefix removed), we must reconstruct to burn + let factory_denom = format!("factory/{}/{}", contract_address, denom); messages.append(&mut self.on_remote(channel_id, &factory_denom, amount)?); } DenomOrigin::Remote { denom } => { @@ -237,6 +283,12 @@ impl<'a> ForTokens for StatefulSendTokens<'a> { } .into()]) } + + fn hash_to_denom(&mut self, denom_hash: Hash) -> Result { + HASH_TO_FOREIGN_DENOM + .load(self.deps.storage, denom_hash) + .map_err(Into::into) + } } struct StatefulRefundTokens<'a> { @@ -275,6 +327,12 @@ impl<'a> ForTokens for StatefulRefundTokens<'a> { } .into()]) } + + fn hash_to_denom(&mut self, denom_hash: Hash) -> Result { + HASH_TO_FOREIGN_DENOM + .load(self.deps.storage, denom_hash) + .map_err(Into::into) + } } pub struct ProtocolCommon<'a> { @@ -480,18 +538,38 @@ impl<'a> TransferProtocol for Ucs01Protocol<'a> { #[cfg(test)] mod tests { - use cosmwasm_std::{Addr, BankMsg, Coin, IbcEndpoint, Uint128, Uint256}; + use cosmwasm_std::{ + wasm_execute, Addr, BankMsg, Coin, CosmosMsg, IbcEndpoint, Uint128, Uint256, + }; use token_factory_api::TokenFactoryMsg; use ucs01_relay_api::types::TransferToken; - use super::{ForTokens, OnReceive}; + use super::{hash_denom, ForTokens, OnReceive}; + use crate::{error::ContractError, msg::ExecuteMsg, state::Hash}; struct TestOnReceive { toggle: bool, } impl OnReceive for TestOnReceive { - fn foreign_toggle(&mut self, _denom: &str) -> Result { - Ok(self.toggle) + fn foreign_toggle( + &mut self, + contract_address: &Addr, + denom: &str, + ) -> Result<(bool, Hash, CosmosMsg), ContractError> { + let hash = hash_denom(denom); + Ok(( + self.toggle, + hash, + wasm_execute( + contract_address, + &ExecuteMsg::RegisterDenom { + denom: denom.to_string(), + hash: hash.into(), + }, + Default::default(), + )? + .into(), + )) } fn local_unescrow( @@ -524,13 +602,25 @@ mod tests { },], ), Ok(vec![ + wasm_execute( + Addr::unchecked("0xDEADC0DE"), + &ExecuteMsg::RegisterDenom { + denom: "transfer/channel-34/from-counterparty".into(), + hash: hex::decode("af30fd00576e1d27471a4d2b0c0487dc6876e0589e") + .unwrap() + .into(), + }, + Default::default() + ) + .unwrap() + .into(), TokenFactoryMsg::CreateDenom { - subdenom: "transfer/channel-34/from-counterparty".into(), + subdenom: "0xaf30fd00576e1d27471a4d2b0c0487dc6876e0589e".into(), metadata: None } .into(), TokenFactoryMsg::MintTokens { - denom: "factory/0xDEADC0DE/transfer/channel-34/from-counterparty".into(), + denom: "factory/0xDEADC0DE/0xaf30fd00576e1d27471a4d2b0c0487dc6876e0589e".into(), amount: Uint128::from(100u128), mint_to_address: "receiver".into() } @@ -559,7 +649,7 @@ mod tests { },], ), Ok(vec![TokenFactoryMsg::MintTokens { - denom: "factory/0xDEADC0DE/transfer/channel-34/from-counterparty".into(), + denom: "factory/0xDEADC0DE/0xaf30fd00576e1d27471a4d2b0c0487dc6876e0589e".into(), amount: Uint128::from(100u128), mint_to_address: "receiver".into() } @@ -629,6 +719,13 @@ mod tests { } .into()]) } + + fn hash_to_denom( + &mut self, + _denom_hash: Hash, + ) -> Result { + todo!() + } } assert_eq!( OnRemoteOnly.execute( @@ -682,6 +779,13 @@ mod tests { > { todo!() } + + fn hash_to_denom( + &mut self, + _denom_hash: Hash, + ) -> Result { + todo!() + } } let mut state = OnLocalOnly { total: 0u8.into() }; assert_eq!( diff --git a/cosmwasm/ucs01-relay/src/state.rs b/cosmwasm/ucs01-relay/src/state.rs index 6f477e6623..200a6eea2f 100644 --- a/cosmwasm/ucs01-relay/src/state.rs +++ b/cosmwasm/ucs01-relay/src/state.rs @@ -13,7 +13,16 @@ pub const CHANNEL_INFO: Map<&str, ChannelInfo> = Map::new("channel_info"); /// indexed by (channel_id, denom) maintaining the balance of the channel in that currency pub const CHANNEL_STATE: Map<(&str, &str), ChannelState> = Map::new("channel_state"); -pub const FOREIGN_TOKEN_CREATED: Map<&str, ()> = Map::new("foreign_tokens"); +// TokenFactory limitation +// MaxSubdenomLength = 44 +// HASH_LENGTH = (MaxSubdenomLength - size_of("0x")) / 2 = 42 +pub const HASH_LENGTH: usize = 21; + +pub type Hash = [u8; HASH_LENGTH]; + +pub const FOREIGN_DENOM_TO_HASH: Map = Map::new("foreign_denom_to_hash"); + +pub const HASH_TO_FOREIGN_DENOM: Map = Map::new("hash_to_foreign_denom"); #[cw_serde] #[derive(Default)]