diff --git a/Cargo.lock b/Cargo.lock index b8847d2..1964793 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,6 +37,12 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bech32" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9ff0bbfd639f15c74af777d81383cf53efb7c93613f6cab67c6c11e05bbf8b" + [[package]] name = "block-buffer" version = "0.9.0" @@ -370,14 +376,14 @@ dependencies = [ [[package]] name = "cw20-ics20-msg" -version = "0.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e04064b936344caa428fa764b4f92859c9619cc0cfc6b3d155d26f8f0a45b13f" +version = "0.0.3" dependencies = [ + "bech32", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 1.0.1", "cw20", + "oraiswap", "schemars", "serde", ] @@ -601,9 +607,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "oraiswap" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d647cd0b70b11b8b78364ca2f885753b0c870785788bcf9d3d05f522fe54344e" +checksum = "2dd3f2603bc41e2c9218e1103ce762e50ad8ee7a3648aabe03f7d48437f841c6" dependencies = [ "cosmwasm-schema", "cosmwasm-std", diff --git a/README.md b/README.md index 8c406da..34c7486 100755 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # IBC transfer flow: Let's assume the network that has the contract cw20-ics20 deployed is network B, the other network is A. + +In the source code, we call the network having cw20-ics20 deployed local chain, other networks are remote chains. + In the cw-ics20-latest contract, there are couple transfer flows in the code below: ## Network A transfers native tokens to B first (A->B, where native token is not IBC token) @@ -26,3 +29,5 @@ This is really important because by using the CosmosMsg, we force the `allow_con If we use CosmosMsg, then the acknowledgement packet will fail entirely, and it will be retried by the relayer as long as we fix the `allow_contract`. Normally, if it is a `ibctransfer` application developed as a submodule in Cosmos SDK, then the refund part must not fail, and we can trust that it will not fail. However, the `allow_contract` can be developed by anyone, and can be replaced => cannot be trusted. + +# build packages \ No newline at end of file diff --git a/contracts/cw-ics20-latest/Cargo.toml b/contracts/cw-ics20-latest/Cargo.toml index b3c697a..7ebf4a7 100644 --- a/contracts/cw-ics20-latest/Cargo.toml +++ b/contracts/cw-ics20-latest/Cargo.toml @@ -22,8 +22,8 @@ cosmwasm-schema = "1.1.9" cw-utils = "0.16.0" cw2 = "1.0.1" cw20 = "1.0.1" -cw20-ics20-msg = "0.0.2" -oraiswap = "1.0.0" +cw20-ics20-msg = { path = "../../packages/cw20-ics20-msg" } +oraiswap = "1.0.1" cosmwasm-std = { version = "1.1.0", features = ["stargate"] } cw-storage-plus = "1.0.1" cw-controllers = "1.0.1" diff --git a/contracts/cw-ics20-latest/src/contract.rs b/contracts/cw-ics20-latest/src/contract.rs index 1e1c393..6e487f1 100644 --- a/contracts/cw-ics20-latest/src/contract.rs +++ b/contracts/cw-ics20-latest/src/contract.rs @@ -1,28 +1,31 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - from_binary, to_binary, Addr, Binary, Deps, DepsMut, Empty, Env, IbcEndpoint, IbcMsg, IbcQuery, - MessageInfo, Order, PortIdResponse, Response, StdResult, + from_binary, to_binary, Addr, Binary, Deps, DepsMut, Empty, Env, IbcEndpoint, IbcQuery, + MessageInfo, Order, PortIdResponse, Response, StdResult, Storage, }; use cw2::set_contract_version; use cw20::{Cw20Coin, Cw20ReceiveMsg}; +use cw20_ics20_msg::helper::parse_ibc_wasm_port_id; use cw_storage_plus::Bound; use oraiswap::asset::AssetInfo; +use oraiswap::router::RouterController; use crate::error::ContractError; use crate::ibc::{ - collect_transfer_fee_msgs, parse_ibc_wasm_port_id, parse_voucher_denom, process_deduct_fee, - Ics20Packet, + build_ibc_send_packet, collect_fee_msgs, parse_voucher_denom, process_deduct_fee, }; use crate::msg::{ AllowMsg, AllowedInfo, AllowedResponse, ChannelResponse, ConfigResponse, DeletePairMsg, ExecuteMsg, InitMsg, ListAllowedResponse, ListChannelsResponse, ListMappingResponse, - MigrateMsg, PairQuery, PortResponse, QueryMsg, TransferBackMsg, TransferMsg, UpdatePairMsg, + MigrateMsg, PairQuery, PortResponse, QueryMsg, RelayerFeeResponse, TransferBackMsg, + UpdatePairMsg, }; use crate::state::{ - get_key_ics20_ibc_denom, ics20_denoms, increase_channel_balance, reduce_channel_balance, - AllowInfo, Config, MappingMetadata, TokenFee, ADMIN, ALLOW_LIST, CHANNEL_FORWARD_STATE, - CHANNEL_INFO, CHANNEL_REVERSE_STATE, CONFIG, TOKEN_FEE, + get_key_ics20_ibc_denom, ics20_denoms, reduce_channel_balance, AllowInfo, Config, + MappingMetadata, RelayerFee, TokenFee, ADMIN, ALLOW_LIST, CHANNEL_INFO, CHANNEL_REVERSE_STATE, + CONFIG, RELAYER_FEE, RELAYER_FEE_ACCUMULATOR, REPLY_ARGS, SINGLE_STEP_REPLY_ARGS, TOKEN_FEE, + TOKEN_FEE_ACCUMULATOR, }; use cw20_ics20_msg::amount::{convert_local_to_remote, Amount}; use cw_utils::{maybe_addr, nonpayable, one_coin}; @@ -45,8 +48,9 @@ pub fn instantiate( default_timeout: msg.default_timeout, default_gas_limit: msg.default_gas_limit, fee_denom: "orai".to_string(), - swap_router_contract: msg.swap_router_contract, - fee_receiver: admin, + swap_router_contract: RouterController(msg.swap_router_contract), + token_fee_receiver: admin.clone(), + relayer_fee_receiver: admin, }; CONFIG.save(deps.storage, &cfg)?; @@ -70,10 +74,10 @@ pub fn execute( ) -> Result { match msg { ExecuteMsg::Receive(msg) => execute_receive(deps, env, info, msg), - ExecuteMsg::Transfer(msg) => { - let coin = one_coin(&info)?; - execute_transfer(deps, env, msg, Amount::Native(coin), info.sender) - } + // ExecuteMsg::Transfer(msg) => { + // let coin = one_coin(&info)?; + // execute_transfer(deps, env, msg, Amount::Native(coin), info.sender) + // } ExecuteMsg::TransferToRemote(msg) => { let coin = one_coin(&info)?; let amount = Amount::from_parts(coin.denom, coin.amount); @@ -90,6 +94,8 @@ pub fn execute( admin, token_fee, fee_receiver, + relayer_fee_receiver, + relayer_fee, } => update_config( deps, info, @@ -100,6 +106,8 @@ pub fn execute( admin, token_fee, fee_receiver, + relayer_fee_receiver, + relayer_fee, ), } } @@ -114,6 +122,8 @@ pub fn update_config( admin: Option, token_fee: Option>, fee_receiver: Option, + relayer_fee_receiver: Option, + relayer_fee: Option>, ) -> Result { ADMIN.assert_admin(deps.as_ref(), &info.sender)?; if let Some(token_fee) = token_fee { @@ -121,6 +131,11 @@ pub fn update_config( TOKEN_FEE.save(deps.storage, &fee.token_denom, &fee.ratio)?; } } + if let Some(relayer_fee) = relayer_fee { + for fee in relayer_fee { + RELAYER_FEE.save(deps.storage, &fee.prefix, &fee.fee)?; + } + } CONFIG.update(deps.storage, |mut config| -> StdResult { if let Some(default_timeout) = default_timeout { config.default_timeout = default_timeout; @@ -129,10 +144,13 @@ pub fn update_config( config.fee_denom = fee_denom; } if let Some(swap_router_contract) = swap_router_contract { - config.swap_router_contract = swap_router_contract; + config.swap_router_contract = RouterController(swap_router_contract); } if let Some(fee_receiver) = fee_receiver { - config.fee_receiver = deps.api.addr_validate(&fee_receiver)?; + config.token_fee_receiver = deps.api.addr_validate(&fee_receiver)?; + } + if let Some(relayer_fee_receiver) = relayer_fee_receiver { + config.relayer_fee_receiver = deps.api.addr_validate(&relayer_fee_receiver)?; } config.default_gas_limit = default_gas_limit; Ok(config) @@ -158,11 +176,11 @@ pub fn execute_receive( }); let api = deps.api; - let msg_result: StdResult = from_binary(&wrapper.msg); - if msg_result.is_ok() { - let msg: TransferMsg = msg_result.unwrap(); - return execute_transfer(deps, env, msg, amount, api.addr_validate(&wrapper.sender)?); - } + // let msg_result: StdResult = from_binary(&wrapper.msg); + // if msg_result.is_ok() { + // let msg: TransferMsg = msg_result.unwrap(); + // return execute_transfer(deps, env, msg, amount, api.addr_validate(&wrapper.sender)?); + // } let msg: TransferBackMsg = from_binary(&wrapper.msg)?; execute_transfer_back_to_remote_chain( @@ -174,79 +192,79 @@ pub fn execute_receive( ) } -pub fn execute_transfer( - deps: DepsMut, - env: Env, - msg: TransferMsg, - amount: Amount, - sender: Addr, -) -> Result { - if amount.is_empty() { - return Err(ContractError::NoFunds {}); - } - // ensure the requested channel is registered - if !CHANNEL_INFO.has(deps.storage, &msg.channel) { - return Err(ContractError::NoSuchChannel { id: msg.channel }); - } - let config = CONFIG.load(deps.storage)?; - - // if cw20 token, validate and ensure it is whitelisted, or we set default gas limit - if let Amount::Cw20(coin) = &amount { - let addr = deps.api.addr_validate(&coin.address)?; - // if limit is set, then we always allow cw20 - if config.default_gas_limit.is_none() { - ALLOW_LIST - .may_load(deps.storage, &addr)? - .ok_or(ContractError::NotOnAllowList)?; - } - }; - - // delta from user is in seconds - let timeout_delta = match msg.timeout { - Some(t) => t, - None => config.default_timeout, - }; - // timeout is in nanoseconds - let timeout = env.block.time.plus_seconds(timeout_delta); - - // build ics20 packet - let packet = Ics20Packet::new( - amount.amount(), - amount.denom(), - sender.as_ref(), - &msg.remote_address, - msg.memo, - ); - packet.validate()?; - - // Update the balance now (optimistically) like ibctransfer modules. - // In on_packet_failure (ack with error message or a timeout), we reduce the balance appropriately. - // This means the channel works fine if success acks are not relayed. - increase_channel_balance( - deps.storage, - &msg.channel, - &amount.denom(), - amount.amount(), - true, - )?; - - // prepare ibc message - let msg = IbcMsg::SendPacket { - channel_id: msg.channel, - data: to_binary(&packet)?, - timeout: timeout.into(), - }; - - // send response - let res = Response::new() - .add_message(msg) - .add_attribute("action", "transfer") - .add_attribute("sender", &packet.sender) - .add_attribute("receiver", &packet.receiver) - .add_attribute("denom", &packet.denom) - .add_attribute("amount", &packet.amount.to_string()); - Ok(res) -} +// pub fn execute_transfer( +// deps: DepsMut, +// env: Env, +// msg: TransferMsg, +// amount: Amount, +// sender: Addr, +// ) -> Result { +// if amount.is_empty() { +// return Err(ContractError::NoFunds {}); +// } +// // ensure the requested channel is registered +// if !CHANNEL_INFO.has(deps.storage, &msg.channel) { +// return Err(ContractError::NoSuchChannel { id: msg.channel }); +// } +// let config = CONFIG.load(deps.storage)?; + +// // if cw20 token, validate and ensure it is whitelisted, or we set default gas limit +// if let Amount::Cw20(coin) = &amount { +// let addr = deps.api.addr_validate(&coin.address)?; +// // if limit is set, then we always allow cw20 +// if config.default_gas_limit.is_none() { +// ALLOW_LIST +// .may_load(deps.storage, &addr)? +// .ok_or(ContractError::NotOnAllowList)?; +// } +// }; + +// // delta from user is in seconds +// let timeout_delta = match msg.timeout { +// Some(t) => t, +// None => config.default_timeout, +// }; +// // timeout is in nanoseconds +// let timeout = env.block.time.plus_seconds(timeout_delta); + +// // build ics20 packet +// let packet = Ics20Packet::new( +// amount.amount(), +// amount.denom(), +// sender.as_ref(), +// &msg.remote_address, +// msg.memo, +// ); +// packet.validate()?; + +// // Update the balance now (optimistically) like ibctransfer modules. +// // In on_packet_failure (ack with error message or a timeout), we reduce the balance appropriately. +// // This means the channel works fine if success acks are not relayed. +// increase_channel_balance( +// deps.storage, +// &msg.channel, +// &amount.denom(), +// amount.amount(), +// true, +// )?; + +// // prepare ibc message +// let msg = IbcMsg::SendPacket { +// channel_id: msg.channel, +// data: to_binary(&packet)?, +// timeout: timeout.into(), +// }; + +// // send response +// let res = Response::new() +// .add_message(msg) +// .add_attribute("action", "transfer") +// .add_attribute("sender", &packet.sender) +// .add_attribute("receiver", &packet.receiver) +// .add_attribute("denom", &packet.denom) +// .add_attribute("amount", &packet.amount.to_string()); +// Ok(res) +// } pub fn execute_transfer_back_to_remote_chain( deps: DepsMut, @@ -258,17 +276,11 @@ pub fn execute_transfer_back_to_remote_chain( if amount.is_empty() { return Err(ContractError::NoFunds {}); } - - let new_deducted_amount = process_deduct_fee( - deps.storage, - &msg.remote_denom, - amount.amount(), - &amount.denom(), - )?; + let config = CONFIG.load(deps.storage)?; // should be in form port/channel/denom let mappings = get_mappings_from_asset_info( - deps.as_ref(), + deps.as_ref().storage, match amount.clone() { Amount::Native(coin) => AssetInfo::NativeToken { denom: coin.denom }, Amount::Cw20(cw20_coin) => AssetInfo::Token { @@ -299,6 +311,18 @@ pub fn execute_transfer_back_to_remote_chain( }) .ok_or(ContractError::MappingPairNotFound {})?; + // if found mapping, then deduct fee based on mapping + let (new_deducted_amount, token_fee, relayer_fee) = process_deduct_fee( + deps.storage, + &deps.querier, + deps.api, + &msg.remote_address, + &msg.remote_denom, + amount, + mapping.pair_mapping.asset_info_decimals, + &config.swap_router_contract, + )?; + let ibc_denom = mapping.key; // ensure the requested channel is registered if !CHANNEL_INFO.has(deps.storage, &msg.local_channel_id) { @@ -306,7 +330,6 @@ pub fn execute_transfer_back_to_remote_chain( id: msg.local_channel_id, }); } - let config = CONFIG.load(deps.storage)?; // delta from user is in seconds let timeout_delta = match msg.timeout { @@ -322,17 +345,8 @@ pub fn execute_transfer_back_to_remote_chain( mapping.pair_mapping.asset_info_decimals, )?; - // build ics20 packet - let packet = Ics20Packet::new( - amount_remote.clone(), - ibc_denom.clone(), // we use ibc denom in form // so that when it is sent back to remote chain, it gets parsed correctly and burned - sender.as_str(), - &msg.remote_address, - msg.memo, - ); - packet.validate()?; - - // because we are transferring back, we reduce the channel's balance + // now this is processed in ack + // // because we are transferring back, we reduce the channel's balance reduce_channel_balance( deps.storage, &msg.local_channel_id, @@ -342,25 +356,39 @@ pub fn execute_transfer_back_to_remote_chain( )?; // prepare ibc message - let msg = IbcMsg::SendPacket { - channel_id: msg.local_channel_id, - data: to_binary(&packet)?, - timeout: timeout.into(), - }; + let ibc_msg = build_ibc_send_packet( + amount_remote, + &ibc_denom, // we use ibc denom in form // so that when it is sent back to remote chain, it gets parsed correctly and burned + sender.as_str(), + &msg.remote_address, + msg.memo, + &msg.local_channel_id, + timeout.into(), + )?; - let mut cosmos_msgs = - collect_transfer_fee_msgs(config.fee_receiver.into_string(), deps.storage)?; - cosmos_msgs.push(msg.into()); + let mut cosmos_msgs = collect_fee_msgs( + deps.storage, + config.token_fee_receiver.into_string(), + TOKEN_FEE_ACCUMULATOR, + )?; + cosmos_msgs.push(ibc_msg.into()); + cosmos_msgs.append(&mut collect_fee_msgs( + deps.storage, + config.relayer_fee_receiver.into_string(), + RELAYER_FEE_ACCUMULATOR, + )?); // send response let res = Response::new() .add_messages(cosmos_msgs) .add_attribute("action", "transfer") .add_attribute("type", "transfer_back_to_remote_chain") - .add_attribute("sender", &packet.sender) - .add_attribute("receiver", &packet.receiver) - .add_attribute("denom", &packet.denom) - .add_attribute("amount", &packet.amount.to_string()); + .add_attribute("sender", sender.as_str()) + .add_attribute("receiver", &msg.remote_address) + .add_attribute("denom", &ibc_denom) + .add_attribute("amount", &amount_remote.to_string()) + .add_attribute("token_fee", token_fee) + .add_attribute("relayer_fee", relayer_fee); Ok(res) } @@ -430,16 +458,19 @@ pub fn execute_update_mapping_pair( deps.storage, &ibc_denom, &MappingMetadata { - asset_info: mapping_pair_msg.asset_info.clone(), + asset_info: mapping_pair_msg.local_asset_info.clone(), remote_decimals: mapping_pair_msg.remote_decimals, - asset_info_decimals: mapping_pair_msg.asset_info_decimals, + asset_info_decimals: mapping_pair_msg.local_asset_info_decimals, }, )?; let res = Response::new() .add_attribute("action", "execute_update_mapping_pair") .add_attribute("denom", mapping_pair_msg.denom) - .add_attribute("new_asset_info", mapping_pair_msg.asset_info.to_string()); + .add_attribute( + "new_asset_info", + mapping_pair_msg.local_asset_info.to_string(), + ); Ok(res) } @@ -469,17 +500,20 @@ pub fn execute_delete_mapping_pair( #[entry_point] pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result { // we don't need to save anything if migrating from the same version - let config: Config = CONFIG.load(deps.storage)?; CONFIG.save( deps.storage, &Config { - default_timeout: config.default_timeout, + default_timeout: msg.default_timeout, default_gas_limit: msg.default_gas_limit, - fee_denom: config.fee_denom, - swap_router_contract: config.swap_router_contract, - fee_receiver: deps.api.addr_validate(&msg.fee_receiver)?, + fee_denom: msg.fee_denom, + swap_router_contract: RouterController(msg.swap_router_contract), + token_fee_receiver: deps.api.addr_validate(&msg.token_fee_receiver)?, + relayer_fee_receiver: deps.api.addr_validate(&msg.relayer_fee_receiver)?, }, )?; + // remove all reply so that after migrating all the data is reset + REPLY_ARGS.remove(deps.storage); + SINGLE_STEP_REPLY_ARGS.remove(deps.storage); set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; Ok(Response::new()) } @@ -504,7 +538,7 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { } => to_binary(&list_cw20_mapping(deps, start_after, limit, order)?), QueryMsg::PairMapping { key } => to_binary(&get_mapping_from_key(deps, key)?), QueryMsg::PairMappingsFromAssetInfo { asset_info } => { - to_binary(&get_mappings_from_asset_info(deps, asset_info)?) + to_binary(&get_mappings_from_asset_info(deps.storage, asset_info)?) } QueryMsg::Admin {} => to_binary(&ADMIN.query_admin(deps)?), QueryMsg::GetTransferTokenFee { remote_token_denom } => { @@ -528,14 +562,10 @@ fn query_list(deps: Deps) -> StdResult { } // make public for ibc tests -pub fn query_channel(deps: Deps, id: String, forward: Option) -> StdResult { +pub fn query_channel(deps: Deps, id: String, _forward: Option) -> StdResult { let info = CHANNEL_INFO.load(deps.storage, &id)?; // this returns Vec<(outstanding, total)> - let channel_state = if forward.is_some() { - CHANNEL_FORWARD_STATE - } else { - CHANNEL_REVERSE_STATE - }; + let channel_state = CHANNEL_REVERSE_STATE; let state = channel_state .prefix(&id) .range(deps.storage, None, None, Order::Ascending) @@ -565,8 +595,28 @@ fn query_config(deps: Deps) -> StdResult { default_timeout: cfg.default_timeout, default_gas_limit: cfg.default_gas_limit, fee_denom: cfg.fee_denom, - swap_router_contract: cfg.swap_router_contract, + swap_router_contract: cfg.swap_router_contract.addr(), gov_contract: admin.into(), + relayer_fee_receiver: cfg.relayer_fee_receiver, + token_fee_receiver: cfg.token_fee_receiver, + token_fees: TOKEN_FEE + .range(deps.storage, None, None, Order::Ascending) + .map(|data_result| { + data_result.map(|data| TokenFee { + token_denom: data.0, + ratio: data.1, + }) + }) + .collect::>>()?, + relayer_fees: RELAYER_FEE + .range(deps.storage, None, None, Order::Ascending) + .map(|data_result| { + data_result.map(|data| RelayerFeeResponse { + prefix: data.0, + amount: data.1, + }) + }) + .collect::>>()?, }; Ok(res) } @@ -646,12 +696,15 @@ fn get_mapping_from_key(deps: Deps, ibc_denom: String) -> StdResult { }) } -fn get_mappings_from_asset_info(deps: Deps, asset_info: AssetInfo) -> StdResult> { +fn get_mappings_from_asset_info( + storage: &dyn Storage, + asset_info: AssetInfo, +) -> StdResult> { let pair_mapping_result: StdResult> = ics20_denoms() .idx .asset_info .prefix(asset_info.to_string()) - .range(deps.storage, None, None, Order::Ascending) + .range(storage, None, None, Order::Ascending) .collect(); if pair_mapping_result.is_err() { return Err(pair_mapping_result.unwrap_err()); @@ -683,19 +736,18 @@ mod test { use std::ops::Sub; use super::*; - use crate::ibc::ibc_packet_receive; - use crate::state::Ratio; + use crate::ibc::{ibc_packet_receive, Ics20Packet}; + use crate::state::{Ratio, TOKEN_FEE_ACCUMULATOR}; use crate::test_helpers::*; use cosmwasm_std::testing::{mock_env, mock_info}; use cosmwasm_std::{ - coin, coins, CosmosMsg, Decimal, IbcEndpoint, IbcMsg, IbcPacket, IbcPacketReceiveMsg, - StdError, Timestamp, Uint128, WasmMsg, + coins, CosmosMsg, Decimal, IbcEndpoint, IbcMsg, IbcPacket, IbcPacketReceiveMsg, StdError, + Timestamp, Uint128, WasmMsg, }; use cw20::Cw20ExecuteMsg; use cw_controllers::AdminError; - use cw_utils::PaymentError; use oraiswap::asset::AssetInfo; #[test] @@ -752,9 +804,9 @@ mod test { let mut update = UpdatePairMsg { local_channel_id: "mars-channel".to_string(), denom: "earth".to_string(), - asset_info: asset_info.clone(), + local_asset_info: asset_info.clone(), remote_decimals: 18, - asset_info_decimals: 18, + local_asset_info_decimals: 18, }; // works with proper funds @@ -771,7 +823,7 @@ mod test { // add another pair with a different asset info update.denom = "moon".to_string(); - update.asset_info = AssetInfo::NativeToken { + update.local_asset_info = AssetInfo::NativeToken { denom: "orai".to_string(), }; msg = ExecuteMsg::UpdateMappingPair(update.clone()); @@ -835,9 +887,9 @@ mod test { let mut update = UpdatePairMsg { local_channel_id: "mars-channel".to_string(), denom: "earth".to_string(), - asset_info: asset_info.clone(), + local_asset_info: asset_info.clone(), remote_decimals: 18, - asset_info_decimals: 18, + local_asset_info_decimals: 18, }; // works with proper funds @@ -884,7 +936,7 @@ mod test { assert_ne!(response.pairs.first().unwrap().key, "foobar".to_string()); // update existing key case must pass - update.asset_info = asset_info_second.clone(); + update.local_asset_info = asset_info_second.clone(); msg = ExecuteMsg::UpdateMappingPair(update.clone()); let info = mock_info("gov", &coins(1234567, "ucosm")); @@ -923,9 +975,9 @@ mod test { let update = UpdatePairMsg { local_channel_id: "mars-channel".to_string(), denom: "earth".to_string(), - asset_info: cw20_denom.clone(), + local_asset_info: cw20_denom.clone(), remote_decimals: 18, - asset_info_decimals: 18, + local_asset_info_decimals: 18, }; // works with proper funds @@ -987,154 +1039,154 @@ mod test { assert_eq!(response.pairs.len(), 0) } - #[test] - fn proper_checks_on_execute_native() { - let send_channel = "channel-5"; - let mut deps = setup(&[send_channel, "channel-10"], &[]); - - let mut transfer = TransferMsg { - channel: send_channel.to_string(), - remote_address: "foreign-address".to_string(), - timeout: None, - memo: Some("memo".to_string()), - }; - - // works with proper funds - let msg = ExecuteMsg::Transfer(transfer.clone()); - let info = mock_info("foobar", &coins(1234567, "ucosm")); - let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); - assert_eq!(res.messages[0].gas_limit, None); - assert_eq!(1, res.messages.len()); - if let CosmosMsg::Ibc(IbcMsg::SendPacket { - channel_id, - data, - timeout, - }) = &res.messages[0].msg - { - let expected_timeout = mock_env().block.time.plus_seconds(DEFAULT_TIMEOUT); - assert_eq!(timeout, &expected_timeout.into()); - assert_eq!(channel_id.as_str(), send_channel); - let msg: Ics20Packet = from_binary(data).unwrap(); - assert_eq!(msg.amount, Uint128::new(1234567)); - assert_eq!(msg.denom.as_str(), "ucosm"); - assert_eq!(msg.sender.as_str(), "foobar"); - assert_eq!(msg.receiver.as_str(), "foreign-address"); - } else { - panic!("Unexpected return message: {:?}", res.messages[0]); - } - - // reject with no funds - let msg = ExecuteMsg::Transfer(transfer.clone()); - let info = mock_info("foobar", &[]); - let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); - assert_eq!(err, ContractError::Payment(PaymentError::NoFunds {})); - - // reject with multiple tokens funds - let msg = ExecuteMsg::Transfer(transfer.clone()); - let info = mock_info("foobar", &[coin(1234567, "ucosm"), coin(54321, "uatom")]); - let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); - assert_eq!(err, ContractError::Payment(PaymentError::MultipleDenoms {})); - - // reject with bad channel id - transfer.channel = "channel-45".to_string(); - let msg = ExecuteMsg::Transfer(transfer); - let info = mock_info("foobar", &coins(1234567, "ucosm")); - let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); - assert_eq!( - err, - ContractError::NoSuchChannel { - id: "channel-45".to_string() - } - ); - } - - #[test] - fn proper_checks_on_execute_cw20() { - let send_channel = "channel-15"; - let cw20_addr = "my-token"; - let mut deps = setup(&["channel-3", send_channel], &[(cw20_addr, 123456)]); - - let transfer = TransferMsg { - channel: send_channel.to_string(), - remote_address: "foreign-address".to_string(), - timeout: Some(7777), - memo: Some("memo".to_string()), - }; - let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { - sender: "my-account".into(), - amount: Uint128::new(888777666), - msg: to_binary(&transfer).unwrap(), - }); - - // works with proper funds - let info = mock_info(cw20_addr, &[]); - let res = execute(deps.as_mut(), mock_env(), info, msg.clone()).unwrap(); - assert_eq!(1, res.messages.len()); - assert_eq!(res.messages[0].gas_limit, None); - if let CosmosMsg::Ibc(IbcMsg::SendPacket { - channel_id, - data, - timeout, - }) = &res.messages[0].msg - { - let expected_timeout = mock_env().block.time.plus_seconds(7777); - assert_eq!(timeout, &expected_timeout.into()); - assert_eq!(channel_id.as_str(), send_channel); - let msg: Ics20Packet = from_binary(data).unwrap(); - assert_eq!(msg.amount, Uint128::new(888777666)); - assert_eq!(msg.denom, format!("cw20:{}", cw20_addr)); - assert_eq!(msg.sender.as_str(), "my-account"); - assert_eq!(msg.receiver.as_str(), "foreign-address"); - } else { - panic!("Unexpected return message: {:?}", res.messages[0]); - } - - // reject with tokens funds - let info = mock_info("foobar", &coins(1234567, "ucosm")); - let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); - assert_eq!(err, ContractError::Payment(PaymentError::NonPayable {})); - } - - #[test] - fn execute_cw20_fails_if_not_whitelisted_unless_default_gas_limit() { - let send_channel = "channel-15"; - let mut deps = setup(&[send_channel], &[]); - - let cw20_addr = "my-token"; - let transfer = TransferMsg { - channel: send_channel.to_string(), - remote_address: "foreign-address".to_string(), - timeout: Some(7777), - memo: Some("memo".to_string()), - }; - let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { - sender: "my-account".into(), - amount: Uint128::new(888777666), - msg: to_binary(&transfer).unwrap(), - }); - - // rejected as not on allow list - let info = mock_info(cw20_addr, &[]); - let err = execute(deps.as_mut(), mock_env(), info.clone(), msg.clone()).unwrap_err(); - assert_eq!(err, ContractError::NotOnAllowList); - - // add a default gas limit - migrate( - deps.as_mut(), - mock_env(), - MigrateMsg { - default_gas_limit: Some(123456), - fee_receiver: "receiver".to_string(), - // default_timeout: 100u64, - // fee_denom: "orai".to_string(), - // swap_router_contract: "foobar".to_string(), - }, - ) - .unwrap(); - - // try again - execute(deps.as_mut(), mock_env(), info, msg).unwrap(); - } + // #[test] + // fn proper_checks_on_execute_native() { + // let send_channel = "channel-5"; + // let mut deps = setup(&[send_channel, "channel-10"], &[]); + + // let mut transfer = TransferMsg { + // channel: send_channel.to_string(), + // remote_address: "foreign-address".to_string(), + // timeout: None, + // memo: Some("memo".to_string()), + // }; + + // // works with proper funds + // let msg = ExecuteMsg::Transfer(transfer.clone()); + // let info = mock_info("foobar", &coins(1234567, "ucosm")); + // let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + // assert_eq!(res.messages[0].gas_limit, None); + // assert_eq!(1, res.messages.len()); + // if let CosmosMsg::Ibc(IbcMsg::SendPacket { + // channel_id, + // data, + // timeout, + // }) = &res.messages[0].msg + // { + // let expected_timeout = mock_env().block.time.plus_seconds(DEFAULT_TIMEOUT); + // assert_eq!(timeout, &expected_timeout.into()); + // assert_eq!(channel_id.as_str(), send_channel); + // let msg: Ics20Packet = from_binary(data).unwrap(); + // assert_eq!(msg.amount, Uint128::new(1234567)); + // assert_eq!(msg.denom.as_str(), "ucosm"); + // assert_eq!(msg.sender.as_str(), "foobar"); + // assert_eq!(msg.receiver.as_str(), "foreign-address"); + // } else { + // panic!("Unexpected return message: {:?}", res.messages[0]); + // } + + // // reject with no funds + // let msg = ExecuteMsg::Transfer(transfer.clone()); + // let info = mock_info("foobar", &[]); + // let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); + // assert_eq!(err, ContractError::Payment(PaymentError::NoFunds {})); + + // // reject with multiple tokens funds + // let msg = ExecuteMsg::Transfer(transfer.clone()); + // let info = mock_info("foobar", &[coin(1234567, "ucosm"), coin(54321, "uatom")]); + // let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); + // assert_eq!(err, ContractError::Payment(PaymentError::MultipleDenoms {})); + + // // reject with bad channel id + // transfer.channel = "channel-45".to_string(); + // let msg = ExecuteMsg::Transfer(transfer); + // let info = mock_info("foobar", &coins(1234567, "ucosm")); + // let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); + // assert_eq!( + // err, + // ContractError::NoSuchChannel { + // id: "channel-45".to_string() + // } + // ); + // } + + // #[test] + // fn proper_checks_on_execute_cw20() { + // let send_channel = "channel-15"; + // let cw20_addr = "my-token"; + // let mut deps = setup(&["channel-3", send_channel], &[(cw20_addr, 123456)]); + + // let transfer = TransferMsg { + // channel: send_channel.to_string(), + // remote_address: "foreign-address".to_string(), + // timeout: Some(7777), + // memo: Some("memo".to_string()), + // }; + // let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { + // sender: "my-account".into(), + // amount: Uint128::new(888777666), + // msg: to_binary(&transfer).unwrap(), + // }); + + // // works with proper funds + // let info = mock_info(cw20_addr, &[]); + // let res = execute(deps.as_mut(), mock_env(), info, msg.clone()).unwrap(); + // assert_eq!(1, res.messages.len()); + // assert_eq!(res.messages[0].gas_limit, None); + // if let CosmosMsg::Ibc(IbcMsg::SendPacket { + // channel_id, + // data, + // timeout, + // }) = &res.messages[0].msg + // { + // let expected_timeout = mock_env().block.time.plus_seconds(7777); + // assert_eq!(timeout, &expected_timeout.into()); + // assert_eq!(channel_id.as_str(), send_channel); + // let msg: Ics20Packet = from_binary(data).unwrap(); + // assert_eq!(msg.amount, Uint128::new(888777666)); + // assert_eq!(msg.denom, format!("cw20:{}", cw20_addr)); + // assert_eq!(msg.sender.as_str(), "my-account"); + // assert_eq!(msg.receiver.as_str(), "foreign-address"); + // } else { + // panic!("Unexpected return message: {:?}", res.messages[0]); + // } + + // // reject with tokens funds + // let info = mock_info("foobar", &coins(1234567, "ucosm")); + // let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); + // assert_eq!(err, ContractError::Payment(PaymentError::NonPayable {})); + // } + + // #[test] + // fn execute_cw20_fails_if_not_whitelisted_unless_default_gas_limit() { + // let send_channel = "channel-15"; + // let mut deps = setup(&[send_channel], &[]); + + // let cw20_addr = "my-token"; + // let transfer = TransferMsg { + // channel: send_channel.to_string(), + // remote_address: "foreign-address".to_string(), + // timeout: Some(7777), + // memo: Some("memo".to_string()), + // }; + // let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { + // sender: "my-account".into(), + // amount: Uint128::new(888777666), + // msg: to_binary(&transfer).unwrap(), + // }); + + // // rejected as not on allow list + // let info = mock_info(cw20_addr, &[]); + // let err = execute(deps.as_mut(), mock_env(), info.clone(), msg.clone()).unwrap_err(); + // assert_eq!(err, ContractError::NotOnAllowList); + + // // add a default gas limit + // migrate( + // deps.as_mut(), + // mock_env(), + // MigrateMsg { + // default_gas_limit: Some(123456), + // fee_receiver: "receiver".to_string(), + // default_timeout: 100u64, + // fee_denom: "orai".to_string(), + // swap_router_contract: "foobar".to_string(), + // }, + // ) + // .unwrap(); + + // // try again + // execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + // } // test execute transfer back to native remote chain fn mock_receive_packet( @@ -1171,6 +1223,7 @@ mod test { fn proper_checks_on_execute_native_transfer_back_to_remote() { // arrange let remote_channel = "channel-5"; + let remote_address = "cosmos1603j3e4juddh7cuhfquxspl0p0nsun046us7n0"; let custom_addr = "custom-addr"; let original_sender = "original_sender"; let denom = "uatom0x"; @@ -1182,11 +1235,11 @@ mod test { let cw20_raw_denom = token_addr.as_str(); let local_channel = "channel-1234"; let ratio = Ratio { - nominator: 1, + numerator: 1, denominator: 10, }; let fee_amount = - Uint128::from(amount) * Decimal::from_ratio(ratio.nominator, ratio.denominator); + Uint128::from(amount) * Decimal::from_ratio(ratio.numerator, ratio.denominator); let mut deps = setup(&[remote_channel, local_channel], &[]); TOKEN_FEE .save(deps.as_mut().storage, denom, &ratio) @@ -1195,9 +1248,9 @@ mod test { let pair = UpdatePairMsg { local_channel_id: local_channel.to_string(), denom: denom.to_string(), - asset_info: asset_info.clone(), + local_asset_info: asset_info.clone(), remote_decimals: 18u8, - asset_info_decimals: 18u8, + local_asset_info_decimals: 18u8, }; let _ = execute( @@ -1211,7 +1264,7 @@ mod test { // execute let mut transfer = TransferBackMsg { local_channel_id: local_channel.to_string(), - remote_address: "foreign-address".to_string(), + remote_address: remote_address.to_string(), remote_denom: denom.to_string(), timeout: Some(DEFAULT_TIMEOUT), memo: None, @@ -1225,15 +1278,25 @@ mod test { // insufficient funds case because we need to receive from remote chain first let info = mock_info(cw20_raw_denom, &[]); - let res = execute(deps.as_mut(), mock_env(), info.clone(), msg.clone()).unwrap_err(); + let res = execute(deps.as_mut(), mock_env(), info.clone(), msg.clone()); + println!("res: {:?}", res); assert_eq!( - res, + res.unwrap_err(), ContractError::NoSuchChannelState { id: local_channel.to_string(), denom: get_key_ics20_ibc_denom("wasm.cosmos2contract", local_channel, denom) } ); + // we need to reset fee accumulator because when execute returns error, the test state is still applied + TOKEN_FEE_ACCUMULATOR + .save( + deps.as_mut().storage, + "cw20:token-addr", + &Uint128::from(0u64), + ) + .unwrap(); + // prepare some mock packets let recv_packet = mock_receive_packet(remote_channel, local_channel, amount, denom, custom_addr); @@ -1276,7 +1339,7 @@ mod test { get_key_ics20_ibc_denom(CONTRACT_PORT, local_channel, denom) ); assert_eq!(msg.sender.as_str(), original_sender); - assert_eq!(msg.receiver.as_str(), "foreign-address"); + assert_eq!(msg.receiver.as_str(), remote_address); // assert_eq!(msg.memo, None); } _ => panic!("Unexpected return message: {:?}", res.messages[0]), @@ -1321,11 +1384,11 @@ mod test { let pair = UpdatePairMsg { local_channel_id: "not_registered_channel".to_string(), denom: denom.to_string(), - asset_info: AssetInfo::Token { + local_asset_info: AssetInfo::Token { contract_addr: Addr::unchecked("random_cw20_denom".to_string()), }, remote_decimals: 18u8, - asset_info_decimals: 18u8, + local_asset_info_decimals: 18u8, }; execute( @@ -1360,19 +1423,24 @@ mod test { TokenFee { token_denom: "orai".to_string(), ratio: Ratio { - nominator: 1, + numerator: 1, denominator: 10, }, }, TokenFee { token_denom: "atom".to_string(), ratio: Ratio { - nominator: 1, + numerator: 1, denominator: 5, }, }, ]), - fee_receiver: None, + relayer_fee: Some(vec![RelayerFee { + prefix: "foo".to_string(), + fee: Uint128::from(1000000u64), + }]), + fee_receiver: Some("token_fee_receiver".to_string()), + relayer_fee_receiver: Some("relayer_fee_receiver".to_string()), }; // unauthorized case let unauthorized_info = mock_info(&String::from("somebody"), &[]); @@ -1394,25 +1462,21 @@ mod test { assert_eq!(config.fee_denom, "hehe".to_string()); assert_eq!(config.swap_router_contract, "new_router".to_string()); assert_eq!( - TOKEN_FEE - .range(deps.as_ref().storage, None, None, Order::Ascending) - .count(), - 2usize + config.relayer_fee_receiver, + Addr::unchecked("relayer_fee_receiver") ); assert_eq!( - TOKEN_FEE - .load(deps.as_ref().storage, "orai") - .unwrap() - .denominator, - 10 + config.token_fee_receiver, + Addr::unchecked("token_fee_receiver") ); - assert_eq!( - TOKEN_FEE - .load(deps.as_ref().storage, "atom") - .unwrap() - .denominator, - 5 - ) + assert_eq!(config.token_fees.len(), 2usize); + assert_eq!(config.token_fees[0].ratio.denominator, 5); + assert_eq!(config.token_fees[0].token_denom, "atom".to_string()); + assert_eq!(config.token_fees[1].ratio.denominator, 10); + assert_eq!(config.token_fees[1].token_denom, "orai".to_string()); + assert_eq!(config.relayer_fees.len(), 1); + assert_eq!(config.relayer_fees[0].prefix, "foo".to_string()); + assert_eq!(config.relayer_fees[0].amount, Uint128::from(1000000u64)); } #[test] diff --git a/contracts/cw-ics20-latest/src/ibc.rs b/contracts/cw-ics20-latest/src/ibc.rs index 055aefd..6174eb7 100644 --- a/contracts/cw-ics20-latest/src/ibc.rs +++ b/contracts/cw-ics20-latest/src/ibc.rs @@ -2,29 +2,34 @@ use std::ops::Mul; use cosmwasm_schema::cw_serde; use cosmwasm_std::{ - attr, coin, entry_point, from_binary, to_binary, Addr, Api, BankMsg, Binary, CosmosMsg, - Decimal, Deps, DepsMut, Env, IbcBasicResponse, IbcChannel, IbcChannelCloseMsg, - IbcChannelConnectMsg, IbcChannelOpenMsg, IbcEndpoint, IbcMsg, IbcOrder, IbcPacket, - IbcPacketAckMsg, IbcPacketReceiveMsg, IbcPacketTimeoutMsg, IbcReceiveResponse, Order, - QuerierWrapper, Reply, Response, StdError, StdResult, Storage, SubMsg, SubMsgResult, Uint128, - WasmMsg, + attr, coin, entry_point, from_binary, to_binary, Addr, Api, Binary, CosmosMsg, Decimal, Deps, + DepsMut, Env, IbcBasicResponse, IbcChannel, IbcChannelCloseMsg, IbcChannelConnectMsg, + IbcChannelOpenMsg, IbcEndpoint, IbcMsg, IbcOrder, IbcPacket, IbcPacketAckMsg, + IbcPacketReceiveMsg, IbcPacketTimeoutMsg, IbcReceiveResponse, IbcTimeout, Order, + QuerierWrapper, Reply, Response, StdError, StdResult, Storage, SubMsg, SubMsgResult, Timestamp, + Uint128, +}; +use cw20_ics20_msg::helper::{ + denom_to_asset_info, get_prefix_decode_bech32, parse_asset_info_denom, }; use cw20_ics20_msg::receiver::DestinationInfo; +use cw_storage_plus::Map; use oraiswap::asset::AssetInfo; -use oraiswap::router::{SimulateSwapOperationsResponse, SwapOperation}; +use oraiswap::router::{RouterController, SwapOperation}; use crate::error::{ContractError, Never}; use crate::state::{ get_key_ics20_ibc_denom, ics20_denoms, increase_channel_balance, reduce_channel_balance, undo_increase_channel_balance, undo_reduce_channel_balance, ChannelInfo, IbcSingleStepData, MappingMetadata, Ratio, ReplyArgs, SingleStepReplyArgs, ALLOW_LIST, CHANNEL_INFO, CONFIG, - REPLY_ARGS, SINGLE_STEP_REPLY_ARGS, TOKEN_FEE, TOKEN_FEE_ACCUMULATOR, + RELAYER_FEE, RELAYER_FEE_ACCUMULATOR, REPLY_ARGS, SINGLE_STEP_REPLY_ARGS, TOKEN_FEE, + TOKEN_FEE_ACCUMULATOR, }; -use cw20::{Cw20ExecuteMsg, Cw20QueryMsg, TokenInfoResponse}; use cw20_ics20_msg::amount::{convert_local_to_remote, convert_remote_to_local, Amount}; pub const ICS20_VERSION: &str = "ics20-1"; pub const ICS20_ORDERING: IbcOrder = IbcOrder::Unordered; +pub const ORAIBRIDGE_PREFIX: &str = "oraib"; /// The format for sending an ics20 packet. /// Proto defined here: https://github.com/cosmos/cosmos-sdk/blob/v0.42.0/proto/ibc/applications/transfer/v1/transfer.proto#L11-L20 @@ -90,47 +95,16 @@ pub fn ack_fail(err: String) -> Binary { to_binary(&res).unwrap() } -pub const RECEIVE_ID: u64 = 1337; +// pub const RECEIVE_ID: u64 = 1337; pub const NATIVE_RECEIVE_ID: u64 = 1338; -pub const FOLLOW_UP_FAILURE_ID: u64 = 1339; +pub const FOLLOW_UP_ERROR_ID: u64 = 1339; +pub const IBC_TRANSFER_NATIVE_ERROR_ID: u64 = 1341; pub const REFUND_FAILURE_ID: u64 = 1340; pub const ACK_FAILURE_ID: u64 = 64023; -// const TRANSFER_BACK_FAILURE_ID: u64 = 1339; #[entry_point] pub fn reply(deps: DepsMut, _env: Env, reply: Reply) -> Result { match reply.id { - RECEIVE_ID => match reply.result { - SubMsgResult::Ok(_) => Ok(Response::new()), - SubMsgResult::Err(err) => { - // Important design note: with ibcv2 and wasmd 0.22 we can implement this all much easier. - // No reply needed... the receive function and submessage should return error on failure and all - // state gets reverted with a proper app-level message auto-generated - - // Since we need compatibility with Juno (Jan 2022), we need to ensure that optimisitic - // state updates in ibc_packet_receive get reverted in the (unlikely) chance of an - // error while sending the token - - // However, this requires passing some state between the ibc_packet_receive function and - // the reply handler. We do this with a singleton, with is "okay" for IBC as there is no - // reentrancy on these functions (cannot be called by another contract). This pattern - // should not be used for ExecuteMsg handlers - let reply_args = REPLY_ARGS.load(deps.storage)?; - undo_reduce_channel_balance( - deps.storage, - &reply_args.channel, - &reply_args.denom, - reply_args.amount, - true, - )?; - - Ok(Response::new().set_data(ack_fail(err)).add_attributes(vec![ - attr("undo_reduce_channel", reply_args.channel), - attr("undo_reduce_channel_ibc_denom", reply_args.denom), - attr("undo_reduce_channel_amount", reply_args.amount), - ])) - } - }, NATIVE_RECEIVE_ID => match reply.result { SubMsgResult::Ok(_) => Ok(Response::new()), SubMsgResult::Err(err) => { @@ -147,6 +121,8 @@ pub fn reply(deps: DepsMut, _env: Env, reply: Reply) -> Result Result match reply.result { + FOLLOW_UP_ERROR_ID => match reply.result { SubMsgResult::Ok(_) => Ok(Response::new()), SubMsgResult::Err(err) => { let reply_args = SINGLE_STEP_REPLY_ARGS.load(deps.storage)?; + SINGLE_STEP_REPLY_ARGS.remove(deps.storage); + // only refund, not undo reduce balance handle_follow_up_failure(deps.storage, reply_args, err) } }, @@ -182,12 +160,12 @@ pub fn reply(deps: DepsMut, _env: Env, reply: Reply) -> Result match reply.result { - // SubMsgResult::Ok(_) => Ok(Response::new()), - // SubMsgResult::Err(err) => Ok(Response::new() - // .set_data(ack_fail(err.clone())) - // .add_attribute("error_refund_cw20_tokens", err)), - // }, + IBC_TRANSFER_NATIVE_ERROR_ID => match reply.result { + SubMsgResult::Ok(_) => Ok(Response::new()), + SubMsgResult::Err(err) => Ok(Response::new() + .set_data(ack_fail(err.clone())) + .add_attribute("error_trying_to_transfer_ibc_native_with_error", err)), + }, _ => Err(ContractError::UnknownReplyId { id: reply.id }), } } @@ -310,8 +288,8 @@ pub fn parse_voucher_denom<'a>( // Returns local denom if the denom is an encoded voucher from the expected endpoint // Otherwise, error -pub fn parse_voucher_denom_without_sanity_checks<'a>(voucher_denom: &'a str) -> StdResult<&'a str> { - let split_denom: Vec<&str> = voucher_denom.splitn(3, '/').collect(); +pub fn parse_ibc_denom_without_sanity_checks<'a>(ibc_denom: &'a str) -> StdResult<&'a str> { + let split_denom: Vec<&str> = ibc_denom.splitn(3, '/').collect(); if split_denom.len() != 3 { return Err(StdError::generic_err( @@ -321,6 +299,19 @@ pub fn parse_voucher_denom_without_sanity_checks<'a>(voucher_denom: &'a str) -> Ok(split_denom[2]) } +// Returns +// Otherwise, error +pub fn parse_ibc_channel_without_sanity_checks<'a>(ibc_denom: &'a str) -> StdResult<&'a str> { + let split_denom: Vec<&str> = ibc_denom.splitn(3, '/').collect(); + + if split_denom.len() != 3 { + return Err(StdError::generic_err( + ContractError::NoForeignTokens {}.to_string(), + )); + } + Ok(split_denom[1]) +} + // this does the work of ibc_packet_receive, we wrap it to turn errors into acknowledgements fn do_ibc_packet_receive( deps: DepsMut, @@ -328,7 +319,7 @@ fn do_ibc_packet_receive( packet: &IbcPacket, ) -> Result { let msg: Ics20Packet = from_binary(&packet.data)?; - let channel = packet.dest.channel_id.clone(); + // let channel = packet.dest.channel_id.clone(); // If the token originated on the remote chain, it looks like "ucosm". // If it originated on our chain, it looks like "port/channel/ucosm". @@ -347,34 +338,34 @@ fn do_ibc_packet_receive( ); } - // make sure we have enough balance for this - reduce_channel_balance(deps.storage, &channel, denom.0, msg.amount, true)?; - - // we need to save the data to update the balances in reply - let reply_args = ReplyArgs { - channel, - denom: denom.0.to_string(), - amount: msg.amount, - }; - REPLY_ARGS.save(deps.storage, &reply_args)?; - - let to_send = Amount::from_parts(denom.0.to_string(), msg.amount); - let gas_limit = check_gas_limit(deps.as_ref(), &to_send)?; - let send = send_amount(to_send, msg.receiver.clone(), None); - let mut submsg = SubMsg::reply_on_error(send, RECEIVE_ID); - submsg.gas_limit = gas_limit; - - let res = IbcReceiveResponse::new() - .set_ack(ack_success()) - .add_submessage(submsg) - .add_attribute("action", "receive") - .add_attribute("sender", msg.sender) - .add_attribute("receiver", msg.receiver) - .add_attribute("denom", denom.0) - .add_attribute("amount", msg.amount) - .add_attribute("success", "true"); - - Ok(res) + // // make sure we have enough balance for this + // reduce_channel_balance(deps.storage, &channel, denom.0, msg.amount, true)?; + + // // we need to save the data to update the balances in reply + // let reply_args = ReplyArgs { + // channel, + // denom: denom.0.to_string(), + // amount: msg.amount, + // }; + // REPLY_ARGS.save(deps.storage, &reply_args)?; + + // let to_send = Amount::from_parts(denom.0.to_string(), msg.amount); + // let gas_limit = check_gas_limit(deps.as_ref(), &to_send)?; + // let send = send_amount(to_send, msg.receiver.clone(), None); + // let mut submsg = SubMsg::reply_on_error(send, RECEIVE_ID); + // submsg.gas_limit = gas_limit; + + // let res = IbcReceiveResponse::new() + // .set_ack(ack_success()) + // .add_submessage(submsg) + // .add_attribute("action", "receive") + // .add_attribute("sender", msg.sender) + // .add_attribute("receiver", msg.receiver) + // .add_attribute("denom", denom.0) + // .add_attribute("amount", msg.amount) + // .add_attribute("success", "true"); + + Err(ContractError::Std(StdError::generic_err("Not suppported"))) } fn handle_ibc_packet_receive_native_remote_chain( @@ -386,7 +377,7 @@ fn handle_ibc_packet_receive_native_remote_chain( packet: &IbcPacket, msg: &Ics20Packet, ) -> Result { - // make sure we have enough balance for this + let config = CONFIG.load(storage)?; // key in form transfer/channel-0/foo let ibc_denom = get_key_ics20_ibc_denom(&packet.dest.port_id, &packet.dest.channel_id, denom); @@ -402,6 +393,7 @@ fn handle_ibc_packet_receive_native_remote_chain( pair_mapping.asset_info_decimals, )?, ); + // will have to increase balance here because if this tx fails then it will be reverted, and the balance on the remote chain will also be reverted increase_channel_balance( storage, &packet.dest.channel_id, @@ -417,10 +409,17 @@ fn handle_ibc_packet_receive_native_remote_chain( }; REPLY_ARGS.save(storage, &reply_args)?; - let new_deducted_to_send = Amount::from_parts( - to_send.denom(), - process_deduct_fee(storage, &msg.denom, to_send.amount(), &to_send.denom())?, - ); + let (new_deducted_amount, token_fee, relayer_fee) = process_deduct_fee( + storage, + querier, + api, + &msg.sender, + &msg.denom, + to_send.clone(), + pair_mapping.asset_info_decimals, + &config.swap_router_contract, + )?; + let new_deducted_to_send = Amount::from_parts(to_send.denom(), new_deducted_amount); // after receiving the cw20 amount, we try to do fee swapping for the user if needed so he / she can create txs on the network let (submsgs, ibc_error_msg) = get_follow_up_msgs( @@ -433,25 +432,31 @@ fn handle_ibc_packet_receive_native_remote_chain( &msg.sender, &msg.receiver, &msg.memo.clone().unwrap_or_default(), - packet, + packet.dest.channel_id.as_str(), )?; - let submsgs: Vec = submsgs - .into_iter() - .map(|msg| SubMsg::reply_on_error(msg, FOLLOW_UP_FAILURE_ID)) - .collect(); - let transfer_fee_to_admin = - collect_transfer_fee_msgs(CONFIG.load(storage)?.fee_receiver.into_string(), storage)?; + let mut fee_msgs = collect_fee_msgs( + storage, + config.token_fee_receiver.into_string(), + TOKEN_FEE_ACCUMULATOR, + )?; + fee_msgs.append(&mut collect_fee_msgs( + storage, + config.relayer_fee_receiver.to_string(), + RELAYER_FEE_ACCUMULATOR, + )?); let mut res = IbcReceiveResponse::new() .set_ack(ack_success()) - .add_messages(transfer_fee_to_admin) + .add_messages(fee_msgs) .add_submessages(submsgs) .add_attribute("action", "receive_native") .add_attribute("sender", msg.sender.clone()) .add_attribute("receiver", msg.receiver.clone()) .add_attribute("denom", denom) .add_attribute("amount", msg.amount.to_string()) - .add_attribute("success", "true"); + .add_attribute("success", "true") + .add_attribute("token_fee", token_fee) + .add_attribute("relayer_fee", relayer_fee); if !ibc_error_msg.is_empty() { res = res.add_attribute("ibc_error_msg", ibc_error_msg); } @@ -469,33 +474,20 @@ pub fn get_follow_up_msgs( sender: &str, receiver: &str, memo: &str, - packet: &IbcPacket, -) -> Result<(Vec, String), ContractError> { + initial_dest_channel_id: &str, // channel id on Oraichain receiving the token from other chain +) -> Result<(Vec, String), ContractError> { let config = CONFIG.load(storage)?; - let mut cosmos_msgs: Vec = vec![]; + let mut sub_msgs: Vec = vec![]; let destination: DestinationInfo = DestinationInfo::from_str(memo); + let send_only_sub_msg = SubMsg::reply_on_error( + to_send.send_amount(receiver.to_string(), None), + NATIVE_RECEIVE_ID, + ); if is_follow_up_msgs_only_send_amount(&memo, &destination.destination_denom) { - return Ok(( - vec![send_amount(to_send, receiver.to_string(), None)], - "".to_string(), - )); + return Ok((vec![send_only_sub_msg], "".to_string())); } // successful case. We dont care if this msg is going to be successful or not because it does not affect our ibc receive flow (just submsgs) - let receiver_asset_info = if querier - .query_wasm_smart::( - destination.destination_denom.clone(), - &Cw20QueryMsg::TokenInfo {}, - ) - .is_ok() - { - AssetInfo::Token { - contract_addr: Addr::unchecked(destination.destination_denom.clone()), - } - } else { - AssetInfo::NativeToken { - denom: destination.destination_denom.clone(), - } - }; + let receiver_asset_info = denom_to_asset_info(querier, api, &destination.destination_denom)?; let swap_operations = build_swap_operations( receiver_asset_info.clone(), initial_receive_asset_info.clone(), @@ -503,14 +495,22 @@ pub fn get_follow_up_msgs( ); let mut minimum_receive = to_send.amount(); if swap_operations.len() > 0 { - let response: SimulateSwapOperationsResponse = querier.query_wasm_smart( - config.swap_router_contract.clone(), - &oraiswap::router::QueryMsg::SimulateSwapOperations { - offer_amount: to_send.amount().clone(), - operations: swap_operations.clone(), - }, - )?; - minimum_receive = response.amount; + let response = config.swap_router_contract.simulate_swap( + querier, + to_send.amount().clone(), + swap_operations.clone(), + ); + if response.is_err() { + return Ok(( + vec![send_only_sub_msg], + format!( + "Cannot simulate swap with ops: {:?} with error: {:?}", + swap_operations, + response.unwrap_err().to_string() + ), + )); + } + minimum_receive = response.unwrap().amount; } let ibc_msg = build_ibc_msg( @@ -518,40 +518,37 @@ pub fn get_follow_up_msgs( env, receiver_asset_info, receiver, - packet.dest.channel_id.as_str(), + initial_dest_channel_id, minimum_receive.clone(), &sender, &destination, config.default_timeout, ); - let mut ibc_error_msg = String::from(""); // by default, the receiver is the original address sent in ics20packet let mut to = Some(api.addr_validate(receiver)?); - if let Some(ibc_msg) = ibc_msg.as_ref().ok() { - cosmos_msgs.push(ibc_msg.to_owned()); + let ibc_error_msg = if let Some(ibc_msg) = ibc_msg.as_ref().ok() { + sub_msgs.push(ibc_msg.to_owned()); // if there's an ibc msg => swap receiver is None so the receiver is this ibc wasm address to = None; + String::from("") } else { - ibc_error_msg = ibc_msg.unwrap_err().to_string(); - } + ibc_msg.unwrap_err().to_string() + }; build_swap_msgs( minimum_receive, &config.swap_router_contract, to_send.amount(), initial_receive_asset_info, to, - &mut cosmos_msgs, + &mut sub_msgs, swap_operations, )?; - // fallback case. If there's no cosmos messages then we return send amount - if cosmos_msgs.is_empty() { - return Ok(( - vec![send_amount(to_send, receiver.to_string(), None)], - ibc_error_msg, - )); + // fallback case. If there's no cosmos messages or ibc error msg is not empty then we return send amount + if sub_msgs.is_empty() { + return Ok((vec![send_only_sub_msg], ibc_error_msg)); } - return Ok((cosmos_msgs, ibc_error_msg)); + return Ok((sub_msgs, ibc_error_msg)); } pub fn is_follow_up_msgs_only_send_amount(memo: &str, destination_denom: &str) -> bool { @@ -595,49 +592,30 @@ pub fn build_swap_operations( pub fn build_swap_msgs( minimum_receive: Uint128, - swap_router_contract: &str, + swap_router_contract: &RouterController, amount: Uint128, initial_receive_asset_info: AssetInfo, to: Option, - cosmos_msgs: &mut Vec, + sub_msgs: &mut Vec, operations: Vec, ) -> StdResult<()> { // the swap msg must be executed before other msgs because we need the ask token amount to create ibc msg => insert in first index if operations.len() == 0 { return Ok(()); } - match initial_receive_asset_info { - AssetInfo::Token { contract_addr } => cosmos_msgs.insert( - 0, - WasmMsg::Execute { - contract_addr: contract_addr.to_string(), - msg: to_binary(&Cw20ExecuteMsg::Send { - contract: swap_router_contract.to_string(), - amount, - msg: to_binary(&oraiswap::router::Cw20HookMsg::ExecuteSwapOperations { - operations, - minimum_receive: Some(minimum_receive.clone()), - to: to.map(|to| to.into_string()), - })?, - })?, - funds: vec![], - } - .into(), + sub_msgs.insert( + 0, + SubMsg::reply_on_error( + swap_router_contract.execute_operations( + initial_receive_asset_info, + amount, + operations, + Some(minimum_receive), + to, + )?, + NATIVE_RECEIVE_ID, ), - AssetInfo::NativeToken { denom } => cosmos_msgs.insert( - 0, - WasmMsg::Execute { - contract_addr: swap_router_contract.to_string(), - msg: to_binary(&oraiswap::router::ExecuteMsg::ExecuteSwapOperations { - operations, - minimum_receive: Some(minimum_receive.clone()), - to, - })?, - funds: vec![coin(amount.u128(), denom)], - } - .into(), - ), - } + ); Ok(()) } @@ -652,7 +630,7 @@ pub fn build_ibc_msg( remote_address: &str, destination: &DestinationInfo, default_timeout: u64, -) -> StdResult { +) -> StdResult { // if there's no dest channel then we stop, no need to transfer ibc if destination.destination_channel.is_empty() { return Err(StdError::generic_err( @@ -660,82 +638,151 @@ pub fn build_ibc_msg( )); } let timeout = env.block.time.plus_seconds(default_timeout); - let mut reply_args = SingleStepReplyArgs { + let reply_args = SingleStepReplyArgs { channel: destination.destination_channel.clone(), refund_asset_info: receiver_asset_info.clone(), ibc_data: None, receiver: local_receiver.to_string(), local_amount: amount, }; - let (is_evm_based, destination) = destination.is_receiver_evm_based(); - if is_evm_based { - // use sender from ICS20Packet as receiver when transferring back - let pair_mappings: Vec<(String, MappingMetadata)> = ics20_denoms() - .idx - .asset_info - .prefix(receiver_asset_info.to_string()) - .range(storage, None, None, Order::Ascending) - .collect::>>()?; + let pair_mappings: Vec<(String, MappingMetadata)> = ics20_denoms() + .idx + .asset_info + .prefix(receiver_asset_info.to_string()) + .range(storage, None, None, Order::Ascending) + .collect::>>()?; + let (is_evm_based, evm_destination) = destination.is_receiver_evm_based(); + if is_evm_based { let mapping = pair_mappings .into_iter() - .find(|(key, _)| key.contains(&destination.destination_channel)) + .find(|(key, _)| { + // eg: 'wasm.orai195269awwnt5m6c843q6w7hp8rt0k7syfu9de4h0wz384slshuzps8y7ccm/channel-29/eth-mainnet0x4c11249814f11b9346808179Cf06e71ac328c1b5' + // parse to get eth-mainnet0x... + // then collect eth-mainnet prefix, and compare with dest channel + convert_remote_denom_to_evm_prefix( + parse_ibc_denom_without_sanity_checks(key).unwrap_or_default(), + ) + .eq(&evm_destination.destination_channel) + }) .ok_or(StdError::generic_err("cannot find pair mappings"))?; - // also deduct fee here because of round trip - let new_deducted_amount = process_deduct_fee( + let msg: CosmosMsg = process_ibc_msg( storage, - parse_voucher_denom_without_sanity_checks(&mapping.0)?, - amount, - &parse_asset_info_denom(receiver_asset_info.clone()), - )?; - let remote_amount = convert_local_to_remote( - new_deducted_amount, - mapping.1.remote_decimals, - mapping.1.asset_info_decimals, - )?; - - // build ics20 packet - let packet = Ics20Packet::new( - remote_amount.clone(), - mapping.0.clone(), // we use ibc denom in form // so that when it is sent back to remote chain, it gets parsed correctly and burned + mapping, + receiver_asset_info, + local_channel_id, env.contract.address.as_str(), - &remote_address, - Some(destination.receiver), - ); - // because we are transferring back, we reduce the channel's balance - reduce_channel_balance( - storage, - &local_channel_id.clone(), - &mapping.0.clone(), - remote_amount, - false, - ) - .map_err(|err| StdError::generic_err(err.to_string()))?; - reply_args.channel = local_channel_id.to_string(); - reply_args.ibc_data = Some(IbcSingleStepData { - ibc_denom: mapping.0, - remote_amount, + remote_address, // use sender from ICS20Packet as receiver when transferring back because we have the actual receiver in memo for evm cases + Some(evm_destination.receiver), + amount, + timeout, + reply_args, + )? + .into(); + return Ok(SubMsg::reply_on_error(msg, NATIVE_RECEIVE_ID)); + } + // 2nd case, where destination network is not evm, but it is still supported on our channel (eg: cw20 ATOM mapped with native ATOM on Cosmos), then we call + let is_cosmos_based = destination.is_receiver_cosmos_based(); + if is_cosmos_based { + // eg: wasm.orai195269awwnt5m6c843q6w7hp8rt0k7syfu9de4h0wz384slshuzps8y7ccm/channel-124/uatom + // for cosmos-based networks, each will have its own channel id => we filter using channel id, no need to check for denom + let mapping = pair_mappings.into_iter().find(|(key, _)| { + parse_ibc_channel_without_sanity_checks(key) + .unwrap_or_default() + .eq(&destination.destination_channel) }); - // keep track of the reply. We need to keep a seperate value because if using REPLY, it could be overriden by the channel increase later on - SINGLE_STEP_REPLY_ARGS.save(storage, &reply_args)?; + if let Some(mapping) = mapping { + let msg: CosmosMsg = process_ibc_msg( + storage, + mapping, + receiver_asset_info, + &destination.destination_channel, + env.contract.address.as_str(), + &destination.receiver, // now we use dest receiver since cosmos based universal swap wont be sent to oraibridge, so the receiver is the correct receive addr + None, // no need memo because it is not used in the remote cosmos based chain + amount, + timeout, + reply_args, + )? + .into(); + return Ok(SubMsg::reply_on_error(msg, NATIVE_RECEIVE_ID)); + } - // prepare ibc message - let msg = IbcMsg::SendPacket { - channel_id: local_channel_id.to_string(), - data: to_binary(&packet)?, + // final case, where the destination token is from a remote chain that we dont have a pair mapping with. + // we use ibc transfer so that attackers cannot manipulate the data to send to oraibridge without reducing the channel balance + // by using ibc transfer, the contract must actually owns native ibc tokens, which is not possible if it's oraibridge tokens + // we do not need to reduce channel balance because this transfer is not on our contract channel, but on destination channel + let ibc_msg: CosmosMsg = IbcMsg::Transfer { + channel_id: destination.destination_channel.clone(), + to_address: destination.receiver.clone(), + amount: coin(amount.u128(), destination.destination_denom.clone()), timeout: timeout.into(), - }; - return Ok(msg.into()); + } + .into(); + return Ok(SubMsg::reply_on_error( + ibc_msg, + IBC_TRANSFER_NATIVE_ERROR_ID, + )); } - // we use ibc transfer so that attackers cannot manipulate the data to send to oraibridge without reducing the channel balance - // by using ibc transfer, the contract must actually owns native ibc tokens, which is not possible if it's oraibridge tokens - let ibc_msg = IbcMsg::Transfer { - channel_id: destination.destination_channel, - to_address: destination.receiver, - amount: coin(amount.u128(), destination.destination_denom), - timeout: timeout.into(), - }; - Ok(ibc_msg.into()) + Err(StdError::generic_err( + "The destination info is neither evm or cosmos based", + )) +} + +// TODO: Write unit tests for relayer fee & cosmos based universal swap in simulate js +pub fn process_ibc_msg( + storage: &mut dyn Storage, + pair_mapping: (String, MappingMetadata), + receiver_asset_info: AssetInfo, + src_channel: &str, + ibc_msg_sender: &str, + ibc_msg_receiver: &str, + memo: Option, + amount: Uint128, + timeout: Timestamp, + mut reply_args: SingleStepReplyArgs, +) -> StdResult { + let (new_deducted_amount, _) = deduct_token_fee( + storage, + parse_ibc_denom_without_sanity_checks(&pair_mapping.0)?, // denom mapping in the form port/channel/denom + amount, + &parse_asset_info_denom(receiver_asset_info.clone()), + )?; + let remote_amount = convert_local_to_remote( + new_deducted_amount, + pair_mapping.1.remote_decimals, + pair_mapping.1.asset_info_decimals, + )?; + + // because we are transferring back, we reduce the channel's balance + reduce_channel_balance( + storage, + src_channel.clone(), + &pair_mapping.0.clone(), + remote_amount, + false, + ) + .map_err(|err| StdError::generic_err(err.to_string()))?; + + // prepare ibc message + let msg = build_ibc_send_packet( + remote_amount, + &pair_mapping.0, + ibc_msg_sender, + ibc_msg_receiver, + memo, + src_channel, + timeout.into(), + )?; + + reply_args.channel = src_channel.to_string(); + reply_args.ibc_data = Some(IbcSingleStepData { + ibc_denom: pair_mapping.0.to_string(), + remote_amount, + }); + // keep track of the reply. We need to keep a seperate value because if using REPLY, it could be overriden by the channel increase later on in reply + SINGLE_STEP_REPLY_ARGS.save(storage, &reply_args)?; + Ok(msg) } pub fn handle_follow_up_failure( @@ -764,7 +811,7 @@ pub fn handle_follow_up_failure( reply_args.local_amount, ); // we send refund to the local receiver of the single-step tx because the funds are currently in this contract - let send = send_amount(refund_amount, reply_args.receiver, None); + let send = refund_amount.send_amount(reply_args.receiver, None); response = response .add_submessage(SubMsg::reply_on_error(send, REFUND_FAILURE_ID)) .set_data(ack_fail(err.clone())) @@ -798,11 +845,68 @@ pub fn check_gas_limit(deps: Deps, amount: &Amount) -> Result, Contr } pub fn process_deduct_fee( + storage: &mut dyn Storage, + querier: &QuerierWrapper, + api: &dyn Api, + remote_sender: &str, + remote_token_denom: &str, + local_amount: Amount, // local amount + decimals: u8, + swap_router_contract: &RouterController, +) -> StdResult<(Uint128, Uint128, Uint128)> { + let (_, token_fee) = deduct_token_fee( + storage, + remote_token_denom, + local_amount.amount(), + &local_amount.denom(), + )?; + // simulate for relayer fee + let offer_asset_info = denom_to_asset_info(querier, api, &local_amount.raw_denom())?; + let offer_amount = Uint128::from(10u64.pow((decimals + 1) as u32) as u64); // +1 to make sure the offer amount is large enough to swap successfully + let token_price = swap_router_contract + .simulate_swap( + querier, + offer_amount, + vec![SwapOperation::OraiSwap { + offer_asset_info, + // always swap with orai. If it does not share a pool with ORAI => ignore, no fee + ask_asset_info: AssetInfo::NativeToken { + denom: "orai".to_string(), + }, + }], + ) + .map(|data| data.amount) + .unwrap_or_default(); + let (_, relayer_fee) = deduct_relayer_fee( + storage, + api, + remote_sender, + remote_token_denom, + local_amount.amount(), + offer_amount, + &local_amount.denom(), + token_price, + )?; + let new_amount = local_amount + .amount() + .checked_sub(token_fee) + .unwrap_or_default() + .checked_sub(relayer_fee) + .unwrap_or_default(); + if new_amount.is_zero() { + return Err(StdError::generic_err( + "Not enough transfer amount to cover the token and relayer fees", + )); + } + Ok((new_amount, token_fee, relayer_fee)) +} + +pub fn deduct_token_fee( storage: &mut dyn Storage, remote_token_denom: &str, amount: Uint128, local_token_denom: &str, -) -> StdResult { +) -> StdResult<(Uint128, Uint128)> { let token_fee = TOKEN_FEE.may_load(storage, &remote_token_denom)?; if let Some(token_fee) = token_fee { let fee = deduct_fee(token_fee, amount); @@ -812,9 +916,62 @@ pub fn process_deduct_fee( |prev_fee| -> StdResult { Ok(prev_fee.unwrap_or_default().checked_add(fee)?) }, )?; let new_deducted_amount = amount.checked_sub(fee)?; - return Ok(new_deducted_amount); + return Ok((new_deducted_amount, fee)); } - Ok(amount) + Ok((amount, Uint128::from(0u64))) +} + +pub fn deduct_relayer_fee( + storage: &mut dyn Storage, + api: &dyn Api, + remote_address: &str, + remote_token_denom: &str, + amount: Uint128, // local amount + offer_amount: Uint128, // offer amount of token that swaps to ORAI + local_token_denom: &str, // local denom + token_price: Uint128, +) -> StdResult<(Uint128, Uint128)> { + // api.debug(format!("token price: {}", token_price).as_str()); + if token_price.is_zero() { + return Ok((amount, Uint128::from(0u64))); + } + + // this is bech32 prefix of sender from other chains. Should not error because we are in the cosmos ecosystem. Every address should have prefix + // evm case, need to filter remote token denom since prefix is always oraib + let mut prefix = get_prefix_decode_bech32(remote_address)?; + // api.debug(format!("prefix: {}", prefix).as_str()); + if prefix.eq(ORAIBRIDGE_PREFIX) { + prefix = convert_remote_denom_to_evm_prefix(remote_token_denom); + } + // api.debug(format!("prefix after evm prefix: {}", prefix).as_str()); + let relayer_fee = RELAYER_FEE.may_load(storage, &prefix)?; + // api.debug(format!("relayer fee: {}", relayer_fee.unwrap_or_default()).as_str()); + // no need to deduct fee if no fee is found in the mapping + if relayer_fee.is_none() { + return Ok((amount, Uint128::from(0u64))); + } + let relayer_fee = relayer_fee.unwrap(); + let required_fee_needed = relayer_fee + .checked_mul(offer_amount) + .unwrap_or_default() + .checked_div(token_price) + .unwrap_or_default(); + // api.debug(format!("required fee needed: {}", required_fee_needed).as_str()); + // accumulate fee so that we can collect it later after everything + // we share the same accumulator because it's the same data structure, and we are accumulating so it's fine + RELAYER_FEE_ACCUMULATOR.update( + storage, + local_token_denom, + |prev_fee| -> StdResult { + Ok(prev_fee + .unwrap_or_default() + .checked_add(required_fee_needed)?) + }, + )?; + Ok(( + amount.checked_sub(required_fee_needed).unwrap_or_default(), + required_fee_needed, + )) } pub fn deduct_fee(token_fee: Ratio, amount: Uint128) -> Uint128 { @@ -823,56 +980,43 @@ pub fn deduct_fee(token_fee: Ratio, amount: Uint128) -> Uint128 { return Uint128::from(0u64); } amount.mul(Decimal::from_ratio( - token_fee.nominator, + token_fee.numerator, token_fee.denominator, )) } -// pub fn convert_remote_denom_to_evm_prefix(remote_denom: &str) -> String { -// match remote_denom.split_once("0x") { -// Some((evm_prefix, _)) => return evm_prefix.to_string(), -// None => "".to_string(), -// } -// } +pub fn convert_remote_denom_to_evm_prefix(remote_denom: &str) -> String { + match remote_denom.split_once("0x") { + Some((evm_prefix, _)) => return evm_prefix.to_string(), + None => "".to_string(), + } +} -pub fn collect_transfer_fee_msgs( - receiver: String, +pub fn collect_fee_msgs( storage: &mut dyn Storage, + receiver: String, + fee_accumulator: Map<&str, Uint128>, ) -> StdResult> { - let cosmos_msgs = TOKEN_FEE_ACCUMULATOR + let cosmos_msgs = fee_accumulator .range(storage, None, None, Order::Ascending) - .filter(|data| { - if let Some(filter_result) = data - .as_ref() - .map(|fee_info| { - if fee_info.1.is_zero() { - return false; - } - true - }) - .ok() - { - return filter_result; - } - false - }) - .map(|data| { + .filter_map(|data| { data.map(|fee_info| { - send_amount( - Amount::from_parts(fee_info.0, fee_info.1), - receiver.clone(), - None, - ) + if fee_info.1.is_zero() { + return None; + } + Some(Amount::from_parts(fee_info.0, fee_info.1).send_amount(receiver.clone(), None)) }) + .ok() }) - .collect::>>(); + .flatten() + .collect::>(); // we reset all the accumulator keys to zero so that it wont accumulate more in the next txs. This action will be reverted if the fee payment txs fail. - TOKEN_FEE_ACCUMULATOR + fee_accumulator .keys(storage, None, None, Order::Ascending) .collect::, StdError>>()? .into_iter() - .for_each(|key| TOKEN_FEE_ACCUMULATOR.remove(storage, &key)); - cosmos_msgs + .for_each(|key| fee_accumulator.remove(storage, &key)); + Ok(cosmos_msgs) } #[entry_point] @@ -933,37 +1077,37 @@ fn on_packet_failure( // in case that the denom is not in the mapping list, meaning that it is not transferred back, but transfer originally from this local chain if ics20_denoms().may_load(deps.storage, &msg.denom)?.is_none() { - // undo the balance update on failure (as we pre-emptively added it on send) - reduce_channel_balance( - deps.storage, - &packet.src.channel_id, - &msg.denom, - msg.amount, - true, - )?; - - let to_send = Amount::from_parts(msg.denom.clone(), msg.amount); - let gas_limit = check_gas_limit(deps.as_ref(), &to_send)?; - let send = send_amount(to_send, msg.sender.clone(), None); - let mut submsg = SubMsg::reply_on_error(send, ACK_FAILURE_ID); - submsg.gas_limit = gas_limit; - - // similar event messages like ibctransfer module - let res = IbcBasicResponse::new() - .add_submessage(submsg) - .add_attribute("action", "acknowledge") - .add_attribute("sender", msg.sender) - .add_attribute("receiver", msg.receiver) - .add_attribute("denom", msg.denom) - .add_attribute("amount", msg.amount.to_string()) - .add_attribute("success", "false") - .add_attribute("error", err); - - return Ok(res); + // // undo the balance update on failure (as we pre-emptively added it on send) + // reduce_channel_balance( + // deps.storage, + // &packet.src.channel_id, + // &msg.denom, + // msg.amount, + // true, + // )?; + + // let to_send = Amount::from_parts(msg.denom.clone(), msg.amount); + // let gas_limit = check_gas_limit(deps.as_ref(), &to_send)?; + // let send = send_amount(to_send, msg.sender.clone(), None); + // let mut submsg = SubMsg::reply_on_error(send, ACK_FAILURE_ID); + // submsg.gas_limit = gas_limit; + + // // similar event messages like ibctransfer module + // let res = IbcBasicResponse::new() + // .add_submessage(submsg) + // .add_attribute("action", "acknowledge") + // .add_attribute("sender", msg.sender) + // .add_attribute("receiver", msg.receiver) + // .add_attribute("denom", msg.denom) + // .add_attribute("amount", msg.amount.to_string()) + // .add_attribute("success", "false") + // .add_attribute("error", err); + + return Ok(IbcBasicResponse::new()); } - // since we reduce the channel's balance optimistically when transferring back, we increase it again when receiving failed ack - increase_channel_balance( + // since we reduce the channel's balance optimistically when transferring back, we undo reduce it again when receiving failed ack + undo_reduce_channel_balance( deps.storage, &packet.src.channel_id, &msg.denom, @@ -981,11 +1125,11 @@ fn on_packet_failure( pair_mapping.asset_info_decimals, )?, ); - let cosmos_msg = send_amount(to_send, msg.sender.clone(), None); - let submsg = SubMsg::reply_on_error(cosmos_msg, ACK_FAILURE_ID); - + let cosmos_msg = to_send.send_amount(msg.sender.clone(), None); // used submsg here & reply on error. This means that if the refund process fails => tokens will be locked in this IBC Wasm contract. We will manually handle that case. No retry // similar event messages like ibctransfer module + let submsg = SubMsg::reply_on_error(cosmos_msg, ACK_FAILURE_ID); + let res = IbcBasicResponse::new() .add_submessage(submsg) .add_attribute("action", "acknowledge") @@ -1001,42 +1145,31 @@ fn on_packet_failure( // send ack fail to custom contract for refund } -pub fn send_amount(amount: Amount, recipient: String, msg: Option) -> CosmosMsg { - match amount { - Amount::Native(coin) => BankMsg::Send { - to_address: recipient, - amount: vec![coin], - } - .into(), - Amount::Cw20(coin) => { - let mut msg_cw20 = Cw20ExecuteMsg::Transfer { - recipient: recipient.clone(), - amount: coin.amount, - }; - if let Some(msg) = msg { - msg_cw20 = Cw20ExecuteMsg::Send { - contract: recipient, - amount: coin.amount, - msg, - }; - } - WasmMsg::Execute { - contract_addr: coin.address, - msg: to_binary(&msg_cw20).unwrap(), - funds: vec![], - } - .into() - } - } -} - -pub fn parse_asset_info_denom(asset_info: AssetInfo) -> String { - match asset_info { - AssetInfo::Token { contract_addr } => format!("cw20:{}", contract_addr.to_string()), - AssetInfo::NativeToken { denom } => denom, - } -} +pub fn build_ibc_send_packet( + amount: Uint128, + denom: &str, + sender: &str, + receiver: &str, + memo: Option, + src_channel: &str, + timeout: IbcTimeout, +) -> StdResult { + // build ics20 packet + let packet = Ics20Packet::new( + amount.clone(), + denom, // we use ibc denom in form // so that when it is sent back to remote chain, it gets parsed correctly and burned + sender, + receiver, + memo, + ); + packet + .validate() + .map_err(|err| StdError::generic_err(err.to_string()))?; -pub fn parse_ibc_wasm_port_id(contract_addr: String) -> String { - format!("wasm.{}", contract_addr) + // prepare ibc message + Ok(IbcMsg::SendPacket { + channel_id: src_channel.to_string(), + data: to_binary(&packet)?, + timeout: timeout.into(), + }) } diff --git a/contracts/cw-ics20-latest/src/ibc_tests.rs b/contracts/cw-ics20-latest/src/ibc_tests.rs index 4d002c6..44fbdc2 100644 --- a/contracts/cw-ics20-latest/src/ibc_tests.rs +++ b/contracts/cw-ics20-latest/src/ibc_tests.rs @@ -1,37 +1,38 @@ #[cfg(test)] mod test { - use cosmwasm_std::{attr, coin, Addr, CosmosMsg, Response, StdError}; + use cosmwasm_std::{attr, coin, Addr, CosmosMsg, IbcTimeout, Response, StdError}; use cw20_ics20_msg::receiver::DestinationInfo; use oraiswap::asset::AssetInfo; use oraiswap::router::SwapOperation; use crate::ibc::{ - ack_fail, build_ibc_msg, build_swap_msgs, check_gas_limit, deduct_fee, + ack_fail, build_ibc_msg, build_swap_msgs, check_gas_limit, + convert_remote_denom_to_evm_prefix, deduct_fee, deduct_relayer_fee, deduct_token_fee, handle_follow_up_failure, ibc_packet_receive, is_follow_up_msgs_only_send_amount, - parse_voucher_denom, parse_voucher_denom_without_sanity_checks, process_deduct_fee, - send_amount, Ics20Ack, Ics20Packet, RECEIVE_ID, REFUND_FAILURE_ID, + parse_ibc_channel_without_sanity_checks, parse_ibc_denom_without_sanity_checks, + parse_voucher_denom, process_ibc_msg, Ics20Ack, Ics20Packet, IBC_TRANSFER_NATIVE_ERROR_ID, + NATIVE_RECEIVE_ID, REFUND_FAILURE_ID, }; use crate::ibc::{build_swap_operations, get_follow_up_msgs}; use crate::test_helpers::*; use cosmwasm_std::{ - from_binary, to_binary, BankMsg, IbcEndpoint, IbcMsg, IbcPacket, IbcPacketReceiveMsg, - IbcTimeout, SubMsg, Timestamp, Uint128, WasmMsg, + from_binary, to_binary, IbcEndpoint, IbcMsg, IbcPacket, IbcPacketReceiveMsg, SubMsg, + Timestamp, Uint128, WasmMsg, }; use crate::error::ContractError; use crate::state::{ - get_key_ics20_ibc_denom, increase_channel_balance, ChannelState, IbcSingleStepData, Ratio, - SingleStepReplyArgs, CHANNEL_REVERSE_STATE, SINGLE_STEP_REPLY_ARGS, TOKEN_FEE, - TOKEN_FEE_ACCUMULATOR, + get_key_ics20_ibc_denom, increase_channel_balance, ChannelState, IbcSingleStepData, + MappingMetadata, Ratio, SingleStepReplyArgs, CHANNEL_REVERSE_STATE, RELAYER_FEE, + RELAYER_FEE_ACCUMULATOR, SINGLE_STEP_REPLY_ARGS, TOKEN_FEE, TOKEN_FEE_ACCUMULATOR, }; use cw20::{Cw20Coin, Cw20ExecuteMsg}; use cw20_ics20_msg::amount::{convert_local_to_remote, Amount}; use crate::contract::{execute, migrate, query_channel}; - use crate::msg::{ExecuteMsg, MigrateMsg, TransferMsg, UpdatePairMsg}; + use crate::msg::{ExecuteMsg, MigrateMsg, UpdatePairMsg}; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; use cosmwasm_std::{coins, to_vec}; - use cw20::Cw20ReceiveMsg; #[test] fn check_ack_json() { @@ -61,239 +62,239 @@ mod test { assert_eq!(expected, encdoded.as_str()); } - fn cw20_payment( - amount: u128, - address: &str, - recipient: &str, - gas_limit: Option, - ) -> SubMsg { - let msg = Cw20ExecuteMsg::Transfer { - recipient: recipient.into(), - amount: Uint128::new(amount), - }; - let exec = WasmMsg::Execute { - contract_addr: address.into(), - msg: to_binary(&msg).unwrap(), - funds: vec![], - }; - let mut msg = SubMsg::reply_on_error(exec, RECEIVE_ID); - msg.gas_limit = gas_limit; - msg - } - - fn native_payment(amount: u128, denom: &str, recipient: &str) -> SubMsg { - SubMsg::reply_on_error( - BankMsg::Send { - to_address: recipient.into(), - amount: coins(amount, denom), - }, - RECEIVE_ID, - ) - } - - fn mock_receive_packet( - my_channel: &str, - amount: u128, - denom: &str, - receiver: &str, - ) -> IbcPacket { - let data = Ics20Packet { - // this is returning a foreign (our) token, thus denom is // - denom: format!("{}/{}/{}", REMOTE_PORT, "channel-1234", denom), - amount: amount.into(), - sender: "remote-sender".to_string(), - receiver: receiver.to_string(), - memo: None, - }; - IbcPacket::new( - to_binary(&data).unwrap(), - IbcEndpoint { - port_id: REMOTE_PORT.to_string(), - channel_id: "channel-1234".to_string(), - }, - IbcEndpoint { - port_id: CONTRACT_PORT.to_string(), - channel_id: my_channel.to_string(), - }, - 3, - Timestamp::from_seconds(1665321069).into(), - ) - } - - #[test] - fn send_receive_cw20() { - let send_channel = "channel-9"; - let cw20_addr = "token-addr"; - let cw20_denom = "cw20:token-addr"; - let gas_limit = 1234567; - let mut deps = setup( - &["channel-1", "channel-7", send_channel], - &[(cw20_addr, gas_limit)], - ); - - // prepare some mock packets - let recv_packet = mock_receive_packet(send_channel, 876543210, cw20_denom, "local-rcpt"); - let recv_high_packet = - mock_receive_packet(send_channel, 1876543210, cw20_denom, "local-rcpt"); - - // cannot receive this denom yet - let msg = IbcPacketReceiveMsg::new(recv_packet.clone()); - let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap(); - assert!(res.messages.is_empty()); - let ack: Ics20Ack = from_binary(&res.acknowledgement).unwrap(); - let no_funds = Ics20Ack::Error( - ContractError::NoSuchChannelState { - id: send_channel.to_string(), - denom: cw20_denom.to_string(), - } - .to_string(), - ); - assert_eq!(ack, no_funds); - - // we send some cw20 tokens over - let transfer = TransferMsg { - channel: send_channel.to_string(), - remote_address: "remote-rcpt".to_string(), - timeout: None, - memo: None, - }; - let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { - sender: "local-sender".to_string(), - amount: Uint128::new(987654321), - msg: to_binary(&transfer).unwrap(), - }); - let info = mock_info(cw20_addr, &[]); - let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); - assert_eq!(1, res.messages.len()); - let expected = Ics20Packet { - denom: cw20_denom.into(), - amount: Uint128::new(987654321), - sender: "local-sender".to_string(), - receiver: "remote-rcpt".to_string(), - memo: None, - }; - let timeout = mock_env().block.time.plus_seconds(DEFAULT_TIMEOUT); - assert_eq!( - &res.messages[0], - &SubMsg::new(IbcMsg::SendPacket { - channel_id: send_channel.to_string(), - data: to_binary(&expected).unwrap(), - timeout: IbcTimeout::with_timestamp(timeout), - }) - ); - - // query channel state|_| - let state = query_channel(deps.as_ref(), send_channel.to_string(), Some(true)).unwrap(); - assert_eq!(state.balances, vec![Amount::cw20(987654321, cw20_addr)]); - assert_eq!(state.total_sent, vec![Amount::cw20(987654321, cw20_addr)]); + // fn cw20_payment( + // amount: u128, + // address: &str, + // recipient: &str, + // gas_limit: Option, + // ) -> SubMsg { + // let msg = Cw20ExecuteMsg::Transfer { + // recipient: recipient.into(), + // amount: Uint128::new(amount), + // }; + // let exec = WasmMsg::Execute { + // contract_addr: address.into(), + // msg: to_binary(&msg).unwrap(), + // funds: vec![], + // }; + // let mut msg = SubMsg::reply_on_error(exec, RECEIVE_ID); + // msg.gas_limit = gas_limit; + // msg + // } - // cannot receive more than we sent - let msg = IbcPacketReceiveMsg::new(recv_high_packet); - let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap(); - assert!(res.messages.is_empty()); - let ack: Ics20Ack = from_binary(&res.acknowledgement).unwrap(); - assert_eq!( - ack, - Ics20Ack::Error( - ContractError::InsufficientFunds { - id: send_channel.to_string(), - denom: cw20_denom.to_string(), - } - .to_string(), - ) - ); + // fn _native_payment(amount: u128, denom: &str, recipient: &str) -> SubMsg { + // SubMsg::reply_on_error( + // BankMsg::Send { + // to_address: recipient.into(), + // amount: coins(amount, denom), + // }, + // RECEIVE_ID, + // ) + // } - // we can receive less than we sent - let msg = IbcPacketReceiveMsg::new(recv_packet); - let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap(); - assert_eq!(1, res.messages.len()); - assert_eq!( - cw20_payment(876543210, cw20_addr, "local-rcpt", Some(gas_limit)), - res.messages[0] - ); - let ack: Ics20Ack = from_binary(&res.acknowledgement).unwrap(); - assert!(matches!(ack, Ics20Ack::Result(_))); + // fn mock_receive_packet( + // my_channel: &str, + // amount: u128, + // denom: &str, + // receiver: &str, + // ) -> IbcPacket { + // let data = Ics20Packet { + // // this is returning a foreign (our) token, thus denom is // + // denom: format!("{}/{}/{}", REMOTE_PORT, "channel-1234", denom), + // amount: amount.into(), + // sender: "remote-sender".to_string(), + // receiver: receiver.to_string(), + // memo: None, + // }; + // IbcPacket::new( + // to_binary(&data).unwrap(), + // IbcEndpoint { + // port_id: REMOTE_PORT.to_string(), + // channel_id: "channel-1234".to_string(), + // }, + // IbcEndpoint { + // port_id: CONTRACT_PORT.to_string(), + // channel_id: my_channel.to_string(), + // }, + // 3, + // Timestamp::from_seconds(1665321069).into(), + // ) + // } - // query channel state - let state = query_channel(deps.as_ref(), send_channel.to_string(), Some(true)).unwrap(); - assert_eq!(state.balances, vec![Amount::cw20(111111111, cw20_addr)]); - assert_eq!(state.total_sent, vec![Amount::cw20(987654321, cw20_addr)]); - } + // #[test] + // fn send_receive_cw20() { + // let send_channel = "channel-9"; + // let cw20_addr = "token-addr"; + // let cw20_denom = "cw20:token-addr"; + // let gas_limit = 1234567; + // let mut deps = setup( + // &["channel-1", "channel-7", send_channel], + // &[(cw20_addr, gas_limit)], + // ); - #[test] - fn send_receive_native() { - let send_channel = "channel-9"; - let mut deps = setup(&["channel-1", "channel-7", send_channel], &[]); + // // prepare some mock packets + // let recv_packet = mock_receive_packet(send_channel, 876543210, cw20_denom, "local-rcpt"); + // let recv_high_packet = + // mock_receive_packet(send_channel, 1876543210, cw20_denom, "local-rcpt"); + + // // cannot receive this denom yet + // let msg = IbcPacketReceiveMsg::new(recv_packet.clone()); + // let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap(); + // assert!(res.messages.is_empty()); + // let ack: Ics20Ack = from_binary(&res.acknowledgement).unwrap(); + // let no_funds = Ics20Ack::Error( + // ContractError::NoSuchChannelState { + // id: send_channel.to_string(), + // denom: cw20_denom.to_string(), + // } + // .to_string(), + // ); + // assert_eq!(ack, no_funds); + + // // we send some cw20 tokens over + // let transfer = TransferMsg { + // channel: send_channel.to_string(), + // remote_address: "remote-rcpt".to_string(), + // timeout: None, + // memo: None, + // }; + // let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { + // sender: "local-sender".to_string(), + // amount: Uint128::new(987654321), + // msg: to_binary(&transfer).unwrap(), + // }); + // let info = mock_info(cw20_addr, &[]); + // let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + // assert_eq!(1, res.messages.len()); + // let expected = Ics20Packet { + // denom: cw20_denom.into(), + // amount: Uint128::new(987654321), + // sender: "local-sender".to_string(), + // receiver: "remote-rcpt".to_string(), + // memo: None, + // }; + // let timeout = mock_env().block.time.plus_seconds(DEFAULT_TIMEOUT); + // assert_eq!( + // &res.messages[0], + // &SubMsg::new(IbcMsg::SendPacket { + // channel_id: send_channel.to_string(), + // data: to_binary(&expected).unwrap(), + // timeout: IbcTimeout::with_timestamp(timeout), + // }) + // ); - let denom = "uatom"; + // // query channel state|_| + // let state = query_channel(deps.as_ref(), send_channel.to_string(), Some(true)).unwrap(); + // assert_eq!(state.balances, vec![Amount::cw20(987654321, cw20_addr)]); + // assert_eq!(state.total_sent, vec![Amount::cw20(987654321, cw20_addr)]); - // prepare some mock packets - let recv_packet = mock_receive_packet(send_channel, 876543210, denom, "local-rcpt"); - let recv_high_packet = mock_receive_packet(send_channel, 1876543210, denom, "local-rcpt"); + // // cannot receive more than we sent + // let msg = IbcPacketReceiveMsg::new(recv_high_packet); + // let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap(); + // assert!(res.messages.is_empty()); + // let ack: Ics20Ack = from_binary(&res.acknowledgement).unwrap(); + // assert_eq!( + // ack, + // Ics20Ack::Error( + // ContractError::InsufficientFunds { + // id: send_channel.to_string(), + // denom: cw20_denom.to_string(), + // } + // .to_string(), + // ) + // ); - // cannot receive this denom yet - let msg = IbcPacketReceiveMsg::new(recv_packet.clone()); - let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap(); - assert!(res.messages.is_empty()); - let ack: Ics20Ack = from_binary(&res.acknowledgement).unwrap(); - let no_funds = Ics20Ack::Error( - ContractError::NoSuchChannelState { - id: send_channel.to_string(), - denom: denom.to_string(), - } - .to_string(), - ); - assert_eq!(ack, no_funds); - - // we transfer some tokens - let msg = ExecuteMsg::Transfer(TransferMsg { - channel: send_channel.to_string(), - remote_address: "my-remote-address".to_string(), - timeout: None, - memo: Some("memo".to_string()), - }); - let info = mock_info("local-sender", &coins(987654321, denom)); - execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + // // we can receive less than we sent + // let msg = IbcPacketReceiveMsg::new(recv_packet); + // let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap(); + // assert_eq!(1, res.messages.len()); + // assert_eq!( + // cw20_payment(876543210, cw20_addr, "local-rcpt", Some(gas_limit)), + // res.messages[0] + // ); + // let ack: Ics20Ack = from_binary(&res.acknowledgement).unwrap(); + // assert!(matches!(ack, Ics20Ack::Result(_))); - // query channel state|_| - let state = query_channel(deps.as_ref(), send_channel.to_string(), Some(true)).unwrap(); - assert_eq!(state.balances, vec![Amount::native(987654321, denom)]); - assert_eq!(state.total_sent, vec![Amount::native(987654321, denom)]); + // // query channel state + // let state = query_channel(deps.as_ref(), send_channel.to_string(), Some(true)).unwrap(); + // assert_eq!(state.balances, vec![Amount::cw20(111111111, cw20_addr)]); + // assert_eq!(state.total_sent, vec![Amount::cw20(987654321, cw20_addr)]); + // } - // cannot receive more than we sent - let msg = IbcPacketReceiveMsg::new(recv_high_packet); - let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap(); - assert!(res.messages.is_empty()); - let ack: Ics20Ack = from_binary(&res.acknowledgement).unwrap(); - assert_eq!( - ack, - Ics20Ack::Error( - ContractError::InsufficientFunds { - id: send_channel.to_string(), - denom: denom.to_string(), - } - .to_string(), - ) - ); + // #[test] + // fn send_receive_native() { + // let send_channel = "channel-9"; + // let mut deps = setup(&["channel-1", "channel-7", send_channel], &[]); + + // let denom = "uatom"; + + // // prepare some mock packets + // let recv_packet = mock_receive_packet(send_channel, 876543210, denom, "local-rcpt"); + // let recv_high_packet = mock_receive_packet(send_channel, 1876543210, denom, "local-rcpt"); + + // // cannot receive this denom yet + // let msg = IbcPacketReceiveMsg::new(recv_packet.clone()); + // let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap(); + // assert!(res.messages.is_empty()); + // let ack: Ics20Ack = from_binary(&res.acknowledgement).unwrap(); + // let no_funds = Ics20Ack::Error( + // ContractError::NoSuchChannelState { + // id: send_channel.to_string(), + // denom: denom.to_string(), + // } + // .to_string(), + // ); + // assert_eq!(ack, no_funds); + + // // we transfer some tokens + // let msg = ExecuteMsg::Transfer(TransferMsg { + // channel: send_channel.to_string(), + // remote_address: "my-remote-address".to_string(), + // timeout: None, + // memo: Some("memo".to_string()), + // }); + // let info = mock_info("local-sender", &coins(987654321, denom)); + // execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + + // // query channel state|_| + // let state = query_channel(deps.as_ref(), send_channel.to_string(), Some(true)).unwrap(); + // assert_eq!(state.balances, vec![Amount::native(987654321, denom)]); + // assert_eq!(state.total_sent, vec![Amount::native(987654321, denom)]); + + // // cannot receive more than we sent + // let msg = IbcPacketReceiveMsg::new(recv_high_packet); + // let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap(); + // assert!(res.messages.is_empty()); + // let ack: Ics20Ack = from_binary(&res.acknowledgement).unwrap(); + // assert_eq!( + // ack, + // Ics20Ack::Error( + // ContractError::InsufficientFunds { + // id: send_channel.to_string(), + // denom: denom.to_string(), + // } + // .to_string(), + // ) + // ); - // we can receive less than we sent - let msg = IbcPacketReceiveMsg::new(recv_packet); - let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap(); - assert_eq!(1, res.messages.len()); - assert_eq!( - native_payment(876543210, denom, "local-rcpt"), - res.messages[0] - ); - let ack: Ics20Ack = from_binary(&res.acknowledgement).unwrap(); - assert!(matches!(ack, Ics20Ack::Result(_))); + // // we can receive less than we sent + // let msg = IbcPacketReceiveMsg::new(recv_packet); + // let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap(); + // assert_eq!(1, res.messages.len()); + // assert_eq!( + // native_payment(876543210, denom, "local-rcpt"), + // res.messages[0] + // ); + // let ack: Ics20Ack = from_binary(&res.acknowledgement).unwrap(); + // assert!(matches!(ack, Ics20Ack::Result(_))); - // only need to call reply block on error case + // // only need to call reply block on error case - // query channel state - let state = query_channel(deps.as_ref(), send_channel.to_string(), Some(true)).unwrap(); - assert_eq!(state.balances, vec![Amount::native(111111111, denom)]); - assert_eq!(state.total_sent, vec![Amount::native(987654321, denom)]); - } + // // query channel state + // let state = query_channel(deps.as_ref(), send_channel.to_string(), Some(true)).unwrap(); + // assert_eq!(state.balances, vec![Amount::native(111111111, denom)]); + // assert_eq!(state.total_sent, vec![Amount::native(987654321, denom)]); + // } #[test] fn check_gas_limit_handles_all_cases() { @@ -317,10 +318,11 @@ mod test { mock_env(), MigrateMsg { default_gas_limit: Some(def_limit), - fee_receiver: "receiver".to_string(), - // default_timeout: 100u64, - // fee_denom: "orai".to_string(), - // swap_router_contract: "foobar".to_string(), + token_fee_receiver: "receiver".to_string(), + relayer_fee_receiver: "relayer_fee_receiver".to_string(), + default_timeout: 100u64, + fee_denom: "orai".to_string(), + swap_router_contract: "foobar".to_string(), }, ) .unwrap(); @@ -340,12 +342,17 @@ mod test { amount: u128, denom: &str, receiver: &str, + sender: Option<&str>, ) -> IbcPacket { let data = Ics20Packet { // this is returning a foreign native token, thus denom is , eg: uatom denom: denom.to_string(), amount: amount.into(), - sender: "remote-sender".to_string(), + sender: if sender.is_none() { + "remote-sender".to_string() + } else { + sender.unwrap().to_string() + }, receiver: receiver.to_string(), memo: None, }; @@ -438,8 +445,13 @@ mod test { ); // prepare some mock packets - let recv_packet = - mock_receive_packet_remote_to_local(send_channel, 876543210, cw20_denom, custom_addr); + let recv_packet = mock_receive_packet_remote_to_local( + send_channel, + 876543210, + cw20_denom, + custom_addr, + None, + ); // we can receive this denom, channel balance should increase let msg = IbcPacketReceiveMsg::new(recv_packet.clone()); @@ -471,7 +483,7 @@ mod test { deps.as_mut().storage, denom, &Ratio { - nominator: 1, + numerator: 1, denominator: 10, }, ) @@ -480,9 +492,9 @@ mod test { let pair = UpdatePairMsg { local_channel_id: send_channel.to_string(), denom: denom.to_string(), - asset_info: asset_info.clone(), + local_asset_info: asset_info.clone(), remote_decimals: 18u8, - asset_info_decimals: 18u8, + local_asset_info_decimals: 18u8, }; let _ = execute( @@ -499,12 +511,13 @@ mod test { send_amount.u128(), denom, custom_addr, + Some("orai1cdhkt9ps47hwn9sqren70uw9cyrfka9fpauuks"), ); // we can receive this denom, channel balance should increase let msg = IbcPacketReceiveMsg::new(recv_packet.clone()); let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap(); - println!("res: {:?}", res.messages); + println!("res: {:?}", res); // TODO: fix test cases. Possibly because we are adding two add_submessages? assert_eq!(res.messages.len(), 2); // 2 messages because we also have deduct fee msg match res.messages[0].msg.clone() { @@ -629,11 +642,11 @@ mod test { }; let native_denom = "foobar"; let to: Option = None; - let mut cosmos_msgs: Vec = vec![]; + let mut cosmos_msgs: Vec = vec![]; let mut operations: Vec = vec![]; build_swap_msgs( minimum_receive.clone(), - swap_router_contract.clone(), + &oraiswap::router::RouterController(swap_router_contract.to_string()), amount.clone(), initial_receive_asset_info.clone(), to.clone(), @@ -648,7 +661,7 @@ mod test { }); build_swap_msgs( minimum_receive.clone(), - swap_router_contract.clone(), + &oraiswap::router::RouterController(swap_router_contract.to_string()), amount.clone(), initial_receive_asset_info.clone(), to.clone(), @@ -666,7 +679,7 @@ mod test { }; build_swap_msgs( minimum_receive.clone(), - swap_router_contract.clone(), + &oraiswap::router::RouterController(swap_router_contract.to_string()), amount.clone(), initial_receive_asset_info.clone(), to.clone(), @@ -679,25 +692,30 @@ mod test { format!("{:?}", cosmos_msgs[0]).contains("execute_swap_operations") ); assert_eq!( - CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: swap_router_contract.to_string(), - msg: to_binary(&oraiswap::router::ExecuteMsg::ExecuteSwapOperations { - operations: operations, - minimum_receive: Some(minimum_receive), - to - }) - .unwrap(), - funds: coins(amount.u128(), native_denom) - }), + SubMsg::reply_on_error( + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: swap_router_contract.to_string(), + msg: to_binary(&oraiswap::router::ExecuteMsg::ExecuteSwapOperations { + operations: operations, + minimum_receive: Some(minimum_receive), + to + }) + .unwrap(), + funds: coins(amount.u128(), native_denom) + }), + NATIVE_RECEIVE_ID + ), cosmos_msgs[0] ); } #[test] - fn test_get_ibc_msg() { + fn test_get_ibc_msg_evm_case() { + // setup let send_channel = "channel-9"; let receive_channel = "channel-1"; let allowed = "foobar"; + let pair_mapping_denom = "trx-mainnet0xa614f803B6FD780986A42c78Ec9c7f77e6DeD13C"; let allowed_gas = 777666; let mut deps = setup(&[send_channel], &[(allowed, allowed_gas)]); let receiver_asset_info = AssetInfo::NativeToken { @@ -739,32 +757,9 @@ mod test { StdError::generic_err("Destination channel empty in build ibc msg") ); - // not evm based case, should be successful & cosmos msg is ibc transfer - destination.destination_channel = "channel-10".to_string(); - let result = build_ibc_msg( - deps.as_mut().storage, - env.clone(), - receiver_asset_info.clone(), - local_receiver, - receive_channel, - amount, - remote_address, - &destination, - timeout, - ) - .unwrap(); - assert_eq!( - result, - CosmosMsg::Ibc(IbcMsg::Transfer { - channel_id: "channel-10".to_string(), - to_address: "0x1234".to_string(), - amount: coin(10u128, "atom"), - timeout: mock_env().block.time.plus_seconds(timeout).into() - }) - ); - // evm based case, error getting pair mapping destination.receiver = "trx-mainnet0x73Ddc880916021EFC4754Cb42B53db6EAB1f9D64".to_string(); + destination.destination_channel = send_channel.to_string(); let err = build_ibc_msg( deps.as_mut().storage, env.clone(), @@ -782,10 +777,10 @@ mod test { // add a pair mapping so we can test the happy case evm based happy case let update = UpdatePairMsg { local_channel_id: "mars-channel".to_string(), - denom: "trx-mainnet".to_string(), - asset_info: receiver_asset_info.clone(), + denom: pair_mapping_denom.to_string(), + local_asset_info: receiver_asset_info.clone(), remote_decimals, - asset_info_decimals, + local_asset_info_decimals: asset_info_decimals, }; // works with proper funds @@ -795,7 +790,7 @@ mod test { execute(deps.as_mut(), mock_env(), info, msg.clone()).unwrap(); let pair_mapping_key = format!( "wasm.{}/{}/{}", - "cosmos2contract", update.local_channel_id, "trx-mainnet" + "cosmos2contract", update.local_channel_id, pair_mapping_denom ); increase_channel_balance( deps.as_mut().storage, @@ -822,18 +817,21 @@ mod test { assert_eq!( result, - CosmosMsg::Ibc(IbcMsg::SendPacket { - channel_id: receive_channel.to_string(), - data: to_binary(&Ics20Packet::new( - remote_amount.clone(), - pair_mapping_key.clone(), - env.contract.address.as_str(), - &remote_address, - Some(destination.receiver), - )) - .unwrap(), - timeout: env.block.time.plus_seconds(timeout).into() - }) + SubMsg::reply_on_error( + CosmosMsg::Ibc(IbcMsg::SendPacket { + channel_id: receive_channel.to_string(), + data: to_binary(&Ics20Packet::new( + remote_amount.clone(), + pair_mapping_key.clone(), + env.contract.address.as_str(), + &remote_address, + Some(destination.receiver), + )) + .unwrap(), + timeout: env.block.time.plus_seconds(timeout).into() + }), + NATIVE_RECEIVE_ID + ) ); let reply_args = SINGLE_STEP_REPLY_ARGS.load(deps.as_mut().storage).unwrap(); let ibc_data = reply_args.ibc_data.unwrap(); @@ -845,9 +843,189 @@ mod test { assert_eq!(reply_args.refund_asset_info, receiver_asset_info) } + #[test] + fn test_get_ibc_msg_cosmos_based_case() { + // setup + let send_channel = "channel-10"; + let allowed = "foobar"; + let allowed_gas = 777666; + let mut deps = setup(&[send_channel], &[(allowed, allowed_gas)]); + let amount = Uint128::from(1000u64); + let pair_mapping_denom = "cosmos1zedxv25ah8fksmg2lzrndrpkvsjqgk4zt5ff7n"; + let receiver_asset_info = AssetInfo::Token { + contract_addr: Addr::unchecked("usdt"), + }; + let local_channel_id = "channel"; + let local_receiver = "receiver"; + let timeout = 10u64; + let remote_amount = convert_local_to_remote(amount.clone(), 18, 6).unwrap(); + let destination = DestinationInfo { + receiver: "cosmos1zedxv25ah8fksmg2lzrndrpkvsjqgk4zt5ff7n".to_string(), + destination_channel: send_channel.to_string(), + destination_denom: "atom".to_string(), + }; + let env = mock_env(); + let remote_address = "foobar"; + let ibc_denom = format!("foo/bar/{}", pair_mapping_denom); + let remote_decimals = 18; + let asset_info_decimals = 6; + let pair_mapping_key = format!( + "wasm.cosmos2contract/{}/{}", + send_channel, pair_mapping_denom + ); + + CHANNEL_REVERSE_STATE + .save( + deps.as_mut().storage, + (local_channel_id, ibc_denom.as_str()), + &ChannelState { + outstanding: remote_amount.clone(), + total_sent: Uint128::from(100u128), + }, + ) + .unwrap(); + + CHANNEL_REVERSE_STATE + .save( + deps.as_mut().storage, + (send_channel, pair_mapping_key.as_str()), + &ChannelState { + outstanding: remote_amount.clone(), + total_sent: Uint128::from(100u128), + }, + ) + .unwrap(); + + // cosmos based case but no mapping found. should be successful & cosmos msg is ibc transfer + let result = build_ibc_msg( + deps.as_mut().storage, + env.clone(), + receiver_asset_info.clone(), + local_receiver, + local_channel_id, + amount, + remote_address, + &destination, + timeout, + ) + .unwrap(); + assert_eq!( + result, + SubMsg::reply_on_error( + CosmosMsg::Ibc(IbcMsg::Transfer { + channel_id: send_channel.to_string(), + to_address: destination.receiver.clone(), + amount: coin(1000u128, "atom"), + timeout: mock_env().block.time.plus_seconds(timeout).into() + }), + IBC_TRANSFER_NATIVE_ERROR_ID + ) + ); + + // cosmos based case with mapping found. Should be successful & cosmos msg is ibc send packet + // add a pair mapping so we can test the happy case evm based happy case + let update = UpdatePairMsg { + local_channel_id: send_channel.to_string(), + denom: pair_mapping_denom.to_string(), + local_asset_info: receiver_asset_info.clone(), + remote_decimals, + local_asset_info_decimals: asset_info_decimals, + }; + + let msg = ExecuteMsg::UpdateMappingPair(update.clone()); + + let info = mock_info("gov", &coins(1234567, "ucosm")); + execute(deps.as_mut(), mock_env(), info, msg.clone()).unwrap(); + + CHANNEL_REVERSE_STATE + .save( + deps.as_mut().storage, + (local_channel_id, &pair_mapping_key), + &ChannelState { + outstanding: remote_amount.clone(), + total_sent: Uint128::from(100u128), + }, + ) + .unwrap(); + + // now we get ibc msg + let result = build_ibc_msg( + deps.as_mut().storage, + env.clone(), + receiver_asset_info.clone(), + local_receiver, + local_channel_id, + amount, + remote_address, + &destination, + timeout, + ) + .unwrap(); + + assert_eq!( + result, + SubMsg::reply_on_error( + CosmosMsg::Ibc(IbcMsg::SendPacket { + channel_id: send_channel.to_string(), + data: to_binary(&Ics20Packet::new( + remote_amount.clone(), + pair_mapping_key.clone(), + env.contract.address.as_str(), + &destination.receiver, + None, + )) + .unwrap(), + timeout: env.block.time.plus_seconds(timeout).into() + }), + NATIVE_RECEIVE_ID + ) + ); + } + + #[test] + fn test_get_ibc_msg_neither_cosmos_or_evm_based_case() { + // setup + let send_channel = "channel-9"; + let allowed = "foobar"; + let allowed_gas = 777666; + let mut deps = setup(&[send_channel], &[(allowed, allowed_gas)]); + let amount = Uint128::from(1000u64); + let receiver_asset_info = AssetInfo::Token { + contract_addr: Addr::unchecked("usdt"), + }; + let local_channel_id = "channel"; + let local_receiver = "receiver"; + let timeout = 10u64; + let destination = DestinationInfo { + receiver: "foo".to_string(), + destination_channel: "channel-10".to_string(), + destination_denom: "atom".to_string(), + }; + let env = mock_env(); + let remote_address = "foobar"; + // cosmos based case but no mapping found. should be successful & cosmos msg is ibc transfer + let result = build_ibc_msg( + deps.as_mut().storage, + env.clone(), + receiver_asset_info.clone(), + local_receiver, + local_channel_id, + amount, + remote_address, + &destination, + timeout, + ) + .unwrap_err(); + assert_eq!( + result, + StdError::generic_err("The destination info is neither evm or cosmos based") + ) + } + #[test] fn test_follow_up_msgs() { let send_channel = "channel-9"; + let local_channel = "channel"; let allowed = "foobar"; let allowed_gas = 777666; let mut deps = setup(&[send_channel], &[(allowed, allowed_gas)]); @@ -874,21 +1052,24 @@ mod test { "foobar", receiver.clone(), "", - &mock_receive_packet_remote_to_local("channel", 1u128, "foobar", "foobar"), + local_channel, ) .unwrap(); assert_eq!( result.0, - vec![CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: env.contract.address.to_string(), - msg: to_binary(&Cw20ExecuteMsg::Transfer { - recipient: receiver.to_string(), - amount: amount.clone() - }) - .unwrap(), - funds: vec![] - })] + vec![SubMsg::reply_on_error( + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: env.contract.address.to_string(), + msg: to_binary(&Cw20ExecuteMsg::Transfer { + recipient: receiver.to_string(), + amount: amount.clone() + }) + .unwrap(), + funds: vec![] + }), + NATIVE_RECEIVE_ID + )] ); // 2nd case, destination denom is empty => destination is collected from memo @@ -906,21 +1087,24 @@ mod test { "foobar", "foobar", memo, - &mock_receive_packet_remote_to_local("channel", 1u128, "foobar", "foobar"), + local_channel, ) .unwrap(); assert_eq!( result.0, - vec![CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: env.contract.address.to_string(), - msg: to_binary(&Cw20ExecuteMsg::Transfer { - recipient: receiver.to_string(), - amount: amount.clone() - }) - .unwrap(), - funds: vec![] - })] + vec![SubMsg::reply_on_error( + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: env.contract.address.to_string(), + msg: to_binary(&Cw20ExecuteMsg::Transfer { + recipient: receiver.to_string(), + amount: amount.clone() + }) + .unwrap(), + funds: vec![] + }), + NATIVE_RECEIVE_ID + )] ); // 3rd case, cosmos msgs empty case, also send amount @@ -940,21 +1124,24 @@ mod test { "foobar", "foobar", memo, - &mock_receive_packet_remote_to_local("channel", 1u128, "foobar", "foobar"), + local_channel, ) .unwrap(); assert_eq!( result.0, - vec![CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: env.contract.address.to_string(), - msg: to_binary(&Cw20ExecuteMsg::Transfer { - recipient: receiver.to_string(), - amount: amount.clone() - }) - .unwrap(), - funds: vec![] - })] + vec![SubMsg::reply_on_error( + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: env.contract.address.to_string(), + msg: to_binary(&Cw20ExecuteMsg::Transfer { + recipient: receiver.to_string(), + amount: amount.clone() + }) + .unwrap(), + funds: vec![] + }), + NATIVE_RECEIVE_ID + )] ); } @@ -986,11 +1173,8 @@ mod test { result, Response::new() .add_submessage(SubMsg::reply_on_error( - send_amount( - Amount::from_parts(native_denom.to_string(), amount.clone()), - single_step_reply_args.receiver.clone(), - None - ), + Amount::from_parts(native_denom.to_string(), amount.clone()) + .send_amount(single_step_reply_args.receiver.clone(), None), REFUND_FAILURE_ID )) .set_data(ack_fail(err.to_string())) @@ -1003,7 +1187,6 @@ mod test { attr("attempt_refund_amount", single_step_reply_args.local_amount), ]) ); - let ibc_denom = "ibc_denom"; let remote_amount = convert_local_to_remote(amount, 18, 6).unwrap(); single_step_reply_args.ibc_data = Some(IbcSingleStepData { @@ -1049,7 +1232,7 @@ mod test { assert_eq!( deduct_fee( Ratio { - nominator: 1, + numerator: 1, denominator: 0, }, Uint128::from(1000u64) @@ -1059,7 +1242,7 @@ mod test { assert_eq!( deduct_fee( Ratio { - nominator: 1, + numerator: 1, denominator: 1, }, Uint128::from(1000u64) @@ -1069,7 +1252,7 @@ mod test { assert_eq!( deduct_fee( Ratio { - nominator: 1, + numerator: 1, denominator: 100, }, Uint128::from(1000u64) @@ -1078,39 +1261,50 @@ mod test { ); } - // #[test] - // fn test_convert_remote_denom_to_evm_prefix() { - // assert_eq!(convert_remote_denom_to_evm_prefix("abcd"), "".to_string()); - // assert_eq!(convert_remote_denom_to_evm_prefix("0x"), "".to_string()); - // assert_eq!( - // convert_remote_denom_to_evm_prefix("evm0x"), - // "evm".to_string() - // ); - // } + #[test] + fn test_convert_remote_denom_to_evm_prefix() { + assert_eq!(convert_remote_denom_to_evm_prefix("abcd"), "".to_string()); + assert_eq!(convert_remote_denom_to_evm_prefix("0x"), "".to_string()); + assert_eq!( + convert_remote_denom_to_evm_prefix("evm0x"), + "evm".to_string() + ); + } #[test] - fn test_parse_voucher_denom_without_sanity_checks() { + fn test_parse_ibc_denom_without_sanity_checks() { + assert_eq!(parse_ibc_denom_without_sanity_checks("foo").is_err(), true); assert_eq!( - parse_voucher_denom_without_sanity_checks("foo").is_err(), + parse_ibc_denom_without_sanity_checks("foo/bar").is_err(), true ); + let result = parse_ibc_denom_without_sanity_checks("foo/bar/helloworld").unwrap(); + assert_eq!(result, "helloworld"); + } + + #[test] + fn test_parse_ibc_channel_without_sanity_checks() { assert_eq!( - parse_voucher_denom_without_sanity_checks("foo/bar").is_err(), + parse_ibc_channel_without_sanity_checks("foo").is_err(), true ); - let result = parse_voucher_denom_without_sanity_checks("foo/bar/helloworld").unwrap(); - assert_eq!(result, "helloworld"); + assert_eq!( + parse_ibc_channel_without_sanity_checks("foo/bar").is_err(), + true + ); + let result = parse_ibc_channel_without_sanity_checks("foo/bar/helloworld").unwrap(); + assert_eq!(result, "bar"); } #[test] - fn test_process_deduct_fee() { + fn test_deduct_token_fee() { let mut deps = mock_dependencies(); let amount = Uint128::from(1000u64); let storage = deps.as_mut().storage; let token_fee_denom = "foo0x"; // should return amount because we have not set relayer fee yet assert_eq!( - process_deduct_fee(storage, "foo", amount, "foo").unwrap(), + deduct_token_fee(storage, "foo", amount, "foo").unwrap().0, amount.clone() ); TOKEN_FEE @@ -1118,13 +1312,15 @@ mod test { storage, token_fee_denom, &Ratio { - nominator: 1, + numerator: 1, denominator: 100, }, ) .unwrap(); assert_eq!( - process_deduct_fee(storage, token_fee_denom, amount, "foo").unwrap(), + deduct_token_fee(storage, token_fee_denom, amount, "foo") + .unwrap() + .0, Uint128::from(990u64) ); assert_eq!( @@ -1132,4 +1328,215 @@ mod test { Uint128::from(10u64) ); } + + #[test] + fn test_deduct_relayer_fee() { + let mut deps = mock_dependencies(); + let amount = Uint128::from(1000u64); + let deps_mut = deps.as_mut(); + let token_fee_denom = "cosmos"; + let remote_address = "cosmos1zedxv25ah8fksmg2lzrndrpkvsjqgk4zt5ff7n"; + let offer_amount = Uint128::from(10u32.pow(0 as u32)); + let token_price = Uint128::from(10u64); + // token price empty case. Should return zero fee + let result = deduct_relayer_fee( + deps_mut.storage, + deps_mut.api, + remote_address, + token_fee_denom, + amount, + offer_amount.clone(), + "local_token_denom", + Uint128::from(0u64), + ) + .unwrap(); + assert_eq!(result.1, Uint128::from(0u64)); + + // remote address is wrong (dont follow bech32 form) + assert_eq!( + deduct_relayer_fee( + deps_mut.storage, + deps_mut.api, + "foobar", + token_fee_denom, + amount, + offer_amount.clone(), + "local_token_denom", + token_price, + ) + .unwrap_err(), + StdError::generic_err("Cannot decode remote sender: foobar") + ); + + // no relayer fee case + assert_eq!( + deduct_relayer_fee( + deps_mut.storage, + deps_mut.api, + remote_address, + token_fee_denom, + amount, + offer_amount.clone(), + "local_token_denom", + token_price, + ) + .unwrap() + .1, + Uint128::from(0u64) + ); + + // oraib prefix case. + RELAYER_FEE + .save(deps_mut.storage, token_fee_denom, &Uint128::from(100u64)) + .unwrap(); + + RELAYER_FEE + .save(deps_mut.storage, "foo", &Uint128::from(1000u64)) + .unwrap(); + + assert_eq!( + deduct_relayer_fee( + deps_mut.storage, + deps_mut.api, + "oraib1603j3e4juddh7cuhfquxspl0p0nsun047wz3rl", + "foo0x", + amount, + offer_amount.clone(), + "local_token_denom", + token_price, + ) + .unwrap() + .1, + Uint128::from(100u64) + ); + + assert_eq!( + RELAYER_FEE_ACCUMULATOR + .load(deps_mut.storage, "local_token_denom") + .unwrap(), + Uint128::from(100u64) + ); + + // normal case with remote address + assert_eq!( + deduct_relayer_fee( + deps_mut.storage, + deps_mut.api, + remote_address, + token_fee_denom, + amount, + offer_amount.clone(), + "local_token_denom", + token_price, + ) + .unwrap() + .1, + Uint128::from(10u64) + ); + + assert_eq!( + RELAYER_FEE_ACCUMULATOR + .load(deps_mut.storage, "local_token_denom") + .unwrap(), + Uint128::from(110u64) + ); + } + + #[test] + fn test_process_ibc_msg() { + // setup + let mut deps = mock_dependencies(); + let amount = Uint128::from(1000u64); + let storage = deps.as_mut().storage; + let ibc_denom = "foo/bar/cosmos"; + let pair_mapping = ( + ibc_denom.to_string(), + MappingMetadata { + asset_info: AssetInfo::NativeToken { + denom: "orai".to_string(), + }, + remote_decimals: 18, + asset_info_decimals: 6, + }, + ); + let receiver_asset_info = AssetInfo::Token { + contract_addr: Addr::unchecked("usdt"), + }; + let local_channel_id = "channel"; + let ibc_msg_sender = "sender"; + let ibc_msg_receiver = "receiver"; + let memo = None; + let timeout = Timestamp::from_seconds(10u64); + let reply_args: SingleStepReplyArgs = SingleStepReplyArgs { + channel: local_channel_id.to_string(), + refund_asset_info: receiver_asset_info.clone(), + ibc_data: None, + local_amount: amount.clone(), + receiver: ibc_msg_receiver.to_string(), + }; + let remote_amount = convert_local_to_remote(amount.clone(), 18, 6).unwrap(); + + CHANNEL_REVERSE_STATE + .save( + storage, + (local_channel_id, ibc_denom), + &ChannelState { + outstanding: remote_amount.clone(), + total_sent: Uint128::from(100u128), + }, + ) + .unwrap(); + + // action + let result = process_ibc_msg( + storage, + pair_mapping, + receiver_asset_info, + local_channel_id, + ibc_msg_sender, + ibc_msg_receiver, + memo, + amount, + timeout, + reply_args, + ) + .unwrap(); + + // assert + // channel balance should reduce to 0 + assert_eq!( + CHANNEL_REVERSE_STATE + .load(storage, (local_channel_id, ibc_denom)) + .unwrap() + .outstanding, + Uint128::from(0u64) + ); + // reply args should have ibc data now + assert_eq!( + SINGLE_STEP_REPLY_ARGS + .load(storage) + .unwrap() + .ibc_data + .unwrap(), + IbcSingleStepData { + ibc_denom: ibc_denom.to_string(), + remote_amount + } + ); + assert_eq!( + result, + IbcMsg::SendPacket { + channel_id: local_channel_id.to_string(), + data: to_binary(&Ics20Packet { + amount: remote_amount.clone(), + denom: ibc_denom.to_string(), + receiver: ibc_msg_receiver.to_string(), + sender: ibc_msg_sender.to_string(), + memo: None + }) + .unwrap(), + timeout: IbcTimeout::with_timestamp(timeout) + } + ); + } } diff --git a/contracts/cw-ics20-latest/src/integration_tests.rs b/contracts/cw-ics20-latest/src/integration_tests.rs index 5e55d87..4ff6f54 100644 --- a/contracts/cw-ics20-latest/src/integration_tests.rs +++ b/contracts/cw-ics20-latest/src/integration_tests.rs @@ -102,9 +102,9 @@ fn initialize_basic_data_for_testings() -> (App, Addr, Addr, IbcEndpoint, String let update_allow_msg = ExecuteMsg::UpdateMappingPair(UpdatePairMsg { local_channel_id: local_channel_id.clone(), denom: native_denom.to_string(), - asset_info: asset_info.clone(), + local_asset_info: asset_info.clone(), remote_decimals, - asset_info_decimals, + local_asset_info_decimals: asset_info_decimals, }); router .execute_contract( diff --git a/contracts/cw-ics20-latest/src/msg.rs b/contracts/cw-ics20-latest/src/msg.rs index 0201098..b161174 100644 --- a/contracts/cw-ics20-latest/src/msg.rs +++ b/contracts/cw-ics20-latest/src/msg.rs @@ -1,9 +1,9 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Binary, IbcEndpoint}; +use cosmwasm_std::{Addr, Binary, IbcEndpoint, Uint128}; use cw20::Cw20ReceiveMsg; use oraiswap::asset::AssetInfo; -use crate::state::{ChannelInfo, MappingMetadata, Ratio, TokenFee}; +use crate::state::{ChannelInfo, MappingMetadata, Ratio, RelayerFee, TokenFee}; use cw20_ics20_msg::amount::Amount; #[cw_serde] @@ -29,11 +29,12 @@ pub struct AllowMsg { #[cw_serde] pub struct MigrateMsg { - // pub default_timeout: u64, + pub default_timeout: u64, pub default_gas_limit: Option, - pub fee_receiver: String, - // pub fee_denom: String, - // pub swap_router_contract: String, + pub fee_denom: String, + pub swap_router_contract: String, + pub token_fee_receiver: String, + pub relayer_fee_receiver: String, } #[cw_serde] @@ -41,7 +42,7 @@ pub enum ExecuteMsg { /// This accepts a properly-encoded ReceiveMsg from a cw20 contract Receive(Cw20ReceiveMsg), /// This allows us to transfer *exactly one* native token - Transfer(TransferMsg), + // Transfer(TransferMsg), TransferToRemote(TransferBackMsg), UpdateMappingPair(UpdatePairMsg), DeleteMappingPair(DeletePairMsg), @@ -55,7 +56,9 @@ pub enum ExecuteMsg { fee_denom: Option, swap_router_contract: Option, token_fee: Option>, + relayer_fee: Option>, fee_receiver: Option, + relayer_fee_receiver: Option, }, } @@ -65,9 +68,9 @@ pub struct UpdatePairMsg { /// native denom of the remote chain. Eg: orai pub denom: String, /// asset info of the local chain. - pub asset_info: AssetInfo, + pub local_asset_info: AssetInfo, pub remote_decimals: u8, - pub asset_info_decimals: u8, + pub local_asset_info_decimals: u8, } #[cw_serde] @@ -78,19 +81,19 @@ pub struct DeletePairMsg { } /// This is the message we accept via Receive -#[cw_serde] -pub struct TransferMsg { - /// The local channel to send the packets on - pub channel: String, - /// The remote address to send to. - /// Don't use HumanAddress as this will likely have a different Bech32 prefix than we use - /// and cannot be validated locally - pub remote_address: String, - /// How long the packet lives in seconds. If not specified, use default_timeout - pub timeout: Option, - /// metadata of the transfer to suit the new fungible token transfer - pub memo: Option, -} +// #[cw_serde] +// pub struct TransferMsg { +// /// The local channel to send the packets on +// pub channel: String, +// /// The remote address to send to. +// /// Don't use HumanAddress as this will likely have a different Bech32 prefix than we use +// /// and cannot be validated locally +// pub remote_address: String, +// /// How long the packet lives in seconds. If not specified, use default_timeout +// pub timeout: Option, +// /// metadata of the transfer to suit the new fungible token transfer +// pub memo: Option, +// } /// This is the message we accept via Receive #[cw_serde] @@ -190,6 +193,16 @@ pub struct ConfigResponse { pub fee_denom: String, pub swap_router_contract: String, pub gov_contract: String, + pub token_fee_receiver: Addr, + pub relayer_fee_receiver: Addr, + pub token_fees: Vec, + pub relayer_fees: Vec, +} + +#[cw_serde] +pub struct RelayerFeeResponse { + pub prefix: String, + pub amount: Uint128, } #[cw_serde] diff --git a/contracts/cw-ics20-latest/src/state.rs b/contracts/cw-ics20-latest/src/state.rs index ffc67ce..1112b9a 100644 --- a/contracts/cw-ics20-latest/src/state.rs +++ b/contracts/cw-ics20-latest/src/state.rs @@ -2,13 +2,13 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, IbcEndpoint, StdResult, Storage, Uint128}; use cw_controllers::Admin; use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, MultiIndex}; -use oraiswap::asset::AssetInfo; +use oraiswap::{asset::AssetInfo, router::RouterController}; use crate::ContractError; pub const ADMIN: Admin = Admin::new("admin"); -pub const CONFIG: Item = Item::new("ics20_config_v11"); +pub const CONFIG: Item = Item::new("ics20_config_v1.0.2"); // Used to pass info from the ibc_packet_receive to the reply handler pub const REPLY_ARGS: Item = Item::new("reply_args"); @@ -18,21 +18,33 @@ pub const SINGLE_STEP_REPLY_ARGS: Item = Item::new("single_ /// static info on one channel that doesn't change pub const CHANNEL_INFO: Map<&str, ChannelInfo> = Map::new("channel_info"); -/// Forward channel state is used when LOCAL chain initiates ibc transfer to remote chain -pub const CHANNEL_FORWARD_STATE: Map<(&str, &str), ChannelState> = - Map::new("channel_forward_state"); +// /// Forward channel state is used when LOCAL chain initiates ibc transfer to remote chain +// pub const CHANNEL_FORWARD_STATE: Map<(&str, &str), ChannelState> = +// Map::new("channel_forward_state"); /// Reverse channel state is used when REMOTE chain initiates ibc transfer to local chain pub const CHANNEL_REVERSE_STATE: Map<(&str, &str), ChannelState> = Map::new("channel_reverse_state"); +/// Reverse channel state is used when LOCAL chain initiates ibc transfer to remote chain +pub const CHANNEL_FORWARD_STATE: Map<(&str, &str), ChannelState> = + Map::new("channel_forward_state"); + /// Every cw20 contract we allow to be sent is stored here, possibly with a gas_limit pub const ALLOW_LIST: Map<&Addr, AllowInfo> = Map::new("allow_list"); pub const TOKEN_FEE: Map<&str, Ratio> = Map::new("token_fee"); +// relayer fee. This fee depends on the network type, not token type +// decimals of relayer fee should always be 10^6 because we use ORAI as relayer fee +pub const RELAYER_FEE: Map<&str, Uint128> = Map::new("relayer_fee"); + +// accumulated token fee pub const TOKEN_FEE_ACCUMULATOR: Map<&str, Uint128> = Map::new("token_fee_accumulator"); +// accumulated relayer fee +pub const RELAYER_FEE_ACCUMULATOR: Map<&str, Uint128> = Map::new("relayer_fee_accumulator"); + // MappingMetadataIndexex structs keeps a list of indexers pub struct MappingMetadataIndexex<'a> { // token.identifier @@ -71,8 +83,9 @@ pub struct Config { pub default_timeout: u64, pub default_gas_limit: Option, pub fee_denom: String, - pub swap_router_contract: String, - pub fee_receiver: Addr, + pub swap_router_contract: RouterController, + pub token_fee_receiver: Addr, + pub relayer_fee_receiver: Addr, } #[cw_serde] @@ -96,9 +109,15 @@ pub struct TokenFee { pub ratio: Ratio, } +#[cw_serde] +pub struct RelayerFee { + pub prefix: String, + pub fee: Uint128, +} + #[cw_serde] pub struct Ratio { - pub nominator: u64, + pub numerator: u64, pub denominator: u64, } diff --git a/packages/cw20-ics20-msg/Cargo.toml b/packages/cw20-ics20-msg/Cargo.toml index e755457..128632f 100644 --- a/packages/cw20-ics20-msg/Cargo.toml +++ b/packages/cw20-ics20-msg/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cw20-ics20-msg" -version = "0.0.2" +version = "0.0.3" authors = ["Oraichain Labs"] edition = "2021" description = "Definition and types for the cw20-ics20-msg interface" @@ -16,4 +16,6 @@ cosmwasm-std = { version = "1.1.9", default-features = false } cw-storage-plus = "1.0.1" cw20 = "1.0.1" schemars = "0.8.1" +bech32 = "0.8.1" +oraiswap = "1.0.1" serde = { version = "1.0.103", default-features = false, features = ["derive"] } diff --git a/packages/cw20-ics20-msg/src/amount.rs b/packages/cw20-ics20-msg/src/amount.rs index 34335b0..1b0ab32 100644 --- a/packages/cw20-ics20-msg/src/amount.rs +++ b/packages/cw20-ics20-msg/src/amount.rs @@ -1,6 +1,8 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Coin, Decimal, StdError, StdResult, Uint128}; -use cw20::Cw20Coin; +use cosmwasm_std::{ + to_binary, BankMsg, Binary, Coin, CosmosMsg, Decimal, StdError, StdResult, Uint128, WasmMsg, +}; +use cw20::{Cw20Coin, Cw20ExecuteMsg}; use std::convert::TryInto; #[cw_serde] @@ -73,6 +75,35 @@ impl Amount { Amount::Cw20(c) => c.amount.is_zero(), } } + + pub fn send_amount(&self, recipient: String, msg: Option) -> CosmosMsg { + match self.to_owned() { + Amount::Native(coin) => BankMsg::Send { + to_address: recipient, + amount: vec![coin], + } + .into(), + Amount::Cw20(coin) => { + let mut msg_cw20 = Cw20ExecuteMsg::Transfer { + recipient: recipient.clone(), + amount: coin.amount, + }; + if let Some(msg) = msg { + msg_cw20 = Cw20ExecuteMsg::Send { + contract: recipient, + amount: coin.amount, + msg, + }; + } + WasmMsg::Execute { + contract_addr: coin.address, + msg: to_binary(&msg_cw20).unwrap(), + funds: vec![], + } + .into() + } + } + } } fn mul_ratio_decimal(amount: Uint128, ratio: Decimal) -> StdResult { diff --git a/packages/cw20-ics20-msg/src/helper.rs b/packages/cw20-ics20-msg/src/helper.rs new file mode 100644 index 0000000..0a8e919 --- /dev/null +++ b/packages/cw20-ics20-msg/src/helper.rs @@ -0,0 +1,51 @@ +use cosmwasm_std::{Api, QuerierWrapper, StdError, StdResult}; +use cw20::{Cw20QueryMsg, TokenInfoResponse}; +use oraiswap::asset::AssetInfo; + +pub fn get_prefix_decode_bech32(address: &str) -> StdResult { + let decode_result = bech32::decode(address); + if decode_result.is_err() { + return Err(StdError::generic_err(format!( + "Cannot decode remote sender: {}", + address + ))); + } + Ok(decode_result.unwrap().0) +} + +pub fn parse_asset_info_denom(asset_info: AssetInfo) -> String { + match asset_info { + AssetInfo::Token { contract_addr } => format!("cw20:{}", contract_addr.to_string()), + AssetInfo::NativeToken { denom } => denom, + } +} + +pub fn parse_ibc_wasm_port_id(contract_addr: String) -> String { + format!("wasm.{}", contract_addr) +} + +pub fn denom_to_asset_info( + querier: &QuerierWrapper, + api: &dyn Api, + denom: &str, +) -> StdResult { + let info = if querier + .query_wasm_smart::(denom.clone(), &Cw20QueryMsg::TokenInfo {}) + .is_ok() + { + AssetInfo::Token { + contract_addr: api.addr_validate(denom)?, + } + } else { + AssetInfo::NativeToken { + denom: denom.to_string(), + } + }; + Ok(info) +} + +#[test] +fn test_get_prefix_decode_bech32() { + let result = get_prefix_decode_bech32("cosmos1g4h64yjt0fvzv5v2j8tyfnpe5kmnetejl67nlm").unwrap(); + assert_eq!(result, "cosmos".to_string()); +} diff --git a/packages/cw20-ics20-msg/src/lib.rs b/packages/cw20-ics20-msg/src/lib.rs index babc670..3027078 100644 --- a/packages/cw20-ics20-msg/src/lib.rs +++ b/packages/cw20-ics20-msg/src/lib.rs @@ -3,4 +3,5 @@ Shared msgs for the cw20-ics20 and other contracts that interact with it */ pub mod amount; +pub mod helper; pub mod receiver; diff --git a/packages/cw20-ics20-msg/src/receiver.rs b/packages/cw20-ics20-msg/src/receiver.rs index 41ccd64..e25acaa 100644 --- a/packages/cw20-ics20-msg/src/receiver.rs +++ b/packages/cw20-ics20-msg/src/receiver.rs @@ -1,5 +1,7 @@ use cosmwasm_schema::cw_serde; +use crate::helper::get_prefix_decode_bech32; + #[cw_serde] pub struct DestinationInfo { pub receiver: String, @@ -29,11 +31,7 @@ impl DestinationInfo { } pub fn is_receiver_evm_based(&self) -> (bool, Self) { - let mut new_destination: DestinationInfo = DestinationInfo { - receiver: self.receiver.clone(), - destination_channel: self.destination_channel.clone(), - destination_denom: self.destination_denom.clone(), - }; + let mut new_destination: DestinationInfo = DestinationInfo { ..self.clone() }; match self.receiver.split_once("0x") { Some((evm_prefix, address)) => { // has to have evm_prefix, otherwise we would not be able to know the real denom @@ -51,6 +49,18 @@ impl DestinationInfo { None => (false, new_destination), } } + + pub fn is_receiver_cosmos_based(&self) -> bool { + match get_prefix_decode_bech32(&self.receiver).ok() { + None => false, + Some(prefix) => { + if prefix.is_empty() { + return false; + } + true + } + } + } } #[test] @@ -75,6 +85,57 @@ fn test_is_evm_based() { ); } +#[test] +fn test_is_cosmos_based() { + let d1 = DestinationInfo::from_str("foo"); + assert_eq!(false, d1.is_receiver_cosmos_based()); + + let d1 = DestinationInfo::from_str("channel-15/foo:usdt"); + assert_eq!(false, d1.is_receiver_cosmos_based()); + + let d1 = + DestinationInfo::from_str("channel-15/cosmos14n3tx8s5ftzhlxvq0w5962v60vd82h30sythlz:usdt"); + let result = d1.is_receiver_cosmos_based(); + assert_eq!(true, result); + + let d1 = + DestinationInfo::from_str("channel-15/akash1g4h64yjt0fvzv5v2j8tyfnpe5kmnetejjpn5xp:usdt"); + let result = d1.is_receiver_cosmos_based(); + assert_eq!(true, result); + + let d1 = + DestinationInfo::from_str("channel-15/bostrom1g4h64yjt0fvzv5v2j8tyfnpe5kmnetejuf2qpu:usdt"); + let result = d1.is_receiver_cosmos_based(); + assert_eq!(true, result); + + let d1 = DestinationInfo::from_str("channel-124/cosmos1g4h64yjt0fvzv5v2j8tyfnpe5kmnetejl67nlm:orai17l2zk3arrx0a0fyuneyx8raln68622a2lrsz8ph75u7gw9tgz3esayqryf"); + let result = d1.is_receiver_cosmos_based(); + assert_eq!(true, result); +} + +#[test] +fn test_destination_info_from_str() { + let d1 = DestinationInfo::from_str("cosmos14n3tx8s5ftzhlxvq0w5962v60vd82h30sythlz"); + assert_eq!(d1.destination_channel, ""); + assert_eq!(d1.receiver, "cosmos14n3tx8s5ftzhlxvq0w5962v60vd82h30sythlz"); + assert_eq!(d1.destination_denom, ""); + + let d1 = DestinationInfo::from_str("cosmos14n3tx8s5ftzhlxvq0w5962v60vd82h30sythlz:foo"); + assert_eq!(d1.destination_channel, ""); + assert_eq!(d1.receiver, "cosmos14n3tx8s5ftzhlxvq0w5962v60vd82h30sythlz"); + assert_eq!(d1.destination_denom, "foo"); + + let d1 = DestinationInfo::from_str("foo/cosmos14n3tx8s5ftzhlxvq0w5962v60vd82h30sythlz"); + assert_eq!(d1.destination_channel, "foo"); + assert_eq!(d1.receiver, "cosmos14n3tx8s5ftzhlxvq0w5962v60vd82h30sythlz"); + assert_eq!(d1.destination_denom, ""); + + let d1 = DestinationInfo::from_str("foo/cosmos14n3tx8s5ftzhlxvq0w5962v60vd82h30sythlz:bar"); + assert_eq!(d1.destination_channel, "foo"); + assert_eq!(d1.receiver, "cosmos14n3tx8s5ftzhlxvq0w5962v60vd82h30sythlz"); + assert_eq!(d1.destination_denom, "bar"); +} + #[test] fn test_parse_destination_info() { // swap to orai then orai to atom, then use swapped amount to transfer ibc to destination @@ -140,4 +201,25 @@ fn test_parse_destination_info() { destination_denom: "usdt".to_string() } ); + // ibc hash case + let d7 = DestinationInfo::from_str("channel-5/trx-mainnet0x73Ddc880916021EFC4754Cb42B53db6EAB1f9D64:ibc/A2E2EEC9057A4A1C2C0A6A4C78B0239118DF5F278830F50B4A6BDD7A66506B78"); + assert_eq!( + d7, + DestinationInfo { + receiver: "trx-mainnet0x73Ddc880916021EFC4754Cb42B53db6EAB1f9D64".to_string(), + destination_channel: "channel-5".to_string(), + destination_denom: + "ibc/A2E2EEC9057A4A1C2C0A6A4C78B0239118DF5F278830F50B4A6BDD7A66506B78".to_string() + } + ); + let d8 = DestinationInfo::from_str("channel-124/cosmos1g4h64yjt0fvzv5v2j8tyfnpe5kmnetejl67nlm:orai17l2zk3arrx0a0fyuneyx8raln68622a2lrsz8ph75u7gw9tgz3esayqryf"); + assert_eq!( + d8, + DestinationInfo { + receiver: "cosmos1g4h64yjt0fvzv5v2j8tyfnpe5kmnetejl67nlm".to_string(), + destination_channel: "channel-124".to_string(), + destination_denom: "orai17l2zk3arrx0a0fyuneyx8raln68622a2lrsz8ph75u7gw9tgz3esayqryf" + .to_string(), + } + ) }