diff --git a/cosmwasm/ucs01-relay-api/src/middleware.rs b/cosmwasm/ucs01-relay-api/src/middleware.rs index 6a167524dc..64efed6c64 100644 --- a/cosmwasm/ucs01-relay-api/src/middleware.rs +++ b/cosmwasm/ucs01-relay-api/src/middleware.rs @@ -6,6 +6,8 @@ use unionlabs::{ validated::{Validate, Validated}, }; +use crate::types::Fees; + pub const DEFAULT_PFM_TIMEOUT: &str = "1m"; pub const DEFAULT_PFM_RETRIES: u8 = 0; @@ -105,6 +107,9 @@ pub struct PacketForward { pub retries: u8, pub next: Option>, pub return_info: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + pub fees: Option, } impl PacketForward { @@ -167,7 +172,5 @@ mod tests { let parsed = serde_json_wasm::from_str::(memo).expect("works"); dbg!(parsed); - - // panic!() } } diff --git a/cosmwasm/ucs01-relay-api/src/protocol.rs b/cosmwasm/ucs01-relay-api/src/protocol.rs index 9b271c7bd5..f859a8979f 100644 --- a/cosmwasm/ucs01-relay-api/src/protocol.rs +++ b/cosmwasm/ucs01-relay-api/src/protocol.rs @@ -436,7 +436,7 @@ mod tests { use crate::{ protocol::{tokens_to_attr, ATTR_ASSETS}, - types::TransferToken, + types::{FeePerU128, TransferToken}, }; #[test] @@ -444,10 +444,12 @@ mod tests { let token = TransferToken { denom: "factory/1/2/3".into(), amount: 0xDEAD_u64.into(), + fee: FeePerU128::zero(), }; let token2 = TransferToken { denom: "factory/1/3/3".into(), amount: 0xC0DE_u64.into(), + fee: FeePerU128::zero(), }; let attr = tokens_to_attr([token, token2]); let coins = cosmwasm_std::from_json::>(attr.value).unwrap(); diff --git a/cosmwasm/ucs01-relay-api/src/types.rs b/cosmwasm/ucs01-relay-api/src/types.rs index cc1d5d01a6..6a8662c054 100644 --- a/cosmwasm/ucs01-relay-api/src/types.rs +++ b/cosmwasm/ucs01-relay-api/src/types.rs @@ -1,7 +1,14 @@ +use std::collections::BTreeMap; + use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Binary, Coin, HexBinary, IbcEndpoint, StdError, Uint128, Uint256}; +use cosmwasm_std::{ + Binary, CheckedMultiplyRatioError, Coin, HexBinary, IbcEndpoint, StdError, Uint128, Uint256, +}; use ethabi::{ParamType, Token}; -use unionlabs::encoding::{self, Decode, Encode, EncodeAs, Encoding}; +use unionlabs::{ + bounded::BoundedU128, + encoding::{self, Decode, Encode, EncodeAs, Encoding}, +}; pub type GenericAck = Result, Vec>; @@ -9,6 +16,8 @@ pub type GenericAck = Result, Vec>; pub enum EncodingError { #[error("ICS20 can handle a single coin only.")] Ics20OnlyOneCoin, + #[error("ICS20 cannot handle fee directly.")] + Ics20CanotHandleFee, #[error("Could not decode UCS01 packet: value: {data}, err: {err:?}", data = serde_utils::to_hex(.value))] InvalidUCS01PacketEncoding { value: Vec, err: ethabi::Error }, #[error("Could not decode UCS01 ack, expected a boolean, got: {data}", data = serde_utils::to_hex(.got))] @@ -69,17 +78,43 @@ pub struct TransferPacketCommon { pub struct TransferToken { pub denom: String, pub amount: Uint128, + pub fee: FeePerU128, +} + +impl TransferToken { + pub fn actual_amounts(&self) -> Result<(Uint128, Uint128), CheckedMultiplyRatioError> { + let fee_amount = self.amount.checked_multiply_ratio(self.fee.0, u128::MAX)?; + let actual_amount = self.amount - fee_amount; + Ok((actual_amount, fee_amount)) + } } -impl From for TransferToken { - fn from(value: Coin) -> Self { +#[cw_serde] +#[derive(Copy, Eq)] +pub struct FeePerU128(pub Uint128); + +impl FeePerU128 { + pub const fn zero() -> Self { + Self(Uint128::zero()) + } + + pub fn percent(x: BoundedU128<0, 100>) -> Result { + Ok(Self(Uint128::MAX.checked_multiply_ratio(x, 100u128)?)) + } +} + +impl From<(Coin, FeePerU128)> for TransferToken { + fn from((coin, fee): (Coin, FeePerU128)) -> Self { Self { - denom: value.denom, - amount: value.amount, + denom: coin.denom, + amount: coin.amount, + fee, } } } +pub type Fees = BTreeMap; + #[derive(Clone, PartialEq, Eq, Debug)] pub struct Ucs01TransferPacket { /// the sender address @@ -127,10 +162,11 @@ impl Encode for Ucs01TransferPacket { Token::Array( self.tokens .into_iter() - .map(|TransferToken { denom, amount }| { + .map(|TransferToken { denom, amount, fee }| { Token::Tuple(vec![ Token::String(denom), Token::Uint(Uint256::from(amount).to_be_bytes().into()), + Token::Uint(Uint256::from(fee.0).to_be_bytes().into()), ]) }) .collect(), @@ -151,6 +187,7 @@ impl Decode for Ucs01TransferPacket { ParamType::Array(Box::new(ParamType::Tuple(vec![ ParamType::String, ParamType::Uint(128), + ParamType::Uint(128), ]))), ParamType::String, ], @@ -172,9 +209,10 @@ impl Decode for Ucs01TransferPacket { .map(|encoded_token| { if let Token::Tuple(encoded_token_inner) = encoded_token { match &encoded_token_inner[..] { - [Token::String(denom), Token::Uint(amount)] => TransferToken { + [Token::String(denom), Token::Uint(amount), Token::Uint(fee)] => TransferToken { denom: denom.clone(), amount: Uint128::new(amount.as_u128()), + fee: FeePerU128(Uint128::new(fee.as_u128())), }, _ => unreachable!(), } @@ -248,6 +286,7 @@ impl TransferPacket for Ics20Packet { vec![TransferToken { denom: self.denom.clone(), amount: self.amount, + fee: FeePerU128(0u128.into()), }] } @@ -330,7 +369,13 @@ impl TryFrom> for Ics20Packet { }: TransferPacketCommon, ) -> Result { let (denom, amount) = match &tokens[..] { - [TransferToken { denom, amount }] => Ok((denom.clone(), *amount)), + [TransferToken { denom, amount, fee }] => { + if fee.0.is_zero() { + Ok((denom.clone(), *amount)) + } else { + Err(EncodingError::Ics20CanotHandleFee) + } + } _ => Err(EncodingError::Ics20OnlyOneCoin), }?; Ok(Self { @@ -378,7 +423,7 @@ mod tests { use unionlabs::encoding::{Decode, DecodeAs, Encode, EncodeAs}; use super::{Ics20Packet, TransferToken, Ucs01Ack, Ucs01TransferPacket}; - use crate::types::{DenomOrigin, Ics20Ack, JsonWasm}; + use crate::types::{DenomOrigin, FeePerU128, Ics20Ack, JsonWasm}; #[test] fn ucs01_packet_encode_decode_iso() { @@ -389,14 +434,17 @@ mod tests { TransferToken { denom: "denom-0".into(), amount: Uint128::from(1u32), + fee: FeePerU128::zero(), }, TransferToken { denom: "denom-1".into(), amount: Uint128::MAX, + fee: FeePerU128::zero(), }, TransferToken { denom: "denom-2".into(), amount: Uint128::from(1337u32), + fee: FeePerU128::zero(), }, ], memo: String::new(), diff --git a/cosmwasm/ucs01-relay/src/contract.rs b/cosmwasm/ucs01-relay/src/contract.rs index ea394a71ff..18969642c9 100644 --- a/cosmwasm/ucs01-relay/src/contract.rs +++ b/cosmwasm/ucs01-relay/src/contract.rs @@ -2,13 +2,13 @@ use cosmwasm_std::entry_point; use cosmwasm_std::{ to_json_binary, Addr, Binary, Coins, Deps, DepsMut, Env, IbcQuery, MessageInfo, Order, - PortIdResponse, Response, StdError, StdResult, + PortIdResponse, Response, StdError, StdResult, Uint128, }; use cw2::set_contract_version; use token_factory_api::TokenFactoryMsg; use ucs01_relay_api::{ protocol::{TransferInput, TransferProtocol}, - types::TransferToken, + types::{FeePerU128, TransferToken}, }; use crate::{ @@ -115,10 +115,20 @@ pub fn execute_transfer( info: MessageInfo, msg: TransferMsg, ) -> Result, ContractError> { + let fees = msg.fees.unwrap_or_default(); let tokens: Vec = Coins::try_from(info.funds.clone()) .map_err(|_| StdError::generic_err("Couldn't decode funds to Coins"))? .into_vec() .into_iter() + .map(|coin| { + let denom = coin.denom.clone(); + ( + coin, + fees.get(&denom) + .copied() + .unwrap_or(FeePerU128(Uint128::zero())), + ) + }) .map(Into::into) .collect(); diff --git a/cosmwasm/ucs01-relay/src/error.rs b/cosmwasm/ucs01-relay/src/error.rs index 592c227070..81c02852af 100644 --- a/cosmwasm/ucs01-relay/src/error.rs +++ b/cosmwasm/ucs01-relay/src/error.rs @@ -1,6 +1,6 @@ use std::string::FromUtf8Error; -use cosmwasm_std::{IbcOrder, OverflowError, StdError, SubMsgResult}; +use cosmwasm_std::{CheckedMultiplyRatioError, IbcOrder, OverflowError, StdError, SubMsgResult}; use cw_controllers::AdminError; use thiserror::Error; use ucs01_relay_api::{middleware::MiddlewareError, protocol::ProtocolError, types::EncodingError}; @@ -71,6 +71,9 @@ pub enum ContractError { #[error("unable to decode json value")] SerdeJson(#[from] serde_json_wasm::de::Error), + + #[error("{0}")] + Arithmetic(#[from] CheckedMultiplyRatioError), } impl From for ContractError { diff --git a/cosmwasm/ucs01-relay/src/ibc.rs b/cosmwasm/ucs01-relay/src/ibc.rs index e7fb368737..e677658ac9 100644 --- a/cosmwasm/ucs01-relay/src/ibc.rs +++ b/cosmwasm/ucs01-relay/src/ibc.rs @@ -239,7 +239,7 @@ pub fn ibc_packet_ack( let channel_info = CHANNEL_INFO.load(deps.storage, &msg.original_packet.src.channel_id)?; let info = MessageInfo { - sender: msg.clone().relayer, + sender: msg.relayer.clone(), funds: Default::default(), }; diff --git a/cosmwasm/ucs01-relay/src/msg.rs b/cosmwasm/ucs01-relay/src/msg.rs index 059d3c99bd..3bb5fca8e5 100644 --- a/cosmwasm/ucs01-relay/src/msg.rs +++ b/cosmwasm/ucs01-relay/src/msg.rs @@ -1,6 +1,7 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Binary, CosmosMsg, IbcChannel, IbcEndpoint, Uint512}; use token_factory_api::TokenFactoryMsg; +use ucs01_relay_api::types::Fees; use crate::state::ChannelInfo; @@ -45,6 +46,8 @@ pub struct TransferMsg { pub timeout: Option, /// The memo pub memo: String, + /// Fee associated with the transfer, denominated in transferred coins + pub fees: Option, } #[cw_serde] diff --git a/cosmwasm/ucs01-relay/src/protocol.rs b/cosmwasm/ucs01-relay/src/protocol.rs index 6493f951cd..cdaafe2f41 100644 --- a/cosmwasm/ucs01-relay/src/protocol.rs +++ b/cosmwasm/ucs01-relay/src/protocol.rs @@ -169,6 +169,7 @@ pub trait TransferProtocolExt<'a>: receiver: forward.receiver.value(), timeout: Some(timeout), memo, + fees: forward.fees, }; // Send forward message @@ -350,6 +351,7 @@ fn normalize_for_ibc_transfer( Ok(TransferToken { denom: normalized_denom, amount: token.amount, + fee: token.fee, }) } trait OnReceive { @@ -373,64 +375,96 @@ trait OnReceive { endpoint: &IbcEndpoint, counterparty_endpoint: &IbcEndpoint, receiver: &str, + relayer: &str, tokens: Vec, ) -> Result<(Vec, Vec>), ContractError> { tokens .into_iter() - .map(|TransferToken { denom, amount }| { - match DenomOrigin::from((denom.as_str(), counterparty_endpoint)) { - DenomOrigin::Local { denom } => { - self.local_unescrow(&endpoint.channel_id, denom, amount)?; - Ok(( - TransferToken { - denom: denom.to_string(), - amount, - }, - vec![BankMsg::Send { - to_address: receiver.to_string(), - amount: vec![Coin { + .map( + |ref token @ TransferToken { + ref denom, + amount, + fee, + }| { + let (actual_amount, fee_amount) = token.actual_amounts()?; + match DenomOrigin::from((denom.as_str(), counterparty_endpoint)) { + DenomOrigin::Local { denom } => { + self.local_unescrow( + &endpoint.channel_id, + denom, + actual_amount + fee_amount, + )?; + Ok(( + TransferToken { denom: denom.to_string(), amount, - }], - } - .into()], - )) - } - DenomOrigin::Remote { denom } => { - let foreign_denom = make_foreign_denom(endpoint, denom); - let (exists, hashed_foreign_denom, register_msg) = - self.foreign_toggle(contract_address, endpoint, &foreign_denom)?; - let normalized_foreign_denom = - format!("0x{}", hex::encode(hashed_foreign_denom)); - let factory_denom = - format!("factory/{}/{}", contract_address, normalized_foreign_denom); - let mint = TokenFactoryMsg::MintTokens { - denom: factory_denom.clone(), - amount, - mint_to_address: receiver.to_string(), - }; - Ok(( - TransferToken { - denom: factory_denom, - amount, - }, - if exists { - vec![mint.into()] - } else { + fee, + }, vec![ - register_msg, - TokenFactoryMsg::CreateDenom { - subdenom: normalized_foreign_denom.clone(), - metadata: None, + BankMsg::Send { + to_address: receiver.to_string(), + amount: vec![Coin { + denom: denom.to_string(), + amount: actual_amount, + }], + } + .into(), + BankMsg::Send { + to_address: relayer.to_string(), + amount: vec![Coin { + denom: denom.to_string(), + amount: fee_amount, + }], } .into(), - mint.into(), - ] - }, - )) + ], + )) + } + DenomOrigin::Remote { denom } => { + let foreign_denom = make_foreign_denom(endpoint, denom); + let (exists, hashed_foreign_denom, register_msg) = + self.foreign_toggle(contract_address, endpoint, &foreign_denom)?; + let normalized_foreign_denom = + format!("0x{}", hex::encode(hashed_foreign_denom)); + let factory_denom = format!( + "factory/{}/{}", + contract_address, normalized_foreign_denom + ); + let mint = TokenFactoryMsg::MintTokens { + denom: factory_denom.clone(), + amount: actual_amount, + mint_to_address: receiver.to_string(), + }; + let mint_fee = TokenFactoryMsg::MintTokens { + denom: factory_denom.clone(), + amount: fee_amount, + mint_to_address: relayer.to_string(), + }; + Ok(( + TransferToken { + denom: factory_denom, + amount, + fee, + }, + if exists { + vec![mint.into(), mint_fee.into()] + } else { + vec![ + register_msg, + TokenFactoryMsg::CreateDenom { + subdenom: normalized_foreign_denom.clone(), + metadata: None, + } + .into(), + mint.into(), + mint_fee.into(), + ] + }, + )) + } } - } - }) + }, + ) .collect::), _>>() .map(|(x, y)| (x, y.into_iter().flatten().collect())) } @@ -484,6 +518,7 @@ trait ForTokens { channel_id: &str, denom: &str, amount: Uint128, + fee_amount: Uint128, ) -> Result>, ContractError>; fn on_remote( @@ -491,6 +526,7 @@ trait ForTokens { channel_id: &str, denom: &str, amount: Uint128, + fee_amount: Uint128, ) -> Result>, ContractError>; fn execute( @@ -500,9 +536,10 @@ trait ForTokens { tokens: Vec, ) -> Result>, ContractError> { let mut messages = Vec::with_capacity(tokens.len()); - for TransferToken { denom, amount } in tokens { + for token in tokens { // This is the origin from the counterparty POV - match DenomOrigin::from((denom.as_str(), endpoint)) { + let (actual_amount, fee_amount) = token.actual_amounts()?; + match DenomOrigin::from((token.denom.as_str(), endpoint)) { DenomOrigin::Local { denom } => { // the denom has been previously normalized (factory/{}/ prefix removed), we must reconstruct to burn let foreign_denom = hash_denom_str(&make_foreign_denom(endpoint, denom)); @@ -510,11 +547,17 @@ trait ForTokens { messages.append(&mut self.on_remote( &endpoint.channel_id, &factory_denom, - amount, + actual_amount, + fee_amount, )?); } DenomOrigin::Remote { denom } => { - messages.append(&mut self.on_local(&endpoint.channel_id, denom, amount)?); + messages.append(&mut self.on_local( + &endpoint.channel_id, + denom, + actual_amount, + fee_amount, + )?); } } } @@ -533,8 +576,9 @@ impl<'a> ForTokens for StatefulSendTokens<'a> { channel_id: &str, denom: &str, amount: Uint128, + fee_amount: Uint128, ) -> Result>, ContractError> { - increase_outstanding(self.deps.branch(), channel_id, denom, amount)?; + increase_outstanding(self.deps.branch(), channel_id, denom, amount + fee_amount)?; Ok(Default::default()) } @@ -543,10 +587,11 @@ impl<'a> ForTokens for StatefulSendTokens<'a> { _channel_id: &str, denom: &str, amount: Uint128, + fee_amount: Uint128, ) -> Result>, ContractError> { Ok(vec![TokenFactoryMsg::BurnTokens { denom: denom.into(), - amount, + amount: amount + fee_amount, burn_from_address: self.contract_address.clone(), } .into()]) @@ -564,13 +609,14 @@ impl<'a> ForTokens for StatefulRefundTokens<'a> { channel_id: &str, denom: &str, amount: Uint128, + fee_amount: Uint128, ) -> Result>, ContractError> { - decrease_outstanding(self.deps.branch(), channel_id, denom, amount)?; + decrease_outstanding(self.deps.branch(), channel_id, denom, amount + fee_amount)?; Ok(vec![BankMsg::Send { to_address: self.receiver.clone(), amount: vec![Coin { denom: denom.into(), - amount, + amount: amount + fee_amount, }], } .into()]) @@ -581,10 +627,11 @@ impl<'a> ForTokens for StatefulRefundTokens<'a> { _channel_id: &str, denom: &str, amount: Uint128, + fee_amount: Uint128, ) -> Result>, ContractError> { Ok(vec![TokenFactoryMsg::MintTokens { denom: denom.into(), - amount, + amount: amount + fee_amount, mint_to_address: self.receiver.clone(), } .into()]) @@ -690,11 +737,12 @@ impl<'a> TransferProtocol for Ics20Protocol<'a> { &self.common.channel.endpoint, &self.common.channel.counterparty_endpoint, receiver.as_str(), + self.common.info.sender.as_str(), tokens, )?; - Ok((tokens, batch_submessages(self.self_addr(), msgs)?)) } + fn normalize_for_ibc_transfer( &mut self, token: TransferToken, @@ -923,6 +971,7 @@ impl<'a> TransferProtocol for Ucs01Protocol<'a> { &self.common.channel.endpoint, &self.common.channel.counterparty_endpoint, receiver.as_str(), + self.common.info.sender.as_str(), tokens, )?; @@ -997,34 +1046,44 @@ impl<'a> TransferProtocol for Ucs01Protocol<'a> { } }; - // If not already processed by other middleware, receive tokens into the contract address. - let mut tokens: Vec = Vec::new(); - if !processed { - msgs.append(&mut match self - .receive_transfer(&override_con_addr.into(), packet.tokens().to_vec()) - { - Ok((t, msgs)) => { - t.into_iter().for_each(|t| { - tokens.push(Coin { - denom: t.denom, - amount: t.amount, - }) - }); - msgs - } - Err(error) => { - return Self::receive_error(error); - } - }); - } + let tokens = { + // Extremely ugly hack: temporary alter the relayer such that the + // total amount (fees + transfer) goes to the contract. + let original_sender = self.common.info.sender.clone(); + let sender = self.self_addr().clone(); + self.common.info.sender = sender; + + // If not already processed by other middleware, receive tokens into the contract address. + let mut tokens: Vec = Vec::new(); + if !processed { + msgs.append(&mut match self + .receive_transfer(&override_con_addr.into(), packet.tokens().clone()) + { + Ok((t, msgs)) => { + t.into_iter().for_each(|t| { + tokens.push(Coin { + denom: t.denom, + amount: t.amount, + }) + }); + msgs + } + Err(error) => { + return Self::receive_error(error); + } + }); + } + + // Revert to the original sender, which is the relayer. + self.common.info.sender = original_sender; + + tokens + }; // Forward the packet - let forward_response = match self.forward_transfer_packet( - tokens, - original_packet.clone(), - forward, - override_addr, - ) { + let forward_result = + self.forward_transfer_packet(tokens, original_packet.clone(), forward, override_addr); + let forward_response = match forward_result { Ok(forward_response) => forward_response, Err(e) => { return IbcReceiveResponse::new(Self::ack_failure(e.to_string()).encode()) @@ -1091,7 +1150,10 @@ mod tests { wasm_execute, Addr, BankMsg, Coin, CosmosMsg, IbcEndpoint, Uint128, }; use token_factory_api::TokenFactoryMsg; - use ucs01_relay_api::{protocol::TransferProtocol, types::TransferToken}; + use ucs01_relay_api::{ + protocol::TransferProtocol, + types::{FeePerU128, TransferToken}, + }; use super::{hash_denom, ForTokens, OnReceive, StatefulOnReceive}; use crate::{ @@ -1175,10 +1237,12 @@ mod tests { channel_id: "channel-34".into(), }, "receiver", + "relayer", vec![TransferToken { denom: "from-counterparty".into(), - amount: Uint128::from(100u128) - },], + amount: Uint128::from(100u128), + fee: FeePerU128::percent(10u128.try_into().unwrap()).unwrap(), + }], ) .unwrap() .1, @@ -1204,10 +1268,16 @@ mod tests { .into(), TokenFactoryMsg::MintTokens { denom: format!("factory/0xDEADC0DE/{}", denom_str), - amount: Uint128::from(100u128), + amount: Uint128::from(91u128), mint_to_address: "receiver".into() } .into(), + TokenFactoryMsg::MintTokens { + denom: format!("factory/0xDEADC0DE/{}", denom_str), + amount: Uint128::from(9u128), + mint_to_address: "relayer".into() + } + .into(), ] ); } @@ -1234,9 +1304,11 @@ mod tests { &source_endpoint_1, &conflicting_destination, "receiver", + "relayer", vec![TransferToken { denom: "from-counterparty".into(), amount: Uint128::from(100u128), + fee: FeePerU128::zero(), }], ) .unwrap() @@ -1252,9 +1324,11 @@ mod tests { &source_endpoint_2, &conflicting_destination, "receiver", + "relayer", vec![TransferToken { denom: "from-counterparty".into(), amount: Uint128::from(100u128), + fee: FeePerU128::zero(), }], ) .unwrap() @@ -1281,22 +1355,35 @@ mod tests { channel_id: "channel-34".into(), }, "receiver", + "relayer", vec![TransferToken { denom: "from-counterparty".into(), - amount: Uint128::from(100u128) + amount: Uint128::from(100u128), + fee: FeePerU128::percent(5u128.try_into().unwrap()).unwrap(), },], ) .unwrap() .1, - vec![TokenFactoryMsg::MintTokens { - denom: format!( - "factory/0xDEADC0DE/{}", - hash_denom_str("wasm.0xDEADC0DE/channel-1/from-counterparty") - ), - amount: Uint128::from(100u128), - mint_to_address: "receiver".into() - } - .into()] + vec![ + TokenFactoryMsg::MintTokens { + denom: format!( + "factory/0xDEADC0DE/{}", + hash_denom_str("wasm.0xDEADC0DE/channel-1/from-counterparty") + ), + amount: Uint128::from(96u128), + mint_to_address: "receiver".into() + } + .into(), + TokenFactoryMsg::MintTokens { + denom: format!( + "factory/0xDEADC0DE/{}", + hash_denom_str("wasm.0xDEADC0DE/channel-1/from-counterparty") + ), + amount: Uint128::from(4u128), + mint_to_address: "relayer".into() + } + .into() + ] ); } @@ -1315,21 +1402,33 @@ mod tests { channel_id: "channel-34".into(), }, "receiver", + "relayer", vec![TransferToken { denom: "transfer/channel-34/local-denom".into(), - amount: Uint128::from(119u128) + amount: Uint128::from(119u128), + fee: FeePerU128::percent(10u128.try_into().unwrap()).unwrap() }], ) .unwrap() .1, - vec![BankMsg::Send { - to_address: "receiver".into(), - amount: vec![Coin { - denom: "local-denom".into(), - amount: Uint128::from(119u128) - }] - } - .into()] + vec![ + BankMsg::Send { + to_address: "receiver".into(), + amount: vec![Coin { + denom: "local-denom".into(), + amount: Uint128::from(108u128) + }] + } + .into(), + BankMsg::Send { + to_address: "relayer".into(), + amount: vec![Coin { + denom: "local-denom".into(), + amount: Uint128::from(11u128) + }] + } + .into() + ] ); } @@ -1342,6 +1441,7 @@ mod tests { _channel_id: &str, _denom: &str, _amount: Uint128, + _fee_amount: Uint128, ) -> Result< Vec>, crate::error::ContractError, @@ -1354,13 +1454,14 @@ mod tests { _channel_id: &str, denom: &str, amount: Uint128, + fee_amount: Uint128, ) -> Result< Vec>, crate::error::ContractError, > { Ok(vec![TokenFactoryMsg::BurnTokens { denom: denom.into(), - amount, + amount: amount + fee_amount, burn_from_address: "0xCAFEBABE".into(), } .into()]) @@ -1378,15 +1479,18 @@ mod tests { vec![ TransferToken { denom: "transfer-source/blabla/remote-denom".into(), - amount: Uint128::from(119u128) + amount: Uint128::from(119u128), + fee: FeePerU128::zero() }, TransferToken { denom: "transfer-source/blabla-2/remote-denom".into(), - amount: Uint128::from(10u128) + amount: Uint128::from(10u128), + fee: FeePerU128::zero() }, TransferToken { denom: "transfer-source/blabla/remote-denom2".into(), - amount: Uint128::from(129u128) + amount: Uint128::from(129u128), + fee: FeePerU128::zero() }, ], ) @@ -1425,11 +1529,12 @@ mod tests { _channel_id: &str, _denom: &str, amount: Uint128, + fee_amount: Uint128, ) -> Result< Vec>, crate::error::ContractError, > { - self.total += amount; + self.total += amount + fee_amount; Ok(Default::default()) } @@ -1438,6 +1543,7 @@ mod tests { _channel_id: &str, _denom: &str, _amount: Uint128, + _fee_amount: Uint128, ) -> Result< Vec>, crate::error::ContractError, @@ -1457,11 +1563,13 @@ mod tests { vec![ TransferToken { denom: "transfer/channel-2/remote-denom".into(), - amount: Uint128::from(119u128) + amount: Uint128::from(119u128), + fee: FeePerU128::zero() }, TransferToken { denom: "transfer/channel-2/remote-denom2".into(), - amount: Uint128::from(129u128) + amount: Uint128::from(129u128), + fee: FeePerU128::zero() } ], ) @@ -1483,13 +1591,15 @@ mod tests { }, TransferToken { denom: "factory/0xDEADC0DE/0xaf30fd00576e1d27471a4d2b0c0487dc6876e0589e".into(), - amount: Uint128::MAX + amount: Uint128::MAX, + fee: FeePerU128::zero() } ) .unwrap(), TransferToken { denom: "factory/0xDEADC0DE/0xaf30fd00576e1d27471a4d2b0c0487dc6876e0589e".into(), - amount: Uint128::MAX + amount: Uint128::MAX, + fee: FeePerU128::zero() } ); assert_eq!( @@ -1502,13 +1612,15 @@ mod tests { }, TransferToken { denom: "factory/0xDEADC0DE/0xaf30fd00576e1d27471a4d2b0c0487dc6876e0589e".into(), - amount: Uint128::MAX + amount: Uint128::MAX, + fee: FeePerU128::zero() } ) .unwrap(), TransferToken { denom: "factory/0xDEADC0DE/0xaf30fd00576e1d27471a4d2b0c0487dc6876e0589e".into(), - amount: Uint128::MAX + amount: Uint128::MAX, + fee: FeePerU128::zero() } ); } @@ -1525,13 +1637,15 @@ mod tests { }, TransferToken { denom: "factory/0xDEADC0DE/0xaf30fd00576e1d27471a4d2b0c0487dc6876e0589e".into(), - amount: Uint128::MAX + amount: Uint128::MAX, + fee: FeePerU128::zero() } ) .unwrap(), TransferToken { denom: "transfer/channel-332/blabla-1".into(), - amount: Uint128::MAX + amount: Uint128::MAX, + fee: FeePerU128::zero() } ); } diff --git a/evm/contracts/apps/ucs/01-relay/Relay.sol b/evm/contracts/apps/ucs/01-relay/Relay.sol index cb3b5fe415..a3a53e69de 100644 --- a/evm/contracts/apps/ucs/01-relay/Relay.sol +++ b/evm/contracts/apps/ucs/01-relay/Relay.sol @@ -8,6 +8,7 @@ import "@openzeppelin-upgradeable/utils/PausableUpgradeable.sol"; import "@openzeppelin/token/ERC20/ERC20.sol"; import "@openzeppelin/token/ERC20/IERC20.sol"; import "@openzeppelin/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/utils/math/Math.sol"; import "solady/utils/LibString.sol"; @@ -23,11 +24,13 @@ import "./ERC20Denom.sol"; struct LocalToken { address denom; uint128 amount; + uint128 fee; } struct Token { string denom; uint128 amount; + uint128 fee; } struct RelayPacket { @@ -92,6 +95,15 @@ library RelayLib { address indexed token, uint256 amount ); + event FeePaid( + uint64 packetSequence, + string channelId, + string sender, + address indexed receiver, + string denom, + address indexed token, + uint256 amount + ); event Sent( uint64 packetSequence, string channelId, @@ -371,7 +383,7 @@ contract UCS01Relay is function onRecvPacketProcessing( IbcCoreChannelV1Packet.Data calldata ibcPacket, - address + address relayer ) public { if (msg.sender != address(this)) { revert RelayLib.ErrUnauthorized(); @@ -386,25 +398,28 @@ contract UCS01Relay is if (token.amount == 0) { revert RelayLib.ErrInvalidAmount(); } - strings.slice memory denomSlice = token.denom.toSlice(); - // This will trim the denom in-place IFF it is prefixed - strings.slice memory trimedDenom = - denomSlice.beyond(prefix.toSlice()); + uint256 feeAmount = + Math.mulDiv(token.amount, token.fee, type(uint128).max); + uint256 actualAmount = token.amount - feeAmount; address receiver = RelayLib.bytesToAddress(packet.receiver); address denomAddress; string memory denom; - if (!denomSlice.equals(token.denom.toSlice())) { + if (token.denom.startsWith(prefix)) { // In this branch the token was originating from // this chain as it was prefixed by the local channel/port. // We need to unescrow the amount. - denom = trimedDenom.toString(); + // This will trim the denom in-place IFF it is prefixed + denom = token.denom.slice(bytes(prefix).length); // It's an ERC20 string 0x prefixed hex address denomAddress = Hex.hexToAddress(denom); // The token must be outstanding. decreaseOutstanding( ibcPacket.destination_channel, denomAddress, token.amount ); - IERC20(denomAddress).transfer(receiver, token.amount); + IERC20(denomAddress).transfer(receiver, actualAmount); + if (feeAmount > 0) { + IERC20(denomAddress).transfer(relayer, feeAmount); + } } else { // In this branch the token was originating from the // counterparty chain. We need to mint the amount. @@ -430,17 +445,32 @@ contract UCS01Relay is denomAddress ); } - IERC20Denom(denomAddress).mint(receiver, token.amount); + IERC20Denom(denomAddress).mint(receiver, actualAmount); + if (feeAmount > 0) { + IERC20Denom(denomAddress).mint(relayer, feeAmount); + } } + string memory senderAddress = packet.sender.toHexString(); emit RelayLib.Received( ibcPacket.sequence, ibcPacket.destination_channel, - packet.sender.toHexString(), + senderAddress, receiver, denom, denomAddress, - token.amount + actualAmount ); + if (feeAmount > 0) { + emit RelayLib.FeePaid( + ibcPacket.sequence, + ibcPacket.destination_channel, + senderAddress, + relayer, + denom, + denomAddress, + feeAmount + ); + } } }