Skip to content

Commit

Permalink
feat(ucs01): collect fees at destination
Browse files Browse the repository at this point in the history
  • Loading branch information
hussein-aitlahcen committed Jul 16, 2024
1 parent d434e84 commit bbb8f9c
Show file tree
Hide file tree
Showing 8 changed files with 351 additions and 132 deletions.
7 changes: 5 additions & 2 deletions cosmwasm/ucs01-relay-api/src/middleware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -105,6 +107,9 @@ pub struct PacketForward {
pub retries: u8,
pub next: Option<Box<PacketForward>>,
pub return_info: Option<PacketId>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub fees: Option<Fees>,
}

impl PacketForward {
Expand Down Expand Up @@ -167,7 +172,5 @@ mod tests {
let parsed = serde_json_wasm::from_str::<Memo>(memo).expect("works");

dbg!(parsed);

// panic!()
}
}
4 changes: 3 additions & 1 deletion cosmwasm/ucs01-relay-api/src/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -436,18 +436,20 @@ mod tests {

use crate::{
protocol::{tokens_to_attr, ATTR_ASSETS},
types::TransferToken,
types::{FeePerU128, TransferToken},
};

#[test]
fn test_token_attr() {
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::<Vec<Coin>>(attr.value).unwrap();
Expand Down
68 changes: 58 additions & 10 deletions cosmwasm/ucs01-relay-api/src/types.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
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<u8>, Vec<u8>>;

#[derive(thiserror::Error, Debug)]
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<u8>, err: ethabi::Error },
#[error("Could not decode UCS01 ack, expected a boolean, got: {data}", data = serde_utils::to_hex(.got))]
Expand Down Expand Up @@ -69,17 +78,43 @@ pub struct TransferPacketCommon<T> {
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<Coin> 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<Self, CheckedMultiplyRatioError> {
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<String, FeePerU128>;

#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Ucs01TransferPacket {
/// the sender address
Expand Down Expand Up @@ -127,10 +162,11 @@ impl Encode<encoding::EthAbi> 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(),
Expand All @@ -151,6 +187,7 @@ impl Decode<encoding::EthAbi> for Ucs01TransferPacket {
ParamType::Array(Box::new(ParamType::Tuple(vec![
ParamType::String,
ParamType::Uint(128),
ParamType::Uint(128),
]))),
ParamType::String,
],
Expand All @@ -172,9 +209,10 @@ impl Decode<encoding::EthAbi> 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!(),
}
Expand Down Expand Up @@ -248,6 +286,7 @@ impl TransferPacket for Ics20Packet {
vec![TransferToken {
denom: self.denom.clone(),
amount: self.amount,
fee: FeePerU128(0u128.into()),
}]
}

Expand Down Expand Up @@ -330,7 +369,13 @@ impl TryFrom<TransferPacketCommon<String>> for Ics20Packet {
}: TransferPacketCommon<String>,
) -> Result<Self, Self::Error> {
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 {
Expand Down Expand Up @@ -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() {
Expand All @@ -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(),
Expand Down
14 changes: 12 additions & 2 deletions cosmwasm/ucs01-relay/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -115,10 +115,20 @@ pub fn execute_transfer(
info: MessageInfo,
msg: TransferMsg,
) -> Result<Response<TokenFactoryMsg>, ContractError> {
let fees = msg.fees.unwrap_or_default();
let tokens: Vec<TransferToken> = 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();

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

Expand Down Expand Up @@ -45,6 +46,8 @@ pub struct TransferMsg {
pub timeout: Option<u64>,
/// The memo
pub memo: String,
/// Fee associated with the transfer, denominated in transferred coins
pub fees: Option<Fees>,
}

#[cw_serde]
Expand Down
Loading

0 comments on commit bbb8f9c

Please sign in to comment.