diff --git a/contracts/cw-ics20-latest/artifacts/cw-ics20-latest.wasm b/contracts/cw-ics20-latest/artifacts/cw-ics20-latest.wasm index da4f53b..7743cca 100644 Binary files a/contracts/cw-ics20-latest/artifacts/cw-ics20-latest.wasm and b/contracts/cw-ics20-latest/artifacts/cw-ics20-latest.wasm differ diff --git a/contracts/cw-ics20-latest/src/contract.rs b/contracts/cw-ics20-latest/src/contract.rs index 40d6c97..717df75 100644 --- a/contracts/cw-ics20-latest/src/contract.rs +++ b/contracts/cw-ics20-latest/src/contract.rs @@ -3,20 +3,22 @@ use cosmwasm_std::entry_point; use cosmwasm_std::{ from_binary, to_binary, Addr, Binary, CosmosMsg, Deps, DepsMut, Empty, Env, IbcEndpoint, IbcQuery, MessageInfo, Order, PortIdResponse, Response, StdError, StdResult, Storage, Uint128, + WasmMsg, }; use cw2::set_contract_version; -use cw20::{Cw20Coin, Cw20ReceiveMsg}; +use cw20::{Cw20Coin, Cw20ExecuteMsg, Cw20ReceiveMsg}; use cw20_ics20_msg::converter::ConverterController; 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::{build_ibc_send_packet, parse_voucher_denom, process_deduct_fee}; use crate::ibc_hooks::ibc_hooks_receive; use crate::msg::{ - AllowMsg, AllowedInfo, AllowedResponse, ChannelResponse, ChannelWithKeyResponse, - ConfigResponse, DeletePairMsg, ExecuteMsg, InitMsg, ListAllowedResponse, ListChannelsResponse, + AllowedInfo, AllowedResponse, ChannelResponse, ChannelWithKeyResponse, ConfigResponse, + DeletePairMsg, ExecuteMsg, InitMsg, ListAllowedResponse, ListChannelsResponse, ListMappingResponse, MigrateMsg, PairQuery, PortResponse, QueryMsg, RelayerFeeResponse, TransferBackMsg, UpdatePairMsg, }; @@ -27,7 +29,7 @@ use crate::state::{ ADMIN, ALLOW_LIST, CHANNEL_INFO, CHANNEL_REVERSE_STATE, CONFIG, RELAYER_FEE, REPLY_ARGS, SINGLE_STEP_REPLY_ARGS, TOKEN_FEE, }; -use cw20_ics20_msg::amount::{convert_local_to_remote, Amount}; +use cw20_ics20_msg::amount::{convert_local_to_remote, convert_remote_to_local, Amount}; use cw_utils::{maybe_addr, nonpayable, one_coin}; // version info for migration info @@ -86,7 +88,7 @@ pub fn execute( } ExecuteMsg::UpdateMappingPair(msg) => execute_update_mapping_pair(deps, env, info, msg), ExecuteMsg::DeleteMappingPair(msg) => execute_delete_mapping_pair(deps, env, info, msg), - ExecuteMsg::Allow(allow) => execute_allow(deps, env, info, allow), + // ExecuteMsg::Allow(allow) => execute_allow(deps, env, info, allow), ExecuteMsg::UpdateConfig { default_timeout, default_gas_limit, @@ -203,7 +205,7 @@ pub fn handle_increase_channel_balance_ibc_receive( remote_amount: Uint128, local_receiver: String, ) -> Result { - is_caller_contract(caller, contract_addr)?; + is_caller_contract(caller, contract_addr.clone())?; // 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( deps.storage, @@ -211,6 +213,28 @@ pub fn handle_increase_channel_balance_ibc_receive( &ibc_denom, remote_amount.clone(), )?; + + let mut cosmos_msgs: Vec = vec![]; + let pair_mapping = ics20_denoms() + .load(deps.storage, &ibc_denom) + .map_err(|_| ContractError::NotOnMappingList {})?; + + let mint_amount = convert_remote_to_local( + remote_amount, + pair_mapping.remote_decimals, + pair_mapping.asset_info_decimals, + )?; + let mint_msg = build_mint_cw20_mapping_msg( + pair_mapping.is_mint_burn, + pair_mapping.asset_info, + mint_amount, + contract_addr.to_string(), + )?; + + if let Some(mint_msg) = mint_msg { + cosmos_msgs.push(mint_msg); + } + // we need to save the data to update the balances in reply let reply_args = ReplyArgs { channel: dst_channel_id.clone(), @@ -219,13 +243,15 @@ pub fn handle_increase_channel_balance_ibc_receive( local_receiver: local_receiver.clone(), }; REPLY_ARGS.save(deps.storage, &reply_args)?; - Ok(Response::default().add_attributes(vec![ - ("action", "increase_channel_balance_ibc_receive"), - ("channel_id", dst_channel_id.as_str()), - ("ibc_denom", ibc_denom.as_str()), - ("amount", remote_amount.to_string().as_str()), - ("local_receiver", local_receiver.as_str()), - ])) + Ok(Response::default() + .add_attributes(vec![ + ("action", "increase_channel_balance_ibc_receive"), + ("channel_id", dst_channel_id.as_str()), + ("ibc_denom", ibc_denom.as_str()), + ("amount", remote_amount.to_string().as_str()), + ("local_receiver", local_receiver.as_str()), + ]) + .add_messages(cosmos_msgs)) } pub fn handle_reduce_channel_balance_ibc_receive( @@ -253,13 +279,36 @@ pub fn handle_reduce_channel_balance_ibc_receive( local_receiver: local_receiver.to_string(), }, )?; - Ok(Response::default().add_attributes(vec![ - ("action", "reduce_channel_balance_ibc_receive"), - ("channel_id", src_channel_id.as_str()), - ("ibc_denom", ibc_denom.as_str()), - ("amount", remote_amount.to_string().as_str()), - ("local_receiver", local_receiver.as_str()), - ])) + + // burn cw20 token if the mechanism is mint burn + + let mut cosmos_msgs: Vec = vec![]; + let pair_mapping = ics20_denoms() + .load(storage, &ibc_denom) + .map_err(|_| ContractError::NotOnMappingList {})?; + let burn_amount = convert_remote_to_local( + remote_amount, + pair_mapping.remote_decimals, + pair_mapping.asset_info_decimals, + )?; + let burn_msg = build_burn_cw20_mapping_msg( + pair_mapping.is_mint_burn, + pair_mapping.asset_info, + burn_amount, + )?; + if let Some(burn_msg) = burn_msg { + cosmos_msgs.push(burn_msg); + } + + Ok(Response::default() + .add_attributes(vec![ + ("action", "reduce_channel_balance_ibc_receive"), + ("channel_id", src_channel_id.as_str()), + ("ibc_denom", ibc_denom.as_str()), + ("amount", remote_amount.to_string().as_str()), + ("local_receiver", local_receiver.as_str()), + ]) + .add_messages(cosmos_msgs)) } pub fn update_config( @@ -427,10 +476,8 @@ pub fn execute_transfer_back_to_remote_chain( let config = CONFIG.load(deps.storage)?; // should be in form port/channel/denom - let mappings = get_mappings_from_asset_info( - deps.as_ref().storage, - amount.into_asset_info(deps.api)?, - )?; + let mappings = + get_mappings_from_asset_info(deps.as_ref().storage, amount.into_asset_info(deps.api)?)?; // parse denom & compare with user input. Should not use string.includes() because hacker can fake a port that has the same remote denom to return true let mapping = mappings @@ -540,6 +587,17 @@ pub fn execute_transfer_back_to_remote_chain( &msg.local_channel_id, timeout.into(), )?; + + // build burn msg if the mechanism is mint/burn + let burn_msg = build_burn_cw20_mapping_msg( + mapping.pair_mapping.is_mint_burn, + mapping.pair_mapping.asset_info, + fee_data.deducted_amount, + )?; + if let Some(burn_msg) = burn_msg { + cosmos_msgs.push(burn_msg); + } + Ok(Response::new() .add_messages(cosmos_msgs) .add_message(ibc_msg) @@ -550,47 +608,106 @@ pub fn execute_transfer_back_to_remote_chain( ])) } -/// The gov contract can allow new contracts, or increase the gas limit on existing contracts. -/// It cannot block or reduce the limit to avoid forcible sticking tokens in the channel. -pub fn execute_allow( - deps: DepsMut, - _env: Env, - info: MessageInfo, - allow: AllowMsg, -) -> Result { - ADMIN.assert_admin(deps.as_ref(), &info.sender)?; - - let contract = deps.api.addr_validate(&allow.contract)?; - let set = AllowInfo { - gas_limit: allow.gas_limit, - }; - ALLOW_LIST.update(deps.storage, &contract, |old| { - if let Some(old) = old { - // we must ensure it increases the limit - match (old.gas_limit, set.gas_limit) { - (None, Some(_)) => return Err(ContractError::CannotLowerGas), - (Some(old), Some(new)) if new < old => return Err(ContractError::CannotLowerGas), - _ => {} - }; +pub fn build_burn_cw20_mapping_msg( + is_mint_burn: bool, + burn_asset_info: AssetInfo, + amount_local: Uint128, +) -> Result, ContractError> { + // burn cw20 token if the mechanism is mint burn + if is_mint_burn { + match burn_asset_info { + AssetInfo::NativeToken { denom } => { + return Err(ContractError::Std(StdError::generic_err(format!( + "Mapping token must be cw20 token. Got {}", + denom + )))); + } + AssetInfo::Token { contract_addr } => { + return Ok(Some(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: contract_addr.to_string(), + msg: to_binary(&Cw20ExecuteMsg::Burn { + amount: amount_local, + })?, + funds: vec![], + }))); + } } - Ok(AllowInfo { - gas_limit: allow.gas_limit, - }) - })?; - - let gas = if let Some(gas) = allow.gas_limit { - gas.to_string() } else { - "None".to_string() - }; + Ok(None) + } +} - let res = Response::new() - .add_attribute("action", "allow") - .add_attribute("contract", allow.contract) - .add_attribute("gas_limit", gas); - Ok(res) +pub fn build_mint_cw20_mapping_msg( + is_mint_burn: bool, + mint_asset_info: AssetInfo, + amount_local: Uint128, + receiver: String, +) -> Result, ContractError> { + if is_mint_burn { + match mint_asset_info { + AssetInfo::NativeToken { denom } => { + return Err(ContractError::Std(StdError::generic_err(format!( + "Mapping token must be cw20 token. Got {}", + denom + )))); + } + AssetInfo::Token { contract_addr } => { + return Ok(Some(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: contract_addr.to_string(), + msg: to_binary(&Cw20ExecuteMsg::Mint { + recipient: receiver, + amount: amount_local, + })?, + funds: vec![], + }))); + } + } + } else { + Ok(None) + } } +// /// The gov contract can allow new contracts, or increase the gas limit on existing contracts. +// /// It cannot block or reduce the limit to avoid forcible sticking tokens in the channel. +// pub fn execute_allow( +// deps: DepsMut, +// _env: Env, +// info: MessageInfo, +// allow: AllowMsg, +// ) -> Result { +// ADMIN.assert_admin(deps.as_ref(), &info.sender)?; + +// let contract = deps.api.addr_validate(&allow.contract)?; +// let set = AllowInfo { +// gas_limit: allow.gas_limit, +// }; +// ALLOW_LIST.update(deps.storage, &contract, |old| { +// if let Some(old) = old { +// // we must ensure it increases the limit +// match (old.gas_limit, set.gas_limit) { +// (None, Some(_)) => return Err(ContractError::CannotLowerGas), +// (Some(old), Some(new)) if new < old => return Err(ContractError::CannotLowerGas), +// _ => {} +// }; +// } +// Ok(AllowInfo { +// gas_limit: allow.gas_limit, +// }) +// })?; + +// let gas = if let Some(gas) = allow.gas_limit { +// gas.to_string() +// } else { +// "None".to_string() +// }; + +// let res = Response::new() +// .add_attribute("action", "allow") +// .add_attribute("contract", allow.contract) +// .add_attribute("gas_limit", gas); +// Ok(res) +// } + /// The gov contract can allow new contracts, or increase the gas limit on existing contracts. /// It cannot block or reduce the limit to avoid forcible sticking tokens in the channel. pub fn execute_update_mapping_pair( @@ -619,6 +736,7 @@ pub fn execute_update_mapping_pair( asset_info: mapping_pair_msg.local_asset_info.clone(), remote_decimals: mapping_pair_msg.remote_decimals, asset_info_decimals: mapping_pair_msg.local_asset_info_decimals, + is_mint_burn: mapping_pair_msg.is_mint_burn.unwrap_or_default(), }, )?; diff --git a/contracts/cw-ics20-latest/src/ibc.rs b/contracts/cw-ics20-latest/src/ibc.rs index 2521e4c..b72efd0 100644 --- a/contracts/cw-ics20-latest/src/ibc.rs +++ b/contracts/cw-ics20-latest/src/ibc.rs @@ -9,6 +9,7 @@ use cosmwasm_std::{ Order, QuerierWrapper, Reply, Response, StdError, StdResult, Storage, SubMsg, SubMsgResult, Timestamp, Uint128, }; + use cw20_ics20_msg::converter::ConvertType; use cw20_ics20_msg::helper::{ denom_to_asset_info, get_prefix_decode_bech32, parse_asset_info_denom, @@ -18,6 +19,7 @@ use cw_storage_plus::Map; use oraiswap::asset::{Asset, AssetInfo}; use oraiswap::router::{RouterController, SwapOperation}; +use crate::contract::build_mint_cw20_mapping_msg; use crate::error::{ContractError, Never}; use crate::msg::{ExecuteMsg, FeeData, FollowUpMsgsData, PairQuery}; use crate::query_helper::get_destination_info_on_orai; @@ -131,6 +133,7 @@ pub fn reply(deps: DepsMut, _env: Env, reply: Reply) -> Result Result Ok(Response::new()), SubMsgResult::Err(err) => { let reply_args = SINGLE_STEP_REPLY_ARGS.load(deps.storage)?; + SINGLE_STEP_REPLY_ARGS.remove(deps.storage); // only time where we undo reduce chann balance because this message is sent and reduced optimistically on Oraichain. If fail then we undo and then refund undo_reduce_channel_balance( @@ -160,6 +164,7 @@ pub fn reply(deps: DepsMut, _env: Env, reply: Reply) -> Result token refunded on OraiBridge yet still refund on Oraichain @@ -447,6 +452,18 @@ fn handle_ibc_packet_receive_native_remote_chain( )?, ); + // increase channel balance submsg. We increase it first before doing other tasks + cosmos_msgs.push(CosmosMsg::Wasm(cosmwasm_std::WasmMsg::Execute { + contract_addr: env.contract.address.to_string(), + msg: to_binary(&ExecuteMsg::IncreaseChannelBalanceIbcReceive { + dest_channel_id: packet.dest.channel_id.clone(), + ibc_denom: ibc_denom.clone(), + amount: msg.amount, + local_receiver: msg.receiver.clone(), + })?, + funds: vec![], + })); + let mut fee_data = process_deduct_fee( storage, querier, @@ -524,6 +541,7 @@ fn handle_ibc_packet_receive_native_remote_chain( if fee_data.deducted_amount.is_zero() { return Ok(IbcReceiveResponse::new() .set_ack(ack_success()) + .add_messages(cosmos_msgs) .add_message(to_send.send_amount(config.token_fee_receiver.into_string(), None)) .add_attributes(attributes) .add_attributes(vec![ @@ -557,17 +575,6 @@ fn handle_ibc_packet_receive_native_remote_chain( destination_pair_mapping, )?; - // increase channel balance submsg. We increase it first before doing other tasks - cosmos_msgs.push(CosmosMsg::Wasm(cosmwasm_std::WasmMsg::Execute { - contract_addr: env.contract.address.to_string(), - msg: to_binary(&ExecuteMsg::IncreaseChannelBalanceIbcReceive { - dest_channel_id: packet.dest.channel_id.clone(), - ibc_denom, - amount: msg.amount, - local_receiver: msg.receiver.clone(), - })?, - funds: vec![], - })); let mut res = IbcReceiveResponse::new() .set_ack(ack_success()) .add_messages(cosmos_msgs) @@ -1183,7 +1190,7 @@ fn on_packet_failure( return Ok(IbcBasicResponse::new()); } - let sub_msg = handle_packet_refund(deps.storage, &msg.sender, &msg.denom, msg.amount)?; + let sub_msg = handle_packet_refund(deps.storage, &msg.sender, &msg.denom, msg.amount, true)?; // 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, msg.amount)?; @@ -1207,18 +1214,38 @@ pub fn handle_packet_refund( packet_sender: &str, packet_denom: &str, packet_amount: Uint128, + with_mint_burn: bool, ) -> Result { // get ibc denom mapping to get cw20 denom & from decimals in case of packet failure, we can refund the corresponding user & amount let pair_mapping = ics20_denoms().load(storage, &packet_denom)?; - let to_send = Amount::from_parts( - parse_asset_info_denom(pair_mapping.asset_info), - convert_remote_to_local( - packet_amount, - pair_mapping.remote_decimals, - pair_mapping.asset_info_decimals, - )?, - ); - let cosmos_msg = to_send.send_amount(packet_sender.to_string(), None); + + let local_amount = convert_remote_to_local( + packet_amount, + pair_mapping.remote_decimals, + pair_mapping.asset_info_decimals, + )?; + + // check if mint_burn mechanism, then mint token for packet sender, if not, send from contract + let send_amount_msg = Amount::from_parts( + parse_asset_info_denom(pair_mapping.asset_info.to_owned()), + local_amount, + ) + .send_amount(packet_sender.to_string(), None); + let cosmos_msg = match build_mint_cw20_mapping_msg( + pair_mapping.is_mint_burn, + pair_mapping.asset_info, + local_amount, + packet_sender.to_string(), + )? { + Some(cosmos_msg) => { + if with_mint_burn { + cosmos_msg + } else { + send_amount_msg + } + } + None => send_amount_msg, + }; // 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 diff --git a/contracts/cw-ics20-latest/src/msg.rs b/contracts/cw-ics20-latest/src/msg.rs index fb71030..8edb5af 100644 --- a/contracts/cw-ics20-latest/src/msg.rs +++ b/contracts/cw-ics20-latest/src/msg.rs @@ -50,7 +50,7 @@ pub enum ExecuteMsg { UpdateMappingPair(UpdatePairMsg), DeleteMappingPair(DeletePairMsg), /// This must be called by gov_contract, will allow a new cw20 token to be sent - Allow(AllowMsg), + // Allow(AllowMsg), /// Change the admin (must be called by current admin) UpdateConfig { admin: Option, @@ -98,6 +98,7 @@ pub struct UpdatePairMsg { pub local_asset_info: AssetInfo, pub remote_decimals: u8, pub local_asset_info_decimals: u8, + pub is_mint_burn: Option, } #[cw_serde] diff --git a/contracts/cw-ics20-latest/src/state.rs b/contracts/cw-ics20-latest/src/state.rs index f1589e7..8468812 100644 --- a/contracts/cw-ics20-latest/src/state.rs +++ b/contracts/cw-ics20-latest/src/state.rs @@ -134,6 +134,8 @@ pub struct MappingMetadata { pub asset_info: AssetInfo, pub remote_decimals: u8, pub asset_info_decimals: u8, + #[serde(default)] + pub is_mint_burn: bool, } #[cw_serde] diff --git a/contracts/cw-ics20-latest/src/testing/ibc_hooks_test.rs b/contracts/cw-ics20-latest/src/testing/ibc_hooks_test.rs index 8d18327..7053f74 100644 --- a/contracts/cw-ics20-latest/src/testing/ibc_hooks_test.rs +++ b/contracts/cw-ics20-latest/src/testing/ibc_hooks_test.rs @@ -102,6 +102,7 @@ fn test_ibc_hooks_receive() { local_asset_info: asset_info.clone(), remote_decimals: 18u8, local_asset_info_decimals: 18u8, + is_mint_burn: None, }; contract_instance diff --git a/contracts/cw-ics20-latest/src/testing/ibc_tests.rs b/contracts/cw-ics20-latest/src/testing/ibc_tests.rs index 025f713..4065ca7 100644 --- a/contracts/cw-ics20-latest/src/testing/ibc_tests.rs +++ b/contracts/cw-ics20-latest/src/testing/ibc_tests.rs @@ -35,10 +35,11 @@ use crate::state::{ TOKEN_FEE, }; use cw20::{Cw20Coin, Cw20ExecuteMsg, Cw20ReceiveMsg}; -use cw20_ics20_msg::amount::{convert_local_to_remote, Amount}; +use cw20_ics20_msg::amount::{convert_local_to_remote, convert_remote_to_local, Amount}; use crate::contract::{ - execute, handle_override_channel_balance, query, query_channel, query_channel_with_key, + build_burn_cw20_mapping_msg, build_mint_cw20_mapping_msg, execute, + handle_override_channel_balance, query, query_channel, query_channel_with_key, }; use crate::msg::{ AllowMsg, ChannelResponse, ConfigResponse, DeletePairMsg, ExecuteMsg, InitMsg, @@ -289,6 +290,7 @@ fn proper_checks_on_execute_native_transfer_back_to_remote() { local_asset_info: asset_info.clone(), remote_decimals: 18u8, local_asset_info_decimals: 18u8, + is_mint_burn: None, }; let _ = execute( @@ -430,6 +432,7 @@ fn proper_checks_on_execute_native_transfer_back_to_remote() { }, remote_decimals: 18u8, local_asset_info_decimals: 18u8, + is_mint_burn: None, }; execute( @@ -528,6 +531,7 @@ fn send_from_remote_to_local_receive_happy_path() { local_asset_info: asset_info.clone(), remote_decimals: 18u8, local_asset_info_decimals: 18u8, + is_mint_burn: None, }; contract_instance @@ -563,7 +567,7 @@ fn send_from_remote_to_local_receive_happy_path() { // TODO: fix test cases. Possibly because we are adding two add_submessages? assert_eq!(res.messages.len(), 3); // 3 messages because we also have deduct fee msg and increase channel balance msg - match res.messages[0].msg.clone() { + match res.messages[1].msg.clone() { CosmosMsg::Wasm(WasmMsg::Execute { contract_addr, msg, @@ -586,7 +590,7 @@ fn send_from_remote_to_local_receive_happy_path() { assert!(matches!(ack, Ics20Ack::Result(_))); // query channel state|_| - match res.messages[1].msg.clone() { + match res.messages[0].msg.clone() { CosmosMsg::Wasm(WasmMsg::Execute { contract_addr, msg, @@ -872,6 +876,7 @@ fn test_get_ibc_msg_evm_case() { local_asset_info: receiver_asset_info.clone(), remote_decimals, local_asset_info_decimals: asset_info_decimals, + is_mint_burn: None, }; // works with proper funds @@ -899,6 +904,7 @@ fn test_get_ibc_msg_evm_case() { asset_info: receiver_asset_info.clone(), remote_decimals, asset_info_decimals: asset_info_decimals.clone(), + is_mint_burn: false, }, }), destination_asset_info_on_orai, @@ -1031,6 +1037,7 @@ fn test_get_ibc_msg_cosmos_based_case() { local_asset_info: receiver_asset_info.clone(), remote_decimals, local_asset_info_decimals: asset_info_decimals, + is_mint_burn: None, }; let msg = ExecuteMsg::UpdateMappingPair(update.clone()); @@ -1063,6 +1070,7 @@ fn test_get_ibc_msg_cosmos_based_case() { asset_info: receiver_asset_info.clone(), remote_decimals, asset_info_decimals, + is_mint_burn: false, }, }), destination_asset_info_on_orai, @@ -1501,6 +1509,7 @@ fn test_process_ibc_msg() { }, remote_decimals: 18, asset_info_decimals: 6, + is_mint_burn: false, }, }; let local_channel_id = "channel"; @@ -1644,6 +1653,7 @@ fn test_query_pair_mapping_by_asset_info() { local_asset_info: asset_info.clone(), remote_decimals: 18, local_asset_info_decimals: 18, + is_mint_burn: None, }; // works with proper funds @@ -1727,6 +1737,7 @@ fn test_update_cw20_mapping() { local_asset_info: asset_info.clone(), remote_decimals: 18, local_asset_info_decimals: 18, + is_mint_burn: None, }; // works with proper funds @@ -1815,6 +1826,7 @@ fn test_delete_cw20_mapping() { local_asset_info: cw20_denom.clone(), remote_decimals: 18, local_asset_info_decimals: 18, + is_mint_burn: None, }; // works with proper funds @@ -2089,6 +2101,7 @@ fn proper_checks_on_execute_cw20_transfer_back_to_remote() { local_asset_info: asset_info.clone(), remote_decimals: 18u8, local_asset_info_decimals: 18u8, + is_mint_burn: None, }; let _ = execute( @@ -2214,6 +2227,7 @@ fn proper_checks_on_execute_cw20_transfer_back_to_remote() { }, remote_decimals: 18u8, local_asset_info_decimals: 18u8, + is_mint_burn: None, }; execute( @@ -2325,8 +2339,8 @@ fn test_handle_packet_refund() { }; let mapping_denom = format!("wasm.cosmos2contract/{}/{}", local_channel_id, native_denom); - let result = - handle_packet_refund(deps.as_mut().storage, sender, native_denom, amount).unwrap_err(); + let result = handle_packet_refund(deps.as_mut().storage, sender, native_denom, amount, false) + .unwrap_err(); assert_eq!( result.to_string(), "cw_ics20_latest::state::MappingMetadata not found" @@ -2335,12 +2349,13 @@ fn test_handle_packet_refund() { // update mapping pair so that we can get refunded // 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 { + let mut update = UpdatePairMsg { local_channel_id: local_channel_id.to_string(), denom: native_denom.to_string(), local_asset_info: local_asset_info.clone(), remote_decimals: 6, local_asset_info_decimals: 6, + is_mint_burn: None, }; let msg = ExecuteMsg::UpdateMappingPair(update.clone()); @@ -2350,7 +2365,7 @@ fn test_handle_packet_refund() { // now we handle packet failure. should get sub msg let result = - handle_packet_refund(deps.as_mut().storage, sender, &mapping_denom, amount).unwrap(); + handle_packet_refund(deps.as_mut().storage, sender, &mapping_denom, amount, false).unwrap(); assert_eq!( result, SubMsg::reply_on_error( @@ -2361,6 +2376,33 @@ fn test_handle_packet_refund() { REFUND_FAILURE_ID ) ); + + // case 2: refunds with mint msg + let local_asset_info = AssetInfo::Token { + contract_addr: Addr::unchecked("token0"), + }; + update.local_asset_info = local_asset_info; + update.is_mint_burn = Some(true); + let msg = ExecuteMsg::UpdateMappingPair(update.clone()); + let info = mock_info("gov", &coins(1234567, "ucosm")); + execute(deps.as_mut(), mock_env(), info, msg.clone()).unwrap(); + let result = + handle_packet_refund(deps.as_mut().storage, sender, &mapping_denom, amount, true).unwrap(); + assert_eq!( + result, + SubMsg::reply_on_error( + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: "token0".to_string(), + msg: to_binary(&Cw20ExecuteMsg::Mint { + recipient: sender.to_string(), + amount + }) + .unwrap(), + funds: vec![] + }), + REFUND_FAILURE_ID + ) + ); } #[test] @@ -2371,6 +2413,33 @@ fn test_increase_channel_balance_ibc_receive() { let local_receiver = "receiver"; let mut deps = setup(&[local_channel_id], &[]); + let local_asset_info = AssetInfo::NativeToken { + denom: "orai".to_string(), + }; + let ibc_denom_keys = format!( + "wasm.{}/{}/{}", + mock_env().contract.address.to_string(), + local_channel_id, + ibc_denom + ); + + // register mapping + let update = UpdatePairMsg { + local_channel_id: local_channel_id.to_string(), + denom: ibc_denom.to_string(), + local_asset_info: local_asset_info.clone(), + remote_decimals: 6, + local_asset_info_decimals: 6, + is_mint_burn: None, + }; + execute( + deps.as_mut(), + mock_env(), + mock_info("gov", &vec![]), + ExecuteMsg::UpdateMappingPair(update), + ) + .unwrap(); + assert_eq!( execute( deps.as_mut(), @@ -2378,7 +2447,7 @@ fn test_increase_channel_balance_ibc_receive() { mock_info("attacker", &vec![]), ExecuteMsg::IncreaseChannelBalanceIbcReceive { dest_channel_id: local_channel_id.to_string(), - ibc_denom: ibc_denom.to_string(), + ibc_denom: ibc_denom_keys.to_string(), amount: amount.clone(), local_receiver: local_receiver.to_string(), }, @@ -2393,21 +2462,21 @@ fn test_increase_channel_balance_ibc_receive() { mock_info(mock_env().contract.address.as_str(), &vec![]), ExecuteMsg::IncreaseChannelBalanceIbcReceive { dest_channel_id: local_channel_id.to_string(), - ibc_denom: ibc_denom.to_string(), + ibc_denom: ibc_denom_keys.to_string(), amount: amount.clone(), local_receiver: local_receiver.to_string(), }, ) .unwrap(); let channel_state = CHANNEL_REVERSE_STATE - .load(deps.as_ref().storage, (local_channel_id, ibc_denom)) + .load(deps.as_ref().storage, (local_channel_id, &ibc_denom_keys)) .unwrap(); assert_eq!(channel_state.outstanding, amount.clone()); assert_eq!(channel_state.total_sent, amount.clone()); let reply_args = REPLY_ARGS.load(deps.as_ref().storage).unwrap(); assert_eq!(reply_args.amount, amount.clone()); assert_eq!(reply_args.channel, local_channel_id); - assert_eq!(reply_args.denom, ibc_denom.to_string()); + assert_eq!(reply_args.denom, ibc_denom_keys.to_string()); assert_eq!(reply_args.local_receiver, local_receiver.to_string()); } @@ -2418,13 +2487,41 @@ fn test_reduce_channel_balance_ibc_receive() { let ibc_denom = "foobar"; let local_receiver = "receiver"; let mut deps = setup(&[local_channel_id], &[]); + let local_asset_info = AssetInfo::NativeToken { + denom: "orai".to_string(), + }; + + let ibc_denom_keys = format!( + "wasm.{}/{}/{}", + mock_env().contract.address.to_string(), + local_channel_id, + ibc_denom + ); + + // register mapping + let update = UpdatePairMsg { + local_channel_id: local_channel_id.to_string(), + denom: ibc_denom.to_string(), + local_asset_info: local_asset_info.clone(), + remote_decimals: 6, + local_asset_info_decimals: 6, + is_mint_burn: None, + }; + execute( + deps.as_mut(), + mock_env(), + mock_info("gov", &vec![]), + ExecuteMsg::UpdateMappingPair(update), + ) + .unwrap(); + execute( deps.as_mut(), mock_env(), mock_info(mock_env().contract.address.as_str(), &vec![]), ExecuteMsg::IncreaseChannelBalanceIbcReceive { dest_channel_id: local_channel_id.to_string(), - ibc_denom: ibc_denom.to_string(), + ibc_denom: ibc_denom_keys.to_string(), amount: amount.clone(), local_receiver: local_receiver.to_string(), }, @@ -2438,7 +2535,7 @@ fn test_reduce_channel_balance_ibc_receive() { mock_info("attacker", &vec![]), ExecuteMsg::ReduceChannelBalanceIbcReceive { src_channel_id: local_channel_id.to_string(), - ibc_denom: ibc_denom.to_string(), + ibc_denom: ibc_denom_keys.to_string(), amount: amount.clone(), local_receiver: local_receiver.to_string(), }, @@ -2453,21 +2550,21 @@ fn test_reduce_channel_balance_ibc_receive() { mock_info(mock_env().contract.address.as_str(), &vec![]), ExecuteMsg::ReduceChannelBalanceIbcReceive { src_channel_id: local_channel_id.to_string(), - ibc_denom: ibc_denom.to_string(), + ibc_denom: ibc_denom_keys.to_string(), amount: amount.clone(), local_receiver: local_receiver.to_string(), }, ) .unwrap(); let channel_state = CHANNEL_REVERSE_STATE - .load(deps.as_ref().storage, (local_channel_id, ibc_denom)) + .load(deps.as_ref().storage, (local_channel_id, &ibc_denom_keys)) .unwrap(); assert_eq!(channel_state.outstanding, Uint128::zero()); assert_eq!(channel_state.total_sent, Uint128::from(10u128)); let reply_args = REPLY_ARGS.load(deps.as_ref().storage).unwrap(); assert_eq!(reply_args.amount, amount.clone()); assert_eq!(reply_args.channel, local_channel_id); - assert_eq!(reply_args.denom, ibc_denom.to_string()); + assert_eq!(reply_args.denom, ibc_denom_keys); assert_eq!(reply_args.local_receiver, local_receiver.to_string()); } @@ -2563,6 +2660,7 @@ fn test_get_destination_info_on_orai() { local_asset_info: asset_info.clone(), remote_decimals: 18, local_asset_info_decimals: 18, + is_mint_burn: None, }; // works with proper funds @@ -2615,7 +2713,8 @@ fn test_get_destination_info_on_orai() { contract_addr: Addr::unchecked("cw20:foobar".to_string()) }, remote_decimals: 18, - asset_info_decimals: 18 + asset_info_decimals: 18, + is_mint_burn: false } }) ); @@ -2637,3 +2736,344 @@ fn test_get_destination_info_on_orai() { ); assert_eq!(destination_info.1, None); } + +#[test] +fn test_build_mint_cw20_mapping_msg() { + let mut deps = setup(&["channel-3", "channel-7"], &[]); + let ibc_denom = "cosmos"; + let local_channel_id = "channel-3"; + let asset_info = AssetInfo::Token { + contract_addr: Addr::unchecked("cw20:foobar".to_string()), + }; + + let amount_local = Uint128::from(10000u128); + let receiver = "receiver"; + + // case 1: on mappinglist, but mapping mechanism is not mint burn + let mut update = UpdatePairMsg { + local_channel_id: local_channel_id.to_string(), + denom: ibc_denom.to_string(), + local_asset_info: asset_info.clone(), + remote_decimals: 18, + local_asset_info_decimals: 18, + is_mint_burn: None, + }; + let msg = ExecuteMsg::UpdateMappingPair(update.clone()); + let info = mock_info("gov", &coins(1234567, "ucosm")); + execute(deps.as_mut(), mock_env(), info, msg.clone()).unwrap(); + let res = build_mint_cw20_mapping_msg( + false, + asset_info.clone(), + amount_local, + receiver.to_string(), + ); + assert_eq!(res, Ok(None)); + + // case 2: on mappinglist, is mint burn but asset info is native + let err = build_mint_cw20_mapping_msg( + true, + AssetInfo::NativeToken { + denom: "orai".to_string(), + } + .clone(), + amount_local, + receiver.to_string(), + ) + .unwrap_err(); + assert_eq!( + err, + ContractError::Std(StdError::generic_err(format!( + "Mapping token must be cw20 token. Got {}", + "orai" + ))) + ); + + // case 3: got mint msg + update.is_mint_burn = Some(true); + let msg = ExecuteMsg::UpdateMappingPair(update.clone()); + let info = mock_info("gov", &coins(1234567, "ucosm")); + execute(deps.as_mut(), mock_env(), info, msg.clone()).unwrap(); + let res = + build_mint_cw20_mapping_msg(true, asset_info, amount_local, receiver.to_string()).unwrap(); + assert_eq!( + res, + Some(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: "cw20:foobar".to_string(), + msg: to_binary(&Cw20ExecuteMsg::Mint { + recipient: receiver.to_string(), + amount: amount_local + }) + .unwrap(), + funds: vec![] + })), + ); +} + +#[test] +fn test_build_burn_cw20_mapping_msg() { + let mut deps = setup(&["channel-3", "channel-7"], &[]); + let ibc_denom = "cosmos"; + let local_channel_id = "channel-3"; + let asset_info = AssetInfo::Token { + contract_addr: Addr::unchecked("cw20:foobar".to_string()), + }; + + let amount_local = Uint128::from(10000u128); + + // case 1: on mappinglist, but mapping mechanism is not mint burn + let mut update = UpdatePairMsg { + local_channel_id: local_channel_id.to_string(), + denom: ibc_denom.to_string(), + local_asset_info: asset_info.clone(), + remote_decimals: 18, + local_asset_info_decimals: 18, + is_mint_burn: None, + }; + let msg = ExecuteMsg::UpdateMappingPair(update.clone()); + let info = mock_info("gov", &coins(1234567, "ucosm")); + execute(deps.as_mut(), mock_env(), info, msg.clone()).unwrap(); + let res = build_burn_cw20_mapping_msg(false, asset_info.clone(), amount_local); + assert_eq!(res, Ok(None)); + + // case 2: on mappinglist, is mint burn but asset info is native + let err = build_burn_cw20_mapping_msg( + true, + AssetInfo::NativeToken { + denom: "orai".to_string(), + } + .clone(), + amount_local, + ) + .unwrap_err(); + assert_eq!( + err, + ContractError::Std(StdError::generic_err(format!( + "Mapping token must be cw20 token. Got {}", + "orai" + ))) + ); + + // case 3: got mint msg + update.is_mint_burn = Some(true); + let msg = ExecuteMsg::UpdateMappingPair(update.clone()); + let info = mock_info("gov", &coins(1234567, "ucosm")); + execute(deps.as_mut(), mock_env(), info, msg.clone()).unwrap(); + let res = build_burn_cw20_mapping_msg(true, asset_info.clone(), amount_local).unwrap(); + assert_eq!( + res, + Some(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: "cw20:foobar".to_string(), + msg: to_binary(&Cw20ExecuteMsg::Burn { + amount: amount_local + }) + .unwrap(), + funds: vec![] + })), + ); +} + +#[test] +fn test_increase_channel_balance_ibc_receive_with_mint_burn() { + let local_channel_id = "channel-0"; + let amount = Uint128::from(1_000_000_000_000_000_000u128); + let ibc_denom = "foobar"; + let local_receiver = "receiver"; + let mut deps = setup(&[local_channel_id], &[]); + let cw20_addr = "cw20"; + + let local_asset_info = AssetInfo::Token { + contract_addr: Addr::unchecked(cw20_addr), + }; + + let ibc_denom_keys = format!( + "wasm.{}/{}/{}", + mock_env().contract.address.to_string(), + local_channel_id, + ibc_denom + ); + + // register mapping + let update = UpdatePairMsg { + local_channel_id: local_channel_id.to_string(), + denom: ibc_denom.to_string(), + local_asset_info: local_asset_info.clone(), + remote_decimals: 18, + local_asset_info_decimals: 6, + is_mint_burn: Some(true), + }; + execute( + deps.as_mut(), + mock_env(), + mock_info("gov", &vec![]), + ExecuteMsg::UpdateMappingPair(update), + ) + .unwrap(); + + assert_eq!( + execute( + deps.as_mut(), + mock_env(), + mock_info("attacker", &vec![]), + ExecuteMsg::IncreaseChannelBalanceIbcReceive { + dest_channel_id: local_channel_id.to_string(), + ibc_denom: ibc_denom_keys.to_string(), + amount: amount.clone(), + local_receiver: local_receiver.to_string(), + }, + ) + .unwrap_err(), + ContractError::Std(StdError::generic_err("Caller is not the contract itself!")) + ); + + let res = execute( + deps.as_mut(), + mock_env(), + mock_info(mock_env().contract.address.as_str(), &vec![]), + ExecuteMsg::IncreaseChannelBalanceIbcReceive { + dest_channel_id: local_channel_id.to_string(), + ibc_denom: ibc_denom_keys.to_string(), + amount: amount.clone(), + local_receiver: local_receiver.to_string(), + }, + ) + .unwrap(); + + match res.messages[0].msg.clone() { + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr, + msg, + funds: _, + }) => { + assert_eq!(contract_addr, cw20_addr); + assert_eq!( + msg, + to_binary(&Cw20ExecuteMsg::Mint { + recipient: mock_env().contract.address.to_string(), + amount: convert_remote_to_local(amount, 18, 6).unwrap() + }) + .unwrap() + ) + } + _ => panic!("Unexpected return message: {:?}", res.messages[0]), + } + + let channel_state = CHANNEL_REVERSE_STATE + .load(deps.as_ref().storage, (local_channel_id, &ibc_denom_keys)) + .unwrap(); + assert_eq!(channel_state.outstanding, amount.clone()); + assert_eq!(channel_state.total_sent, amount.clone()); + let reply_args = REPLY_ARGS.load(deps.as_ref().storage).unwrap(); + assert_eq!(reply_args.amount, amount.clone()); + assert_eq!(reply_args.channel, local_channel_id); + assert_eq!(reply_args.denom, ibc_denom_keys.to_string()); + assert_eq!(reply_args.local_receiver, local_receiver.to_string()); +} + +#[test] +fn test_reduce_channel_balance_ibc_receive_with_mint_burn() { + let local_channel_id = "channel-0"; + let amount = Uint128::from(1_000_000_000_000_000_000u128); + let ibc_denom = "foobar"; + let local_receiver = "receiver"; + let mut deps = setup(&[local_channel_id], &[]); + let cw20_addr = "cw20"; + + let local_asset_info = AssetInfo::Token { + contract_addr: Addr::unchecked(cw20_addr), + }; + + let ibc_denom_keys = format!( + "wasm.{}/{}/{}", + mock_env().contract.address.to_string(), + local_channel_id, + ibc_denom + ); + + // register mapping + let update = UpdatePairMsg { + local_channel_id: local_channel_id.to_string(), + denom: ibc_denom.to_string(), + local_asset_info: local_asset_info.clone(), + remote_decimals: 18, + local_asset_info_decimals: 6, + is_mint_burn: Some(true), + }; + execute( + deps.as_mut(), + mock_env(), + mock_info("gov", &vec![]), + ExecuteMsg::UpdateMappingPair(update), + ) + .unwrap(); + + execute( + deps.as_mut(), + mock_env(), + mock_info(mock_env().contract.address.as_str(), &vec![]), + ExecuteMsg::IncreaseChannelBalanceIbcReceive { + dest_channel_id: local_channel_id.to_string(), + ibc_denom: ibc_denom_keys.to_string(), + amount: amount.clone(), + local_receiver: local_receiver.to_string(), + }, + ) + .unwrap(); + + assert_eq!( + execute( + deps.as_mut(), + mock_env(), + mock_info("attacker", &vec![]), + ExecuteMsg::ReduceChannelBalanceIbcReceive { + src_channel_id: local_channel_id.to_string(), + ibc_denom: ibc_denom_keys.to_string(), + amount: amount.clone(), + local_receiver: local_receiver.to_string(), + }, + ) + .unwrap_err(), + ContractError::Std(StdError::generic_err("Caller is not the contract itself!")) + ); + + let res = execute( + deps.as_mut(), + mock_env(), + mock_info(mock_env().contract.address.as_str(), &vec![]), + ExecuteMsg::ReduceChannelBalanceIbcReceive { + src_channel_id: local_channel_id.to_string(), + ibc_denom: ibc_denom_keys.to_string(), + amount: amount.clone(), + local_receiver: local_receiver.to_string(), + }, + ) + .unwrap(); + + match res.messages[0].msg.clone() { + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr, + msg, + funds: _, + }) => { + assert_eq!(contract_addr, cw20_addr); + assert_eq!( + msg, + to_binary(&Cw20ExecuteMsg::Burn { + amount: convert_remote_to_local(amount, 18, 6).unwrap() + }) + .unwrap() + ) + } + _ => panic!("Unexpected return message: {:?}", res.messages[0]), + } + + let channel_state = CHANNEL_REVERSE_STATE + .load(deps.as_ref().storage, (local_channel_id, &ibc_denom_keys)) + .unwrap(); + assert_eq!(channel_state.outstanding, Uint128::zero()); + assert_eq!(channel_state.total_sent, Uint128::from(amount)); + let reply_args = REPLY_ARGS.load(deps.as_ref().storage).unwrap(); + assert_eq!(reply_args.amount, amount.clone()); + assert_eq!(reply_args.channel, local_channel_id); + assert_eq!(reply_args.denom, ibc_denom_keys); + assert_eq!(reply_args.local_receiver, local_receiver.to_string()); +} diff --git a/simulate-tests/bridge-contract.spec.ts b/simulate-tests/bridge-contract.spec.ts index 5916f9e..7b37b8d 100644 --- a/simulate-tests/bridge-contract.spec.ts +++ b/simulate-tests/bridge-contract.spec.ts @@ -17,7 +17,8 @@ import { OraiswapPairClient, OraiswapOracleClient, } from "@oraichain/oraidex-contracts-sdk"; -import { CwIcs20LatestClient } from "@oraichain/common-contracts-sdk"; +// import { CwIcs20LatestClient } from "@oraichain/common-contracts-sdk"; +import { CwIcs20LatestClient } from "./contracts-sdk/CwIcs20Latest.client"; import * as oraidexArtifacts from "@oraichain/oraidex-contracts-build"; import { FungibleTokenPacketData } from "cosmjs-types/ibc/applications/transfer/v2/packet"; import { @@ -1625,7 +1626,6 @@ describe.only("IBCModule", () => { }, relayer: relayerAddress, }); - console.dir(result, { depth: null }); const transferEvent = result.events.find( (event) => @@ -1781,7 +1781,7 @@ describe.only("IBCModule", () => { }, relayer: relayerAddress, }); - console.dir(result, { depth: null }); + const hasRelayerFee = result.events.find( (event) => event.type === "wasm" && @@ -1874,7 +1874,7 @@ describe.only("IBCModule", () => { (attr) => attr.key === "amount" && attr.value === expectedTotalFee ) ); - console.dir(result, { depth: null }); + expect(hasFees).not.toBeUndefined(); expect( result.attributes.find( diff --git a/simulate-tests/bridge-with-mint-burn.spec.ts b/simulate-tests/bridge-with-mint-burn.spec.ts new file mode 100644 index 0000000..dc29c50 --- /dev/null +++ b/simulate-tests/bridge-with-mint-burn.spec.ts @@ -0,0 +1,1804 @@ +import { Event, toBinary } from "@cosmjs/cosmwasm-stargate"; +import { Coin, coins, coin } from "@cosmjs/proto-signing"; +import { + CWSimulateApp, + GenericError, + IbcOrder, + IbcPacket, + SimulateCosmWasmClient, +} from "@oraichain/cw-simulate"; +import { Ok } from "ts-results"; +import bech32 from "bech32"; +import { readFileSync } from "fs"; +import { + OraiswapFactoryClient, + OraiswapRouterClient, + OraiswapTokenClient, + OraiswapPairClient, + OraiswapOracleClient, +} from "@oraichain/oraidex-contracts-sdk"; +// import { CwIcs20LatestClient } from "@oraichain/common-contracts-sdk"; +import { CwIcs20LatestClient } from "./contracts-sdk/CwIcs20Latest.client"; +import * as oraidexArtifacts from "@oraichain/oraidex-contracts-build"; +import { FungibleTokenPacketData } from "cosmjs-types/ibc/applications/transfer/v2/packet"; +import { + deployIcs20Token, + deployToken, + senderAddress as oraiSenderAddress, + senderAddress, +} from "./common"; +import { oraib2oraichain, toAmount } from "@oraichain/oraidex-common"; +import { ORAI } from "@oraichain/oraidex-common"; +import { + AssetInfo, + TransferBackMsg, +} from "@oraichain/common-contracts-sdk/build/CwIcs20Latest.types"; +import { toDisplay } from "@oraichain/oraidex-common"; +import { parseToIbcWasmMemo } from "./proto-gen"; + +let cosmosChain: CWSimulateApp; +// oraichain support cosmwasm +let oraiClient: SimulateCosmWasmClient; + +const bobAddress = "orai1ur2vsjrjarygawpdwtqteaazfchvw4fg6uql76"; +const bobAddressEth = "0x8754032Ac7966A909e2E753308dF56bb08DabD69"; +const bridgeReceiver = "tron-testnet0x3C5C6b570C1DA469E8B24A2E8Ed33c278bDA3222"; +const routerContractAddress = "placeholder"; // we will update the contract config later when we need to deploy the actual router contract +const converterContractAddress = "converter"; // we will update the contract config later when we need to deploy the actual converter contract +const cosmosSenderAddress = bech32.encode( + "cosmos", + bech32.decode(oraiSenderAddress).words +); +const relayerAddress = "orai1704r4dhuwdqvt7vs35m0360py6ep6cwwxeyfxn"; +const oraibridgeSenderAddress = bech32.encode( + "oraib", + bech32.decode(oraiSenderAddress).words +); +console.log({ cosmosSenderAddress }); +const ibcTransferAmount = "100000000"; +const initialBalanceAmount = "10000000000000"; + +describe.only("IBCModuleWithMintBurn", () => { + let oraiPort: string; + let oraiIbcDenom: string = + "tron-testnet0xA325Ad6D9c92B55A3Fc5aD7e412B1518F96441C0"; + let airiIbcDenom: string = + "tron-testnet0x7e2A35C746F2f7C240B664F1Da4DD100141AE71F"; + let usdtIbcDenom: string = + "tron-testnet0xdac17f958d2ee523a2206206994597c13d831ec7"; + let AtomDenom = + "ibc/A2E2EEC9057A4A1C2C0A6A4C78B0239118DF5F278830F50B4A6BDD7A66506B78"; + let atomChannel = "channel-15"; + let cosmosPort: string = "transfer"; + let channel = "channel-0"; + let ics20Contract: CwIcs20LatestClient; + let airiToken: OraiswapTokenClient; + let packetData = { + src: { + port_id: cosmosPort, + channel_id: channel, + }, + dest: { + port_id: oraiPort, + channel_id: channel, + }, + sequence: 27, + timeout: { + block: { + revision: 1, + height: 12345678, + }, + }, + }; + beforeEach(async () => { + // reset state for every test + cosmosChain = new CWSimulateApp({ + chainId: "cosmoshub-4", + bech32Prefix: "cosmos", + }); + + oraiClient = new SimulateCosmWasmClient({ + chainId: "Oraichain", + bech32Prefix: ORAI, + metering: process.env.METERING === "true", + }); + + ics20Contract = await deployIcs20Token(oraiClient, { + swap_router_contract: routerContractAddress, + converter_contract: converterContractAddress, + }); + oraiPort = "wasm." + ics20Contract.contractAddress; + packetData.dest.port_id = oraiPort; + + // init cw20 AIRI token + airiToken = await deployToken(oraiClient, { + decimals: 6, + symbol: "AIRI", + name: "Airight token", + initial_balances: [], + minter: ics20Contract.contractAddress, + }); + + // init ibc channel between two chains + oraiClient.app.ibc.relay( + channel, + oraiPort, + channel, + cosmosPort, + cosmosChain + ); + await cosmosChain.ibc.sendChannelOpen({ + open_init: { + channel: { + counterparty_endpoint: { + port_id: oraiPort, + channel_id: channel, + }, + endpoint: { + port_id: cosmosPort, + channel_id: channel, + }, + order: IbcOrder.Unordered, + version: "ics20-1", + connection_id: "connection-0", + }, + }, + }); + + await cosmosChain.ibc.sendChannelConnect({ + open_ack: { + channel: { + counterparty_endpoint: { + port_id: oraiPort, + channel_id: channel, + }, + endpoint: { + port_id: cosmosPort, + channel_id: channel, + }, + order: IbcOrder.Unordered, + version: "ics20-1", + connection_id: "connection-0", + }, + counterparty_version: "ics20-1", + }, + }); + + cosmosChain.ibc.addMiddleWare((msg, app) => { + const data = msg.data.packet as IbcPacket; + if (Number(data.timeout.timestamp) < cosmosChain.time) { + throw new GenericError("timeout at " + data.timeout.timestamp); + } + }); + // topup + oraiClient.app.bank.setBalance( + ics20Contract.contractAddress, + coins(initialBalanceAmount, ORAI) + ); + + await ics20Contract.updateMappingPair({ + localAssetInfo: { + token: { + contract_addr: airiToken.contractAddress, + }, + }, + localAssetInfoDecimals: 6, + denom: airiIbcDenom, + remoteDecimals: 6, + localChannelId: channel, + isMintBurn: true, + }); + }); + + it("mint-burn-demo-getting-channel-state-ibc-wasm-should-increase-balances-and-total-sent", async () => { + // fixture. Setup everything from the ics 20 contract to ibc relayer + const oraiClient = new SimulateCosmWasmClient({ + chainId: "Oraichain", + bech32Prefix: ORAI, + metering: process.env.METERING === "true", + }); + + const ics20Contract = await deployIcs20Token(oraiClient, { + swap_router_contract: routerContractAddress, + converter_contract: converterContractAddress, + }); + const oraiPort = "wasm." + ics20Contract.contractAddress; + let newPacketData = { + src: { + port_id: cosmosPort, + channel_id: channel, + }, + dest: { + port_id: oraiPort, + channel_id: channel, + }, + sequence: 27, + timeout: { + block: { + revision: 1, + height: 12345678, + }, + }, + }; + newPacketData.dest.port_id = oraiPort; + + // init cw20 AIRI token + const airiToken = await deployToken(oraiClient, { + decimals: 6, + symbol: "AIRI", + name: "Airight token", + initial_balances: [ + { + address: ics20Contract.contractAddress, + amount: initialBalanceAmount, + }, + ], + }); + + // init ibc channel between two chains + oraiClient.app.ibc.relay( + channel, + oraiPort, + channel, + cosmosPort, + cosmosChain + ); + await cosmosChain.ibc.sendChannelOpen({ + open_init: { + channel: { + counterparty_endpoint: { + port_id: oraiPort, + channel_id: channel, + }, + endpoint: { + port_id: cosmosPort, + channel_id: channel, + }, + order: IbcOrder.Unordered, + version: "ics20-1", + connection_id: "connection-0", + }, + }, + }); + + await cosmosChain.ibc.sendChannelConnect({ + open_ack: { + channel: { + counterparty_endpoint: { + port_id: oraiPort, + channel_id: channel, + }, + endpoint: { + port_id: cosmosPort, + channel_id: channel, + }, + order: IbcOrder.Unordered, + version: "ics20-1", + connection_id: "connection-0", + }, + counterparty_version: "ics20-1", + }, + }); + + cosmosChain.ibc.addMiddleWare((msg, app) => { + const data = msg.data.packet as IbcPacket; + if (Number(data.timeout.timestamp) < cosmosChain.time) { + throw new GenericError("timeout at " + data.timeout.timestamp); + } + }); + // topup + await ics20Contract.updateMappingPair({ + localAssetInfo: { + token: { + contract_addr: airiToken.contractAddress, + }, + }, + localAssetInfoDecimals: 6, + denom: airiIbcDenom, + remoteDecimals: 6, + localChannelId: channel, + }); + + const icsPackage: FungibleTokenPacketData = { + amount: ibcTransferAmount, + denom: airiIbcDenom, + receiver: bobAddress, + sender: cosmosSenderAddress, + memo: "", + }; + // transfer from cosmos to oraichain, should pass. This should increase the balances & total sent + await cosmosChain.ibc.sendPacketReceive({ + packet: { + data: toBinary(icsPackage), + ...newPacketData, + }, + relayer: relayerAddress, + }); + + const { channels } = await ics20Contract.listChannels(); + for (let channel of channels) { + const { balances } = await ics20Contract.channel({ id: channel.id }); + console.log(balances); + for (let balance of balances) { + if ("native" in balance) { + const pairMapping = await ics20Contract.pairMapping({ + key: balance.native.denom, + }); + const { balance: channelBalance } = + await ics20Contract.channelWithKey({ + channelId: channel.id, + denom: balance.native.denom, + }); + if ("native" in channelBalance) { + const trueBalance = toDisplay( + channelBalance.native.amount, + pairMapping.pair_mapping.remote_decimals + ); + expect(trueBalance).toEqual( + parseInt(ibcTransferAmount) / + 10 ** pairMapping.pair_mapping.remote_decimals + ); + } + } else { + // do nothing because currently we dont have any cw20 balance in the channel + } + } + } + }); + + // TODO: test with native_token + it.each([ + [ + false, + { + native_token: { + denom: ORAI, + }, + }, + ibcTransferAmount, + oraiIbcDenom, + coins(ibcTransferAmount, ORAI), + "cw-ics20-success-should-increase-native-balance-remote-to-local", + ], + [ + false, + null, + ibcTransferAmount, + oraiIbcDenom, + [], + "cw-ics20-fail-no-pair-mapping-should-not-send-balance-remote-to-local", + ], + [ + false, + { + native_token: { + denom: ORAI, + }, + }, + "10000000000001", + oraiIbcDenom, + [], + "cw-ics20-fail-transfer-native-fail-insufficient-funds-should-not-send-balance-remote-to-local", + ], + [ + true, + { + token: { + contract_addr: "orai18cvw806fj5n7xxz06ak8vjunveeks4zzzn37cu", // has to hard-code address airi due to jest issue: https://github.com/facebook/jest/issues/6888 + }, + }, + ibcTransferAmount, + airiIbcDenom, + [{ amount: ibcTransferAmount, denom: "" }], + "cw-ics20-success-transfer-cw20-should-increase-cw20-balance-remote-to-local", + ], + ])( + "mint-burn-bridge-test-cw-ics20-transfer-remote-to-local-given %j %s %s should return expected amount %j", //reference: https://jestjs.io/docs/api#1-testeachtablename-fn-timeout + async ( + isMintBurn: boolean, + assetInfo: AssetInfo, + transferAmount: string, + transferDenom: string, + expectedBalance: Coin[], + _name: string + ) => { + // create mapping + if (assetInfo) { + const pair = { + localAssetInfo: assetInfo, + localAssetInfoDecimals: 6, + denom: transferDenom, + remoteDecimals: 6, + localChannelId: channel, + isMintBurn, + }; + await ics20Contract.updateMappingPair(pair); + } + const icsPackage: FungibleTokenPacketData = { + amount: transferAmount, + denom: transferDenom, + receiver: bobAddress, + sender: cosmosSenderAddress, + memo: "", + }; + await cosmosChain.ibc.sendPacketReceive({ + packet: { + data: toBinary(icsPackage), + ...packetData, + }, + relayer: relayerAddress, + }); + + if (assetInfo && (assetInfo as any).token) { + const bobBalance = await airiToken.balance({ address: bobAddress }); + console.log("bob balance contract address: ", bobBalance); + expect(bobBalance.balance).toEqual(expectedBalance[0].amount); + return; + } + const bobBalance = oraiClient.app.bank.getBalance(icsPackage.receiver); + expect(bobBalance).toMatchObject(expectedBalance); + } + ); + + it("mint-burn-cw-ics20-success-cw20-should-transfer-balance-to-ibc-wasm-contract-local-to-remote", async () => { + let ibcWasmAiriBalance = await airiToken.balance({ + address: ics20Contract.contractAddress, + }); + expect(ibcWasmAiriBalance.balance).toEqual("0"); + // now send ibc package + const icsPackage: FungibleTokenPacketData = { + amount: ibcTransferAmount, + denom: airiIbcDenom, + receiver: bobAddress, + sender: cosmosSenderAddress, + memo: "", + }; + // transfer from cosmos to oraichain, should pass + await cosmosChain.ibc.sendPacketReceive({ + packet: { + data: toBinary(icsPackage), + ...packetData, + }, + relayer: relayerAddress, + }); + + const transferBackMsg: TransferBackMsg = { + local_channel_id: channel, + remote_address: cosmosSenderAddress, + remote_denom: airiIbcDenom, + }; + airiToken.sender = bobAddress; + await airiToken.send({ + amount: ibcTransferAmount, + contract: ics20Contract.contractAddress, + msg: Buffer.from(JSON.stringify(transferBackMsg)).toString("base64"), + }); + ibcWasmAiriBalance = await airiToken.balance({ + address: ics20Contract.contractAddress, + }); + expect(ibcWasmAiriBalance.balance).toEqual("0"); + }); + + it.each([ + [ + parseToIbcWasmMemo("", "", ""), + ibcTransferAmount, + "empty-memo-should-fallback-to-transfer-to-receiver", + ], + [ + parseToIbcWasmMemo(bobAddress, "", ""), + ibcTransferAmount, + "only-receiver-memo-should-fallback-to-transfer-to-receiver", + ], + [ + parseToIbcWasmMemo(bobAddress, oraib2oraichain, ""), + ibcTransferAmount, + "receiver-and-channel-memo-should-fallback-to-transfer-to-receiver", + ], + ])( + "mint-burn-cw-ics20-test-single-step-invalid-dest-denom-memo-remote-to-local-given %s should-get-expected-amount %s", + async (memo: string, expectedAmount: string, _name: string) => { + // now send ibc package + const icsPackage: FungibleTokenPacketData = { + amount: ibcTransferAmount, + denom: airiIbcDenom, + receiver: bobAddress, + sender: cosmosSenderAddress, + memo, + }; + // transfer from cosmos to oraichain, should pass + await cosmosChain.ibc.sendPacketReceive({ + packet: { + data: toBinary(icsPackage), + ...packetData, + }, + relayer: relayerAddress, + }); + const ibcWasmAiriBalance = await airiToken.balance({ + address: bobAddress, + }); + expect(ibcWasmAiriBalance.balance).toEqual(expectedAmount); + } + ); + + describe("mint-burn-cw-ics20-test-single-step-swap-to-tokens", () => { + let factoryContract: OraiswapFactoryClient; + let routerContract: OraiswapRouterClient; + let usdtToken: OraiswapTokenClient; + let oracleContract: OraiswapOracleClient; + let assetInfos: AssetInfo[]; + let lpId: number; + let icsPackage: FungibleTokenPacketData = { + amount: ibcTransferAmount, + denom: airiIbcDenom, + receiver: bobAddress, + sender: cosmosSenderAddress, + memo: "", + }; + const findWasmEvent = (events: Event[], key: string, value: string) => + events.find( + (event) => + event.type === "wasm" && + event.attributes.find( + (attr) => attr.key === key && attr.value === value + ) + ); + beforeEach(async () => { + assetInfos = [ + { native_token: { denom: ORAI } }, + { token: { contract_addr: airiToken.contractAddress } }, + ]; + // upload pair & lp token code id + const { codeId: pairCodeId } = await oraiClient.upload( + oraiSenderAddress, + readFileSync(oraidexArtifacts.getContractDir("oraiswap_pair")), + "auto" + ); + const { codeId: lpCodeId } = await oraiClient.upload( + oraiSenderAddress, + readFileSync(oraidexArtifacts.getContractDir("oraiswap_token")), + "auto" + ); + lpId = lpCodeId; + // deploy another cw20 for oraiswap testing + const { contractAddress: usdtAddress } = await oraiClient.instantiate( + oraiSenderAddress, + lpCodeId, + { + decimals: 6, + symbol: "USDT", + name: "USDT token", + initial_balances: [], + mint: { + minter: ics20Contract.contractAddress, + }, + }, + "cw20-usdt" + ); + usdtToken = new OraiswapTokenClient( + oraiClient, + oraiSenderAddress, + usdtAddress + ); + // deploy oracle addr + const { contractAddress: oracleAddress } = + await oraidexArtifacts.deployContract( + oraiClient, + oraiSenderAddress, + {}, + "oraiswap-oracle", + "oraiswap_oracle" + ); + // deploy factory contract + oracleContract = new OraiswapOracleClient( + oraiClient, + oraiSenderAddress, + oracleAddress + ); + + await oracleContract.updateTaxRate({ rate: "0" }); + await oracleContract.updateTaxCap({ denom: AtomDenom, cap: "100000" }); + const { contractAddress: factoryAddress } = + await oraidexArtifacts.deployContract( + oraiClient, + oraiSenderAddress, + { + commission_rate: "0", + oracle_addr: oracleAddress, + pair_code_id: pairCodeId, + token_code_id: lpCodeId, + }, + "oraiswap-factory", + "oraiswap_factory" + ); + + const { contractAddress: routerAddress } = + await oraidexArtifacts.deployContract( + oraiClient, + oraiSenderAddress, + { + factory_addr: factoryAddress, + factory_addr_v2: factoryAddress, + }, + "oraiswap-router", + "oraiswap_router" + ); + factoryContract = new OraiswapFactoryClient( + oraiClient, + oraiSenderAddress, + factoryAddress + ); + routerContract = new OraiswapRouterClient( + oraiClient, + oraiSenderAddress, + routerAddress + ); + + // set correct router contract to prepare for the tests + await ics20Contract.updateConfig({ swapRouterContract: routerAddress }); + // create mapping + await ics20Contract.updateMappingPair({ + localAssetInfo: { + token: { + contract_addr: airiToken.contractAddress, + }, + }, + localAssetInfoDecimals: 6, + denom: airiIbcDenom, + remoteDecimals: 6, + localChannelId: channel, + isMintBurn: true, + }); + await ics20Contract.updateMappingPair({ + localAssetInfo: { + token: { + contract_addr: usdtToken.contractAddress, + }, + }, + localAssetInfoDecimals: 6, + denom: usdtIbcDenom, + remoteDecimals: 6, + localChannelId: channel, + isMintBurn: true, + }); + await factoryContract.createPair({ + assetInfos, + }); + await factoryContract.createPair({ + assetInfos: [ + assetInfos[0], + { token: { contract_addr: usdtToken.contractAddress } }, + ], + }); + await factoryContract.createPair({ + assetInfos: [ + assetInfos[0], + { + native_token: { + denom: AtomDenom, + }, + }, + ], + }); + + const firstPairInfo = await factoryContract.pair({ + assetInfos, + }); + const secondPairInfo = await factoryContract.pair({ + assetInfos: [ + assetInfos[0], + { token: { contract_addr: usdtToken.contractAddress } }, + ], + }); + const thirdPairInfo = await factoryContract.pair({ + assetInfos: [ + assetInfos[0], + { + native_token: { + denom: AtomDenom, + }, + }, + ], + }); + + // mint lots of orai, airi for the pair contracts to mock provide lp + // here, ratio is 1:1 => 1 AIRI = 1 ORAI + oraiClient.app.bank.setBalance( + firstPairInfo.contract_addr, + coins(initialBalanceAmount, ORAI) + ); + + airiToken.sender = ics20Contract.contractAddress; + await airiToken.mint({ + amount: initialBalanceAmount, + recipient: firstPairInfo.contract_addr, + }); + oraiClient.app.bank.setBalance( + secondPairInfo.contract_addr, + coins(initialBalanceAmount, ORAI) + ); + + usdtToken.sender = ics20Contract.contractAddress; + await usdtToken.mint({ + amount: initialBalanceAmount, + recipient: secondPairInfo.contract_addr, + }); + oraiClient.app.bank.setBalance(thirdPairInfo.contract_addr, [ + coin(initialBalanceAmount, ORAI), + coin(initialBalanceAmount, AtomDenom), + ]); + }); + + it("mint-burn-test-simulate-withdraw-liquidity", async () => { + // deploy another cw20 for oraiswap testing + let scatomToken: OraiswapTokenClient; + const atomIbc = + "ibc/A2E2EEC9057A4A1C2C0A6A4C78B0239118DF5F278830F50B4A6BDD7A66506B78"; + const { contractAddress: scatomAddress } = await oraiClient.instantiate( + oraiSenderAddress, + lpId, + { + decimals: 6, + symbol: "scATOM", + name: "scATOM token", + initial_balances: [ + { address: oraiSenderAddress, amount: initialBalanceAmount }, + ], + mint: { + minter: oraiSenderAddress, + }, + }, + "cw20-scatom" + ); + scatomToken = new OraiswapTokenClient( + oraiClient, + oraiSenderAddress, + scatomAddress + ); + const assetInfos = [ + { native_token: { denom: atomIbc } }, + { token: { contract_addr: scatomAddress } }, + ]; + await factoryContract.createPair({ + assetInfos, + }); + const firstPairInfo = await factoryContract.pair({ + assetInfos, + }); + const pairAddress = firstPairInfo.contract_addr; + await scatomToken.increaseAllowance({ + amount: initialBalanceAmount, + spender: pairAddress, + }); + oraiClient.app.bank.setBalance( + pairAddress, + coins(initialBalanceAmount, atomIbc) + ); + oraiClient.app.bank.setBalance( + oraiSenderAddress, + coins(initialBalanceAmount, atomIbc) + ); + + const pairContract = new OraiswapPairClient( + oraiClient, + oraiSenderAddress, + pairAddress + ); + await pairContract.provideLiquidity( + { + assets: [ + { + amount: "10000000", + info: { token: { contract_addr: scatomAddress } }, + }, + { amount: "10000000", info: { native_token: { denom: atomIbc } } }, + ], + }, + "auto", + undefined, + [{ denom: atomIbc, amount: "10000000" }] + ); + // query liquidity balance + const lpToken = new OraiswapTokenClient( + oraiClient, + oraiSenderAddress, + firstPairInfo.liquidity_token + ); + const result = await lpToken.balance({ address: oraiSenderAddress }); + + // set tax rate + await oracleContract.updateTaxRate({ rate: "0.003" }); + await oracleContract.updateTaxCap({ denom: atomIbc, cap: "1000000" }); + + // now we withdraw lp + await lpToken.send({ + amount: "1000", + contract: pairAddress, + msg: Buffer.from(JSON.stringify({ withdraw_liquidity: {} })).toString( + "base64" + ), + }); + }); + + it("mint-burn-cw-ics20-test-simulate-swap-ops-mock-pair-contract", async () => { + const simulateResult = await routerContract.simulateSwapOperations({ + offerAmount: "1", + operations: [ + { + orai_swap: { + offer_asset_info: assetInfos[1], + ask_asset_info: assetInfos[0], + }, + }, + { + orai_swap: { + offer_asset_info: assetInfos[0], + ask_asset_info: { + token: { contract_addr: usdtToken.contractAddress }, + }, + }, + }, + ], + }); + expect(simulateResult.amount).toEqual("1"); + }); + + it.each<[string, string, string]>([ + [ + parseToIbcWasmMemo(bobAddress, "", "orai"), + bobAddress, + "Generic error: Destination channel empty in build ibc msg", + ], + [ + parseToIbcWasmMemo( + "not-evm-based-nor-cosmos-based", + channel, + oraiIbcDenom + ), + bobAddress, + "Generic error: The destination info is neither evm or cosmos based", + ], + ])( + "mint-burn-cw-ics20-test-single-step-native-token-swap-operations-to-dest-denom memo %s expected recipient %s", + async ( + memo: string, + expectedRecipient: string, + expectedIbcErrorMsg: string + ) => { + await ics20Contract.updateMappingPair({ + localAssetInfo: { + native_token: { + denom: ORAI, + }, + }, + localAssetInfoDecimals: 6, + denom: oraiIbcDenom, + remoteDecimals: 6, + localChannelId: channel, + isMintBurn: false, + }); + + // now send ibc package + icsPackage.memo = memo; + console.log(icsPackage); + // transfer from cosmos to oraichain, should pass + const result = await cosmosChain.ibc.sendPacketReceive({ + packet: { + data: toBinary(icsPackage), + ...packetData, + }, + relayer: relayerAddress, + }); + + const bobBalance = oraiClient.app.bank.getBalance(expectedRecipient); + expect(bobBalance.length).toBeGreaterThan(0); + expect(bobBalance[0].denom).toEqual(ORAI); + expect(parseInt(bobBalance[0].amount)).toBeGreaterThan(0); + const transferEvent = result.events.find( + (event) => + event.type === "transfer" && + event.attributes.find( + (attr) => + attr.key === "recipient" && attr.value === expectedRecipient + ) + ); + expect(transferEvent).not.toBeUndefined(); + const ibcErrorMsg = result.attributes.find( + (attr) => attr.key === "ibc_error_msg" + ); + expect(ibcErrorMsg).not.toBeUndefined(); + expect(ibcErrorMsg.value).toEqual(expectedIbcErrorMsg); + } + ); + + it.each([ + [ + `${bobAddress}`, + "orai1n6fwuamldz6mv5f3qwe9296pudjjemhmkfcgc3", + bobAddress, + "Generic error: Destination channel empty in build ibc msg", + ], // hard-coded usdt address + [ + `${bobAddress}`, + "orai18cvw806fj5n7xxz06ak8vjunveeks4zzzn37cu", + bobAddress, + "Generic error: Destination channel empty in build ibc msg", + ], // edge case, dest denom is also airi + ])( + "mint-burn-cw-ics20-test-single-step-cw20-token-swap-operations-to-dest-denom memo %s dest denom %s expected recipient %s", + async ( + destReceiver: string, + destDenom: string, + expectedRecipient: string, + expectedIbcErrorMsg: string + ) => { + // now send ibc package + icsPackage.memo = parseToIbcWasmMemo(destReceiver, "", destDenom); + // transfer from cosmos to oraichain, should pass + const result = await cosmosChain.ibc.sendPacketReceive({ + packet: { + data: toBinary(icsPackage), + ...packetData, + }, + relayer: relayerAddress, + }); + + const token = new OraiswapTokenClient( + oraiClient, + oraiSenderAddress, + destDenom + ); + const cw20Balance = await token.balance({ address: expectedRecipient }); + expect(parseInt(cw20Balance.balance)).toBeGreaterThan(1000); + expect( + result.attributes.find((attr) => attr.key === "ibc_error_msg").value + ).toEqual(expectedIbcErrorMsg); + } + ); + + it("mint-burn-cw-ics20-test-single-step-cw20-token-swap-operations-to-dest-denom-FAILED-cannot-simulate-swap", async () => { + // now send ibc package + + // => dest token on Orai = ibc/EB7094899ACFB7A6F2A67DB084DEE2E9A83DEFAA5DEF92D9A9814FFD9FF673FA + icsPackage.memo = parseToIbcWasmMemo(bobAddressEth, "channel-0", "foo"); + // transfer from cosmos to oraichain, should pass + const result = await cosmosChain.ibc.sendPacketReceive({ + packet: { + data: toBinary(icsPackage), + ...packetData, + }, + relayer: relayerAddress, + }); + expect( + result.attributes.find((attr) => attr.key === "ibc_error_msg").value + ).toEqual( + 'Cannot simulate swap with ops: [OraiSwap { offer_asset_info: Token { contract_addr: Addr("orai18cvw806fj5n7xxz06ak8vjunveeks4zzzn37cu") }, ask_asset_info: NativeToken { denom: "orai" } }, OraiSwap { offer_asset_info: NativeToken { denom: "orai" }, ask_asset_info: NativeToken { denom: "ibc/EB7094899ACFB7A6F2A67DB084DEE2E9A83DEFAA5DEF92D9A9814FFD9FF673FA" } }] with error: "Error parsing into type oraiswap::router::SimulateSwapOperationsResponse: unknown field `ok`, expected `amount`"' + ); + }); + + it("mint-burn-cw-ics20-test-single-step-cw20-FAILED-IBC_TRANSFER_NATIVE_ERROR_ID-ack-SUCCESS", async () => { + // fixture + // icsPackage.memo = `unknown-channel/${bobAddress}:${usdtToken.contractAddress}`; + + // dest denom on orai: ibc/79E5EC9A42F2FC01B2BA609F13C985393779BE5153E01D24E79C2681B0DFB592 + icsPackage.memo = parseToIbcWasmMemo(bobAddress, "channel-15", "uatom"); + icsPackage.amount = initialBalanceAmount; + + // transfer from cosmos to oraichain, should pass + const result = await cosmosChain.ibc.sendPacketReceive({ + packet: { + data: toBinary(icsPackage), + ...packetData, + }, + relayer: relayerAddress, + }); + console.log(result); + // refunding also fails because of not enough balance to refund + expect( + findWasmEvent(result.events, "action", "ibc_transfer_native_error_id") + ).not.toBeUndefined(); + // ack should be successful + expect(result.acknowledgement).toEqual( + Buffer.from('{"result":"MQ=="}').toString("base64") + ); + expect( + findWasmEvent(result.events, "undo_increase_channel", channel) + ).toBeUndefined(); + + // other types of reply id must not be called + expect( + findWasmEvent(result.events, "action", "swap_ops_failure_id") + ).toBeUndefined(); + expect( + findWasmEvent(result.events, "action", "native_receive_id") + ).toBeUndefined(); + expect( + findWasmEvent(result.events, "action", "follow_up_failure_id") + ).toBeUndefined(); + expect( + findWasmEvent(result.events, "action", "refund_failure_id") + ).toBeUndefined(); + + // for ibc native transfer case, we wont have refund either + expect( + result.events.find( + (ev) => + ev.type === "wasm" && + ev.attributes.find( + (attr) => attr.key === "action" && attr.value === "transfer" + ) && + ev.attributes.find( + (attr) => attr.key === "to" && attr.value === bobAddress + ) + ) + ).toBeUndefined(); + }); + + it("mint-burn-cw-ics20-test-single-step-cw20-success-FOLLOW_UP_IBC_SEND_FAILURE_ID-must-not-have-SWAP_OPS_FAILURE_ID-or-on_packet_failure-ack-SUCCESS", async () => { + // fixture + // icsPackage.memo = `${channel}/${bobAddress}:${airiToken.contractAddress}`; + icsPackage.memo = parseToIbcWasmMemo(bobAddress, channel, airiIbcDenom); + icsPackage.amount = initialBalanceAmount; + // transfer from cosmos to oraichain, should pass + const result = await cosmosChain.ibc.sendPacketReceive({ + packet: { + data: toBinary(icsPackage), + ...packetData, + }, + relayer: relayerAddress, + }); + // all id types of reply id must not be called, especially swap_ops_failure_id + expect( + findWasmEvent(result.events, "action", "swap_ops_failure_id") + ).toBeUndefined(); + expect( + findWasmEvent(result.events, "action", "native_receive_id") + ).toBeUndefined(); + expect( + findWasmEvent(result.events, "action", "follow_up_failure_id") + ).toBeUndefined(); + expect( + findWasmEvent(result.events, "action", "refund_failure_id") + ).toBeUndefined(); + expect( + findWasmEvent(result.events, "action", "ibc_transfer_native_error_id") + ).toBeUndefined(); + expect( + findWasmEvent(result.events, "action", "acknowledge") + ).toBeUndefined(); + expect( + findWasmEvent(result.events, "undo_reduce_channel", channel) + ).toBeUndefined(); + // ack should be successful + expect(result.acknowledgement).toEqual( + Buffer.from('{"result":"MQ=="}').toString("base64") + ); + + // for ibc native transfer case, we wont have refund either + expect( + result.events.find( + (ev) => + ev.type === "wasm" && + ev.attributes.find( + (attr) => attr.key === "action" && attr.value === "transfer" + ) && + ev.attributes.find( + (attr) => attr.key === "to" && attr.value === bobAddress + ) + ) + ).toBeUndefined(); + }); + + it.each([ + [channel, "abcd", usdtIbcDenom], // hard-coded usdt address + [channel, "0x", airiIbcDenom], + [channel, "0xabcd", usdtIbcDenom], + [channel, "tron-testnet0xabcd", airiIbcDenom], // bad evm address case + ])( + "mint-burn-cw-ics20-test-single-step-has-ibc-msg-dest-fail memo %s dest denom %s expected error", + async (destChannel: string, destReceiver: string, destDenom: string) => { + // now send ibc package + // icsPackage.memo = `${destChannel}/${destReceiver}:${destDenom}`; + icsPackage.memo = parseToIbcWasmMemo( + destReceiver, + destChannel, + destDenom + ); + // transfer from cosmos to oraichain, should pass + const result = await cosmosChain.ibc.sendPacketReceive({ + packet: { + data: toBinary(icsPackage), + ...packetData, + }, + relayer: relayerAddress, + }); + const ibcEvent = result.events.find( + (event) => + event.type === "transfer" && + event.attributes.find((attr) => attr.key === "channel") + ); + // get swap operation event + expect(ibcEvent).toBeUndefined(); + const ibcErrorMsg = result.attributes.find( + (attr) => + attr.key === "ibc_error_msg" && + attr.value === + "Generic error: The destination info is neither evm or cosmos based" + ); + expect(ibcErrorMsg).not.toBeUndefined(); + } + ); + + it.each([ + [channel, bridgeReceiver, airiIbcDenom], // hard-coded airi + ])( + "mint-burn-cw-ics20-test-single-step-has-ibc-msg-dest-receiver-evm-based memo %s dest denom %s expected recipient %s", + async (destChannel: string, destReceiver: string, destDenom: string) => { + // now send ibc package + // icsPackage.memo = `${destChannel}/${destReceiver}:${destDenom}`; + icsPackage.memo = parseToIbcWasmMemo( + destReceiver, + destChannel, + destDenom + ); + + // transfer from cosmos to oraichain, should pass + let result = await cosmosChain.ibc.sendPacketReceive({ + packet: { + data: toBinary(icsPackage), + ...packetData, + }, + relayer: relayerAddress, + }); + const sendPacketEvent = result.events.find( + (event) => event.type === "send_packet" + ); + expect(sendPacketEvent).not.toBeUndefined(); + const packetHex = sendPacketEvent.attributes.find( + (attr) => attr.key === "packet_data_hex" + ).value; + expect(packetHex).not.toBeUndefined(); + const packet = JSON.parse( + Buffer.from(packetHex, "hex").toString("ascii") + ); + expect(packet.receiver).toEqual(icsPackage.sender); + expect(packet.sender).toEqual(ics20Contract.contractAddress); + // expect(packet.memo).toEqual(ics20Contract.contractAddress); + + // pass 1 day with 86_400 seconds + cosmosChain.store.tx((setter) => + Ok(setter("time")(cosmosChain.time + 86_400 * 1e9)) + ); + + // transfer from cosmos to oraichain, should pass + result = await cosmosChain.ibc.sendPacketReceive({ + packet: { + data: toBinary(icsPackage), + ...packetData, + }, + relayer: relayerAddress, + }); + // expect( + // flatten(result.events.map((e) => e.attributes)).find((a) => a.key === 'error_follow_up_msgs').value + // ).toContain('Generic error: timeout at'); + } + ); + + it("mint-burn-cw-ics20-test-single-step-ibc-msg-map-with-fee-denom-orai-and-airi-destination-denom-should-swap-normally", async () => { + await ics20Contract.updateMappingPair({ + localAssetInfo: { + native_token: { + denom: ORAI, + }, + }, + localAssetInfoDecimals: 6, + denom: oraiIbcDenom, + remoteDecimals: 6, + localChannelId: channel, + }); + + let packetData = { + src: { + port_id: cosmosPort, + channel_id: channel, + }, + dest: { + port_id: oraiPort, + channel_id: channel, + }, + sequence: 27, + timeout: { + block: { + revision: 1, + height: 12345678, + }, + }, + }; + const icsPackage: FungibleTokenPacketData = { + amount: ibcTransferAmount, + denom: oraiIbcDenom, + receiver: bobAddress, + sender: cosmosSenderAddress, + // memo: `${bobAddress}:${airiToken.contractAddress}`, + memo: parseToIbcWasmMemo(bobAddress, "", airiToken.contractAddress), + }; + // transfer from cosmos to oraichain, should pass + let result = await cosmosChain.ibc.sendPacketReceive({ + packet: { + data: toBinary(icsPackage), + ...packetData, + }, + relayer: relayerAddress, + }); + + const swapEvent = result.events.find( + (event) => + event.type === "wasm" && + event.attributes.find((attr) => attr.value === "swap") + ); + expect( + swapEvent.attributes.filter( + (attr) => attr.key === "offer_asset" && attr.value === ORAI + ).length + ).toBeGreaterThan(0); + expect( + swapEvent.attributes.filter( + (attr) => + attr.key === "ask_asset" && attr.value === airiToken.contractAddress + ).length + ).toBeGreaterThan(0); + }); + + it("mint-burn-cw-ics20-test-single-step-ibc-msg-map-with-fee-denom-orai-and-orai-destination-denom-should-transfer-normally", async () => { + await ics20Contract.updateMappingPair({ + localAssetInfo: { + native_token: { + denom: ORAI, + }, + }, + localAssetInfoDecimals: 6, + denom: oraiIbcDenom, + remoteDecimals: 6, + localChannelId: channel, + }); + + let packetData = { + src: { + port_id: cosmosPort, + channel_id: channel, + }, + dest: { + port_id: oraiPort, + channel_id: channel, + }, + sequence: 27, + timeout: { + block: { + revision: 1, + height: 12345678, + }, + }, + }; + const icsPackage: FungibleTokenPacketData = { + amount: ibcTransferAmount, + denom: oraiIbcDenom, + receiver: bobAddress, + sender: cosmosSenderAddress, + // memo: `${bobAddress}:orai`, + memo: parseToIbcWasmMemo(bobAddress, "", "orai"), + }; + // transfer from cosmos to oraichain, should pass + let result = await cosmosChain.ibc.sendPacketReceive({ + packet: { + data: toBinary(icsPackage), + ...packetData, + }, + relayer: relayerAddress, + }); + const transferEvent = result.events.find( + (event) => event.type === "transfer" + ); + expect( + transferEvent.attributes.filter( + (attr) => attr.key === "recipient" && attr.value === bobAddress + ).length + ).toBeGreaterThan(0); + expect( + transferEvent.attributes.filter( + (attr) => + attr.key === "amount" && + attr.value === + JSON.stringify([{ denom: ORAI, amount: ibcTransferAmount }]) + ).length + ).toBeGreaterThan(0); + }); + + describe("test-single-step-cosmos-based-ibc-transfer-native", () => { + // unknowChannel is channel to cosmos + const unknownChannel = "channel-15"; + beforeEach(async () => { + // fixture + // needs to fake a new ibc channel so that we can successfully do ibc transfer + oraiClient.app.ibc.relay( + unknownChannel, + oraiPort, + unknownChannel, + cosmosPort, + cosmosChain + ); + await cosmosChain.ibc.sendChannelOpen({ + open_init: { + channel: { + counterparty_endpoint: { + port_id: oraiPort, + channel_id: unknownChannel, + }, + endpoint: { + port_id: cosmosPort, + channel_id: unknownChannel, + }, + order: IbcOrder.Unordered, + version: "ics20-1", + connection_id: "connection-0", + }, + }, + }); + + await cosmosChain.ibc.sendChannelConnect({ + open_ack: { + channel: { + counterparty_endpoint: { + port_id: oraiPort, + channel_id: unknownChannel, + }, + endpoint: { + port_id: cosmosPort, + channel_id: unknownChannel, + }, + order: IbcOrder.Unordered, + version: "ics20-1", + connection_id: "connection-0", + }, + counterparty_version: "ics20-1", + }, + }); + + cosmosChain.ibc.addMiddleWare((msg, app) => { + if ("packet" in msg.data) { + const data = msg.data.packet as IbcPacket; + if (Number(data.timeout.timestamp) < cosmosChain.time) { + throw new GenericError("timeout at " + data.timeout.timestamp); + } + } + }); + }); + + it.each([ + ["channel-15", "orai1g4h64yjt0fvzv5v2j8tyfnpe5kmnetejvfgs7g", "uatom"], // edge case, dest denom is also airi + ])( + "mint-burn-cw-ics20-test-single-step-has-ibc-msg-dest-receiver-cosmos-based dest channel %s dest denom %s expected recipient %s", + async ( + destChannel: string, + destReceiver: string, + destDenom: string + ) => { + // now send ibc package + // icsPackage.memo = `${destChannel}/${destReceiver}:${destDenom}`; + icsPackage.memo = parseToIbcWasmMemo( + destReceiver, + destChannel, + destDenom + ); + // transfer from cosmos to oraichain, should pass + const result = await cosmosChain.ibc.sendPacketReceive({ + packet: { + data: toBinary(icsPackage), + ...packetData, + }, + relayer: relayerAddress, + }); + + const ibcEvent = result.events.find( + (event) => + event.type === "transfer" && + event.attributes.find((attr) => attr.key === "channel") + ); + + // get swap operation event + expect(ibcEvent).not.toBeUndefined(); + expect( + ibcEvent.attributes.find((attr) => attr.key === "channel").value + ).toEqual(destChannel); + expect( + ibcEvent.attributes.find((attr) => attr.key === "recipient").value + ).toEqual(destReceiver); + expect( + ibcEvent.attributes.find((attr) => attr.key === "sender").value + ).toEqual(ics20Contract.contractAddress); + expect( + ibcEvent.attributes.find((attr) => attr.key === "amount").value + ).toContain(AtomDenom); + } + ); + + it("mint-burn-cw-ics20-test-single-step-ibc-msg-SUCCESS-map-with-fee-denom-orai-and-orai-destination-denom-with-dest-channel-not-matched-with-mapping-pair-should-do-ibctransfer", async () => { + await ics20Contract.updateMappingPair({ + localAssetInfo: { + native_token: { + denom: ORAI, + }, + }, + localAssetInfoDecimals: 6, + denom: oraiIbcDenom, + remoteDecimals: 6, + localChannelId: channel, + }); + + let packetData = { + src: { + port_id: cosmosPort, + channel_id: channel, + }, + dest: { + port_id: oraiPort, + channel_id: channel, + }, + sequence: 27, + timeout: { + block: { + revision: 1, + height: 12345678, + }, + }, + }; + const icsPackage: FungibleTokenPacketData = { + amount: ibcTransferAmount, + denom: oraiIbcDenom, + receiver: bobAddress, + sender: cosmosSenderAddress, + // memo: `${unknownChannel}/${bobAddress}:orai`, + memo: parseToIbcWasmMemo(bobAddress, atomChannel, "uatom"), + }; + // transfer from cosmos to oraichain, should pass + let result = await cosmosChain.ibc.sendPacketReceive({ + packet: { + data: toBinary(icsPackage), + ...packetData, + }, + relayer: relayerAddress, + }); + + const transferEvent = result.events.find( + (event) => + event.type === "transfer" && + event.attributes.find((attr) => attr.key === "channel") + ); + console.log(transferEvent); + expect( + transferEvent.attributes.filter( + (attr) => attr.key === "recipient" && attr.value === bobAddress + ).length + ).toBeGreaterThan(0); + expect( + transferEvent.attributes.filter( + (attr) => attr.key === "amount" && attr.value.includes(AtomDenom) + ).length + ).toBeGreaterThan(0); + expect( + transferEvent.attributes.filter( + (attr) => attr.key === "channel" && attr.value === unknownChannel + ).length + ).toBeGreaterThan(0); + }); + }); + + it("mint-burn-cw-ics20-test-single-step-handle_ibc_packet_receive_native_remote_chain-has-relayer-fee-should-be-deducted", async () => { + // setup relayer fee + const relayerFee = "100000"; + await ics20Contract.updateConfig({ + relayerFee: [{ prefix: "tron-testnet", fee: relayerFee }], + }); + + const icsPackage: FungibleTokenPacketData = { + amount: ibcTransferAmount, + denom: airiIbcDenom, + receiver: bobAddress, + sender: oraibridgeSenderAddress, + memo: parseToIbcWasmMemo(bobAddress, channel, oraiIbcDenom), + }; + // transfer from cosmos to oraichain, should pass + let result = await cosmosChain.ibc.sendPacketReceive({ + packet: { + data: toBinary(icsPackage), + ...packetData, + }, + relayer: relayerAddress, + }); + const hasRelayerFee = result.events.find( + (event) => + event.type === "wasm" && + event.attributes.find( + (attr) => attr.key === "to" && attr.value === relayerAddress + ) && + event.attributes.find( + (attr) => attr.key === "amount" && attr.value === relayerFee + ) + ); + expect(hasRelayerFee).not.toBeUndefined(); + expect( + result.attributes.find( + (attr) => attr.key === "relayer_fee" && attr.value === relayerFee + ) + ).not.toBeUndefined(); + }); + + it.each<[string, string]>([ + [parseToIbcWasmMemo(bobAddress, channel, oraiIbcDenom), "20000000"], + [parseToIbcWasmMemo(bobAddress, "", "orai"), "10000000"], + ])( + "mint-burn-cw-ics20-test-single-step-ibc-handle_ibc_packet_receive_native_remote_chain-has-token-fee-should-be-deducted", + async (memo, expectedTokenFee) => { + // setup relayer fee + await ics20Contract.updateConfig({ + tokenFee: [ + { + token_denom: airiIbcDenom, + ratio: { nominator: 1, denominator: 10 }, + }, + ], + }); + + const icsPackage: FungibleTokenPacketData = { + amount: ibcTransferAmount, + denom: airiIbcDenom, + receiver: bobAddress, + sender: oraibridgeSenderAddress, + memo, + }; + // transfer from cosmos to oraichain, should pass + let result = await cosmosChain.ibc.sendPacketReceive({ + packet: { + data: toBinary(icsPackage), + ...packetData, + }, + relayer: relayerAddress, + }); + const hasTokenFee = result.events.filter( + (event) => + event.type === "wasm" && + event.attributes.find( + (attr) => attr.key === "to" && attr.value === senderAddress + ) + ); + expect(hasTokenFee).not.toBeUndefined(); + expect( + result.attributes.find( + (attr) => attr.key === "token_fee" && expectedTokenFee + ) + ).not.toBeUndefined(); + } + ); + + it.each<[string, string, string]>([ + [ + parseToIbcWasmMemo(bobAddress, channel, airiIbcDenom), + "20000000", + "100000", + ], + [ + parseToIbcWasmMemo(bridgeReceiver, channel, airiIbcDenom), + "20000000", + "200000", + ], // double deducted when there's an outgoing ibc msg after receiving the packet + [parseToIbcWasmMemo(bobAddress, "", "orai"), "10000000", "100000"], + ])( + "mint-burn-test-handle_ibc_packet_receive_native_remote_chain-has-both-token-fee-and-relayer-fee-should-be-both-deducted-given memo %s should give expected token fee %s and expected relayer fee %s", + async (memo, expectedTokenFee, expectedRelayerFee) => { + // setup relayer fee + const relayerFee = "100000"; + await ics20Contract.updateConfig({ + tokenFee: [ + { + token_denom: airiIbcDenom, + ratio: { nominator: 1, denominator: 10 }, + }, + { token_denom: "orai", ratio: { nominator: 1, denominator: 10 } }, + ], + relayerFee: [{ prefix: "tron-testnet", fee: relayerFee }], + }); + + const icsPackage: FungibleTokenPacketData = { + amount: ibcTransferAmount, + denom: airiIbcDenom, + receiver: bobAddress, + sender: oraibridgeSenderAddress, + memo, + }; + // transfer from cosmos to oraichain, should pass + let result = await cosmosChain.ibc.sendPacketReceive({ + packet: { + data: toBinary(icsPackage), + ...packetData, + }, + relayer: relayerAddress, + }); + + const hasRelayerFee = result.events.find( + (event) => + event.type === "wasm" && + event.attributes.find( + (attr) => attr.key === "to" && attr.value === relayerAddress + ) && + event.attributes.find( + (attr) => + attr.key === "amount" && attr.value === expectedRelayerFee + ) + ); + expect(hasRelayerFee).not.toBeUndefined(); + expect( + result.attributes.find( + (attr) => + attr.key === "relayer_fee" && attr.value === expectedRelayerFee + ) + ).not.toBeUndefined(); + + const hasTokenFee = result.events.find( + (event) => + event.type === "wasm" && + event.attributes.find( + (attr) => attr.key === "to" && attr.value === senderAddress + ) && + event.attributes.find( + (attr) => attr.key === "amount" && attr.value === expectedTokenFee + ) + ); + expect(hasTokenFee).not.toBeUndefined(); + expect( + result.attributes.find( + (attr) => + attr.key === "token_fee" && attr.value === expectedTokenFee + ) + ).not.toBeUndefined(); + } + ); + + it.each<[string, string, string, string, string]>([ + [ + ibcTransferAmount, + ibcTransferAmount, + "10000000", + "90000000", + ibcTransferAmount, + ], + ])( + "mint-burn-cw-ics20-test-single-step-handle_ibc_packet_receive_native_remote_chain-deducted-amount-is-zero-should-still-charge-fees", + async ( + transferAmount, + relayerFee, + expectedTokenFee, + expectedRelayerFee, + expectedTotalFee + ) => { + await ics20Contract.updateConfig({ + tokenFee: [ + { + token_denom: airiIbcDenom, + ratio: { nominator: 1, denominator: 10 }, + }, + ], + relayerFee: [{ prefix: "tron-testnet", fee: relayerFee }], + }); + + const icsPackage: FungibleTokenPacketData = { + amount: transferAmount, + denom: airiIbcDenom, + receiver: bobAddress, + sender: oraibridgeSenderAddress, + memo: parseToIbcWasmMemo(bobAddress, "", "orai"), + }; + // transfer from cosmos to oraichain, should pass + let result = await cosmosChain.ibc.sendPacketReceive({ + packet: { + data: toBinary(icsPackage), + ...packetData, + }, + relayer: relayerAddress, + }); + + const hasFees = result.events.find( + (event) => + event.type === "wasm" && + event.attributes.find( + (attr) => attr.key === "to" && attr.value === senderAddress + ) && + event.attributes.find( + (attr) => attr.key === "amount" && attr.value === expectedTotalFee + ) + ); + + expect(hasFees).not.toBeUndefined(); + expect( + result.attributes.find( + (attr) => + attr.key === "token_fee" && attr.value === expectedTokenFee + ) + ).not.toBeUndefined(); + expect( + result.attributes.find( + (attr) => + attr.key === "relayer_fee" && attr.value === expectedRelayerFee + ) + ).not.toBeUndefined(); + } + ); + + // execute transfer to remote test cases + it("mint-burn-test-execute_transfer_back_to_remote_chain-native-FAILED-no-funds-sent", async () => { + oraiClient.app.bank.setBalance( + senderAddress, + coins(initialBalanceAmount, ORAI) + ); + try { + await ics20Contract.transferToRemote( + { + localChannelId: "1", + memo: null, + remoteAddress: "a", + remoteDenom: "a", + timeout: 60, + }, + "auto", + null + ); + } catch (error) { + expect(error.toString()).toContain("No funds sent"); + } + }); + + it("mint-burn-test-execute_transfer_back_to_remote_chain-native-FAILED-no-mapping-found", async () => { + oraiClient.app.bank.setBalance( + senderAddress, + coins(initialBalanceAmount, ORAI) + ); + try { + await ics20Contract.transferToRemote( + { + localChannelId: "1", + memo: null, + remoteAddress: "a", + remoteDenom: "a", + timeout: 60, + }, + "auto", + null, + [{ denom: ORAI, amount: "100" }] + ); + } catch (error) { + expect(error.toString()).toContain("Could not find the mapping pair"); + } + }); + + it("mint-burn-test-execute_transfer_back_to_remote_chain-native-FAILED-no-mapping-found", async () => { + oraiClient.app.bank.setBalance( + senderAddress, + coins(initialBalanceAmount, ORAI) + ); + try { + await ics20Contract.transferToRemote( + { + localChannelId: "1", + memo: null, + remoteAddress: "a", + remoteDenom: "a", + timeout: 60, + }, + "auto", + null, + [{ denom: ORAI, amount: "100" }] + ); + } catch (error) { + expect(error.toString()).toContain("Could not find the mapping pair"); + } + }); + }); +}); diff --git a/simulate-tests/common.ts b/simulate-tests/common.ts index abbaa49..62830aa 100644 --- a/simulate-tests/common.ts +++ b/simulate-tests/common.ts @@ -1,6 +1,7 @@ import { SimulateCosmWasmClient } from "@oraichain/cw-simulate"; import { OraiswapTokenClient } from "@oraichain/oraidex-contracts-sdk"; -import { CwIcs20LatestClient } from "@oraichain/common-contracts-sdk"; +// import { CwIcs20LatestClient } from "@oraichain/common-contracts-sdk"; +import { CwIcs20LatestClient } from "./contracts-sdk/CwIcs20Latest.client"; import * as oraidexArtifacts from "@oraichain/oraidex-contracts-build"; import * as commonArtifacts from "@oraichain/common-contracts-build"; import { readFileSync } from "fs"; @@ -27,11 +28,13 @@ export const deployToken = async ( name, decimals = 6, initial_balances = [{ address: senderAddress, amount: "1000000000" }], + minter = senderAddress, }: { symbol: string; name: string; decimals?: number; initial_balances?: Cw20Coin[]; + minter?: string; } ): Promise => { return new OraiswapTokenClient( @@ -46,7 +49,7 @@ export const deployToken = async ( decimals, symbol, name, - mint: { minter: senderAddress }, + mint: { minter: minter }, initial_balances, }, "token", diff --git a/simulate-tests/contracts-sdk/CwIcs20Latest.client.ts b/simulate-tests/contracts-sdk/CwIcs20Latest.client.ts new file mode 100644 index 0000000..6a5b895 --- /dev/null +++ b/simulate-tests/contracts-sdk/CwIcs20Latest.client.ts @@ -0,0 +1,558 @@ +/** +* This file was automatically generated by @oraichain/ts-codegen@0.35.8. +* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, +* and run the @oraichain/ts-codegen generate command to regenerate this file. +*/ + +import { CosmWasmClient, SigningCosmWasmClient, ExecuteResult } from "@cosmjs/cosmwasm-stargate"; +import { StdFee } from "@cosmjs/amino"; +import {InstantiateMsg, AllowMsg, ExecuteMsg, Uint128, Binary, AssetInfo, Addr, HookMethods, Cw20ReceiveMsg, TransferBackMsg, UpdatePairMsg, DeletePairMsg, RelayerFee, TokenFee, Ratio, QueryMsg, AdminResponse, AllowedResponse, Amount, ChannelResponse, Coin, Cw20Coin, ChannelInfo, IbcEndpoint, ChannelWithKeyResponse, ConfigResponse, RelayerFeeResponse, ListAllowedResponse, AllowedInfo, ListChannelsResponse, PairQuery, MappingMetadata, ArrayOfPairQuery, PortResponse} from "./CwIcs20Latest.types"; +export interface CwIcs20LatestReadOnlyInterface { + contractAddress: string; + port: () => Promise; + listChannels: () => Promise; + channel: ({ + id + }: { + id: string; + }) => Promise; + channelWithKey: ({ + channelId, + denom + }: { + channelId: string; + denom: string; + }) => Promise; + config: () => Promise; + admin: () => Promise; + allowed: ({ + contract + }: { + contract: string; + }) => Promise; + listAllowed: ({ + limit, + order, + startAfter + }: { + limit?: number; + order?: number; + startAfter?: string; + }) => Promise; + pairMappings: ({ + limit, + order, + startAfter + }: { + limit?: number; + order?: number; + startAfter?: string; + }) => Promise; + pairMapping: ({ + key + }: { + key: string; + }) => Promise; + pairMappingsFromAssetInfo: ({ + assetInfo + }: { + assetInfo: AssetInfo; + }) => Promise; + getTransferTokenFee: ({ + remoteTokenDenom + }: { + remoteTokenDenom: string; + }) => Promise; +} +export class CwIcs20LatestQueryClient implements CwIcs20LatestReadOnlyInterface { + client: CosmWasmClient; + contractAddress: string; + + constructor(client: CosmWasmClient, contractAddress: string) { + this.client = client; + this.contractAddress = contractAddress; + this.port = this.port.bind(this); + this.listChannels = this.listChannels.bind(this); + this.channel = this.channel.bind(this); + this.channelWithKey = this.channelWithKey.bind(this); + this.config = this.config.bind(this); + this.admin = this.admin.bind(this); + this.allowed = this.allowed.bind(this); + this.listAllowed = this.listAllowed.bind(this); + this.pairMappings = this.pairMappings.bind(this); + this.pairMapping = this.pairMapping.bind(this); + this.pairMappingsFromAssetInfo = this.pairMappingsFromAssetInfo.bind(this); + this.getTransferTokenFee = this.getTransferTokenFee.bind(this); + } + + port = async (): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + port: {} + }); + }; + listChannels = async (): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + list_channels: {} + }); + }; + channel = async ({ + id + }: { + id: string; + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + channel: { + id + } + }); + }; + channelWithKey = async ({ + channelId, + denom + }: { + channelId: string; + denom: string; + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + channel_with_key: { + channel_id: channelId, + denom + } + }); + }; + config = async (): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + config: {} + }); + }; + admin = async (): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + admin: {} + }); + }; + allowed = async ({ + contract + }: { + contract: string; + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + allowed: { + contract + } + }); + }; + listAllowed = async ({ + limit, + order, + startAfter + }: { + limit?: number; + order?: number; + startAfter?: string; + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + list_allowed: { + limit, + order, + start_after: startAfter + } + }); + }; + pairMappings = async ({ + limit, + order, + startAfter + }: { + limit?: number; + order?: number; + startAfter?: string; + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + pair_mappings: { + limit, + order, + start_after: startAfter + } + }); + }; + pairMapping = async ({ + key + }: { + key: string; + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + pair_mapping: { + key + } + }); + }; + pairMappingsFromAssetInfo = async ({ + assetInfo + }: { + assetInfo: AssetInfo; + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + pair_mappings_from_asset_info: { + asset_info: assetInfo + } + }); + }; + getTransferTokenFee = async ({ + remoteTokenDenom + }: { + remoteTokenDenom: string; + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + get_transfer_token_fee: { + remote_token_denom: remoteTokenDenom + } + }); + }; +} +export interface CwIcs20LatestInterface extends CwIcs20LatestReadOnlyInterface { + contractAddress: string; + sender: string; + receive: ({ + amount, + msg, + sender + }: { + amount: Uint128; + msg: Binary; + sender: string; + }, _fee?: number | StdFee | "auto", _memo?: string, _funds?: Coin[]) => Promise; + transferToRemote: ({ + localChannelId, + memo, + remoteAddress, + remoteDenom, + timeout + }: { + localChannelId: string; + memo?: string; + remoteAddress: string; + remoteDenom: string; + timeout?: number; + }, _fee?: number | StdFee | "auto", _memo?: string, _funds?: Coin[]) => Promise; + updateMappingPair: ({ + denom, + isMintBurn, + localAssetInfo, + localAssetInfoDecimals, + localChannelId, + remoteDecimals + }: { + denom: string; + isMintBurn?: boolean; + localAssetInfo: AssetInfo; + localAssetInfoDecimals: number; + localChannelId: string; + remoteDecimals: number; + }, _fee?: number | StdFee | "auto", _memo?: string, _funds?: Coin[]) => Promise; + deleteMappingPair: ({ + denom, + localChannelId + }: { + denom: string; + localChannelId: string; + }, _fee?: number | StdFee | "auto", _memo?: string, _funds?: Coin[]) => Promise; + allow: ({ + contract, + gasLimit + }: { + contract: string; + gasLimit?: number; + }, _fee?: number | StdFee | "auto", _memo?: string, _funds?: Coin[]) => Promise; + updateConfig: ({ + admin, + converterContract, + defaultGasLimit, + defaultTimeout, + feeDenom, + feeReceiver, + relayerFee, + relayerFeeReceiver, + swapRouterContract, + tokenFee + }: { + admin?: string; + converterContract?: string; + defaultGasLimit?: number; + defaultTimeout?: number; + feeDenom?: string; + feeReceiver?: string; + relayerFee?: RelayerFee[]; + relayerFeeReceiver?: string; + swapRouterContract?: string; + tokenFee?: TokenFee[]; + }, _fee?: number | StdFee | "auto", _memo?: string, _funds?: Coin[]) => Promise; + increaseChannelBalanceIbcReceive: ({ + amount, + destChannelId, + ibcDenom, + localReceiver + }: { + amount: Uint128; + destChannelId: string; + ibcDenom: string; + localReceiver: string; + }, _fee?: number | StdFee | "auto", _memo?: string, _funds?: Coin[]) => Promise; + reduceChannelBalanceIbcReceive: ({ + amount, + ibcDenom, + localReceiver, + srcChannelId + }: { + amount: Uint128; + ibcDenom: string; + localReceiver: string; + srcChannelId: string; + }, _fee?: number | StdFee | "auto", _memo?: string, _funds?: Coin[]) => Promise; + overrideChannelBalance: ({ + channelId, + ibcDenom, + outstanding, + totalSent + }: { + channelId: string; + ibcDenom: string; + outstanding: Uint128; + totalSent?: Uint128; + }, _fee?: number | StdFee | "auto", _memo?: string, _funds?: Coin[]) => Promise; + ibcHooksReceive: ({ + args, + func + }: { + args: Binary; + func: HookMethods; + }, _fee?: number | StdFee | "auto", _memo?: string, _funds?: Coin[]) => Promise; +} +export class CwIcs20LatestClient extends CwIcs20LatestQueryClient implements CwIcs20LatestInterface { + client: SigningCosmWasmClient; + sender: string; + contractAddress: string; + + constructor(client: SigningCosmWasmClient, sender: string, contractAddress: string) { + super(client, contractAddress); + this.client = client; + this.sender = sender; + this.contractAddress = contractAddress; + this.receive = this.receive.bind(this); + this.transferToRemote = this.transferToRemote.bind(this); + this.updateMappingPair = this.updateMappingPair.bind(this); + this.deleteMappingPair = this.deleteMappingPair.bind(this); + this.allow = this.allow.bind(this); + this.updateConfig = this.updateConfig.bind(this); + this.increaseChannelBalanceIbcReceive = this.increaseChannelBalanceIbcReceive.bind(this); + this.reduceChannelBalanceIbcReceive = this.reduceChannelBalanceIbcReceive.bind(this); + this.overrideChannelBalance = this.overrideChannelBalance.bind(this); + this.ibcHooksReceive = this.ibcHooksReceive.bind(this); + } + + receive = async ({ + amount, + msg, + sender + }: { + amount: Uint128; + msg: Binary; + sender: string; + }, _fee: number | StdFee | "auto" = "auto", _memo?: string, _funds?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + receive: { + amount, + msg, + sender + } + }, _fee, _memo, _funds); + }; + transferToRemote = async ({ + localChannelId, + memo, + remoteAddress, + remoteDenom, + timeout + }: { + localChannelId: string; + memo?: string; + remoteAddress: string; + remoteDenom: string; + timeout?: number; + }, _fee: number | StdFee | "auto" = "auto", _memo?: string, _funds?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + transfer_to_remote: { + local_channel_id: localChannelId, + memo, + remote_address: remoteAddress, + remote_denom: remoteDenom, + timeout + } + }, _fee, _memo, _funds); + }; + updateMappingPair = async ({ + denom, + isMintBurn, + localAssetInfo, + localAssetInfoDecimals, + localChannelId, + remoteDecimals + }: { + denom: string; + isMintBurn?: boolean; + localAssetInfo: AssetInfo; + localAssetInfoDecimals: number; + localChannelId: string; + remoteDecimals: number; + }, _fee: number | StdFee | "auto" = "auto", _memo?: string, _funds?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + update_mapping_pair: { + denom, + is_mint_burn: isMintBurn, + local_asset_info: localAssetInfo, + local_asset_info_decimals: localAssetInfoDecimals, + local_channel_id: localChannelId, + remote_decimals: remoteDecimals + } + }, _fee, _memo, _funds); + }; + deleteMappingPair = async ({ + denom, + localChannelId + }: { + denom: string; + localChannelId: string; + }, _fee: number | StdFee | "auto" = "auto", _memo?: string, _funds?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + delete_mapping_pair: { + denom, + local_channel_id: localChannelId + } + }, _fee, _memo, _funds); + }; + allow = async ({ + contract, + gasLimit + }: { + contract: string; + gasLimit?: number; + }, _fee: number | StdFee | "auto" = "auto", _memo?: string, _funds?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + allow: { + contract, + gas_limit: gasLimit + } + }, _fee, _memo, _funds); + }; + updateConfig = async ({ + admin, + converterContract, + defaultGasLimit, + defaultTimeout, + feeDenom, + feeReceiver, + relayerFee, + relayerFeeReceiver, + swapRouterContract, + tokenFee + }: { + admin?: string; + converterContract?: string; + defaultGasLimit?: number; + defaultTimeout?: number; + feeDenom?: string; + feeReceiver?: string; + relayerFee?: RelayerFee[]; + relayerFeeReceiver?: string; + swapRouterContract?: string; + tokenFee?: TokenFee[]; + }, _fee: number | StdFee | "auto" = "auto", _memo?: string, _funds?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + update_config: { + admin, + converter_contract: converterContract, + default_gas_limit: defaultGasLimit, + default_timeout: defaultTimeout, + fee_denom: feeDenom, + fee_receiver: feeReceiver, + relayer_fee: relayerFee, + relayer_fee_receiver: relayerFeeReceiver, + swap_router_contract: swapRouterContract, + token_fee: tokenFee + } + }, _fee, _memo, _funds); + }; + increaseChannelBalanceIbcReceive = async ({ + amount, + destChannelId, + ibcDenom, + localReceiver + }: { + amount: Uint128; + destChannelId: string; + ibcDenom: string; + localReceiver: string; + }, _fee: number | StdFee | "auto" = "auto", _memo?: string, _funds?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + increase_channel_balance_ibc_receive: { + amount, + dest_channel_id: destChannelId, + ibc_denom: ibcDenom, + local_receiver: localReceiver + } + }, _fee, _memo, _funds); + }; + reduceChannelBalanceIbcReceive = async ({ + amount, + ibcDenom, + localReceiver, + srcChannelId + }: { + amount: Uint128; + ibcDenom: string; + localReceiver: string; + srcChannelId: string; + }, _fee: number | StdFee | "auto" = "auto", _memo?: string, _funds?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + reduce_channel_balance_ibc_receive: { + amount, + ibc_denom: ibcDenom, + local_receiver: localReceiver, + src_channel_id: srcChannelId + } + }, _fee, _memo, _funds); + }; + overrideChannelBalance = async ({ + channelId, + ibcDenom, + outstanding, + totalSent + }: { + channelId: string; + ibcDenom: string; + outstanding: Uint128; + totalSent?: Uint128; + }, _fee: number | StdFee | "auto" = "auto", _memo?: string, _funds?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + override_channel_balance: { + channel_id: channelId, + ibc_denom: ibcDenom, + outstanding, + total_sent: totalSent + } + }, _fee, _memo, _funds); + }; + ibcHooksReceive = async ({ + args, + func + }: { + args: Binary; + func: HookMethods; + }, _fee: number | StdFee | "auto" = "auto", _memo?: string, _funds?: Coin[]): Promise => { + return await this.client.execute(this.sender, this.contractAddress, { + ibc_hooks_receive: { + args, + func + } + }, _fee, _memo, _funds); + }; +} \ No newline at end of file diff --git a/simulate-tests/contracts-sdk/CwIcs20Latest.types.ts b/simulate-tests/contracts-sdk/CwIcs20Latest.types.ts new file mode 100644 index 0000000..8a495f8 --- /dev/null +++ b/simulate-tests/contracts-sdk/CwIcs20Latest.types.ts @@ -0,0 +1,236 @@ +export interface InstantiateMsg { + allowlist: AllowMsg[]; + converter_contract: string; + default_gas_limit?: number | null; + default_timeout: number; + gov_contract: string; + swap_router_contract: string; +} +export interface AllowMsg { + contract: string; + gas_limit?: number | null; +} +export type ExecuteMsg = { + receive: Cw20ReceiveMsg; +} | { + transfer_to_remote: TransferBackMsg; +} | { + update_mapping_pair: UpdatePairMsg; +} | { + delete_mapping_pair: DeletePairMsg; +} | { + allow: AllowMsg; +} | { + update_config: { + admin?: string | null; + converter_contract?: string | null; + default_gas_limit?: number | null; + default_timeout?: number | null; + fee_denom?: string | null; + fee_receiver?: string | null; + relayer_fee?: RelayerFee[] | null; + relayer_fee_receiver?: string | null; + swap_router_contract?: string | null; + token_fee?: TokenFee[] | null; + }; +} | { + increase_channel_balance_ibc_receive: { + amount: Uint128; + dest_channel_id: string; + ibc_denom: string; + local_receiver: string; + }; +} | { + reduce_channel_balance_ibc_receive: { + amount: Uint128; + ibc_denom: string; + local_receiver: string; + src_channel_id: string; + }; +} | { + override_channel_balance: { + channel_id: string; + ibc_denom: string; + outstanding: Uint128; + total_sent?: Uint128 | null; + }; +} | { + ibc_hooks_receive: { + args: Binary; + func: HookMethods; + }; +}; +export type Uint128 = string; +export type Binary = string; +export type AssetInfo = { + token: { + contract_addr: Addr; + }; +} | { + native_token: { + denom: string; + }; +}; +export type Addr = string; +export type HookMethods = "universal_swap"; +export interface Cw20ReceiveMsg { + amount: Uint128; + msg: Binary; + sender: string; +} +export interface TransferBackMsg { + local_channel_id: string; + memo?: string | null; + remote_address: string; + remote_denom: string; + timeout?: number | null; +} +export interface UpdatePairMsg { + denom: string; + is_mint_burn?: boolean | null; + local_asset_info: AssetInfo; + local_asset_info_decimals: number; + local_channel_id: string; + remote_decimals: number; +} +export interface DeletePairMsg { + denom: string; + local_channel_id: string; +} +export interface RelayerFee { + fee: Uint128; + prefix: string; +} +export interface TokenFee { + ratio: Ratio; + token_denom: string; +} +export interface Ratio { + denominator: number; + nominator: number; +} +export type QueryMsg = { + port: {}; +} | { + list_channels: {}; +} | { + channel: { + id: string; + }; +} | { + channel_with_key: { + channel_id: string; + denom: string; + }; +} | { + config: {}; +} | { + admin: {}; +} | { + allowed: { + contract: string; + }; +} | { + list_allowed: { + limit?: number | null; + order?: number | null; + start_after?: string | null; + }; +} | { + pair_mappings: { + limit?: number | null; + order?: number | null; + start_after?: string | null; + }; +} | { + pair_mapping: { + key: string; + }; +} | { + pair_mappings_from_asset_info: { + asset_info: AssetInfo; + }; +} | { + get_transfer_token_fee: { + remote_token_denom: string; + }; +}; +export interface AdminResponse { + admin?: string | null; +} +export interface AllowedResponse { + gas_limit?: number | null; + is_allowed: boolean; +} +export type Amount = { + native: Coin; +} | { + cw20: Cw20Coin; +}; +export interface ChannelResponse { + balances: Amount[]; + info: ChannelInfo; + total_sent: Amount[]; +} +export interface Coin { + amount: Uint128; + denom: string; +} +export interface Cw20Coin { + address: string; + amount: Uint128; +} +export interface ChannelInfo { + connection_id: string; + counterparty_endpoint: IbcEndpoint; + id: string; +} +export interface IbcEndpoint { + channel_id: string; + port_id: string; +} +export interface ChannelWithKeyResponse { + balance: Amount; + info: ChannelInfo; + total_sent: Amount; +} +export interface ConfigResponse { + converter_contract: string; + default_gas_limit?: number | null; + default_timeout: number; + fee_denom: string; + gov_contract: string; + relayer_fee_receiver: Addr; + relayer_fees: RelayerFeeResponse[]; + swap_router_contract: string; + token_fee_receiver: Addr; + token_fees: TokenFee[]; +} +export interface RelayerFeeResponse { + amount: Uint128; + prefix: string; +} +export interface ListAllowedResponse { + allow: AllowedInfo[]; +} +export interface AllowedInfo { + contract: string; + gas_limit?: number | null; +} +export interface ListChannelsResponse { + channels: ChannelInfo[]; +} +export interface PairQuery { + key: string; + pair_mapping: MappingMetadata; +} +export interface MappingMetadata { + asset_info: AssetInfo; + asset_info_decimals: number; + is_mint_burn?: boolean; + remote_decimals: number; +} +export type ArrayOfPairQuery = PairQuery[]; +export interface PortResponse { + port_id: string; +} \ No newline at end of file diff --git a/simulate-tests/contracts-sdk/index.ts b/simulate-tests/contracts-sdk/index.ts new file mode 100644 index 0000000..5ab2419 --- /dev/null +++ b/simulate-tests/contracts-sdk/index.ts @@ -0,0 +1,2 @@ +export * as CwIcs20LatestTypes from './CwIcs20Latest.types'; +export * from './CwIcs20Latest.client'; \ No newline at end of file