Skip to content

Commit

Permalink
fix(ucs01): respect tokenfactory limits when handling foreign denom
Browse files Browse the repository at this point in the history
  • Loading branch information
hussein-aitlahcen committed Oct 18, 2023
1 parent b44a6b8 commit d5dc111
Show file tree
Hide file tree
Showing 8 changed files with 174 additions and 39 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion cosmwasm/ucs01-relay-api/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 2 additions & 0 deletions cosmwasm/ucs01-relay/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
15 changes: 14 additions & 1 deletion cosmwasm/ucs01-relay/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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())
}
}
}
}

Expand Down
3 changes: 3 additions & 0 deletions cosmwasm/ucs01-relay/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<FromUtf8Error> for ContractError {
Expand Down
4 changes: 3 additions & 1 deletion cosmwasm/ucs01-relay/src/msg.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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 },
}
Expand Down
174 changes: 139 additions & 35 deletions cosmwasm/ucs01-relay/src/protocol.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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<IbcOrder> {
match version {
Ics20Protocol::VERSION => Some(Ics20Protocol::ORDERING),
Expand Down Expand Up @@ -73,7 +85,11 @@ fn decrease_outstanding(
}

trait OnReceive {
fn foreign_toggle(&mut self, denom: &str) -> Result<bool, ContractError>;
fn foreign_toggle(
&mut self,
contract_address: &Addr,
denom: &str,
) -> Result<(bool, Hash, CosmosMsg<TokenFactoryMsg>), ContractError>;

fn local_unescrow(
&mut self,
Expand Down Expand Up @@ -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(),
]
})
}
Expand All @@ -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<bool, ContractError> {
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<TokenFactoryMsg>), 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(
Expand All @@ -165,6 +196,8 @@ impl<'a> OnReceive for StatefulOnReceive<'a> {
}

trait ForTokens {
fn hash_to_denom(&mut self, denom_hash: Hash) -> Result<String, ContractError>;

fn on_local(
&mut self,
channel_id: &str,
Expand All @@ -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 } => {
Expand Down Expand Up @@ -237,6 +283,12 @@ impl<'a> ForTokens for StatefulSendTokens<'a> {
}
.into()])
}

fn hash_to_denom(&mut self, denom_hash: Hash) -> Result<String, ContractError> {
HASH_TO_FOREIGN_DENOM
.load(self.deps.storage, denom_hash)
.map_err(Into::into)
}
}

struct StatefulRefundTokens<'a> {
Expand Down Expand Up @@ -275,6 +327,12 @@ impl<'a> ForTokens for StatefulRefundTokens<'a> {
}
.into()])
}

fn hash_to_denom(&mut self, denom_hash: Hash) -> Result<String, ContractError> {
HASH_TO_FOREIGN_DENOM
.load(self.deps.storage, denom_hash)
.map_err(Into::into)
}
}

pub struct ProtocolCommon<'a> {
Expand Down Expand Up @@ -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<bool, crate::error::ContractError> {
Ok(self.toggle)
fn foreign_toggle(
&mut self,
contract_address: &Addr,
denom: &str,
) -> Result<(bool, Hash, CosmosMsg<TokenFactoryMsg>), 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(
Expand Down Expand Up @@ -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()
}
Expand Down Expand Up @@ -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()
}
Expand Down Expand Up @@ -629,6 +719,13 @@ mod tests {
}
.into()])
}

fn hash_to_denom(
&mut self,
_denom_hash: Hash,
) -> Result<String, crate::error::ContractError> {
todo!()
}
}
assert_eq!(
OnRemoteOnly.execute(
Expand Down Expand Up @@ -682,6 +779,13 @@ mod tests {
> {
todo!()
}

fn hash_to_denom(
&mut self,
_denom_hash: Hash,
) -> Result<String, crate::error::ContractError> {
todo!()
}
}
let mut state = OnLocalOnly { total: 0u8.into() };
assert_eq!(
Expand Down
11 changes: 10 additions & 1 deletion cosmwasm/ucs01-relay/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Hash> = Map::new("foreign_denom_to_hash");

pub const HASH_TO_FOREIGN_DENOM: Map<Hash, String> = Map::new("hash_to_foreign_denom");

#[cw_serde]
#[derive(Default)]
Expand Down

0 comments on commit d5dc111

Please sign in to comment.