From bbb8f9ca2ae057ceb2db303da3e76031a66a6453 Mon Sep 17 00:00:00 2001 From: Hussein Ait Lahcen Date: Tue, 16 Jul 2024 16:31:33 +0200 Subject: [PATCH] feat(ucs01): collect fees at destination --- cosmwasm/ucs01-relay-api/src/middleware.rs | 7 +- cosmwasm/ucs01-relay-api/src/protocol.rs | 4 +- cosmwasm/ucs01-relay-api/src/types.rs | 68 ++++- cosmwasm/ucs01-relay/src/contract.rs | 14 +- cosmwasm/ucs01-relay/src/error.rs | 5 +- cosmwasm/ucs01-relay/src/msg.rs | 3 + cosmwasm/ucs01-relay/src/protocol.rs | 330 ++++++++++++++------- evm/contracts/apps/ucs/01-relay/Relay.sol | 52 +++- 8 files changed, 351 insertions(+), 132 deletions(-) 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/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..b7302658e0 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,92 @@ 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)?; + 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 +514,7 @@ trait ForTokens { channel_id: &str, denom: &str, amount: Uint128, + fee_amount: Uint128, ) -> Result>, ContractError>; fn on_remote( @@ -491,6 +522,7 @@ trait ForTokens { channel_id: &str, denom: &str, amount: Uint128, + fee_amount: Uint128, ) -> Result>, ContractError>; fn execute( @@ -500,9 +532,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 +543,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 +572,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 +583,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()]) @@ -556,6 +597,7 @@ impl<'a> ForTokens for StatefulSendTokens<'a> { struct StatefulRefundTokens<'a> { deps: DepsMut<'a>, receiver: String, + relayer: String, } impl<'a> ForTokens for StatefulRefundTokens<'a> { @@ -564,16 +606,27 @@ 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)?; - Ok(vec![BankMsg::Send { - to_address: self.receiver.clone(), - amount: vec![Coin { - denom: denom.into(), - amount, - }], - } - .into()]) + Ok(vec![ + BankMsg::Send { + to_address: self.receiver.clone(), + amount: vec![Coin { + denom: denom.into(), + amount, + }], + } + .into(), + BankMsg::Send { + to_address: self.relayer.clone(), + amount: vec![Coin { + denom: denom.into(), + amount: fee_amount, + }], + } + .into(), + ]) } fn on_remote( @@ -581,13 +634,22 @@ 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, - mint_to_address: self.receiver.clone(), - } - .into()]) + Ok(vec![ + TokenFactoryMsg::MintTokens { + denom: denom.into(), + amount, + mint_to_address: self.receiver.clone(), + } + .into(), + TokenFactoryMsg::MintTokens { + denom: denom.into(), + amount: fee_amount, + mint_to_address: self.relayer.clone(), + } + .into(), + ]) } } @@ -668,6 +730,7 @@ impl<'a> TransferProtocol for Ics20Protocol<'a> { StatefulRefundTokens { deps: self.common.deps.branch(), receiver: sender.into(), + relayer: self.common.info.sender.to_string(), } .execute( &self.common.env.contract.address, @@ -690,9 +753,9 @@ 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( @@ -895,6 +958,7 @@ impl<'a> TransferProtocol for Ucs01Protocol<'a> { StatefulRefundTokens { deps: self.common.deps.branch(), receiver: addr.to_string(), + relayer: self.common.info.sender.to_string(), } .execute( &self.common.env.contract.address, @@ -923,6 +987,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, )?; @@ -1091,7 +1156,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 +1243,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 +1274,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 +1310,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 +1330,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 +1361,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 +1408,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 +1447,7 @@ mod tests { _channel_id: &str, _denom: &str, _amount: Uint128, + _fee_amount: Uint128, ) -> Result< Vec>, crate::error::ContractError, @@ -1354,13 +1460,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 +1485,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 +1535,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 +1549,7 @@ mod tests { _channel_id: &str, _denom: &str, _amount: Uint128, + _fee_amount: Uint128, ) -> Result< Vec>, crate::error::ContractError, @@ -1457,11 +1569,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 +1597,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 +1618,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 +1643,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 + ); + } } }