From c23780eb9e43708de741261dd9592b623fdc9971 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Wed, 7 Jun 2023 18:35:56 -0700 Subject: [PATCH 01/28] fixed migrate failed config no fee receiver --- contracts/cw-ics20-latest/src/contract.rs | 13 ++++++------- contracts/cw-ics20-latest/src/ibc_tests.rs | 6 +++--- contracts/cw-ics20-latest/src/msg.rs | 6 +++--- contracts/cw-ics20-latest/src/state.rs | 2 +- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/contracts/cw-ics20-latest/src/contract.rs b/contracts/cw-ics20-latest/src/contract.rs index 1e1c393..707e6e1 100644 --- a/contracts/cw-ics20-latest/src/contract.rs +++ b/contracts/cw-ics20-latest/src/contract.rs @@ -469,14 +469,13 @@ pub fn execute_delete_mapping_pair( #[entry_point] pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result { // we don't need to save anything if migrating from the same version - let config: Config = CONFIG.load(deps.storage)?; CONFIG.save( deps.storage, &Config { - default_timeout: config.default_timeout, + default_timeout: msg.default_timeout, default_gas_limit: msg.default_gas_limit, - fee_denom: config.fee_denom, - swap_router_contract: config.swap_router_contract, + fee_denom: msg.fee_denom, + swap_router_contract: msg.swap_router_contract, fee_receiver: deps.api.addr_validate(&msg.fee_receiver)?, }, )?; @@ -1125,9 +1124,9 @@ mod test { MigrateMsg { default_gas_limit: Some(123456), fee_receiver: "receiver".to_string(), - // default_timeout: 100u64, - // fee_denom: "orai".to_string(), - // swap_router_contract: "foobar".to_string(), + default_timeout: 100u64, + fee_denom: "orai".to_string(), + swap_router_contract: "foobar".to_string(), }, ) .unwrap(); diff --git a/contracts/cw-ics20-latest/src/ibc_tests.rs b/contracts/cw-ics20-latest/src/ibc_tests.rs index 4d002c6..6fa5125 100644 --- a/contracts/cw-ics20-latest/src/ibc_tests.rs +++ b/contracts/cw-ics20-latest/src/ibc_tests.rs @@ -318,9 +318,9 @@ mod test { MigrateMsg { default_gas_limit: Some(def_limit), fee_receiver: "receiver".to_string(), - // default_timeout: 100u64, - // fee_denom: "orai".to_string(), - // swap_router_contract: "foobar".to_string(), + default_timeout: 100u64, + fee_denom: "orai".to_string(), + swap_router_contract: "foobar".to_string(), }, ) .unwrap(); diff --git a/contracts/cw-ics20-latest/src/msg.rs b/contracts/cw-ics20-latest/src/msg.rs index 0201098..e422cb0 100644 --- a/contracts/cw-ics20-latest/src/msg.rs +++ b/contracts/cw-ics20-latest/src/msg.rs @@ -29,11 +29,11 @@ pub struct AllowMsg { #[cw_serde] pub struct MigrateMsg { - // pub default_timeout: u64, + pub default_timeout: u64, pub default_gas_limit: Option, + pub fee_denom: String, + pub swap_router_contract: String, pub fee_receiver: String, - // pub fee_denom: String, - // pub swap_router_contract: String, } #[cw_serde] diff --git a/contracts/cw-ics20-latest/src/state.rs b/contracts/cw-ics20-latest/src/state.rs index ffc67ce..17f58a7 100644 --- a/contracts/cw-ics20-latest/src/state.rs +++ b/contracts/cw-ics20-latest/src/state.rs @@ -8,7 +8,7 @@ use crate::ContractError; pub const ADMIN: Admin = Admin::new("admin"); -pub const CONFIG: Item = Item::new("ics20_config_v11"); +pub const CONFIG: Item = Item::new("ics20_config_v1.0.2"); // Used to pass info from the ibc_packet_receive to the reply handler pub const REPLY_ARGS: Item = Item::new("reply_args"); From 15264e98480ca669a05870afaebf192c518917df Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Tue, 25 Jul 2023 18:31:45 +0700 Subject: [PATCH 02/28] removed forward transfer logic since we are not using it --- contracts/cw-ics20-latest/src/contract.rs | 481 ++++++++++----------- contracts/cw-ics20-latest/src/ibc.rs | 129 +++--- contracts/cw-ics20-latest/src/ibc_tests.rs | 451 ++++++++++--------- contracts/cw-ics20-latest/src/msg.rs | 28 +- contracts/cw-ics20-latest/src/state.rs | 38 +- 5 files changed, 557 insertions(+), 570 deletions(-) diff --git a/contracts/cw-ics20-latest/src/contract.rs b/contracts/cw-ics20-latest/src/contract.rs index 707e6e1..ebee682 100644 --- a/contracts/cw-ics20-latest/src/contract.rs +++ b/contracts/cw-ics20-latest/src/contract.rs @@ -17,12 +17,12 @@ use crate::ibc::{ use crate::msg::{ AllowMsg, AllowedInfo, AllowedResponse, ChannelResponse, ConfigResponse, DeletePairMsg, ExecuteMsg, InitMsg, ListAllowedResponse, ListChannelsResponse, ListMappingResponse, - MigrateMsg, PairQuery, PortResponse, QueryMsg, TransferBackMsg, TransferMsg, UpdatePairMsg, + MigrateMsg, PairQuery, PortResponse, QueryMsg, TransferBackMsg, UpdatePairMsg, }; use crate::state::{ - get_key_ics20_ibc_denom, ics20_denoms, increase_channel_balance, reduce_channel_balance, - AllowInfo, Config, MappingMetadata, TokenFee, ADMIN, ALLOW_LIST, CHANNEL_FORWARD_STATE, - CHANNEL_INFO, CHANNEL_REVERSE_STATE, CONFIG, TOKEN_FEE, + get_key_ics20_ibc_denom, ics20_denoms, reduce_channel_balance, AllowInfo, Config, + MappingMetadata, TokenFee, ADMIN, ALLOW_LIST, CHANNEL_INFO, CHANNEL_REVERSE_STATE, CONFIG, + TOKEN_FEE, }; use cw20_ics20_msg::amount::{convert_local_to_remote, Amount}; use cw_utils::{maybe_addr, nonpayable, one_coin}; @@ -70,10 +70,10 @@ pub fn execute( ) -> Result { match msg { ExecuteMsg::Receive(msg) => execute_receive(deps, env, info, msg), - ExecuteMsg::Transfer(msg) => { - let coin = one_coin(&info)?; - execute_transfer(deps, env, msg, Amount::Native(coin), info.sender) - } + // ExecuteMsg::Transfer(msg) => { + // let coin = one_coin(&info)?; + // execute_transfer(deps, env, msg, Amount::Native(coin), info.sender) + // } ExecuteMsg::TransferToRemote(msg) => { let coin = one_coin(&info)?; let amount = Amount::from_parts(coin.denom, coin.amount); @@ -158,11 +158,11 @@ pub fn execute_receive( }); let api = deps.api; - let msg_result: StdResult = from_binary(&wrapper.msg); - if msg_result.is_ok() { - let msg: TransferMsg = msg_result.unwrap(); - return execute_transfer(deps, env, msg, amount, api.addr_validate(&wrapper.sender)?); - } + // let msg_result: StdResult = from_binary(&wrapper.msg); + // if msg_result.is_ok() { + // let msg: TransferMsg = msg_result.unwrap(); + // return execute_transfer(deps, env, msg, amount, api.addr_validate(&wrapper.sender)?); + // } let msg: TransferBackMsg = from_binary(&wrapper.msg)?; execute_transfer_back_to_remote_chain( @@ -174,79 +174,79 @@ pub fn execute_receive( ) } -pub fn execute_transfer( - deps: DepsMut, - env: Env, - msg: TransferMsg, - amount: Amount, - sender: Addr, -) -> Result { - if amount.is_empty() { - return Err(ContractError::NoFunds {}); - } - // ensure the requested channel is registered - if !CHANNEL_INFO.has(deps.storage, &msg.channel) { - return Err(ContractError::NoSuchChannel { id: msg.channel }); - } - let config = CONFIG.load(deps.storage)?; - - // if cw20 token, validate and ensure it is whitelisted, or we set default gas limit - if let Amount::Cw20(coin) = &amount { - let addr = deps.api.addr_validate(&coin.address)?; - // if limit is set, then we always allow cw20 - if config.default_gas_limit.is_none() { - ALLOW_LIST - .may_load(deps.storage, &addr)? - .ok_or(ContractError::NotOnAllowList)?; - } - }; - - // delta from user is in seconds - let timeout_delta = match msg.timeout { - Some(t) => t, - None => config.default_timeout, - }; - // timeout is in nanoseconds - let timeout = env.block.time.plus_seconds(timeout_delta); - - // build ics20 packet - let packet = Ics20Packet::new( - amount.amount(), - amount.denom(), - sender.as_ref(), - &msg.remote_address, - msg.memo, - ); - packet.validate()?; - - // Update the balance now (optimistically) like ibctransfer modules. - // In on_packet_failure (ack with error message or a timeout), we reduce the balance appropriately. - // This means the channel works fine if success acks are not relayed. - increase_channel_balance( - deps.storage, - &msg.channel, - &amount.denom(), - amount.amount(), - true, - )?; - - // prepare ibc message - let msg = IbcMsg::SendPacket { - channel_id: msg.channel, - data: to_binary(&packet)?, - timeout: timeout.into(), - }; - - // send response - let res = Response::new() - .add_message(msg) - .add_attribute("action", "transfer") - .add_attribute("sender", &packet.sender) - .add_attribute("receiver", &packet.receiver) - .add_attribute("denom", &packet.denom) - .add_attribute("amount", &packet.amount.to_string()); - Ok(res) -} +// pub fn execute_transfer( +// deps: DepsMut, +// env: Env, +// msg: TransferMsg, +// amount: Amount, +// sender: Addr, +// ) -> Result { +// if amount.is_empty() { +// return Err(ContractError::NoFunds {}); +// } +// // ensure the requested channel is registered +// if !CHANNEL_INFO.has(deps.storage, &msg.channel) { +// return Err(ContractError::NoSuchChannel { id: msg.channel }); +// } +// let config = CONFIG.load(deps.storage)?; + +// // if cw20 token, validate and ensure it is whitelisted, or we set default gas limit +// if let Amount::Cw20(coin) = &amount { +// let addr = deps.api.addr_validate(&coin.address)?; +// // if limit is set, then we always allow cw20 +// if config.default_gas_limit.is_none() { +// ALLOW_LIST +// .may_load(deps.storage, &addr)? +// .ok_or(ContractError::NotOnAllowList)?; +// } +// }; + +// // delta from user is in seconds +// let timeout_delta = match msg.timeout { +// Some(t) => t, +// None => config.default_timeout, +// }; +// // timeout is in nanoseconds +// let timeout = env.block.time.plus_seconds(timeout_delta); + +// // build ics20 packet +// let packet = Ics20Packet::new( +// amount.amount(), +// amount.denom(), +// sender.as_ref(), +// &msg.remote_address, +// msg.memo, +// ); +// packet.validate()?; + +// // Update the balance now (optimistically) like ibctransfer modules. +// // In on_packet_failure (ack with error message or a timeout), we reduce the balance appropriately. +// // This means the channel works fine if success acks are not relayed. +// increase_channel_balance( +// deps.storage, +// &msg.channel, +// &amount.denom(), +// amount.amount(), +// true, +// )?; + +// // prepare ibc message +// let msg = IbcMsg::SendPacket { +// channel_id: msg.channel, +// data: to_binary(&packet)?, +// timeout: timeout.into(), +// }; + +// // send response +// let res = Response::new() +// .add_message(msg) +// .add_attribute("action", "transfer") +// .add_attribute("sender", &packet.sender) +// .add_attribute("receiver", &packet.receiver) +// .add_attribute("denom", &packet.denom) +// .add_attribute("amount", &packet.amount.to_string()); +// Ok(res) +// } pub fn execute_transfer_back_to_remote_chain( deps: DepsMut, @@ -527,14 +527,10 @@ fn query_list(deps: Deps) -> StdResult { } // make public for ibc tests -pub fn query_channel(deps: Deps, id: String, forward: Option) -> StdResult { +pub fn query_channel(deps: Deps, id: String, _forward: Option) -> StdResult { let info = CHANNEL_INFO.load(deps.storage, &id)?; // this returns Vec<(outstanding, total)> - let channel_state = if forward.is_some() { - CHANNEL_FORWARD_STATE - } else { - CHANNEL_REVERSE_STATE - }; + let channel_state = CHANNEL_REVERSE_STATE; let state = channel_state .prefix(&id) .range(deps.storage, None, None, Order::Ascending) @@ -688,13 +684,12 @@ mod test { use cosmwasm_std::testing::{mock_env, mock_info}; use cosmwasm_std::{ - coin, coins, CosmosMsg, Decimal, IbcEndpoint, IbcMsg, IbcPacket, IbcPacketReceiveMsg, - StdError, Timestamp, Uint128, WasmMsg, + coins, CosmosMsg, Decimal, IbcEndpoint, IbcMsg, IbcPacket, IbcPacketReceiveMsg, StdError, + Timestamp, Uint128, WasmMsg, }; use cw20::Cw20ExecuteMsg; use cw_controllers::AdminError; - use cw_utils::PaymentError; use oraiswap::asset::AssetInfo; #[test] @@ -986,154 +981,154 @@ mod test { assert_eq!(response.pairs.len(), 0) } - #[test] - fn proper_checks_on_execute_native() { - let send_channel = "channel-5"; - let mut deps = setup(&[send_channel, "channel-10"], &[]); - - let mut transfer = TransferMsg { - channel: send_channel.to_string(), - remote_address: "foreign-address".to_string(), - timeout: None, - memo: Some("memo".to_string()), - }; - - // works with proper funds - let msg = ExecuteMsg::Transfer(transfer.clone()); - let info = mock_info("foobar", &coins(1234567, "ucosm")); - let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); - assert_eq!(res.messages[0].gas_limit, None); - assert_eq!(1, res.messages.len()); - if let CosmosMsg::Ibc(IbcMsg::SendPacket { - channel_id, - data, - timeout, - }) = &res.messages[0].msg - { - let expected_timeout = mock_env().block.time.plus_seconds(DEFAULT_TIMEOUT); - assert_eq!(timeout, &expected_timeout.into()); - assert_eq!(channel_id.as_str(), send_channel); - let msg: Ics20Packet = from_binary(data).unwrap(); - assert_eq!(msg.amount, Uint128::new(1234567)); - assert_eq!(msg.denom.as_str(), "ucosm"); - assert_eq!(msg.sender.as_str(), "foobar"); - assert_eq!(msg.receiver.as_str(), "foreign-address"); - } else { - panic!("Unexpected return message: {:?}", res.messages[0]); - } - - // reject with no funds - let msg = ExecuteMsg::Transfer(transfer.clone()); - let info = mock_info("foobar", &[]); - let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); - assert_eq!(err, ContractError::Payment(PaymentError::NoFunds {})); - - // reject with multiple tokens funds - let msg = ExecuteMsg::Transfer(transfer.clone()); - let info = mock_info("foobar", &[coin(1234567, "ucosm"), coin(54321, "uatom")]); - let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); - assert_eq!(err, ContractError::Payment(PaymentError::MultipleDenoms {})); - - // reject with bad channel id - transfer.channel = "channel-45".to_string(); - let msg = ExecuteMsg::Transfer(transfer); - let info = mock_info("foobar", &coins(1234567, "ucosm")); - let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); - assert_eq!( - err, - ContractError::NoSuchChannel { - id: "channel-45".to_string() - } - ); - } - - #[test] - fn proper_checks_on_execute_cw20() { - let send_channel = "channel-15"; - let cw20_addr = "my-token"; - let mut deps = setup(&["channel-3", send_channel], &[(cw20_addr, 123456)]); - - let transfer = TransferMsg { - channel: send_channel.to_string(), - remote_address: "foreign-address".to_string(), - timeout: Some(7777), - memo: Some("memo".to_string()), - }; - let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { - sender: "my-account".into(), - amount: Uint128::new(888777666), - msg: to_binary(&transfer).unwrap(), - }); - - // works with proper funds - let info = mock_info(cw20_addr, &[]); - let res = execute(deps.as_mut(), mock_env(), info, msg.clone()).unwrap(); - assert_eq!(1, res.messages.len()); - assert_eq!(res.messages[0].gas_limit, None); - if let CosmosMsg::Ibc(IbcMsg::SendPacket { - channel_id, - data, - timeout, - }) = &res.messages[0].msg - { - let expected_timeout = mock_env().block.time.plus_seconds(7777); - assert_eq!(timeout, &expected_timeout.into()); - assert_eq!(channel_id.as_str(), send_channel); - let msg: Ics20Packet = from_binary(data).unwrap(); - assert_eq!(msg.amount, Uint128::new(888777666)); - assert_eq!(msg.denom, format!("cw20:{}", cw20_addr)); - assert_eq!(msg.sender.as_str(), "my-account"); - assert_eq!(msg.receiver.as_str(), "foreign-address"); - } else { - panic!("Unexpected return message: {:?}", res.messages[0]); - } - - // reject with tokens funds - let info = mock_info("foobar", &coins(1234567, "ucosm")); - let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); - assert_eq!(err, ContractError::Payment(PaymentError::NonPayable {})); - } - - #[test] - fn execute_cw20_fails_if_not_whitelisted_unless_default_gas_limit() { - let send_channel = "channel-15"; - let mut deps = setup(&[send_channel], &[]); - - let cw20_addr = "my-token"; - let transfer = TransferMsg { - channel: send_channel.to_string(), - remote_address: "foreign-address".to_string(), - timeout: Some(7777), - memo: Some("memo".to_string()), - }; - let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { - sender: "my-account".into(), - amount: Uint128::new(888777666), - msg: to_binary(&transfer).unwrap(), - }); - - // rejected as not on allow list - let info = mock_info(cw20_addr, &[]); - let err = execute(deps.as_mut(), mock_env(), info.clone(), msg.clone()).unwrap_err(); - assert_eq!(err, ContractError::NotOnAllowList); - - // add a default gas limit - migrate( - deps.as_mut(), - mock_env(), - MigrateMsg { - default_gas_limit: Some(123456), - fee_receiver: "receiver".to_string(), - default_timeout: 100u64, - fee_denom: "orai".to_string(), - swap_router_contract: "foobar".to_string(), - }, - ) - .unwrap(); - - // try again - execute(deps.as_mut(), mock_env(), info, msg).unwrap(); - } + // #[test] + // fn proper_checks_on_execute_native() { + // let send_channel = "channel-5"; + // let mut deps = setup(&[send_channel, "channel-10"], &[]); + + // let mut transfer = TransferMsg { + // channel: send_channel.to_string(), + // remote_address: "foreign-address".to_string(), + // timeout: None, + // memo: Some("memo".to_string()), + // }; + + // // works with proper funds + // let msg = ExecuteMsg::Transfer(transfer.clone()); + // let info = mock_info("foobar", &coins(1234567, "ucosm")); + // let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + // assert_eq!(res.messages[0].gas_limit, None); + // assert_eq!(1, res.messages.len()); + // if let CosmosMsg::Ibc(IbcMsg::SendPacket { + // channel_id, + // data, + // timeout, + // }) = &res.messages[0].msg + // { + // let expected_timeout = mock_env().block.time.plus_seconds(DEFAULT_TIMEOUT); + // assert_eq!(timeout, &expected_timeout.into()); + // assert_eq!(channel_id.as_str(), send_channel); + // let msg: Ics20Packet = from_binary(data).unwrap(); + // assert_eq!(msg.amount, Uint128::new(1234567)); + // assert_eq!(msg.denom.as_str(), "ucosm"); + // assert_eq!(msg.sender.as_str(), "foobar"); + // assert_eq!(msg.receiver.as_str(), "foreign-address"); + // } else { + // panic!("Unexpected return message: {:?}", res.messages[0]); + // } + + // // reject with no funds + // let msg = ExecuteMsg::Transfer(transfer.clone()); + // let info = mock_info("foobar", &[]); + // let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); + // assert_eq!(err, ContractError::Payment(PaymentError::NoFunds {})); + + // // reject with multiple tokens funds + // let msg = ExecuteMsg::Transfer(transfer.clone()); + // let info = mock_info("foobar", &[coin(1234567, "ucosm"), coin(54321, "uatom")]); + // let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); + // assert_eq!(err, ContractError::Payment(PaymentError::MultipleDenoms {})); + + // // reject with bad channel id + // transfer.channel = "channel-45".to_string(); + // let msg = ExecuteMsg::Transfer(transfer); + // let info = mock_info("foobar", &coins(1234567, "ucosm")); + // let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); + // assert_eq!( + // err, + // ContractError::NoSuchChannel { + // id: "channel-45".to_string() + // } + // ); + // } + + // #[test] + // fn proper_checks_on_execute_cw20() { + // let send_channel = "channel-15"; + // let cw20_addr = "my-token"; + // let mut deps = setup(&["channel-3", send_channel], &[(cw20_addr, 123456)]); + + // let transfer = TransferMsg { + // channel: send_channel.to_string(), + // remote_address: "foreign-address".to_string(), + // timeout: Some(7777), + // memo: Some("memo".to_string()), + // }; + // let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { + // sender: "my-account".into(), + // amount: Uint128::new(888777666), + // msg: to_binary(&transfer).unwrap(), + // }); + + // // works with proper funds + // let info = mock_info(cw20_addr, &[]); + // let res = execute(deps.as_mut(), mock_env(), info, msg.clone()).unwrap(); + // assert_eq!(1, res.messages.len()); + // assert_eq!(res.messages[0].gas_limit, None); + // if let CosmosMsg::Ibc(IbcMsg::SendPacket { + // channel_id, + // data, + // timeout, + // }) = &res.messages[0].msg + // { + // let expected_timeout = mock_env().block.time.plus_seconds(7777); + // assert_eq!(timeout, &expected_timeout.into()); + // assert_eq!(channel_id.as_str(), send_channel); + // let msg: Ics20Packet = from_binary(data).unwrap(); + // assert_eq!(msg.amount, Uint128::new(888777666)); + // assert_eq!(msg.denom, format!("cw20:{}", cw20_addr)); + // assert_eq!(msg.sender.as_str(), "my-account"); + // assert_eq!(msg.receiver.as_str(), "foreign-address"); + // } else { + // panic!("Unexpected return message: {:?}", res.messages[0]); + // } + + // // reject with tokens funds + // let info = mock_info("foobar", &coins(1234567, "ucosm")); + // let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); + // assert_eq!(err, ContractError::Payment(PaymentError::NonPayable {})); + // } + + // #[test] + // fn execute_cw20_fails_if_not_whitelisted_unless_default_gas_limit() { + // let send_channel = "channel-15"; + // let mut deps = setup(&[send_channel], &[]); + + // let cw20_addr = "my-token"; + // let transfer = TransferMsg { + // channel: send_channel.to_string(), + // remote_address: "foreign-address".to_string(), + // timeout: Some(7777), + // memo: Some("memo".to_string()), + // }; + // let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { + // sender: "my-account".into(), + // amount: Uint128::new(888777666), + // msg: to_binary(&transfer).unwrap(), + // }); + + // // rejected as not on allow list + // let info = mock_info(cw20_addr, &[]); + // let err = execute(deps.as_mut(), mock_env(), info.clone(), msg.clone()).unwrap_err(); + // assert_eq!(err, ContractError::NotOnAllowList); + + // // add a default gas limit + // migrate( + // deps.as_mut(), + // mock_env(), + // MigrateMsg { + // default_gas_limit: Some(123456), + // fee_receiver: "receiver".to_string(), + // default_timeout: 100u64, + // fee_denom: "orai".to_string(), + // swap_router_contract: "foobar".to_string(), + // }, + // ) + // .unwrap(); + + // // try again + // execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + // } // test execute transfer back to native remote chain fn mock_receive_packet( diff --git a/contracts/cw-ics20-latest/src/ibc.rs b/contracts/cw-ics20-latest/src/ibc.rs index 055aefd..d890d5b 100644 --- a/contracts/cw-ics20-latest/src/ibc.rs +++ b/contracts/cw-ics20-latest/src/ibc.rs @@ -90,47 +90,46 @@ pub fn ack_fail(err: String) -> Binary { to_binary(&res).unwrap() } -pub const RECEIVE_ID: u64 = 1337; +// pub const RECEIVE_ID: u64 = 1337; pub const NATIVE_RECEIVE_ID: u64 = 1338; pub const FOLLOW_UP_FAILURE_ID: u64 = 1339; pub const REFUND_FAILURE_ID: u64 = 1340; pub const ACK_FAILURE_ID: u64 = 64023; -// const TRANSFER_BACK_FAILURE_ID: u64 = 1339; #[entry_point] pub fn reply(deps: DepsMut, _env: Env, reply: Reply) -> Result { match reply.id { - RECEIVE_ID => match reply.result { - SubMsgResult::Ok(_) => Ok(Response::new()), - SubMsgResult::Err(err) => { - // Important design note: with ibcv2 and wasmd 0.22 we can implement this all much easier. - // No reply needed... the receive function and submessage should return error on failure and all - // state gets reverted with a proper app-level message auto-generated - - // Since we need compatibility with Juno (Jan 2022), we need to ensure that optimisitic - // state updates in ibc_packet_receive get reverted in the (unlikely) chance of an - // error while sending the token - - // However, this requires passing some state between the ibc_packet_receive function and - // the reply handler. We do this with a singleton, with is "okay" for IBC as there is no - // reentrancy on these functions (cannot be called by another contract). This pattern - // should not be used for ExecuteMsg handlers - let reply_args = REPLY_ARGS.load(deps.storage)?; - undo_reduce_channel_balance( - deps.storage, - &reply_args.channel, - &reply_args.denom, - reply_args.amount, - true, - )?; - - Ok(Response::new().set_data(ack_fail(err)).add_attributes(vec![ - attr("undo_reduce_channel", reply_args.channel), - attr("undo_reduce_channel_ibc_denom", reply_args.denom), - attr("undo_reduce_channel_amount", reply_args.amount), - ])) - } - }, + // RECEIVE_ID => match reply.result { + // SubMsgResult::Ok(_) => Ok(Response::new()), + // SubMsgResult::Err(err) => { + // // Important design note: with ibcv2 and wasmd 0.22 we can implement this all much easier. + // // No reply needed... the receive function and submessage should return error on failure and all + // // state gets reverted with a proper app-level message auto-generated + + // // Since we need compatibility with Juno (Jan 2022), we need to ensure that optimisitic + // // state updates in ibc_packet_receive get reverted in the (unlikely) chance of an + // // error while sending the token + + // // However, this requires passing some state between the ibc_packet_receive function and + // // the reply handler. We do this with a singleton, with is "okay" for IBC as there is no + // // reentrancy on these functions (cannot be called by another contract). This pattern + // // should not be used for ExecuteMsg handlers + // let reply_args = REPLY_ARGS.load(deps.storage)?; + // undo_reduce_channel_balance( + // deps.storage, + // &reply_args.channel, + // &reply_args.denom, + // reply_args.amount, + // true, + // )?; + + // Ok(Response::new().set_data(ack_fail(err)).add_attributes(vec![ + // attr("undo_reduce_channel", reply_args.channel), + // attr("undo_reduce_channel_ibc_denom", reply_args.denom), + // attr("undo_reduce_channel_amount", reply_args.amount), + // ])) + // } + // }, NATIVE_RECEIVE_ID => match reply.result { SubMsgResult::Ok(_) => Ok(Response::new()), SubMsgResult::Err(err) => { @@ -182,12 +181,6 @@ pub fn reply(deps: DepsMut, _env: Env, reply: Reply) -> Result match reply.result { - // SubMsgResult::Ok(_) => Ok(Response::new()), - // SubMsgResult::Err(err) => Ok(Response::new() - // .set_data(ack_fail(err.clone())) - // .add_attribute("error_refund_cw20_tokens", err)), - // }, _ => Err(ContractError::UnknownReplyId { id: reply.id }), } } @@ -328,7 +321,7 @@ fn do_ibc_packet_receive( packet: &IbcPacket, ) -> Result { let msg: Ics20Packet = from_binary(&packet.data)?; - let channel = packet.dest.channel_id.clone(); + // let channel = packet.dest.channel_id.clone(); // If the token originated on the remote chain, it looks like "ucosm". // If it originated on our chain, it looks like "port/channel/ucosm". @@ -347,34 +340,34 @@ fn do_ibc_packet_receive( ); } - // make sure we have enough balance for this - reduce_channel_balance(deps.storage, &channel, denom.0, msg.amount, true)?; - - // we need to save the data to update the balances in reply - let reply_args = ReplyArgs { - channel, - denom: denom.0.to_string(), - amount: msg.amount, - }; - REPLY_ARGS.save(deps.storage, &reply_args)?; - - let to_send = Amount::from_parts(denom.0.to_string(), msg.amount); - let gas_limit = check_gas_limit(deps.as_ref(), &to_send)?; - let send = send_amount(to_send, msg.receiver.clone(), None); - let mut submsg = SubMsg::reply_on_error(send, RECEIVE_ID); - submsg.gas_limit = gas_limit; - - let res = IbcReceiveResponse::new() - .set_ack(ack_success()) - .add_submessage(submsg) - .add_attribute("action", "receive") - .add_attribute("sender", msg.sender) - .add_attribute("receiver", msg.receiver) - .add_attribute("denom", denom.0) - .add_attribute("amount", msg.amount) - .add_attribute("success", "true"); - - Ok(res) + // // make sure we have enough balance for this + // reduce_channel_balance(deps.storage, &channel, denom.0, msg.amount, true)?; + + // // we need to save the data to update the balances in reply + // let reply_args = ReplyArgs { + // channel, + // denom: denom.0.to_string(), + // amount: msg.amount, + // }; + // REPLY_ARGS.save(deps.storage, &reply_args)?; + + // let to_send = Amount::from_parts(denom.0.to_string(), msg.amount); + // let gas_limit = check_gas_limit(deps.as_ref(), &to_send)?; + // let send = send_amount(to_send, msg.receiver.clone(), None); + // let mut submsg = SubMsg::reply_on_error(send, RECEIVE_ID); + // submsg.gas_limit = gas_limit; + + // let res = IbcReceiveResponse::new() + // .set_ack(ack_success()) + // .add_submessage(submsg) + // .add_attribute("action", "receive") + // .add_attribute("sender", msg.sender) + // .add_attribute("receiver", msg.receiver) + // .add_attribute("denom", denom.0) + // .add_attribute("amount", msg.amount) + // .add_attribute("success", "true"); + + Err(ContractError::Std(StdError::generic_err("Not suppported"))) } fn handle_ibc_packet_receive_native_remote_chain( diff --git a/contracts/cw-ics20-latest/src/ibc_tests.rs b/contracts/cw-ics20-latest/src/ibc_tests.rs index 6fa5125..fbef91f 100644 --- a/contracts/cw-ics20-latest/src/ibc_tests.rs +++ b/contracts/cw-ics20-latest/src/ibc_tests.rs @@ -9,13 +9,13 @@ mod test { ack_fail, build_ibc_msg, build_swap_msgs, check_gas_limit, deduct_fee, handle_follow_up_failure, ibc_packet_receive, is_follow_up_msgs_only_send_amount, parse_voucher_denom, parse_voucher_denom_without_sanity_checks, process_deduct_fee, - send_amount, Ics20Ack, Ics20Packet, RECEIVE_ID, REFUND_FAILURE_ID, + send_amount, Ics20Ack, Ics20Packet, REFUND_FAILURE_ID, }; use crate::ibc::{build_swap_operations, get_follow_up_msgs}; use crate::test_helpers::*; use cosmwasm_std::{ - from_binary, to_binary, BankMsg, IbcEndpoint, IbcMsg, IbcPacket, IbcPacketReceiveMsg, - IbcTimeout, SubMsg, Timestamp, Uint128, WasmMsg, + from_binary, to_binary, IbcEndpoint, IbcMsg, IbcPacket, IbcPacketReceiveMsg, SubMsg, + Timestamp, Uint128, WasmMsg, }; use crate::error::ContractError; @@ -28,10 +28,9 @@ mod test { use cw20_ics20_msg::amount::{convert_local_to_remote, Amount}; use crate::contract::{execute, migrate, query_channel}; - use crate::msg::{ExecuteMsg, MigrateMsg, TransferMsg, UpdatePairMsg}; + use crate::msg::{ExecuteMsg, MigrateMsg, UpdatePairMsg}; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; use cosmwasm_std::{coins, to_vec}; - use cw20::Cw20ReceiveMsg; #[test] fn check_ack_json() { @@ -61,239 +60,239 @@ mod test { assert_eq!(expected, encdoded.as_str()); } - fn cw20_payment( - amount: u128, - address: &str, - recipient: &str, - gas_limit: Option, - ) -> SubMsg { - let msg = Cw20ExecuteMsg::Transfer { - recipient: recipient.into(), - amount: Uint128::new(amount), - }; - let exec = WasmMsg::Execute { - contract_addr: address.into(), - msg: to_binary(&msg).unwrap(), - funds: vec![], - }; - let mut msg = SubMsg::reply_on_error(exec, RECEIVE_ID); - msg.gas_limit = gas_limit; - msg - } - - fn native_payment(amount: u128, denom: &str, recipient: &str) -> SubMsg { - SubMsg::reply_on_error( - BankMsg::Send { - to_address: recipient.into(), - amount: coins(amount, denom), - }, - RECEIVE_ID, - ) - } - - fn mock_receive_packet( - my_channel: &str, - amount: u128, - denom: &str, - receiver: &str, - ) -> IbcPacket { - let data = Ics20Packet { - // this is returning a foreign (our) token, thus denom is // - denom: format!("{}/{}/{}", REMOTE_PORT, "channel-1234", denom), - amount: amount.into(), - sender: "remote-sender".to_string(), - receiver: receiver.to_string(), - memo: None, - }; - IbcPacket::new( - to_binary(&data).unwrap(), - IbcEndpoint { - port_id: REMOTE_PORT.to_string(), - channel_id: "channel-1234".to_string(), - }, - IbcEndpoint { - port_id: CONTRACT_PORT.to_string(), - channel_id: my_channel.to_string(), - }, - 3, - Timestamp::from_seconds(1665321069).into(), - ) - } - - #[test] - fn send_receive_cw20() { - let send_channel = "channel-9"; - let cw20_addr = "token-addr"; - let cw20_denom = "cw20:token-addr"; - let gas_limit = 1234567; - let mut deps = setup( - &["channel-1", "channel-7", send_channel], - &[(cw20_addr, gas_limit)], - ); - - // prepare some mock packets - let recv_packet = mock_receive_packet(send_channel, 876543210, cw20_denom, "local-rcpt"); - let recv_high_packet = - mock_receive_packet(send_channel, 1876543210, cw20_denom, "local-rcpt"); - - // cannot receive this denom yet - let msg = IbcPacketReceiveMsg::new(recv_packet.clone()); - let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap(); - assert!(res.messages.is_empty()); - let ack: Ics20Ack = from_binary(&res.acknowledgement).unwrap(); - let no_funds = Ics20Ack::Error( - ContractError::NoSuchChannelState { - id: send_channel.to_string(), - denom: cw20_denom.to_string(), - } - .to_string(), - ); - assert_eq!(ack, no_funds); - - // we send some cw20 tokens over - let transfer = TransferMsg { - channel: send_channel.to_string(), - remote_address: "remote-rcpt".to_string(), - timeout: None, - memo: None, - }; - let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { - sender: "local-sender".to_string(), - amount: Uint128::new(987654321), - msg: to_binary(&transfer).unwrap(), - }); - let info = mock_info(cw20_addr, &[]); - let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); - assert_eq!(1, res.messages.len()); - let expected = Ics20Packet { - denom: cw20_denom.into(), - amount: Uint128::new(987654321), - sender: "local-sender".to_string(), - receiver: "remote-rcpt".to_string(), - memo: None, - }; - let timeout = mock_env().block.time.plus_seconds(DEFAULT_TIMEOUT); - assert_eq!( - &res.messages[0], - &SubMsg::new(IbcMsg::SendPacket { - channel_id: send_channel.to_string(), - data: to_binary(&expected).unwrap(), - timeout: IbcTimeout::with_timestamp(timeout), - }) - ); - - // query channel state|_| - let state = query_channel(deps.as_ref(), send_channel.to_string(), Some(true)).unwrap(); - assert_eq!(state.balances, vec![Amount::cw20(987654321, cw20_addr)]); - assert_eq!(state.total_sent, vec![Amount::cw20(987654321, cw20_addr)]); + // fn cw20_payment( + // amount: u128, + // address: &str, + // recipient: &str, + // gas_limit: Option, + // ) -> SubMsg { + // let msg = Cw20ExecuteMsg::Transfer { + // recipient: recipient.into(), + // amount: Uint128::new(amount), + // }; + // let exec = WasmMsg::Execute { + // contract_addr: address.into(), + // msg: to_binary(&msg).unwrap(), + // funds: vec![], + // }; + // let mut msg = SubMsg::reply_on_error(exec, RECEIVE_ID); + // msg.gas_limit = gas_limit; + // msg + // } - // cannot receive more than we sent - let msg = IbcPacketReceiveMsg::new(recv_high_packet); - let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap(); - assert!(res.messages.is_empty()); - let ack: Ics20Ack = from_binary(&res.acknowledgement).unwrap(); - assert_eq!( - ack, - Ics20Ack::Error( - ContractError::InsufficientFunds { - id: send_channel.to_string(), - denom: cw20_denom.to_string(), - } - .to_string(), - ) - ); + // fn _native_payment(amount: u128, denom: &str, recipient: &str) -> SubMsg { + // SubMsg::reply_on_error( + // BankMsg::Send { + // to_address: recipient.into(), + // amount: coins(amount, denom), + // }, + // RECEIVE_ID, + // ) + // } - // we can receive less than we sent - let msg = IbcPacketReceiveMsg::new(recv_packet); - let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap(); - assert_eq!(1, res.messages.len()); - assert_eq!( - cw20_payment(876543210, cw20_addr, "local-rcpt", Some(gas_limit)), - res.messages[0] - ); - let ack: Ics20Ack = from_binary(&res.acknowledgement).unwrap(); - assert!(matches!(ack, Ics20Ack::Result(_))); + // fn mock_receive_packet( + // my_channel: &str, + // amount: u128, + // denom: &str, + // receiver: &str, + // ) -> IbcPacket { + // let data = Ics20Packet { + // // this is returning a foreign (our) token, thus denom is // + // denom: format!("{}/{}/{}", REMOTE_PORT, "channel-1234", denom), + // amount: amount.into(), + // sender: "remote-sender".to_string(), + // receiver: receiver.to_string(), + // memo: None, + // }; + // IbcPacket::new( + // to_binary(&data).unwrap(), + // IbcEndpoint { + // port_id: REMOTE_PORT.to_string(), + // channel_id: "channel-1234".to_string(), + // }, + // IbcEndpoint { + // port_id: CONTRACT_PORT.to_string(), + // channel_id: my_channel.to_string(), + // }, + // 3, + // Timestamp::from_seconds(1665321069).into(), + // ) + // } - // query channel state - let state = query_channel(deps.as_ref(), send_channel.to_string(), Some(true)).unwrap(); - assert_eq!(state.balances, vec![Amount::cw20(111111111, cw20_addr)]); - assert_eq!(state.total_sent, vec![Amount::cw20(987654321, cw20_addr)]); - } + // #[test] + // fn send_receive_cw20() { + // let send_channel = "channel-9"; + // let cw20_addr = "token-addr"; + // let cw20_denom = "cw20:token-addr"; + // let gas_limit = 1234567; + // let mut deps = setup( + // &["channel-1", "channel-7", send_channel], + // &[(cw20_addr, gas_limit)], + // ); - #[test] - fn send_receive_native() { - let send_channel = "channel-9"; - let mut deps = setup(&["channel-1", "channel-7", send_channel], &[]); + // // prepare some mock packets + // let recv_packet = mock_receive_packet(send_channel, 876543210, cw20_denom, "local-rcpt"); + // let recv_high_packet = + // mock_receive_packet(send_channel, 1876543210, cw20_denom, "local-rcpt"); + + // // cannot receive this denom yet + // let msg = IbcPacketReceiveMsg::new(recv_packet.clone()); + // let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap(); + // assert!(res.messages.is_empty()); + // let ack: Ics20Ack = from_binary(&res.acknowledgement).unwrap(); + // let no_funds = Ics20Ack::Error( + // ContractError::NoSuchChannelState { + // id: send_channel.to_string(), + // denom: cw20_denom.to_string(), + // } + // .to_string(), + // ); + // assert_eq!(ack, no_funds); + + // // we send some cw20 tokens over + // let transfer = TransferMsg { + // channel: send_channel.to_string(), + // remote_address: "remote-rcpt".to_string(), + // timeout: None, + // memo: None, + // }; + // let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { + // sender: "local-sender".to_string(), + // amount: Uint128::new(987654321), + // msg: to_binary(&transfer).unwrap(), + // }); + // let info = mock_info(cw20_addr, &[]); + // let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + // assert_eq!(1, res.messages.len()); + // let expected = Ics20Packet { + // denom: cw20_denom.into(), + // amount: Uint128::new(987654321), + // sender: "local-sender".to_string(), + // receiver: "remote-rcpt".to_string(), + // memo: None, + // }; + // let timeout = mock_env().block.time.plus_seconds(DEFAULT_TIMEOUT); + // assert_eq!( + // &res.messages[0], + // &SubMsg::new(IbcMsg::SendPacket { + // channel_id: send_channel.to_string(), + // data: to_binary(&expected).unwrap(), + // timeout: IbcTimeout::with_timestamp(timeout), + // }) + // ); - let denom = "uatom"; + // // query channel state|_| + // let state = query_channel(deps.as_ref(), send_channel.to_string(), Some(true)).unwrap(); + // assert_eq!(state.balances, vec![Amount::cw20(987654321, cw20_addr)]); + // assert_eq!(state.total_sent, vec![Amount::cw20(987654321, cw20_addr)]); - // prepare some mock packets - let recv_packet = mock_receive_packet(send_channel, 876543210, denom, "local-rcpt"); - let recv_high_packet = mock_receive_packet(send_channel, 1876543210, denom, "local-rcpt"); + // // cannot receive more than we sent + // let msg = IbcPacketReceiveMsg::new(recv_high_packet); + // let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap(); + // assert!(res.messages.is_empty()); + // let ack: Ics20Ack = from_binary(&res.acknowledgement).unwrap(); + // assert_eq!( + // ack, + // Ics20Ack::Error( + // ContractError::InsufficientFunds { + // id: send_channel.to_string(), + // denom: cw20_denom.to_string(), + // } + // .to_string(), + // ) + // ); - // cannot receive this denom yet - let msg = IbcPacketReceiveMsg::new(recv_packet.clone()); - let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap(); - assert!(res.messages.is_empty()); - let ack: Ics20Ack = from_binary(&res.acknowledgement).unwrap(); - let no_funds = Ics20Ack::Error( - ContractError::NoSuchChannelState { - id: send_channel.to_string(), - denom: denom.to_string(), - } - .to_string(), - ); - assert_eq!(ack, no_funds); - - // we transfer some tokens - let msg = ExecuteMsg::Transfer(TransferMsg { - channel: send_channel.to_string(), - remote_address: "my-remote-address".to_string(), - timeout: None, - memo: Some("memo".to_string()), - }); - let info = mock_info("local-sender", &coins(987654321, denom)); - execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + // // we can receive less than we sent + // let msg = IbcPacketReceiveMsg::new(recv_packet); + // let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap(); + // assert_eq!(1, res.messages.len()); + // assert_eq!( + // cw20_payment(876543210, cw20_addr, "local-rcpt", Some(gas_limit)), + // res.messages[0] + // ); + // let ack: Ics20Ack = from_binary(&res.acknowledgement).unwrap(); + // assert!(matches!(ack, Ics20Ack::Result(_))); - // query channel state|_| - let state = query_channel(deps.as_ref(), send_channel.to_string(), Some(true)).unwrap(); - assert_eq!(state.balances, vec![Amount::native(987654321, denom)]); - assert_eq!(state.total_sent, vec![Amount::native(987654321, denom)]); + // // query channel state + // let state = query_channel(deps.as_ref(), send_channel.to_string(), Some(true)).unwrap(); + // assert_eq!(state.balances, vec![Amount::cw20(111111111, cw20_addr)]); + // assert_eq!(state.total_sent, vec![Amount::cw20(987654321, cw20_addr)]); + // } - // cannot receive more than we sent - let msg = IbcPacketReceiveMsg::new(recv_high_packet); - let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap(); - assert!(res.messages.is_empty()); - let ack: Ics20Ack = from_binary(&res.acknowledgement).unwrap(); - assert_eq!( - ack, - Ics20Ack::Error( - ContractError::InsufficientFunds { - id: send_channel.to_string(), - denom: denom.to_string(), - } - .to_string(), - ) - ); + // #[test] + // fn send_receive_native() { + // let send_channel = "channel-9"; + // let mut deps = setup(&["channel-1", "channel-7", send_channel], &[]); + + // let denom = "uatom"; + + // // prepare some mock packets + // let recv_packet = mock_receive_packet(send_channel, 876543210, denom, "local-rcpt"); + // let recv_high_packet = mock_receive_packet(send_channel, 1876543210, denom, "local-rcpt"); + + // // cannot receive this denom yet + // let msg = IbcPacketReceiveMsg::new(recv_packet.clone()); + // let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap(); + // assert!(res.messages.is_empty()); + // let ack: Ics20Ack = from_binary(&res.acknowledgement).unwrap(); + // let no_funds = Ics20Ack::Error( + // ContractError::NoSuchChannelState { + // id: send_channel.to_string(), + // denom: denom.to_string(), + // } + // .to_string(), + // ); + // assert_eq!(ack, no_funds); + + // // we transfer some tokens + // let msg = ExecuteMsg::Transfer(TransferMsg { + // channel: send_channel.to_string(), + // remote_address: "my-remote-address".to_string(), + // timeout: None, + // memo: Some("memo".to_string()), + // }); + // let info = mock_info("local-sender", &coins(987654321, denom)); + // execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + + // // query channel state|_| + // let state = query_channel(deps.as_ref(), send_channel.to_string(), Some(true)).unwrap(); + // assert_eq!(state.balances, vec![Amount::native(987654321, denom)]); + // assert_eq!(state.total_sent, vec![Amount::native(987654321, denom)]); + + // // cannot receive more than we sent + // let msg = IbcPacketReceiveMsg::new(recv_high_packet); + // let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap(); + // assert!(res.messages.is_empty()); + // let ack: Ics20Ack = from_binary(&res.acknowledgement).unwrap(); + // assert_eq!( + // ack, + // Ics20Ack::Error( + // ContractError::InsufficientFunds { + // id: send_channel.to_string(), + // denom: denom.to_string(), + // } + // .to_string(), + // ) + // ); - // we can receive less than we sent - let msg = IbcPacketReceiveMsg::new(recv_packet); - let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap(); - assert_eq!(1, res.messages.len()); - assert_eq!( - native_payment(876543210, denom, "local-rcpt"), - res.messages[0] - ); - let ack: Ics20Ack = from_binary(&res.acknowledgement).unwrap(); - assert!(matches!(ack, Ics20Ack::Result(_))); + // // we can receive less than we sent + // let msg = IbcPacketReceiveMsg::new(recv_packet); + // let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap(); + // assert_eq!(1, res.messages.len()); + // assert_eq!( + // native_payment(876543210, denom, "local-rcpt"), + // res.messages[0] + // ); + // let ack: Ics20Ack = from_binary(&res.acknowledgement).unwrap(); + // assert!(matches!(ack, Ics20Ack::Result(_))); - // only need to call reply block on error case + // // only need to call reply block on error case - // query channel state - let state = query_channel(deps.as_ref(), send_channel.to_string(), Some(true)).unwrap(); - assert_eq!(state.balances, vec![Amount::native(111111111, denom)]); - assert_eq!(state.total_sent, vec![Amount::native(987654321, denom)]); - } + // // query channel state + // let state = query_channel(deps.as_ref(), send_channel.to_string(), Some(true)).unwrap(); + // assert_eq!(state.balances, vec![Amount::native(111111111, denom)]); + // assert_eq!(state.total_sent, vec![Amount::native(987654321, denom)]); + // } #[test] fn check_gas_limit_handles_all_cases() { diff --git a/contracts/cw-ics20-latest/src/msg.rs b/contracts/cw-ics20-latest/src/msg.rs index e422cb0..9dad5ce 100644 --- a/contracts/cw-ics20-latest/src/msg.rs +++ b/contracts/cw-ics20-latest/src/msg.rs @@ -41,7 +41,7 @@ pub enum ExecuteMsg { /// This accepts a properly-encoded ReceiveMsg from a cw20 contract Receive(Cw20ReceiveMsg), /// This allows us to transfer *exactly one* native token - Transfer(TransferMsg), + // Transfer(TransferMsg), TransferToRemote(TransferBackMsg), UpdateMappingPair(UpdatePairMsg), DeleteMappingPair(DeletePairMsg), @@ -78,19 +78,19 @@ pub struct DeletePairMsg { } /// This is the message we accept via Receive -#[cw_serde] -pub struct TransferMsg { - /// The local channel to send the packets on - pub channel: String, - /// The remote address to send to. - /// Don't use HumanAddress as this will likely have a different Bech32 prefix than we use - /// and cannot be validated locally - pub remote_address: String, - /// How long the packet lives in seconds. If not specified, use default_timeout - pub timeout: Option, - /// metadata of the transfer to suit the new fungible token transfer - pub memo: Option, -} +// #[cw_serde] +// pub struct TransferMsg { +// /// The local channel to send the packets on +// pub channel: String, +// /// The remote address to send to. +// /// Don't use HumanAddress as this will likely have a different Bech32 prefix than we use +// /// and cannot be validated locally +// pub remote_address: String, +// /// How long the packet lives in seconds. If not specified, use default_timeout +// pub timeout: Option, +// /// metadata of the transfer to suit the new fungible token transfer +// pub memo: Option, +// } /// This is the message we accept via Receive #[cw_serde] diff --git a/contracts/cw-ics20-latest/src/state.rs b/contracts/cw-ics20-latest/src/state.rs index 17f58a7..f3211d7 100644 --- a/contracts/cw-ics20-latest/src/state.rs +++ b/contracts/cw-ics20-latest/src/state.rs @@ -18,9 +18,9 @@ pub const SINGLE_STEP_REPLY_ARGS: Item = Item::new("single_ /// static info on one channel that doesn't change pub const CHANNEL_INFO: Map<&str, ChannelInfo> = Map::new("channel_info"); -/// Forward channel state is used when LOCAL chain initiates ibc transfer to remote chain -pub const CHANNEL_FORWARD_STATE: Map<(&str, &str), ChannelState> = - Map::new("channel_forward_state"); +// /// Forward channel state is used when LOCAL chain initiates ibc transfer to remote chain +// pub const CHANNEL_FORWARD_STATE: Map<(&str, &str), ChannelState> = +// Map::new("channel_forward_state"); /// Reverse channel state is used when REMOTE chain initiates ibc transfer to local chain pub const CHANNEL_REVERSE_STATE: Map<(&str, &str), ChannelState> = @@ -137,12 +137,12 @@ pub fn increase_channel_balance( channel: &str, denom: &str, amount: Uint128, - forward: bool, + _forward: bool, ) -> Result<(), ContractError> { let mut state = CHANNEL_REVERSE_STATE; - if forward { - state = CHANNEL_FORWARD_STATE; - } + // if forward { + // state = CHANNEL_FORWARD_STATE; + // } state.update(storage, (channel, denom), |orig| -> StdResult<_> { let mut state = orig.unwrap_or_default(); @@ -158,12 +158,12 @@ pub fn reduce_channel_balance( channel: &str, denom: &str, amount: Uint128, - forward: bool, + _forward: bool, ) -> Result<(), ContractError> { let mut state = CHANNEL_REVERSE_STATE; - if forward { - state = CHANNEL_FORWARD_STATE; - } + // if forward { + // state = CHANNEL_FORWARD_STATE; + // } state.update( storage, (channel, denom), @@ -193,12 +193,12 @@ pub fn undo_reduce_channel_balance( channel: &str, denom: &str, amount: Uint128, - forward: bool, + _forward: bool, ) -> Result<(), ContractError> { let mut state = CHANNEL_REVERSE_STATE; - if forward { - state = CHANNEL_FORWARD_STATE; - } + // if forward { + // state = CHANNEL_FORWARD_STATE; + // } state.update(storage, (channel, denom), |orig| -> StdResult<_> { let mut state = orig.unwrap_or_default(); state.outstanding += amount; @@ -214,12 +214,12 @@ pub fn undo_increase_channel_balance( channel: &str, denom: &str, amount: Uint128, - forward: bool, + _forward: bool, ) -> Result<(), ContractError> { let mut state = CHANNEL_REVERSE_STATE; - if forward { - state = CHANNEL_FORWARD_STATE; - } + // if forward { + // state = CHANNEL_FORWARD_STATE; + // } state.update(storage, (channel, denom), |orig| -> StdResult<_> { let mut state = orig.unwrap_or_default(); state.outstanding -= amount; From d8984410b22941a1218f390b7228e592766c4eb5 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Tue, 25 Jul 2023 18:42:07 +0700 Subject: [PATCH 03/28] refactored get_follow_up_msgs --- contracts/cw-ics20-latest/src/ibc.rs | 6 +++--- contracts/cw-ics20-latest/src/ibc_tests.rs | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/contracts/cw-ics20-latest/src/ibc.rs b/contracts/cw-ics20-latest/src/ibc.rs index d890d5b..fae4b51 100644 --- a/contracts/cw-ics20-latest/src/ibc.rs +++ b/contracts/cw-ics20-latest/src/ibc.rs @@ -426,7 +426,7 @@ fn handle_ibc_packet_receive_native_remote_chain( &msg.sender, &msg.receiver, &msg.memo.clone().unwrap_or_default(), - packet, + packet.dest.channel_id.as_str(), )?; let submsgs: Vec = submsgs .into_iter() @@ -462,7 +462,7 @@ pub fn get_follow_up_msgs( sender: &str, receiver: &str, memo: &str, - packet: &IbcPacket, + initial_dest_channel_id: &str, // channel id on Oraichain receiving the token from other chain ) -> Result<(Vec, String), ContractError> { let config = CONFIG.load(storage)?; let mut cosmos_msgs: Vec = vec![]; @@ -511,7 +511,7 @@ pub fn get_follow_up_msgs( env, receiver_asset_info, receiver, - packet.dest.channel_id.as_str(), + initial_dest_channel_id, minimum_receive.clone(), &sender, &destination, diff --git a/contracts/cw-ics20-latest/src/ibc_tests.rs b/contracts/cw-ics20-latest/src/ibc_tests.rs index fbef91f..d84d99c 100644 --- a/contracts/cw-ics20-latest/src/ibc_tests.rs +++ b/contracts/cw-ics20-latest/src/ibc_tests.rs @@ -847,6 +847,7 @@ mod test { #[test] fn test_follow_up_msgs() { let send_channel = "channel-9"; + let local_channel = "channel"; let allowed = "foobar"; let allowed_gas = 777666; let mut deps = setup(&[send_channel], &[(allowed, allowed_gas)]); @@ -873,7 +874,7 @@ mod test { "foobar", receiver.clone(), "", - &mock_receive_packet_remote_to_local("channel", 1u128, "foobar", "foobar"), + local_channel, ) .unwrap(); @@ -905,7 +906,7 @@ mod test { "foobar", "foobar", memo, - &mock_receive_packet_remote_to_local("channel", 1u128, "foobar", "foobar"), + local_channel, ) .unwrap(); @@ -939,7 +940,7 @@ mod test { "foobar", "foobar", memo, - &mock_receive_packet_remote_to_local("channel", 1u128, "foobar", "foobar"), + local_channel, ) .unwrap(); From fd3b2cfc683806c6ba5385ffd74e8d8ca06f86c5 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Wed, 26 Jul 2023 09:37:24 +0700 Subject: [PATCH 04/28] validate destination denom for cw20 token --- contracts/cw-ics20-latest/src/ibc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/cw-ics20-latest/src/ibc.rs b/contracts/cw-ics20-latest/src/ibc.rs index fae4b51..e1b86c0 100644 --- a/contracts/cw-ics20-latest/src/ibc.rs +++ b/contracts/cw-ics20-latest/src/ibc.rs @@ -482,7 +482,7 @@ pub fn get_follow_up_msgs( .is_ok() { AssetInfo::Token { - contract_addr: Addr::unchecked(destination.destination_denom.clone()), + contract_addr: api.addr_validate(&destination.destination_denom)?, } } else { AssetInfo::NativeToken { From 8b822ab558e38ff0090cb17123486c5385103451 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Wed, 26 Jul 2023 10:57:24 +0700 Subject: [PATCH 05/28] refactored router contract actions to use router controller --- Cargo.lock | 4 +- contracts/cw-ics20-latest/Cargo.toml | 2 +- contracts/cw-ics20-latest/src/contract.rs | 9 ++-- contracts/cw-ics20-latest/src/ibc.rs | 56 +++++++--------------- contracts/cw-ics20-latest/src/ibc_tests.rs | 6 +-- contracts/cw-ics20-latest/src/state.rs | 12 ++--- 6 files changed, 33 insertions(+), 56 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b8847d2..70cc278 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -601,9 +601,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "oraiswap" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d647cd0b70b11b8b78364ca2f885753b0c870785788bcf9d3d05f522fe54344e" +checksum = "2dd3f2603bc41e2c9218e1103ce762e50ad8ee7a3648aabe03f7d48437f841c6" dependencies = [ "cosmwasm-schema", "cosmwasm-std", diff --git a/contracts/cw-ics20-latest/Cargo.toml b/contracts/cw-ics20-latest/Cargo.toml index b3c697a..9b0e502 100644 --- a/contracts/cw-ics20-latest/Cargo.toml +++ b/contracts/cw-ics20-latest/Cargo.toml @@ -23,7 +23,7 @@ cw-utils = "0.16.0" cw2 = "1.0.1" cw20 = "1.0.1" cw20-ics20-msg = "0.0.2" -oraiswap = "1.0.0" +oraiswap = "1.0.1" cosmwasm-std = { version = "1.1.0", features = ["stargate"] } cw-storage-plus = "1.0.1" cw-controllers = "1.0.1" diff --git a/contracts/cw-ics20-latest/src/contract.rs b/contracts/cw-ics20-latest/src/contract.rs index ebee682..a2c9212 100644 --- a/contracts/cw-ics20-latest/src/contract.rs +++ b/contracts/cw-ics20-latest/src/contract.rs @@ -8,6 +8,7 @@ use cw2::set_contract_version; use cw20::{Cw20Coin, Cw20ReceiveMsg}; use cw_storage_plus::Bound; use oraiswap::asset::AssetInfo; +use oraiswap::router::RouterController; use crate::error::ContractError; use crate::ibc::{ @@ -45,7 +46,7 @@ pub fn instantiate( default_timeout: msg.default_timeout, default_gas_limit: msg.default_gas_limit, fee_denom: "orai".to_string(), - swap_router_contract: msg.swap_router_contract, + swap_router_contract: RouterController(msg.swap_router_contract), fee_receiver: admin, }; CONFIG.save(deps.storage, &cfg)?; @@ -129,7 +130,7 @@ pub fn update_config( config.fee_denom = fee_denom; } if let Some(swap_router_contract) = swap_router_contract { - config.swap_router_contract = swap_router_contract; + config.swap_router_contract = RouterController(swap_router_contract); } if let Some(fee_receiver) = fee_receiver { config.fee_receiver = deps.api.addr_validate(&fee_receiver)?; @@ -475,7 +476,7 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result StdResult { default_timeout: cfg.default_timeout, default_gas_limit: cfg.default_gas_limit, fee_denom: cfg.fee_denom, - swap_router_contract: cfg.swap_router_contract, + swap_router_contract: cfg.swap_router_contract.addr(), gov_contract: admin.into(), }; Ok(res) diff --git a/contracts/cw-ics20-latest/src/ibc.rs b/contracts/cw-ics20-latest/src/ibc.rs index e1b86c0..854255c 100644 --- a/contracts/cw-ics20-latest/src/ibc.rs +++ b/contracts/cw-ics20-latest/src/ibc.rs @@ -11,7 +11,7 @@ use cosmwasm_std::{ }; use cw20_ics20_msg::receiver::DestinationInfo; use oraiswap::asset::AssetInfo; -use oraiswap::router::{SimulateSwapOperationsResponse, SwapOperation}; +use oraiswap::router::{RouterController, SwapOperation}; use crate::error::{ContractError, Never}; use crate::state::{ @@ -496,12 +496,10 @@ pub fn get_follow_up_msgs( ); let mut minimum_receive = to_send.amount(); if swap_operations.len() > 0 { - let response: SimulateSwapOperationsResponse = querier.query_wasm_smart( - config.swap_router_contract.clone(), - &oraiswap::router::QueryMsg::SimulateSwapOperations { - offer_amount: to_send.amount().clone(), - operations: swap_operations.clone(), - }, + let response = config.swap_router_contract.simulate_swap( + querier, + to_send.amount().clone(), + swap_operations.clone(), )?; minimum_receive = response.amount; } @@ -588,7 +586,7 @@ pub fn build_swap_operations( pub fn build_swap_msgs( minimum_receive: Uint128, - swap_router_contract: &str, + swap_router_contract: &RouterController, amount: Uint128, initial_receive_asset_info: AssetInfo, to: Option, @@ -599,38 +597,16 @@ pub fn build_swap_msgs( if operations.len() == 0 { return Ok(()); } - match initial_receive_asset_info { - AssetInfo::Token { contract_addr } => cosmos_msgs.insert( - 0, - WasmMsg::Execute { - contract_addr: contract_addr.to_string(), - msg: to_binary(&Cw20ExecuteMsg::Send { - contract: swap_router_contract.to_string(), - amount, - msg: to_binary(&oraiswap::router::Cw20HookMsg::ExecuteSwapOperations { - operations, - minimum_receive: Some(minimum_receive.clone()), - to: to.map(|to| to.into_string()), - })?, - })?, - funds: vec![], - } - .into(), - ), - AssetInfo::NativeToken { denom } => cosmos_msgs.insert( - 0, - WasmMsg::Execute { - contract_addr: swap_router_contract.to_string(), - msg: to_binary(&oraiswap::router::ExecuteMsg::ExecuteSwapOperations { - operations, - minimum_receive: Some(minimum_receive.clone()), - to, - })?, - funds: vec![coin(amount.u128(), denom)], - } - .into(), - ), - } + cosmos_msgs.insert( + 0, + swap_router_contract.execute_operations( + initial_receive_asset_info, + amount, + operations, + Some(minimum_receive), + to, + )?, + ); Ok(()) } diff --git a/contracts/cw-ics20-latest/src/ibc_tests.rs b/contracts/cw-ics20-latest/src/ibc_tests.rs index d84d99c..3b7d994 100644 --- a/contracts/cw-ics20-latest/src/ibc_tests.rs +++ b/contracts/cw-ics20-latest/src/ibc_tests.rs @@ -632,7 +632,7 @@ mod test { let mut operations: Vec = vec![]; build_swap_msgs( minimum_receive.clone(), - swap_router_contract.clone(), + &oraiswap::router::RouterController(swap_router_contract.to_string()), amount.clone(), initial_receive_asset_info.clone(), to.clone(), @@ -647,7 +647,7 @@ mod test { }); build_swap_msgs( minimum_receive.clone(), - swap_router_contract.clone(), + &oraiswap::router::RouterController(swap_router_contract.to_string()), amount.clone(), initial_receive_asset_info.clone(), to.clone(), @@ -665,7 +665,7 @@ mod test { }; build_swap_msgs( minimum_receive.clone(), - swap_router_contract.clone(), + &oraiswap::router::RouterController(swap_router_contract.to_string()), amount.clone(), initial_receive_asset_info.clone(), to.clone(), diff --git a/contracts/cw-ics20-latest/src/state.rs b/contracts/cw-ics20-latest/src/state.rs index f3211d7..c5ae4d9 100644 --- a/contracts/cw-ics20-latest/src/state.rs +++ b/contracts/cw-ics20-latest/src/state.rs @@ -2,7 +2,7 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, IbcEndpoint, StdResult, Storage, Uint128}; use cw_controllers::Admin; use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, MultiIndex}; -use oraiswap::asset::AssetInfo; +use oraiswap::{asset::AssetInfo, router::RouterController}; use crate::ContractError; @@ -71,7 +71,7 @@ pub struct Config { pub default_timeout: u64, pub default_gas_limit: Option, pub fee_denom: String, - pub swap_router_contract: String, + pub swap_router_contract: RouterController, pub fee_receiver: Addr, } @@ -139,7 +139,7 @@ pub fn increase_channel_balance( amount: Uint128, _forward: bool, ) -> Result<(), ContractError> { - let mut state = CHANNEL_REVERSE_STATE; + let state = CHANNEL_REVERSE_STATE; // if forward { // state = CHANNEL_FORWARD_STATE; // } @@ -160,7 +160,7 @@ pub fn reduce_channel_balance( amount: Uint128, _forward: bool, ) -> Result<(), ContractError> { - let mut state = CHANNEL_REVERSE_STATE; + let state = CHANNEL_REVERSE_STATE; // if forward { // state = CHANNEL_FORWARD_STATE; // } @@ -195,7 +195,7 @@ pub fn undo_reduce_channel_balance( amount: Uint128, _forward: bool, ) -> Result<(), ContractError> { - let mut state = CHANNEL_REVERSE_STATE; + let state = CHANNEL_REVERSE_STATE; // if forward { // state = CHANNEL_FORWARD_STATE; // } @@ -216,7 +216,7 @@ pub fn undo_increase_channel_balance( amount: Uint128, _forward: bool, ) -> Result<(), ContractError> { - let mut state = CHANNEL_REVERSE_STATE; + let state = CHANNEL_REVERSE_STATE; // if forward { // state = CHANNEL_FORWARD_STATE; // } From 9e2d3ed839a6af42895544a968a276230bfc3624 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Wed, 26 Jul 2023 14:07:40 +0700 Subject: [PATCH 06/28] refactored code to reduce / increase channel balance in reply & ack --- contracts/cw-ics20-latest/src/contract.rs | 76 +++-- contracts/cw-ics20-latest/src/ibc.rs | 316 ++++++++++----------- contracts/cw-ics20-latest/src/ibc_tests.rs | 52 +--- contracts/cw-ics20-latest/src/state.rs | 113 ++++---- 4 files changed, 263 insertions(+), 294 deletions(-) diff --git a/contracts/cw-ics20-latest/src/contract.rs b/contracts/cw-ics20-latest/src/contract.rs index a2c9212..2cb1b09 100644 --- a/contracts/cw-ics20-latest/src/contract.rs +++ b/contracts/cw-ics20-latest/src/contract.rs @@ -2,7 +2,7 @@ use cosmwasm_std::entry_point; use cosmwasm_std::{ from_binary, to_binary, Addr, Binary, Deps, DepsMut, Empty, Env, IbcEndpoint, IbcMsg, IbcQuery, - MessageInfo, Order, PortIdResponse, Response, StdResult, + MessageInfo, Order, PortIdResponse, Response, StdResult, Storage, }; use cw2::set_contract_version; use cw20::{Cw20Coin, Cw20ReceiveMsg}; @@ -21,9 +21,8 @@ use crate::msg::{ MigrateMsg, PairQuery, PortResponse, QueryMsg, TransferBackMsg, UpdatePairMsg, }; use crate::state::{ - get_key_ics20_ibc_denom, ics20_denoms, reduce_channel_balance, AllowInfo, Config, - MappingMetadata, TokenFee, ADMIN, ALLOW_LIST, CHANNEL_INFO, CHANNEL_REVERSE_STATE, CONFIG, - TOKEN_FEE, + get_key_ics20_ibc_denom, ics20_denoms, AllowInfo, Config, MappingMetadata, TokenFee, ADMIN, + ALLOW_LIST, CHANNEL_INFO, CHANNEL_REVERSE_STATE, CONFIG, TOKEN_FEE, }; use cw20_ics20_msg::amount::{convert_local_to_remote, Amount}; use cw_utils::{maybe_addr, nonpayable, one_coin}; @@ -269,7 +268,7 @@ pub fn execute_transfer_back_to_remote_chain( // should be in form port/channel/denom let mappings = get_mappings_from_asset_info( - deps.as_ref(), + deps.as_ref().storage, match amount.clone() { Amount::Native(coin) => AssetInfo::NativeToken { denom: coin.denom }, Amount::Cw20(cw20_coin) => AssetInfo::Token { @@ -333,14 +332,15 @@ pub fn execute_transfer_back_to_remote_chain( ); packet.validate()?; - // because we are transferring back, we reduce the channel's balance - reduce_channel_balance( - deps.storage, - &msg.local_channel_id, - &ibc_denom, - amount_remote, - false, - )?; + // now this is processed in ack + // // because we are transferring back, we reduce the channel's balance + // reduce_channel_balance( + // deps.storage, + // &msg.local_channel_id, + // &ibc_denom, + // amount_remote, + // false, + // )?; // prepare ibc message let msg = IbcMsg::SendPacket { @@ -504,7 +504,7 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { } => to_binary(&list_cw20_mapping(deps, start_after, limit, order)?), QueryMsg::PairMapping { key } => to_binary(&get_mapping_from_key(deps, key)?), QueryMsg::PairMappingsFromAssetInfo { asset_info } => { - to_binary(&get_mappings_from_asset_info(deps, asset_info)?) + to_binary(&get_mappings_from_asset_info(deps.storage, asset_info)?) } QueryMsg::Admin {} => to_binary(&ADMIN.query_admin(deps)?), QueryMsg::GetTransferTokenFee { remote_token_denom } => { @@ -642,12 +642,15 @@ fn get_mapping_from_key(deps: Deps, ibc_denom: String) -> StdResult { }) } -fn get_mappings_from_asset_info(deps: Deps, asset_info: AssetInfo) -> StdResult> { +fn get_mappings_from_asset_info( + storage: &dyn Storage, + asset_info: AssetInfo, +) -> StdResult> { let pair_mapping_result: StdResult> = ics20_denoms() .idx .asset_info .prefix(asset_info.to_string()) - .range(deps.storage, None, None, Order::Ascending) + .range(storage, None, None, Order::Ascending) .collect(); if pair_mapping_result.is_err() { return Err(pair_mapping_result.unwrap_err()); @@ -680,7 +683,7 @@ mod test { use super::*; use crate::ibc::ibc_packet_receive; - use crate::state::Ratio; + use crate::state::{reduce_channel_balance, Ratio}; use crate::test_helpers::*; use cosmwasm_std::testing::{mock_env, mock_info}; @@ -1212,22 +1215,23 @@ mod test { memo: None, }; - let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { - sender: original_sender.to_string(), - amount: Uint128::from(amount), - msg: to_binary(&transfer).unwrap(), - }); + // let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { + // sender: original_sender.to_string(), + // amount: Uint128::from(amount), + // msg: to_binary(&transfer).unwrap(), + // }); - // insufficient funds case because we need to receive from remote chain first + // // insufficient funds case because we need to receive from remote chain first let info = mock_info(cw20_raw_denom, &[]); - let res = execute(deps.as_mut(), mock_env(), info.clone(), msg.clone()).unwrap_err(); - assert_eq!( - res, - ContractError::NoSuchChannelState { - id: local_channel.to_string(), - denom: get_key_ics20_ibc_denom("wasm.cosmos2contract", local_channel, denom) - } - ); + // let res = execute(deps.as_mut(), mock_env(), info.clone(), msg.clone()); + // println!("res: {:?}", res); + // assert_eq!( + // res.unwrap_err(), + // ContractError::NoSuchChannelState { + // id: local_channel.to_string(), + // denom: get_key_ics20_ibc_denom("wasm.cosmos2contract", local_channel, denom) + // } + // ); // prepare some mock packets let recv_packet = @@ -1295,6 +1299,16 @@ mod test { _ => panic!("Unexpected return message: {:?}", res.messages[0]), } + // since we reduce channel balance afterwards => need to manually reduce balance in test + reduce_channel_balance( + deps.as_mut().storage, + local_channel.into(), + &get_key_ics20_ibc_denom(CONTRACT_PORT, local_channel, denom), + Uint128::from(amount).checked_sub(fee_amount).unwrap(), + false, + ) + .unwrap(); + // check new channel state after reducing balance let chan = query_channel(deps.as_ref(), local_channel.into(), None).unwrap(); assert_eq!( diff --git a/contracts/cw-ics20-latest/src/ibc.rs b/contracts/cw-ics20-latest/src/ibc.rs index 854255c..28fdbfc 100644 --- a/contracts/cw-ics20-latest/src/ibc.rs +++ b/contracts/cw-ics20-latest/src/ibc.rs @@ -16,9 +16,9 @@ use oraiswap::router::{RouterController, SwapOperation}; use crate::error::{ContractError, Never}; use crate::state::{ get_key_ics20_ibc_denom, ics20_denoms, increase_channel_balance, reduce_channel_balance, - undo_increase_channel_balance, undo_reduce_channel_balance, ChannelInfo, IbcSingleStepData, - MappingMetadata, Ratio, ReplyArgs, SingleStepReplyArgs, ALLOW_LIST, CHANNEL_INFO, CONFIG, - REPLY_ARGS, SINGLE_STEP_REPLY_ARGS, TOKEN_FEE, TOKEN_FEE_ACCUMULATOR, + ChannelInfo, IbcSingleStepData, MappingMetadata, Ratio, ReplyArgs, SingleStepReplyArgs, + ALLOW_LIST, CHANNEL_INFO, CONFIG, RELAYER_FEE, REPLY_ARGS, SINGLE_STEP_REPLY_ARGS, TOKEN_FEE, + TOKEN_FEE_ACCUMULATOR, }; use cw20::{Cw20ExecuteMsg, Cw20QueryMsg, TokenInfoResponse}; use cw20_ics20_msg::amount::{convert_local_to_remote, convert_remote_to_local, Amount}; @@ -91,84 +91,32 @@ pub fn ack_fail(err: String) -> Binary { } // pub const RECEIVE_ID: u64 = 1337; -pub const NATIVE_RECEIVE_ID: u64 = 1338; -pub const FOLLOW_UP_FAILURE_ID: u64 = 1339; +pub const FOLLOW_UP_ID: u64 = 1339; pub const REFUND_FAILURE_ID: u64 = 1340; pub const ACK_FAILURE_ID: u64 = 64023; #[entry_point] pub fn reply(deps: DepsMut, _env: Env, reply: Reply) -> Result { match reply.id { - // RECEIVE_ID => match reply.result { - // SubMsgResult::Ok(_) => Ok(Response::new()), - // SubMsgResult::Err(err) => { - // // Important design note: with ibcv2 and wasmd 0.22 we can implement this all much easier. - // // No reply needed... the receive function and submessage should return error on failure and all - // // state gets reverted with a proper app-level message auto-generated - - // // Since we need compatibility with Juno (Jan 2022), we need to ensure that optimisitic - // // state updates in ibc_packet_receive get reverted in the (unlikely) chance of an - // // error while sending the token - - // // However, this requires passing some state between the ibc_packet_receive function and - // // the reply handler. We do this with a singleton, with is "okay" for IBC as there is no - // // reentrancy on these functions (cannot be called by another contract). This pattern - // // should not be used for ExecuteMsg handlers - // let reply_args = REPLY_ARGS.load(deps.storage)?; - // undo_reduce_channel_balance( - // deps.storage, - // &reply_args.channel, - // &reply_args.denom, - // reply_args.amount, - // true, - // )?; - - // Ok(Response::new().set_data(ack_fail(err)).add_attributes(vec![ - // attr("undo_reduce_channel", reply_args.channel), - // attr("undo_reduce_channel_ibc_denom", reply_args.denom), - // attr("undo_reduce_channel_amount", reply_args.amount), - // ])) - // } - // }, - NATIVE_RECEIVE_ID => match reply.result { - SubMsgResult::Ok(_) => Ok(Response::new()), - SubMsgResult::Err(err) => { - // Important design note: with ibcv2 and wasmd 0.22 we can implement this all much easier. - // No reply needed... the receive function and submessage should return error on failure and all - // state gets reverted with a proper app-level message auto-generated - - // Since we need compatibility with Juno (Jan 2022), we need to ensure that optimisitic - // state updates in ibc_packet_receive get reverted in the (unlikely) chance of an - // error while sending the token - - // However, this requires passing some state between the ibc_packet_receive function and - // the reply handler. We do this with a singleton, with is "okay" for IBC as there is no - // reentrancy on these functions (cannot be called by another contract). This pattern - // should not be used for ExecuteMsg handlers - let reply_args = REPLY_ARGS.load(deps.storage)?; - undo_increase_channel_balance( - deps.storage, - &reply_args.channel, - &reply_args.denom, - reply_args.amount, - false, - )?; - - Ok(Response::new() - .set_data(ack_fail(err.clone())) - .add_attribute("error_transferring_ibc_tokens_to_cw20", err) - .add_attributes(vec![ - attr("undo_increase_channel", reply_args.channel), - attr("undo_increase_channel_ibc_denom", reply_args.denom), - attr("undo_increase_channel_amount", reply_args.amount), - ])) + FOLLOW_UP_ID => match reply.result { + SubMsgResult::Ok(_) => { + let reply_args = SINGLE_STEP_REPLY_ARGS.load(deps.storage)?; + if let Some(ibc_data) = reply_args.ibc_data { + // only reduce balance when everything is done and successful. Not reduce optimistically + reduce_channel_balance( + deps.storage, + &reply_args.channel, + &ibc_data.ibc_denom, + ibc_data.remote_amount, + false, + )?; + } + Ok(Response::new()) } - }, - FOLLOW_UP_FAILURE_ID => match reply.result { - SubMsgResult::Ok(_) => Ok(Response::new()), SubMsgResult::Err(err) => { let reply_args = SINGLE_STEP_REPLY_ARGS.load(deps.storage)?; - handle_follow_up_failure(deps.storage, reply_args, err) + // only refund, not undo reduce balance + handle_follow_up_failure(reply_args, err) } }, ACK_FAILURE_ID => match reply.result { @@ -395,6 +343,7 @@ fn handle_ibc_packet_receive_native_remote_chain( pair_mapping.asset_info_decimals, )?, ); + // will have to increase balance here because if this tx fails then it will be reverted, and the balance on the remote chain will also be reverted increase_channel_balance( storage, &packet.dest.channel_id, @@ -430,7 +379,7 @@ fn handle_ibc_packet_receive_native_remote_chain( )?; let submsgs: Vec = submsgs .into_iter() - .map(|msg| SubMsg::reply_on_error(msg, FOLLOW_UP_FAILURE_ID)) + .map(|msg| SubMsg::reply_always(msg, FOLLOW_UP_ID)) .collect(); let transfer_fee_to_admin = @@ -636,16 +585,15 @@ pub fn build_ibc_msg( receiver: local_receiver.to_string(), local_amount: amount, }; + // use sender from ICS20Packet as receiver when transferring back + let pair_mappings: Vec<(String, MappingMetadata)> = ics20_denoms() + .idx + .asset_info + .prefix(receiver_asset_info.to_string()) + .range(storage, None, None, Order::Ascending) + .collect::>>()?; let (is_evm_based, destination) = destination.is_receiver_evm_based(); if is_evm_based { - // use sender from ICS20Packet as receiver when transferring back - let pair_mappings: Vec<(String, MappingMetadata)> = ics20_denoms() - .idx - .asset_info - .prefix(receiver_asset_info.to_string()) - .range(storage, None, None, Order::Ascending) - .collect::>>()?; - let mapping = pair_mappings .into_iter() .find(|(key, _)| key.contains(&destination.destination_channel)) @@ -653,7 +601,7 @@ pub fn build_ibc_msg( // also deduct fee here because of round trip let new_deducted_amount = process_deduct_fee( storage, - parse_voucher_denom_without_sanity_checks(&mapping.0)?, + parse_voucher_denom_without_sanity_checks(&mapping.0)?, // denom mapping in the form port/channel/denom amount, &parse_asset_info_denom(receiver_asset_info.clone()), )?; @@ -671,21 +619,21 @@ pub fn build_ibc_msg( &remote_address, Some(destination.receiver), ); - // because we are transferring back, we reduce the channel's balance - reduce_channel_balance( - storage, - &local_channel_id.clone(), - &mapping.0.clone(), - remote_amount, - false, - ) - .map_err(|err| StdError::generic_err(err.to_string()))?; + // // because we are transferring back, we reduce the channel's balance + // reduce_channel_balance( + // storage, + // &local_channel_id.clone(), + // &mapping.0.clone(), + // remote_amount, + // false, + // ) + // .map_err(|err| StdError::generic_err(err.to_string()))?; reply_args.channel = local_channel_id.to_string(); reply_args.ibc_data = Some(IbcSingleStepData { ibc_denom: mapping.0, remote_amount, }); - // keep track of the reply. We need to keep a seperate value because if using REPLY, it could be overriden by the channel increase later on + // keep track of the reply. We need to keep a seperate value because if using REPLY, it could be overriden by the channel increase later on in reply SINGLE_STEP_REPLY_ARGS.save(storage, &reply_args)?; // prepare ibc message @@ -696,8 +644,11 @@ pub fn build_ibc_msg( }; return Ok(msg.into()); } + // 2nd case, where destination network is not evm, but it is still supported on our channel (eg: cw20 ATOM mapped with native ATOM on Cosmos), then we call + // final case, where the destination token is from a remote chain that we dont have a pair mapping with. // we use ibc transfer so that attackers cannot manipulate the data to send to oraibridge without reducing the channel balance // by using ibc transfer, the contract must actually owns native ibc tokens, which is not possible if it's oraibridge tokens + // we do not need to reduce channel balance because this transfer is not on our contract channel, but on destination channel let ibc_msg = IbcMsg::Transfer { channel_id: destination.destination_channel, to_address: destination.receiver, @@ -708,26 +659,25 @@ pub fn build_ibc_msg( } pub fn handle_follow_up_failure( - storage: &mut dyn Storage, reply_args: SingleStepReplyArgs, err: String, ) -> Result { // if there's an error but no ibc msg aka no channel balance reduce => wont undo reduce let mut response: Response = Response::new(); - if let Some(ibc_data) = reply_args.ibc_data { - undo_reduce_channel_balance( - storage, - &reply_args.channel, - &ibc_data.ibc_denom, - ibc_data.remote_amount, - false, - )?; - response = response.add_attributes(vec![ - attr("undo_reduce_channel", reply_args.channel), - attr("undo_reduce_channel_ibc_denom", ibc_data.ibc_denom), - attr("undo_reduce_channel_balance", ibc_data.remote_amount), - ]); - } + // if let Some(ibc_data) = reply_args.ibc_data { + // undo_reduce_channel_balance( + // storage, + // &reply_args.channel, + // &ibc_data.ibc_denom, + // ibc_data.remote_amount, + // false, + // )?; + // response = response.add_attributes(vec![ + // attr("undo_reduce_channel", reply_args.channel), + // attr("undo_reduce_channel_ibc_denom", ibc_data.ibc_denom), + // attr("undo_reduce_channel_balance", ibc_data.remote_amount), + // ]); + // } let refund_amount = Amount::from_parts( parse_asset_info_denom(reply_args.refund_asset_info.clone()), reply_args.local_amount, @@ -771,6 +721,19 @@ pub fn process_deduct_fee( remote_token_denom: &str, amount: Uint128, local_token_denom: &str, +) -> StdResult { + let mut new_deducted_fee = + deduct_token_fee(storage, remote_token_denom, amount, local_token_denom)?; + + // new_deducted_fee = deduct_relayer_fee(storage, remote_token_denom, amount, local_token_denom)?; + Ok(new_deducted_fee) +} + +pub fn deduct_token_fee( + storage: &mut dyn Storage, + remote_token_denom: &str, + amount: Uint128, + local_token_denom: &str, ) -> StdResult { let token_fee = TOKEN_FEE.may_load(storage, &remote_token_denom)?; if let Some(token_fee) = token_fee { @@ -786,6 +749,26 @@ pub fn process_deduct_fee( Ok(amount) } +pub fn deduct_relayer_fee( + storage: &mut dyn Storage, + remote_token_denom: &str, + amount: Uint128, + local_token_denom: &str, +) -> StdResult { + // let token_fee = RELAYER_FEE.may_load(storage, &remote_token_denom)?; + // if let Some(token_fee) = token_fee { + // let fee = deduct_fee(token_fee, amount); + // RELAYER_FEE.update( + // storage, + // local_token_denom, + // |prev_fee| -> StdResult { Ok(prev_fee.unwrap_or_default().checked_add(fee)?) }, + // )?; + // let new_deducted_amount = amount.checked_sub(fee)?; + // return Ok(new_deducted_amount); + // } + Ok(amount) +} + pub fn deduct_fee(token_fee: Ratio, amount: Uint128) -> Uint128 { // ignore case where denominator is zero since we cannot divide with 0 if token_fee.denominator == 0 { @@ -810,38 +793,28 @@ pub fn collect_transfer_fee_msgs( ) -> StdResult> { let cosmos_msgs = TOKEN_FEE_ACCUMULATOR .range(storage, None, None, Order::Ascending) - .filter(|data| { - if let Some(filter_result) = data - .as_ref() - .map(|fee_info| { - if fee_info.1.is_zero() { - return false; - } - true - }) - .ok() - { - return filter_result; - } - false - }) - .map(|data| { + .filter_map(|data| { data.map(|fee_info| { - send_amount( + if fee_info.1.is_zero() { + return None; + } + Some(send_amount( Amount::from_parts(fee_info.0, fee_info.1), receiver.clone(), None, - ) + )) }) + .ok() }) - .collect::>>(); + .flatten() + .collect::>(); // we reset all the accumulator keys to zero so that it wont accumulate more in the next txs. This action will be reverted if the fee payment txs fail. TOKEN_FEE_ACCUMULATOR .keys(storage, None, None, Order::Ascending) .collect::, StdError>>()? .into_iter() .for_each(|key| TOKEN_FEE_ACCUMULATOR.remove(storage, &key)); - cosmos_msgs + Ok(cosmos_msgs) } #[entry_point] @@ -872,9 +845,18 @@ pub fn ibc_packet_timeout( } // update the balance stored on this (channel, denom) index -fn on_packet_success(_deps: DepsMut, packet: IbcPacket) -> Result { +fn on_packet_success(deps: DepsMut, packet: IbcPacket) -> Result { let msg: Ics20Packet = from_binary(&packet.data)?; + // if packet success it means that we transfer back to remote chain successfully. We now should reduce the balance + reduce_channel_balance( + deps.storage, + packet.src.channel_id.as_str(), + &msg.denom, + msg.amount, + false, + )?; + // similar event messages like ibctransfer module let attributes = vec![ attr("action", "acknowledge"), @@ -901,44 +883,44 @@ fn on_packet_failure( let msg: Ics20Packet = from_binary(&packet.data)?; // in case that the denom is not in the mapping list, meaning that it is not transferred back, but transfer originally from this local chain - if ics20_denoms().may_load(deps.storage, &msg.denom)?.is_none() { - // undo the balance update on failure (as we pre-emptively added it on send) - reduce_channel_balance( - deps.storage, - &packet.src.channel_id, - &msg.denom, - msg.amount, - true, - )?; - - let to_send = Amount::from_parts(msg.denom.clone(), msg.amount); - let gas_limit = check_gas_limit(deps.as_ref(), &to_send)?; - let send = send_amount(to_send, msg.sender.clone(), None); - let mut submsg = SubMsg::reply_on_error(send, ACK_FAILURE_ID); - submsg.gas_limit = gas_limit; - - // similar event messages like ibctransfer module - let res = IbcBasicResponse::new() - .add_submessage(submsg) - .add_attribute("action", "acknowledge") - .add_attribute("sender", msg.sender) - .add_attribute("receiver", msg.receiver) - .add_attribute("denom", msg.denom) - .add_attribute("amount", msg.amount.to_string()) - .add_attribute("success", "false") - .add_attribute("error", err); - - return Ok(res); - } + // if ics20_denoms().may_load(deps.storage, &msg.denom)?.is_none() { + // // undo the balance update on failure (as we pre-emptively added it on send) + // reduce_channel_balance( + // deps.storage, + // &packet.src.channel_id, + // &msg.denom, + // msg.amount, + // true, + // )?; + + // let to_send = Amount::from_parts(msg.denom.clone(), msg.amount); + // let gas_limit = check_gas_limit(deps.as_ref(), &to_send)?; + // let send = send_amount(to_send, msg.sender.clone(), None); + // let mut submsg = SubMsg::reply_on_error(send, ACK_FAILURE_ID); + // submsg.gas_limit = gas_limit; + + // // similar event messages like ibctransfer module + // let res = IbcBasicResponse::new() + // .add_submessage(submsg) + // .add_attribute("action", "acknowledge") + // .add_attribute("sender", msg.sender) + // .add_attribute("receiver", msg.receiver) + // .add_attribute("denom", msg.denom) + // .add_attribute("amount", msg.amount.to_string()) + // .add_attribute("success", "false") + // .add_attribute("error", err); + + // return Ok(res); + // } - // since we reduce the channel's balance optimistically when transferring back, we increase it again when receiving failed ack - increase_channel_balance( - deps.storage, - &packet.src.channel_id, - &msg.denom, - msg.amount, - false, - )?; + // // 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, + // false, + // )?; // 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(deps.storage, &msg.denom)?; @@ -951,10 +933,10 @@ fn on_packet_failure( )?, ); let cosmos_msg = send_amount(to_send, msg.sender.clone(), None); - let submsg = SubMsg::reply_on_error(cosmos_msg, ACK_FAILURE_ID); - // used submsg here & reply on error. This means that if the refund process fails => tokens will be locked in this IBC Wasm contract. We will manually handle that case. No retry // similar event messages like ibctransfer module + let submsg = SubMsg::reply_on_error(cosmos_msg, ACK_FAILURE_ID); + let res = IbcBasicResponse::new() .add_submessage(submsg) .add_attribute("action", "acknowledge") diff --git a/contracts/cw-ics20-latest/src/ibc_tests.rs b/contracts/cw-ics20-latest/src/ibc_tests.rs index 3b7d994..3a99915 100644 --- a/contracts/cw-ics20-latest/src/ibc_tests.rs +++ b/contracts/cw-ics20-latest/src/ibc_tests.rs @@ -6,10 +6,10 @@ mod test { use oraiswap::router::SwapOperation; use crate::ibc::{ - ack_fail, build_ibc_msg, build_swap_msgs, check_gas_limit, deduct_fee, + ack_fail, build_ibc_msg, build_swap_msgs, check_gas_limit, deduct_fee, deduct_token_fee, handle_follow_up_failure, ibc_packet_receive, is_follow_up_msgs_only_send_amount, - parse_voucher_denom, parse_voucher_denom_without_sanity_checks, process_deduct_fee, - send_amount, Ics20Ack, Ics20Packet, REFUND_FAILURE_ID, + parse_voucher_denom, parse_voucher_denom_without_sanity_checks, send_amount, Ics20Ack, + Ics20Packet, REFUND_FAILURE_ID, }; use crate::ibc::{build_swap_operations, get_follow_up_msgs}; use crate::test_helpers::*; @@ -961,7 +961,6 @@ mod test { #[test] fn test_handle_follow_up_failure() { let local_channel_id = "channel-0"; - let mut deps = setup(&[local_channel_id], &[]); let native_denom = "cosmos"; let refund_asset_info = AssetInfo::NativeToken { denom: native_denom.to_string(), @@ -969,19 +968,15 @@ mod test { let amount = Uint128::from(100u128); let receiver = "receiver"; let err = "ack_failed"; - let mut single_step_reply_args = SingleStepReplyArgs { + let single_step_reply_args = SingleStepReplyArgs { channel: local_channel_id.to_string(), refund_asset_info: refund_asset_info.clone(), ibc_data: None, local_amount: amount, receiver: receiver.to_string(), }; - let result = handle_follow_up_failure( - deps.as_mut().storage, - single_step_reply_args.clone(), - err.to_string(), - ) - .unwrap(); + let result = + handle_follow_up_failure(single_step_reply_args.clone(), err.to_string()).unwrap(); assert_eq!( result, Response::new() @@ -1003,35 +998,6 @@ mod test { attr("attempt_refund_amount", single_step_reply_args.local_amount), ]) ); - - let ibc_denom = "ibc_denom"; - let remote_amount = convert_local_to_remote(amount, 18, 6).unwrap(); - single_step_reply_args.ibc_data = Some(IbcSingleStepData { - ibc_denom: ibc_denom.to_string(), - remote_amount: remote_amount.clone(), - }); - // if has ibc denom then it's evm based, need to undo reducing balance - CHANNEL_REVERSE_STATE - .save( - deps.as_mut().storage, - (local_channel_id, ibc_denom), - &ChannelState { - outstanding: Uint128::from(0u128), - total_sent: Uint128::from(100u128), - }, - ) - .unwrap(); - handle_follow_up_failure( - deps.as_mut().storage, - single_step_reply_args.clone(), - err.to_string(), - ) - .unwrap(); - let channel_state = CHANNEL_REVERSE_STATE - .load(deps.as_mut().storage, (local_channel_id, ibc_denom)) - .unwrap(); - // should undo reduce channel state - assert_eq!(channel_state.outstanding, remote_amount) } #[test] @@ -1103,14 +1069,14 @@ mod test { } #[test] - fn test_process_deduct_fee() { + fn test_deduct_token_fee() { let mut deps = mock_dependencies(); let amount = Uint128::from(1000u64); let storage = deps.as_mut().storage; let token_fee_denom = "foo0x"; // should return amount because we have not set relayer fee yet assert_eq!( - process_deduct_fee(storage, "foo", amount, "foo").unwrap(), + deduct_token_fee(storage, "foo", amount, "foo").unwrap(), amount.clone() ); TOKEN_FEE @@ -1124,7 +1090,7 @@ mod test { ) .unwrap(); assert_eq!( - process_deduct_fee(storage, token_fee_denom, amount, "foo").unwrap(), + deduct_token_fee(storage, token_fee_denom, amount, "foo").unwrap(), Uint128::from(990u64) ); assert_eq!( diff --git a/contracts/cw-ics20-latest/src/state.rs b/contracts/cw-ics20-latest/src/state.rs index c5ae4d9..3abb7ec 100644 --- a/contracts/cw-ics20-latest/src/state.rs +++ b/contracts/cw-ics20-latest/src/state.rs @@ -1,5 +1,5 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, IbcEndpoint, StdResult, Storage, Uint128}; +use cosmwasm_std::{Addr, Decimal, IbcEndpoint, StdResult, Storage, Uint128}; use cw_controllers::Admin; use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, MultiIndex}; use oraiswap::{asset::AssetInfo, router::RouterController}; @@ -26,13 +26,20 @@ pub const CHANNEL_INFO: Map<&str, ChannelInfo> = Map::new("channel_info"); pub const CHANNEL_REVERSE_STATE: Map<(&str, &str), ChannelState> = Map::new("channel_reverse_state"); +/// Reverse channel state is used when LOCAL chain initiates ibc transfer to remote chain +pub const CHANNEL_FORWARD_STATE: Map<(&str, &str), ChannelState> = + Map::new("channel_forward_state"); + /// Every cw20 contract we allow to be sent is stored here, possibly with a gas_limit pub const ALLOW_LIST: Map<&Addr, AllowInfo> = Map::new("allow_list"); pub const TOKEN_FEE: Map<&str, Ratio> = Map::new("token_fee"); - pub const TOKEN_FEE_ACCUMULATOR: Map<&str, Uint128> = Map::new("token_fee_accumulator"); +// relayer fee. This fee depends on the network type, not token type +pub const RELAYER_FEE: Map<&str, Decimal> = Map::new("relayer_fee"); +pub const RELAYER_FEE_ACCUMULATOR: Map<&str, Uint128> = Map::new("relayer_fee_accumulator"); + // MappingMetadataIndexex structs keeps a list of indexers pub struct MappingMetadataIndexex<'a> { // token.identifier @@ -137,12 +144,12 @@ pub fn increase_channel_balance( channel: &str, denom: &str, amount: Uint128, - _forward: bool, + forward: bool, ) -> Result<(), ContractError> { - let state = CHANNEL_REVERSE_STATE; - // if forward { - // state = CHANNEL_FORWARD_STATE; - // } + let mut state = CHANNEL_REVERSE_STATE; + if forward { + state = CHANNEL_FORWARD_STATE; + } state.update(storage, (channel, denom), |orig| -> StdResult<_> { let mut state = orig.unwrap_or_default(); @@ -158,12 +165,12 @@ pub fn reduce_channel_balance( channel: &str, denom: &str, amount: Uint128, - _forward: bool, + forward: bool, ) -> Result<(), ContractError> { - let state = CHANNEL_REVERSE_STATE; - // if forward { - // state = CHANNEL_FORWARD_STATE; - // } + let mut state = CHANNEL_REVERSE_STATE; + if forward { + state = CHANNEL_FORWARD_STATE; + } state.update( storage, (channel, denom), @@ -186,47 +193,47 @@ pub fn reduce_channel_balance( Ok(()) } -// this is like increase, but it only "un-subtracts" (= adds) outstanding, not total_sent -// calling `reduce_channel_balance` and then `undo_reduce_channel_balance` should leave state unchanged. -pub fn undo_reduce_channel_balance( - storage: &mut dyn Storage, - channel: &str, - denom: &str, - amount: Uint128, - _forward: bool, -) -> Result<(), ContractError> { - let state = CHANNEL_REVERSE_STATE; - // if forward { - // state = CHANNEL_FORWARD_STATE; - // } - state.update(storage, (channel, denom), |orig| -> StdResult<_> { - let mut state = orig.unwrap_or_default(); - state.outstanding += amount; - Ok(state) - })?; - Ok(()) -} - -// this is like decrease, but it only "un-add" (= adds) outstanding, not total_sent -// calling `increase_channel_balance` and then `undo_increase_channel_balance` should leave state unchanged. -pub fn undo_increase_channel_balance( - storage: &mut dyn Storage, - channel: &str, - denom: &str, - amount: Uint128, - _forward: bool, -) -> Result<(), ContractError> { - let state = CHANNEL_REVERSE_STATE; - // if forward { - // state = CHANNEL_FORWARD_STATE; - // } - state.update(storage, (channel, denom), |orig| -> StdResult<_> { - let mut state = orig.unwrap_or_default(); - state.outstanding -= amount; - Ok(state) - })?; - Ok(()) -} +// // this is like increase, but it only "un-subtracts" (= adds) outstanding, not total_sent +// // calling `reduce_channel_balance` and then `undo_reduce_channel_balance` should leave state unchanged. +// pub fn undo_reduce_channel_balance( +// storage: &mut dyn Storage, +// channel: &str, +// denom: &str, +// amount: Uint128, +// forward: bool, +// ) -> Result<(), ContractError> { +// let mut state = CHANNEL_REVERSE_STATE; +// if forward { +// state = CHANNEL_FORWARD_STATE; +// } +// state.update(storage, (channel, denom), |orig| -> StdResult<_> { +// let mut state = orig.unwrap_or_default(); +// state.outstanding += amount; +// Ok(state) +// })?; +// Ok(()) +// } + +// // this is like decrease, but it only "un-add" (= adds) outstanding, not total_sent +// // calling `increase_channel_balance` and then `undo_increase_channel_balance` should leave state unchanged. +// pub fn undo_increase_channel_balance( +// storage: &mut dyn Storage, +// channel: &str, +// denom: &str, +// amount: Uint128, +// forward: bool, +// ) -> Result<(), ContractError> { +// let mut state = CHANNEL_REVERSE_STATE; +// if forward { +// state = CHANNEL_FORWARD_STATE; +// } +// state.update(storage, (channel, denom), |orig| -> StdResult<_> { +// let mut state = orig.unwrap_or_default(); +// state.outstanding -= amount; +// Ok(state) +// })?; +// Ok(()) +// } pub fn get_key_ics20_ibc_denom(port_id: &str, channel_id: &str, denom: &str) -> String { format!("{}/{}/{}", port_id, channel_id, denom) From ed6bcd02ac8b32c253eeb9165239b20a451299c3 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Wed, 26 Jul 2023 16:31:14 +0700 Subject: [PATCH 07/28] added relayer fee logic --- Cargo.lock | 7 + contracts/cw-ics20-latest/Cargo.toml | 1 + contracts/cw-ics20-latest/src/contract.rs | 51 +++++-- contracts/cw-ics20-latest/src/ibc.rs | 164 +++++++++++++++------ contracts/cw-ics20-latest/src/ibc_tests.rs | 30 ++-- contracts/cw-ics20-latest/src/msg.rs | 3 +- contracts/cw-ics20-latest/src/state.rs | 16 +- 7 files changed, 201 insertions(+), 71 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 70cc278..62e1d76 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,6 +37,12 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bech32" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9ff0bbfd639f15c74af777d81383cf53efb7c93613f6cab67c6c11e05bbf8b" + [[package]] name = "block-buffer" version = "0.9.0" @@ -224,6 +230,7 @@ dependencies = [ name = "cw-ics20" version = "1.0.2" dependencies = [ + "bech32", "cosmwasm-schema", "cosmwasm-std", "cw-controllers", diff --git a/contracts/cw-ics20-latest/Cargo.toml b/contracts/cw-ics20-latest/Cargo.toml index 9b0e502..495decc 100644 --- a/contracts/cw-ics20-latest/Cargo.toml +++ b/contracts/cw-ics20-latest/Cargo.toml @@ -31,6 +31,7 @@ schemars = "0.8.1" semver = "1" serde = { version = "1.0.103", default-features = false, features = ["derive"] } thiserror = { version = "1.0.23" } +bech32 = "0.8.1" [dev-dependencies] cw-multi-test = "0.16.0" diff --git a/contracts/cw-ics20-latest/src/contract.rs b/contracts/cw-ics20-latest/src/contract.rs index 2cb1b09..72b1a89 100644 --- a/contracts/cw-ics20-latest/src/contract.rs +++ b/contracts/cw-ics20-latest/src/contract.rs @@ -21,8 +21,9 @@ use crate::msg::{ MigrateMsg, PairQuery, PortResponse, QueryMsg, TransferBackMsg, UpdatePairMsg, }; use crate::state::{ - get_key_ics20_ibc_denom, ics20_denoms, AllowInfo, Config, MappingMetadata, TokenFee, ADMIN, - ALLOW_LIST, CHANNEL_INFO, CHANNEL_REVERSE_STATE, CONFIG, TOKEN_FEE, + get_key_ics20_ibc_denom, ics20_denoms, AllowInfo, Config, MappingMetadata, RelayerFee, + TokenFee, ADMIN, ALLOW_LIST, CHANNEL_INFO, CHANNEL_REVERSE_STATE, CONFIG, RELAYER_FEE, + TOKEN_FEE, }; use cw20_ics20_msg::amount::{convert_local_to_remote, Amount}; use cw_utils::{maybe_addr, nonpayable, one_coin}; @@ -90,6 +91,7 @@ pub fn execute( admin, token_fee, fee_receiver, + relayer_fee, } => update_config( deps, info, @@ -100,6 +102,7 @@ pub fn execute( admin, token_fee, fee_receiver, + relayer_fee, ), } } @@ -114,6 +117,7 @@ pub fn update_config( admin: Option, token_fee: Option>, fee_receiver: Option, + relayer_fee: Option>, ) -> Result { ADMIN.assert_admin(deps.as_ref(), &info.sender)?; if let Some(token_fee) = token_fee { @@ -121,6 +125,11 @@ pub fn update_config( TOKEN_FEE.save(deps.storage, &fee.token_denom, &fee.ratio)?; } } + if let Some(relayer_fee) = relayer_fee { + for fee in relayer_fee { + RELAYER_FEE.save(deps.storage, &fee.prefix, &fee.fee)?; + } + } CONFIG.update(deps.storage, |mut config| -> StdResult { if let Some(default_timeout) = default_timeout { config.default_timeout = default_timeout; @@ -258,13 +267,7 @@ pub fn execute_transfer_back_to_remote_chain( if amount.is_empty() { return Err(ContractError::NoFunds {}); } - - let new_deducted_amount = process_deduct_fee( - deps.storage, - &msg.remote_denom, - amount.amount(), - &amount.denom(), - )?; + let config = CONFIG.load(deps.storage)?; // should be in form port/channel/denom let mappings = get_mappings_from_asset_info( @@ -299,6 +302,18 @@ pub fn execute_transfer_back_to_remote_chain( }) .ok_or(ContractError::MappingPairNotFound {})?; + // if found mapping, then deduct fee based on mapping + let new_deducted_amount = process_deduct_fee( + deps.storage, + &deps.querier, + &msg.remote_address, + &msg.remote_denom, + amount.amount(), + mapping.pair_mapping.asset_info_decimals, + &amount.denom(), + &config.swap_router_contract, + )?; + let ibc_denom = mapping.key; // ensure the requested channel is registered if !CHANNEL_INFO.has(deps.storage, &msg.local_channel_id) { @@ -306,7 +321,6 @@ pub fn execute_transfer_back_to_remote_chain( id: msg.local_channel_id, }); } - let config = CONFIG.load(deps.storage)?; // delta from user is in seconds let timeout_delta = match msg.timeout { @@ -1169,6 +1183,7 @@ mod test { fn proper_checks_on_execute_native_transfer_back_to_remote() { // arrange let remote_channel = "channel-5"; + let remote_address = "cosmos1603j3e4juddh7cuhfquxspl0p0nsun046us7n0"; let custom_addr = "custom-addr"; let original_sender = "original_sender"; let denom = "uatom0x"; @@ -1209,7 +1224,7 @@ mod test { // execute let mut transfer = TransferBackMsg { local_channel_id: local_channel.to_string(), - remote_address: "foreign-address".to_string(), + remote_address: remote_address.to_string(), remote_denom: denom.to_string(), timeout: Some(DEFAULT_TIMEOUT), memo: None, @@ -1275,7 +1290,7 @@ mod test { get_key_ics20_ibc_denom(CONTRACT_PORT, local_channel, denom) ); assert_eq!(msg.sender.as_str(), original_sender); - assert_eq!(msg.receiver.as_str(), "foreign-address"); + assert_eq!(msg.receiver.as_str(), remote_address); // assert_eq!(msg.memo, None); } _ => panic!("Unexpected return message: {:?}", res.messages[0]), @@ -1291,7 +1306,7 @@ mod test { msg, to_binary(&Cw20ExecuteMsg::Transfer { recipient: "gov".to_string(), - amount: fee_amount.clone() + amount: fee_amount.clone().checked_mul(Uint128::from(2u64)).unwrap() // mul with 2 because deduct when receive token then transfer back }) .unwrap() ); @@ -1381,6 +1396,10 @@ mod test { }, }, ]), + relayer_fee: Some(vec![RelayerFee { + prefix: "foo".to_string(), + fee: Uint128::from(1000000u64), + }]), fee_receiver: None, }; // unauthorized case @@ -1421,7 +1440,11 @@ mod test { .unwrap() .denominator, 5 - ) + ); + assert_eq!( + RELAYER_FEE.load(deps.as_ref().storage, "foo").unwrap(), + Uint128::from(1000000u64), + ); } #[test] diff --git a/contracts/cw-ics20-latest/src/ibc.rs b/contracts/cw-ics20-latest/src/ibc.rs index 28fdbfc..b1f5b8e 100644 --- a/contracts/cw-ics20-latest/src/ibc.rs +++ b/contracts/cw-ics20-latest/src/ibc.rs @@ -327,7 +327,7 @@ fn handle_ibc_packet_receive_native_remote_chain( packet: &IbcPacket, msg: &Ics20Packet, ) -> Result { - // make sure we have enough balance for this + let config = CONFIG.load(storage)?; // key in form transfer/channel-0/foo let ibc_denom = get_key_ics20_ibc_denom(&packet.dest.port_id, &packet.dest.channel_id, denom); @@ -361,7 +361,16 @@ fn handle_ibc_packet_receive_native_remote_chain( let new_deducted_to_send = Amount::from_parts( to_send.denom(), - process_deduct_fee(storage, &msg.denom, to_send.amount(), &to_send.denom())?, + process_deduct_fee( + storage, + querier, + &msg.sender, + &msg.denom, + to_send.amount(), + pair_mapping.asset_info_decimals, + &to_send.denom(), + &config.swap_router_contract, + )?, ); // after receiving the cw20 amount, we try to do fee swapping for the user if needed so he / she can create txs on the network @@ -423,21 +432,7 @@ pub fn get_follow_up_msgs( )); } // successful case. We dont care if this msg is going to be successful or not because it does not affect our ibc receive flow (just submsgs) - let receiver_asset_info = if querier - .query_wasm_smart::( - destination.destination_denom.clone(), - &Cw20QueryMsg::TokenInfo {}, - ) - .is_ok() - { - AssetInfo::Token { - contract_addr: api.addr_validate(&destination.destination_denom)?, - } - } else { - AssetInfo::NativeToken { - denom: destination.destination_denom.clone(), - } - }; + let receiver_asset_info = denom_to_asset_info(querier, &destination.destination_denom); let swap_operations = build_swap_operations( receiver_asset_info.clone(), initial_receive_asset_info.clone(), @@ -599,7 +594,8 @@ pub fn build_ibc_msg( .find(|(key, _)| key.contains(&destination.destination_channel)) .ok_or(StdError::generic_err("cannot find pair mappings"))?; // also deduct fee here because of round trip - let new_deducted_amount = process_deduct_fee( + // TODO: add relayer fee here? + let (new_deducted_amount, _) = deduct_token_fee( storage, parse_voucher_denom_without_sanity_checks(&mapping.0)?, // denom mapping in the form port/channel/denom amount, @@ -718,15 +714,36 @@ pub fn check_gas_limit(deps: Deps, amount: &Amount) -> Result, Contr pub fn process_deduct_fee( storage: &mut dyn Storage, + querier: &QuerierWrapper, + remote_sender: &str, remote_token_denom: &str, - amount: Uint128, - local_token_denom: &str, + amount: Uint128, // local amount + decimals: u8, + local_token_denom: &str, // local denom + swap_router_contract: &RouterController, ) -> StdResult { - let mut new_deducted_fee = - deduct_token_fee(storage, remote_token_denom, amount, local_token_denom)?; - - // new_deducted_fee = deduct_relayer_fee(storage, remote_token_denom, amount, local_token_denom)?; - Ok(new_deducted_fee) + let (_, token_fee) = deduct_token_fee(storage, remote_token_denom, amount, local_token_denom)?; + let (_, relayer_fee) = deduct_relayer_fee( + storage, + querier, + remote_sender, + remote_token_denom, + amount, + decimals, + local_token_denom, + &swap_router_contract, + )?; + let new_amount = amount + .checked_sub(token_fee) + .unwrap_or_default() + .checked_sub(relayer_fee) + .unwrap_or_default(); + if new_amount.is_zero() { + return Err(StdError::generic_err( + "Not enough transfer amount to cover the token and relayer fees", + )); + } + Ok(new_amount) } pub fn deduct_token_fee( @@ -734,7 +751,7 @@ pub fn deduct_token_fee( remote_token_denom: &str, amount: Uint128, local_token_denom: &str, -) -> StdResult { +) -> StdResult<(Uint128, Uint128)> { let token_fee = TOKEN_FEE.may_load(storage, &remote_token_denom)?; if let Some(token_fee) = token_fee { let fee = deduct_fee(token_fee, amount); @@ -744,29 +761,76 @@ pub fn deduct_token_fee( |prev_fee| -> StdResult { Ok(prev_fee.unwrap_or_default().checked_add(fee)?) }, )?; let new_deducted_amount = amount.checked_sub(fee)?; - return Ok(new_deducted_amount); + return Ok((new_deducted_amount, fee)); } - Ok(amount) + Ok((amount, Uint128::from(0u64))) } pub fn deduct_relayer_fee( storage: &mut dyn Storage, + querier: &QuerierWrapper, + remote_sender: &str, remote_token_denom: &str, - amount: Uint128, - local_token_denom: &str, -) -> StdResult { - // let token_fee = RELAYER_FEE.may_load(storage, &remote_token_denom)?; - // if let Some(token_fee) = token_fee { - // let fee = deduct_fee(token_fee, amount); - // RELAYER_FEE.update( - // storage, - // local_token_denom, - // |prev_fee| -> StdResult { Ok(prev_fee.unwrap_or_default().checked_add(fee)?) }, - // )?; - // let new_deducted_amount = amount.checked_sub(fee)?; - // return Ok(new_deducted_amount); - // } - Ok(amount) + amount: Uint128, // local amount + decimals: u8, + local_token_denom: &str, // local denom + swap_router_contract: &RouterController, +) -> StdResult<(Uint128, Uint128)> { + let decode_result = bech32::decode(remote_sender); + if decode_result.is_err() { + return Err(StdError::generic_err(format!( + "Cannot decode remote sender: {}", + remote_sender + ))); + } + // this is bech32 prefix of sender from other chains. Should not error because we are in the cosmos ecosystem. Every address should have prefix + let mut prefix = decode_result.unwrap().0; + // evm case, need to filter remote token denom since prefix is always oraib + if prefix.eq("oraib") { + prefix = match remote_token_denom.split_once("0x") { + Some((evm_prefix, _)) => evm_prefix.to_string(), + None => "".to_string(), + } + } + let relayer_fee = RELAYER_FEE.may_load(storage, &prefix)?; + // no need to deduct fee if no fee is found in the mapping + if relayer_fee.is_none() { + return Ok((amount, Uint128::from(0u64))); + } + let relayer_fee = relayer_fee.unwrap(); + let offer_asset_info = denom_to_asset_info(querier, local_token_denom); + let token_price = swap_router_contract.simulate_swap( + querier, + Uint128::from(1u64).checked_mul(Uint128::from(10 * decimals as u32))?, + vec![SwapOperation::OraiSwap { + offer_asset_info, + // always swap with orai. If it does not share a pool with ORAI => ignore, no fee + ask_asset_info: AssetInfo::NativeToken { + denom: "orai".to_string(), + }, + }], + ); + if token_price.is_err() { + // no fee is deducted + return Ok((amount, Uint128::from(0u64))); + } + let token_price = token_price.unwrap().amount; + let required_fee_needed = relayer_fee.checked_div(token_price).unwrap_or_default(); + // accumulate fee so that we can collect it later after everything + // we share the same accumulator because it's the same data structure, and we are accumulating so it's fine + TOKEN_FEE_ACCUMULATOR.update( + storage, + local_token_denom, + |prev_fee| -> StdResult { + Ok(prev_fee + .unwrap_or_default() + .checked_add(required_fee_needed)?) + }, + )?; + Ok(( + amount.checked_sub(required_fee_needed).unwrap_or_default(), + required_fee_needed, + )) } pub fn deduct_fee(token_fee: Ratio, amount: Uint128) -> Uint128 { @@ -991,3 +1055,17 @@ pub fn parse_asset_info_denom(asset_info: AssetInfo) -> String { pub fn parse_ibc_wasm_port_id(contract_addr: String) -> String { format!("wasm.{}", contract_addr) } + +pub fn denom_to_asset_info(querier: &QuerierWrapper, denom: &str) -> AssetInfo { + if querier + .query_wasm_smart::(denom.clone(), &Cw20QueryMsg::TokenInfo {}) + .is_ok() + { + return AssetInfo::Token { + contract_addr: Addr::unchecked(denom), + }; + } + AssetInfo::NativeToken { + denom: denom.to_string(), + } +} diff --git a/contracts/cw-ics20-latest/src/ibc_tests.rs b/contracts/cw-ics20-latest/src/ibc_tests.rs index 3a99915..befda9c 100644 --- a/contracts/cw-ics20-latest/src/ibc_tests.rs +++ b/contracts/cw-ics20-latest/src/ibc_tests.rs @@ -20,9 +20,8 @@ mod test { use crate::error::ContractError; use crate::state::{ - get_key_ics20_ibc_denom, increase_channel_balance, ChannelState, IbcSingleStepData, Ratio, - SingleStepReplyArgs, CHANNEL_REVERSE_STATE, SINGLE_STEP_REPLY_ARGS, TOKEN_FEE, - TOKEN_FEE_ACCUMULATOR, + get_key_ics20_ibc_denom, increase_channel_balance, Ratio, SingleStepReplyArgs, + SINGLE_STEP_REPLY_ARGS, TOKEN_FEE, TOKEN_FEE_ACCUMULATOR, }; use cw20::{Cw20Coin, Cw20ExecuteMsg}; use cw20_ics20_msg::amount::{convert_local_to_remote, Amount}; @@ -339,12 +338,17 @@ mod test { amount: u128, denom: &str, receiver: &str, + sender: Option<&str>, ) -> IbcPacket { let data = Ics20Packet { // this is returning a foreign native token, thus denom is , eg: uatom denom: denom.to_string(), amount: amount.into(), - sender: "remote-sender".to_string(), + sender: if sender.is_none() { + "remote-sender".to_string() + } else { + sender.unwrap().to_string() + }, receiver: receiver.to_string(), memo: None, }; @@ -437,8 +441,13 @@ mod test { ); // prepare some mock packets - let recv_packet = - mock_receive_packet_remote_to_local(send_channel, 876543210, cw20_denom, custom_addr); + let recv_packet = mock_receive_packet_remote_to_local( + send_channel, + 876543210, + cw20_denom, + custom_addr, + None, + ); // we can receive this denom, channel balance should increase let msg = IbcPacketReceiveMsg::new(recv_packet.clone()); @@ -498,12 +507,13 @@ mod test { send_amount.u128(), denom, custom_addr, + Some("orai1cdhkt9ps47hwn9sqren70uw9cyrfka9fpauuks"), ); // we can receive this denom, channel balance should increase let msg = IbcPacketReceiveMsg::new(recv_packet.clone()); let res = ibc_packet_receive(deps.as_mut(), mock_env(), msg).unwrap(); - println!("res: {:?}", res.messages); + println!("res: {:?}", res); // TODO: fix test cases. Possibly because we are adding two add_submessages? assert_eq!(res.messages.len(), 2); // 2 messages because we also have deduct fee msg match res.messages[0].msg.clone() { @@ -1076,7 +1086,7 @@ mod test { let token_fee_denom = "foo0x"; // should return amount because we have not set relayer fee yet assert_eq!( - deduct_token_fee(storage, "foo", amount, "foo").unwrap(), + deduct_token_fee(storage, "foo", amount, "foo").unwrap().0, amount.clone() ); TOKEN_FEE @@ -1090,7 +1100,9 @@ mod test { ) .unwrap(); assert_eq!( - deduct_token_fee(storage, token_fee_denom, amount, "foo").unwrap(), + deduct_token_fee(storage, token_fee_denom, amount, "foo") + .unwrap() + .0, Uint128::from(990u64) ); assert_eq!( diff --git a/contracts/cw-ics20-latest/src/msg.rs b/contracts/cw-ics20-latest/src/msg.rs index 9dad5ce..7f71143 100644 --- a/contracts/cw-ics20-latest/src/msg.rs +++ b/contracts/cw-ics20-latest/src/msg.rs @@ -3,7 +3,7 @@ use cosmwasm_std::{Binary, IbcEndpoint}; use cw20::Cw20ReceiveMsg; use oraiswap::asset::AssetInfo; -use crate::state::{ChannelInfo, MappingMetadata, Ratio, TokenFee}; +use crate::state::{ChannelInfo, MappingMetadata, Ratio, RelayerFee, TokenFee}; use cw20_ics20_msg::amount::Amount; #[cw_serde] @@ -55,6 +55,7 @@ pub enum ExecuteMsg { fee_denom: Option, swap_router_contract: Option, token_fee: Option>, + relayer_fee: Option>, fee_receiver: Option, }, } diff --git a/contracts/cw-ics20-latest/src/state.rs b/contracts/cw-ics20-latest/src/state.rs index 3abb7ec..bd12925 100644 --- a/contracts/cw-ics20-latest/src/state.rs +++ b/contracts/cw-ics20-latest/src/state.rs @@ -1,5 +1,5 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, Decimal, IbcEndpoint, StdResult, Storage, Uint128}; +use cosmwasm_std::{Addr, IbcEndpoint, StdResult, Storage, Uint128}; use cw_controllers::Admin; use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, MultiIndex}; use oraiswap::{asset::AssetInfo, router::RouterController}; @@ -34,11 +34,13 @@ pub const CHANNEL_FORWARD_STATE: Map<(&str, &str), ChannelState> = pub const ALLOW_LIST: Map<&Addr, AllowInfo> = Map::new("allow_list"); pub const TOKEN_FEE: Map<&str, Ratio> = Map::new("token_fee"); -pub const TOKEN_FEE_ACCUMULATOR: Map<&str, Uint128> = Map::new("token_fee_accumulator"); // relayer fee. This fee depends on the network type, not token type -pub const RELAYER_FEE: Map<&str, Decimal> = Map::new("relayer_fee"); -pub const RELAYER_FEE_ACCUMULATOR: Map<&str, Uint128> = Map::new("relayer_fee_accumulator"); +// decimals of relayer fee should always be 10^6 because we use ORAI as relayer fee +pub const RELAYER_FEE: Map<&str, Uint128> = Map::new("relayer_fee"); + +// shared accumulator fee for token & relayer +pub const TOKEN_FEE_ACCUMULATOR: Map<&str, Uint128> = Map::new("token_fee_accumulator"); // MappingMetadataIndexex structs keeps a list of indexers pub struct MappingMetadataIndexex<'a> { @@ -103,6 +105,12 @@ pub struct TokenFee { pub ratio: Ratio, } +#[cw_serde] +pub struct RelayerFee { + pub prefix: String, + pub fee: Uint128, +} + #[cw_serde] pub struct Ratio { pub nominator: u64, From 25c49eb6f17d2efa3f95c2873dada418a063a30d Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Wed, 26 Jul 2023 17:34:28 +0700 Subject: [PATCH 08/28] reverted channel balance handling --- contracts/cw-ics20-latest/src/contract.rs | 71 +++++----- contracts/cw-ics20-latest/src/ibc.rs | 157 +++++++++------------ contracts/cw-ics20-latest/src/ibc_tests.rs | 44 +++++- contracts/cw-ics20-latest/src/state.rs | 82 +++++------ 4 files changed, 183 insertions(+), 171 deletions(-) diff --git a/contracts/cw-ics20-latest/src/contract.rs b/contracts/cw-ics20-latest/src/contract.rs index 72b1a89..ef7e5e0 100644 --- a/contracts/cw-ics20-latest/src/contract.rs +++ b/contracts/cw-ics20-latest/src/contract.rs @@ -21,9 +21,9 @@ use crate::msg::{ MigrateMsg, PairQuery, PortResponse, QueryMsg, TransferBackMsg, UpdatePairMsg, }; use crate::state::{ - get_key_ics20_ibc_denom, ics20_denoms, AllowInfo, Config, MappingMetadata, RelayerFee, - TokenFee, ADMIN, ALLOW_LIST, CHANNEL_INFO, CHANNEL_REVERSE_STATE, CONFIG, RELAYER_FEE, - TOKEN_FEE, + get_key_ics20_ibc_denom, ics20_denoms, reduce_channel_balance, AllowInfo, Config, + MappingMetadata, RelayerFee, TokenFee, ADMIN, ALLOW_LIST, CHANNEL_INFO, CHANNEL_REVERSE_STATE, + CONFIG, RELAYER_FEE, TOKEN_FEE, }; use cw20_ics20_msg::amount::{convert_local_to_remote, Amount}; use cw_utils::{maybe_addr, nonpayable, one_coin}; @@ -348,13 +348,13 @@ pub fn execute_transfer_back_to_remote_chain( // now this is processed in ack // // because we are transferring back, we reduce the channel's balance - // reduce_channel_balance( - // deps.storage, - // &msg.local_channel_id, - // &ibc_denom, - // amount_remote, - // false, - // )?; + reduce_channel_balance( + deps.storage, + &msg.local_channel_id, + &ibc_denom, + amount_remote, + false, + )?; // prepare ibc message let msg = IbcMsg::SendPacket { @@ -697,7 +697,7 @@ mod test { use super::*; use crate::ibc::ibc_packet_receive; - use crate::state::{reduce_channel_balance, Ratio}; + use crate::state::{Ratio, TOKEN_FEE_ACCUMULATOR}; use crate::test_helpers::*; use cosmwasm_std::testing::{mock_env, mock_info}; @@ -1230,23 +1230,32 @@ mod test { memo: None, }; - // let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { - // sender: original_sender.to_string(), - // amount: Uint128::from(amount), - // msg: to_binary(&transfer).unwrap(), - // }); + let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { + sender: original_sender.to_string(), + amount: Uint128::from(amount), + msg: to_binary(&transfer).unwrap(), + }); - // // insufficient funds case because we need to receive from remote chain first + // insufficient funds case because we need to receive from remote chain first let info = mock_info(cw20_raw_denom, &[]); - // let res = execute(deps.as_mut(), mock_env(), info.clone(), msg.clone()); - // println!("res: {:?}", res); - // assert_eq!( - // res.unwrap_err(), - // ContractError::NoSuchChannelState { - // id: local_channel.to_string(), - // denom: get_key_ics20_ibc_denom("wasm.cosmos2contract", local_channel, denom) - // } - // ); + let res = execute(deps.as_mut(), mock_env(), info.clone(), msg.clone()); + println!("res: {:?}", res); + assert_eq!( + res.unwrap_err(), + ContractError::NoSuchChannelState { + id: local_channel.to_string(), + denom: get_key_ics20_ibc_denom("wasm.cosmos2contract", local_channel, denom) + } + ); + + // we need to reset fee accumulator because when execute returns error, the test state is still applied + TOKEN_FEE_ACCUMULATOR + .save( + deps.as_mut().storage, + "cw20:token-addr", + &Uint128::from(0u64), + ) + .unwrap(); // prepare some mock packets let recv_packet = @@ -1314,16 +1323,6 @@ mod test { _ => panic!("Unexpected return message: {:?}", res.messages[0]), } - // since we reduce channel balance afterwards => need to manually reduce balance in test - reduce_channel_balance( - deps.as_mut().storage, - local_channel.into(), - &get_key_ics20_ibc_denom(CONTRACT_PORT, local_channel, denom), - Uint128::from(amount).checked_sub(fee_amount).unwrap(), - false, - ) - .unwrap(); - // check new channel state after reducing balance let chan = query_channel(deps.as_ref(), local_channel.into(), None).unwrap(); assert_eq!( diff --git a/contracts/cw-ics20-latest/src/ibc.rs b/contracts/cw-ics20-latest/src/ibc.rs index b1f5b8e..320f93d 100644 --- a/contracts/cw-ics20-latest/src/ibc.rs +++ b/contracts/cw-ics20-latest/src/ibc.rs @@ -16,9 +16,9 @@ use oraiswap::router::{RouterController, SwapOperation}; use crate::error::{ContractError, Never}; use crate::state::{ get_key_ics20_ibc_denom, ics20_denoms, increase_channel_balance, reduce_channel_balance, - ChannelInfo, IbcSingleStepData, MappingMetadata, Ratio, ReplyArgs, SingleStepReplyArgs, - ALLOW_LIST, CHANNEL_INFO, CONFIG, RELAYER_FEE, REPLY_ARGS, SINGLE_STEP_REPLY_ARGS, TOKEN_FEE, - TOKEN_FEE_ACCUMULATOR, + undo_reduce_channel_balance, ChannelInfo, IbcSingleStepData, MappingMetadata, Ratio, ReplyArgs, + SingleStepReplyArgs, ALLOW_LIST, CHANNEL_INFO, CONFIG, RELAYER_FEE, REPLY_ARGS, + SINGLE_STEP_REPLY_ARGS, TOKEN_FEE, TOKEN_FEE_ACCUMULATOR, }; use cw20::{Cw20ExecuteMsg, Cw20QueryMsg, TokenInfoResponse}; use cw20_ics20_msg::amount::{convert_local_to_remote, convert_remote_to_local, Amount}; @@ -99,24 +99,11 @@ pub const ACK_FAILURE_ID: u64 = 64023; pub fn reply(deps: DepsMut, _env: Env, reply: Reply) -> Result { match reply.id { FOLLOW_UP_ID => match reply.result { - SubMsgResult::Ok(_) => { - let reply_args = SINGLE_STEP_REPLY_ARGS.load(deps.storage)?; - if let Some(ibc_data) = reply_args.ibc_data { - // only reduce balance when everything is done and successful. Not reduce optimistically - reduce_channel_balance( - deps.storage, - &reply_args.channel, - &ibc_data.ibc_denom, - ibc_data.remote_amount, - false, - )?; - } - Ok(Response::new()) - } + SubMsgResult::Ok(_) => Ok(Response::new()), SubMsgResult::Err(err) => { let reply_args = SINGLE_STEP_REPLY_ARGS.load(deps.storage)?; // only refund, not undo reduce balance - handle_follow_up_failure(reply_args, err) + handle_follow_up_failure(deps.storage, reply_args, err) } }, ACK_FAILURE_ID => match reply.result { @@ -388,7 +375,7 @@ fn handle_ibc_packet_receive_native_remote_chain( )?; let submsgs: Vec = submsgs .into_iter() - .map(|msg| SubMsg::reply_always(msg, FOLLOW_UP_ID)) + .map(|msg| SubMsg::reply_on_error(msg, FOLLOW_UP_ID)) .collect(); let transfer_fee_to_admin = @@ -615,15 +602,15 @@ pub fn build_ibc_msg( &remote_address, Some(destination.receiver), ); - // // because we are transferring back, we reduce the channel's balance - // reduce_channel_balance( - // storage, - // &local_channel_id.clone(), - // &mapping.0.clone(), - // remote_amount, - // false, - // ) - // .map_err(|err| StdError::generic_err(err.to_string()))?; + // because we are transferring back, we reduce the channel's balance + reduce_channel_balance( + storage, + &local_channel_id.clone(), + &mapping.0.clone(), + remote_amount, + false, + ) + .map_err(|err| StdError::generic_err(err.to_string()))?; reply_args.channel = local_channel_id.to_string(); reply_args.ibc_data = Some(IbcSingleStepData { ibc_denom: mapping.0, @@ -655,25 +642,26 @@ pub fn build_ibc_msg( } pub fn handle_follow_up_failure( + storage: &mut dyn Storage, reply_args: SingleStepReplyArgs, err: String, ) -> Result { // if there's an error but no ibc msg aka no channel balance reduce => wont undo reduce let mut response: Response = Response::new(); - // if let Some(ibc_data) = reply_args.ibc_data { - // undo_reduce_channel_balance( - // storage, - // &reply_args.channel, - // &ibc_data.ibc_denom, - // ibc_data.remote_amount, - // false, - // )?; - // response = response.add_attributes(vec![ - // attr("undo_reduce_channel", reply_args.channel), - // attr("undo_reduce_channel_ibc_denom", ibc_data.ibc_denom), - // attr("undo_reduce_channel_balance", ibc_data.remote_amount), - // ]); - // } + if let Some(ibc_data) = reply_args.ibc_data { + undo_reduce_channel_balance( + storage, + &reply_args.channel, + &ibc_data.ibc_denom, + ibc_data.remote_amount, + false, + )?; + response = response.add_attributes(vec![ + attr("undo_reduce_channel", reply_args.channel), + attr("undo_reduce_channel_ibc_denom", ibc_data.ibc_denom), + attr("undo_reduce_channel_balance", ibc_data.remote_amount), + ]); + } let refund_amount = Amount::from_parts( parse_asset_info_denom(reply_args.refund_asset_info.clone()), reply_args.local_amount, @@ -909,18 +897,9 @@ pub fn ibc_packet_timeout( } // update the balance stored on this (channel, denom) index -fn on_packet_success(deps: DepsMut, packet: IbcPacket) -> Result { +fn on_packet_success(_deps: DepsMut, packet: IbcPacket) -> Result { let msg: Ics20Packet = from_binary(&packet.data)?; - // if packet success it means that we transfer back to remote chain successfully. We now should reduce the balance - reduce_channel_balance( - deps.storage, - packet.src.channel_id.as_str(), - &msg.denom, - msg.amount, - false, - )?; - // similar event messages like ibctransfer module let attributes = vec![ attr("action", "acknowledge"), @@ -947,44 +926,44 @@ fn on_packet_failure( let msg: Ics20Packet = from_binary(&packet.data)?; // in case that the denom is not in the mapping list, meaning that it is not transferred back, but transfer originally from this local chain - // if ics20_denoms().may_load(deps.storage, &msg.denom)?.is_none() { - // // undo the balance update on failure (as we pre-emptively added it on send) - // reduce_channel_balance( - // deps.storage, - // &packet.src.channel_id, - // &msg.denom, - // msg.amount, - // true, - // )?; - - // let to_send = Amount::from_parts(msg.denom.clone(), msg.amount); - // let gas_limit = check_gas_limit(deps.as_ref(), &to_send)?; - // let send = send_amount(to_send, msg.sender.clone(), None); - // let mut submsg = SubMsg::reply_on_error(send, ACK_FAILURE_ID); - // submsg.gas_limit = gas_limit; - - // // similar event messages like ibctransfer module - // let res = IbcBasicResponse::new() - // .add_submessage(submsg) - // .add_attribute("action", "acknowledge") - // .add_attribute("sender", msg.sender) - // .add_attribute("receiver", msg.receiver) - // .add_attribute("denom", msg.denom) - // .add_attribute("amount", msg.amount.to_string()) - // .add_attribute("success", "false") - // .add_attribute("error", err); - - // return Ok(res); - // } + if ics20_denoms().may_load(deps.storage, &msg.denom)?.is_none() { + // // undo the balance update on failure (as we pre-emptively added it on send) + // reduce_channel_balance( + // deps.storage, + // &packet.src.channel_id, + // &msg.denom, + // msg.amount, + // true, + // )?; + + // let to_send = Amount::from_parts(msg.denom.clone(), msg.amount); + // let gas_limit = check_gas_limit(deps.as_ref(), &to_send)?; + // let send = send_amount(to_send, msg.sender.clone(), None); + // let mut submsg = SubMsg::reply_on_error(send, ACK_FAILURE_ID); + // submsg.gas_limit = gas_limit; + + // // similar event messages like ibctransfer module + // let res = IbcBasicResponse::new() + // .add_submessage(submsg) + // .add_attribute("action", "acknowledge") + // .add_attribute("sender", msg.sender) + // .add_attribute("receiver", msg.receiver) + // .add_attribute("denom", msg.denom) + // .add_attribute("amount", msg.amount.to_string()) + // .add_attribute("success", "false") + // .add_attribute("error", err); + + return Ok(IbcBasicResponse::new()); + } - // // 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, - // false, - // )?; + // 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, + false, + )?; // 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(deps.storage, &msg.denom)?; diff --git a/contracts/cw-ics20-latest/src/ibc_tests.rs b/contracts/cw-ics20-latest/src/ibc_tests.rs index befda9c..55efc66 100644 --- a/contracts/cw-ics20-latest/src/ibc_tests.rs +++ b/contracts/cw-ics20-latest/src/ibc_tests.rs @@ -20,8 +20,9 @@ mod test { use crate::error::ContractError; use crate::state::{ - get_key_ics20_ibc_denom, increase_channel_balance, Ratio, SingleStepReplyArgs, - SINGLE_STEP_REPLY_ARGS, TOKEN_FEE, TOKEN_FEE_ACCUMULATOR, + get_key_ics20_ibc_denom, increase_channel_balance, ChannelState, IbcSingleStepData, Ratio, + SingleStepReplyArgs, CHANNEL_REVERSE_STATE, SINGLE_STEP_REPLY_ARGS, TOKEN_FEE, + TOKEN_FEE_ACCUMULATOR, }; use cw20::{Cw20Coin, Cw20ExecuteMsg}; use cw20_ics20_msg::amount::{convert_local_to_remote, Amount}; @@ -971,6 +972,7 @@ mod test { #[test] fn test_handle_follow_up_failure() { let local_channel_id = "channel-0"; + let mut deps = setup(&[local_channel_id], &[]); let native_denom = "cosmos"; let refund_asset_info = AssetInfo::NativeToken { denom: native_denom.to_string(), @@ -978,15 +980,19 @@ mod test { let amount = Uint128::from(100u128); let receiver = "receiver"; let err = "ack_failed"; - let single_step_reply_args = SingleStepReplyArgs { + let mut single_step_reply_args = SingleStepReplyArgs { channel: local_channel_id.to_string(), refund_asset_info: refund_asset_info.clone(), ibc_data: None, local_amount: amount, receiver: receiver.to_string(), }; - let result = - handle_follow_up_failure(single_step_reply_args.clone(), err.to_string()).unwrap(); + let result = handle_follow_up_failure( + deps.as_mut().storage, + single_step_reply_args.clone(), + err.to_string(), + ) + .unwrap(); assert_eq!( result, Response::new() @@ -1008,6 +1014,34 @@ mod test { attr("attempt_refund_amount", single_step_reply_args.local_amount), ]) ); + let ibc_denom = "ibc_denom"; + let remote_amount = convert_local_to_remote(amount, 18, 6).unwrap(); + single_step_reply_args.ibc_data = Some(IbcSingleStepData { + ibc_denom: ibc_denom.to_string(), + remote_amount: remote_amount.clone(), + }); + // if has ibc denom then it's evm based, need to undo reducing balance + CHANNEL_REVERSE_STATE + .save( + deps.as_mut().storage, + (local_channel_id, ibc_denom), + &ChannelState { + outstanding: Uint128::from(0u128), + total_sent: Uint128::from(100u128), + }, + ) + .unwrap(); + handle_follow_up_failure( + deps.as_mut().storage, + single_step_reply_args.clone(), + err.to_string(), + ) + .unwrap(); + let channel_state = CHANNEL_REVERSE_STATE + .load(deps.as_mut().storage, (local_channel_id, ibc_denom)) + .unwrap(); + // should undo reduce channel state + assert_eq!(channel_state.outstanding, remote_amount) } #[test] diff --git a/contracts/cw-ics20-latest/src/state.rs b/contracts/cw-ics20-latest/src/state.rs index bd12925..f76452d 100644 --- a/contracts/cw-ics20-latest/src/state.rs +++ b/contracts/cw-ics20-latest/src/state.rs @@ -201,47 +201,47 @@ pub fn reduce_channel_balance( Ok(()) } -// // this is like increase, but it only "un-subtracts" (= adds) outstanding, not total_sent -// // calling `reduce_channel_balance` and then `undo_reduce_channel_balance` should leave state unchanged. -// pub fn undo_reduce_channel_balance( -// storage: &mut dyn Storage, -// channel: &str, -// denom: &str, -// amount: Uint128, -// forward: bool, -// ) -> Result<(), ContractError> { -// let mut state = CHANNEL_REVERSE_STATE; -// if forward { -// state = CHANNEL_FORWARD_STATE; -// } -// state.update(storage, (channel, denom), |orig| -> StdResult<_> { -// let mut state = orig.unwrap_or_default(); -// state.outstanding += amount; -// Ok(state) -// })?; -// Ok(()) -// } - -// // this is like decrease, but it only "un-add" (= adds) outstanding, not total_sent -// // calling `increase_channel_balance` and then `undo_increase_channel_balance` should leave state unchanged. -// pub fn undo_increase_channel_balance( -// storage: &mut dyn Storage, -// channel: &str, -// denom: &str, -// amount: Uint128, -// forward: bool, -// ) -> Result<(), ContractError> { -// let mut state = CHANNEL_REVERSE_STATE; -// if forward { -// state = CHANNEL_FORWARD_STATE; -// } -// state.update(storage, (channel, denom), |orig| -> StdResult<_> { -// let mut state = orig.unwrap_or_default(); -// state.outstanding -= amount; -// Ok(state) -// })?; -// Ok(()) -// } +// this is like increase, but it only "un-subtracts" (= adds) outstanding, not total_sent +// calling `reduce_channel_balance` and then `undo_reduce_channel_balance` should leave state unchanged. +pub fn undo_reduce_channel_balance( + storage: &mut dyn Storage, + channel: &str, + denom: &str, + amount: Uint128, + forward: bool, +) -> Result<(), ContractError> { + let mut state = CHANNEL_REVERSE_STATE; + if forward { + state = CHANNEL_FORWARD_STATE; + } + state.update(storage, (channel, denom), |orig| -> StdResult<_> { + let mut state = orig.unwrap_or_default(); + state.outstanding += amount; + Ok(state) + })?; + Ok(()) +} + +// this is like decrease, but it only "un-add" (= adds) outstanding, not total_sent +// calling `increase_channel_balance` and then `undo_increase_channel_balance` should leave state unchanged. +pub fn undo_increase_channel_balance( + storage: &mut dyn Storage, + channel: &str, + denom: &str, + amount: Uint128, + forward: bool, +) -> Result<(), ContractError> { + let mut state = CHANNEL_REVERSE_STATE; + if forward { + state = CHANNEL_FORWARD_STATE; + } + state.update(storage, (channel, denom), |orig| -> StdResult<_> { + let mut state = orig.unwrap_or_default(); + state.outstanding -= amount; + Ok(state) + })?; + Ok(()) +} pub fn get_key_ics20_ibc_denom(port_id: &str, channel_id: &str, denom: &str) -> String { format!("{}/{}/{}", port_id, channel_id, denom) From e602a50f00d0592339536a02ad0384af146ce937 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Wed, 26 Jul 2023 18:28:26 +0700 Subject: [PATCH 09/28] passed all test cases for relayer fees --- contracts/cw-ics20-latest/src/contract.rs | 2 +- contracts/cw-ics20-latest/src/ibc.rs | 53 ++++++------ contracts/cw-ics20-latest/src/ibc_tests.rs | 99 ++++++++++++++++++++-- 3 files changed, 121 insertions(+), 33 deletions(-) diff --git a/contracts/cw-ics20-latest/src/contract.rs b/contracts/cw-ics20-latest/src/contract.rs index ef7e5e0..db19dde 100644 --- a/contracts/cw-ics20-latest/src/contract.rs +++ b/contracts/cw-ics20-latest/src/contract.rs @@ -1315,7 +1315,7 @@ mod test { msg, to_binary(&Cw20ExecuteMsg::Transfer { recipient: "gov".to_string(), - amount: fee_amount.clone().checked_mul(Uint128::from(2u64)).unwrap() // mul with 2 because deduct when receive token then transfer back + amount: fee_amount.clone() }) .unwrap() ); diff --git a/contracts/cw-ics20-latest/src/ibc.rs b/contracts/cw-ics20-latest/src/ibc.rs index 320f93d..040809d 100644 --- a/contracts/cw-ics20-latest/src/ibc.rs +++ b/contracts/cw-ics20-latest/src/ibc.rs @@ -711,15 +711,29 @@ pub fn process_deduct_fee( swap_router_contract: &RouterController, ) -> StdResult { let (_, token_fee) = deduct_token_fee(storage, remote_token_denom, amount, local_token_denom)?; + // simulate for relayer fee + let offer_asset_info = denom_to_asset_info(querier, local_token_denom); + let token_price = swap_router_contract + .simulate_swap( + querier, + Uint128::from(1u64).checked_mul(Uint128::from(10 * decimals as u32))?, + vec![SwapOperation::OraiSwap { + offer_asset_info, + // always swap with orai. If it does not share a pool with ORAI => ignore, no fee + ask_asset_info: AssetInfo::NativeToken { + denom: "orai".to_string(), + }, + }], + ) + .map(|data| data.amount) + .unwrap_or_default(); let (_, relayer_fee) = deduct_relayer_fee( storage, - querier, remote_sender, remote_token_denom, amount, - decimals, local_token_denom, - &swap_router_contract, + token_price, )?; let new_amount = amount .checked_sub(token_fee) @@ -756,19 +770,21 @@ pub fn deduct_token_fee( pub fn deduct_relayer_fee( storage: &mut dyn Storage, - querier: &QuerierWrapper, - remote_sender: &str, + remote_address: &str, remote_token_denom: &str, - amount: Uint128, // local amount - decimals: u8, + amount: Uint128, // local amount local_token_denom: &str, // local denom - swap_router_contract: &RouterController, + token_price: Uint128, ) -> StdResult<(Uint128, Uint128)> { - let decode_result = bech32::decode(remote_sender); + if token_price.is_zero() { + return Ok((amount, Uint128::from(0u64))); + } + + let decode_result = bech32::decode(remote_address); if decode_result.is_err() { return Err(StdError::generic_err(format!( "Cannot decode remote sender: {}", - remote_sender + remote_address ))); } // this is bech32 prefix of sender from other chains. Should not error because we are in the cosmos ecosystem. Every address should have prefix @@ -786,23 +802,6 @@ pub fn deduct_relayer_fee( return Ok((amount, Uint128::from(0u64))); } let relayer_fee = relayer_fee.unwrap(); - let offer_asset_info = denom_to_asset_info(querier, local_token_denom); - let token_price = swap_router_contract.simulate_swap( - querier, - Uint128::from(1u64).checked_mul(Uint128::from(10 * decimals as u32))?, - vec![SwapOperation::OraiSwap { - offer_asset_info, - // always swap with orai. If it does not share a pool with ORAI => ignore, no fee - ask_asset_info: AssetInfo::NativeToken { - denom: "orai".to_string(), - }, - }], - ); - if token_price.is_err() { - // no fee is deducted - return Ok((amount, Uint128::from(0u64))); - } - let token_price = token_price.unwrap().amount; let required_fee_needed = relayer_fee.checked_div(token_price).unwrap_or_default(); // accumulate fee so that we can collect it later after everything // we share the same accumulator because it's the same data structure, and we are accumulating so it's fine diff --git a/contracts/cw-ics20-latest/src/ibc_tests.rs b/contracts/cw-ics20-latest/src/ibc_tests.rs index 55efc66..87d7974 100644 --- a/contracts/cw-ics20-latest/src/ibc_tests.rs +++ b/contracts/cw-ics20-latest/src/ibc_tests.rs @@ -6,10 +6,11 @@ mod test { use oraiswap::router::SwapOperation; use crate::ibc::{ - ack_fail, build_ibc_msg, build_swap_msgs, check_gas_limit, deduct_fee, deduct_token_fee, - handle_follow_up_failure, ibc_packet_receive, is_follow_up_msgs_only_send_amount, - parse_voucher_denom, parse_voucher_denom_without_sanity_checks, send_amount, Ics20Ack, - Ics20Packet, REFUND_FAILURE_ID, + ack_fail, build_ibc_msg, build_swap_msgs, check_gas_limit, deduct_fee, deduct_relayer_fee, + deduct_token_fee, handle_follow_up_failure, ibc_packet_receive, + is_follow_up_msgs_only_send_amount, parse_voucher_denom, + parse_voucher_denom_without_sanity_checks, send_amount, Ics20Ack, Ics20Packet, + REFUND_FAILURE_ID, }; use crate::ibc::{build_swap_operations, get_follow_up_msgs}; use crate::test_helpers::*; @@ -21,7 +22,7 @@ mod test { use crate::error::ContractError; use crate::state::{ get_key_ics20_ibc_denom, increase_channel_balance, ChannelState, IbcSingleStepData, Ratio, - SingleStepReplyArgs, CHANNEL_REVERSE_STATE, SINGLE_STEP_REPLY_ARGS, TOKEN_FEE, + SingleStepReplyArgs, CHANNEL_REVERSE_STATE, RELAYER_FEE, SINGLE_STEP_REPLY_ARGS, TOKEN_FEE, TOKEN_FEE_ACCUMULATOR, }; use cw20::{Cw20Coin, Cw20ExecuteMsg}; @@ -1144,4 +1145,92 @@ mod test { Uint128::from(10u64) ); } + + #[test] + fn test_deduct_relayer_fee() { + let mut deps = mock_dependencies(); + let amount = Uint128::from(1000u64); + let storage = deps.as_mut().storage; + let token_fee_denom = "cosmos"; + let remote_address = "cosmos1zedxv25ah8fksmg2lzrndrpkvsjqgk4zt5ff7n"; + let token_price = Uint128::from(10u64); + // token price empty case. Should return zero fee + let result = deduct_relayer_fee( + storage, + remote_address, + token_fee_denom, + amount, + "local_token_denom", + Uint128::from(0u64), + ) + .unwrap(); + assert_eq!(result.1, Uint128::from(0u64)); + + // remote address is wrong (dont follow bech32 form) + assert_eq!( + deduct_relayer_fee( + storage, + "foobar", + token_fee_denom, + amount, + "local_token_denom", + token_price, + ) + .unwrap_err(), + StdError::generic_err("Cannot decode remote sender: foobar") + ); + + // no relayer fee case + assert_eq!( + deduct_relayer_fee( + storage, + remote_address, + token_fee_denom, + amount, + "local_token_denom", + token_price, + ) + .unwrap() + .1, + Uint128::from(0u64) + ); + + // oraib prefix case. + RELAYER_FEE + .save(storage, token_fee_denom, &Uint128::from(100u64)) + .unwrap(); + + RELAYER_FEE + .save(storage, "foo", &Uint128::from(1000u64)) + .unwrap(); + + assert_eq!( + deduct_relayer_fee( + storage, + "oraib1603j3e4juddh7cuhfquxspl0p0nsun047wz3rl", + "foo0x", + amount, + "local_token_denom", + token_price, + ) + .unwrap() + .1, + Uint128::from(100u64) + ); + + // normal case with remote address + assert_eq!( + deduct_relayer_fee( + storage, + remote_address, + token_fee_denom, + amount, + "local_token_denom", + token_price, + ) + .unwrap() + .1, + Uint128::from(10u64) + ); + } } From d227bf958626082ec3ceabdd29e4498d41d7c4bb Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Thu, 27 Jul 2023 13:53:02 +0700 Subject: [PATCH 10/28] splitted convert to evm prefix to function --- contracts/cw-ics20-latest/src/ibc.rs | 17 ++++++------- contracts/cw-ics20-latest/src/ibc_tests.rs | 28 +++++++++++----------- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/contracts/cw-ics20-latest/src/ibc.rs b/contracts/cw-ics20-latest/src/ibc.rs index 040809d..097f95a 100644 --- a/contracts/cw-ics20-latest/src/ibc.rs +++ b/contracts/cw-ics20-latest/src/ibc.rs @@ -791,10 +791,7 @@ pub fn deduct_relayer_fee( let mut prefix = decode_result.unwrap().0; // evm case, need to filter remote token denom since prefix is always oraib if prefix.eq("oraib") { - prefix = match remote_token_denom.split_once("0x") { - Some((evm_prefix, _)) => evm_prefix.to_string(), - None => "".to_string(), - } + prefix = convert_remote_denom_to_evm_prefix(remote_token_denom); } let relayer_fee = RELAYER_FEE.may_load(storage, &prefix)?; // no need to deduct fee if no fee is found in the mapping @@ -831,12 +828,12 @@ pub fn deduct_fee(token_fee: Ratio, amount: Uint128) -> Uint128 { )) } -// pub fn convert_remote_denom_to_evm_prefix(remote_denom: &str) -> String { -// match remote_denom.split_once("0x") { -// Some((evm_prefix, _)) => return evm_prefix.to_string(), -// None => "".to_string(), -// } -// } +pub fn convert_remote_denom_to_evm_prefix(remote_denom: &str) -> String { + match remote_denom.split_once("0x") { + Some((evm_prefix, _)) => return evm_prefix.to_string(), + None => "".to_string(), + } +} pub fn collect_transfer_fee_msgs( receiver: String, diff --git a/contracts/cw-ics20-latest/src/ibc_tests.rs b/contracts/cw-ics20-latest/src/ibc_tests.rs index 87d7974..a4f4358 100644 --- a/contracts/cw-ics20-latest/src/ibc_tests.rs +++ b/contracts/cw-ics20-latest/src/ibc_tests.rs @@ -6,11 +6,11 @@ mod test { use oraiswap::router::SwapOperation; use crate::ibc::{ - ack_fail, build_ibc_msg, build_swap_msgs, check_gas_limit, deduct_fee, deduct_relayer_fee, - deduct_token_fee, handle_follow_up_failure, ibc_packet_receive, - is_follow_up_msgs_only_send_amount, parse_voucher_denom, - parse_voucher_denom_without_sanity_checks, send_amount, Ics20Ack, Ics20Packet, - REFUND_FAILURE_ID, + ack_fail, build_ibc_msg, build_swap_msgs, check_gas_limit, + convert_remote_denom_to_evm_prefix, deduct_fee, deduct_relayer_fee, deduct_token_fee, + handle_follow_up_failure, ibc_packet_receive, is_follow_up_msgs_only_send_amount, + parse_voucher_denom, parse_voucher_denom_without_sanity_checks, send_amount, Ics20Ack, + Ics20Packet, REFUND_FAILURE_ID, }; use crate::ibc::{build_swap_operations, get_follow_up_msgs}; use crate::test_helpers::*; @@ -1089,15 +1089,15 @@ mod test { ); } - // #[test] - // fn test_convert_remote_denom_to_evm_prefix() { - // assert_eq!(convert_remote_denom_to_evm_prefix("abcd"), "".to_string()); - // assert_eq!(convert_remote_denom_to_evm_prefix("0x"), "".to_string()); - // assert_eq!( - // convert_remote_denom_to_evm_prefix("evm0x"), - // "evm".to_string() - // ); - // } + #[test] + fn test_convert_remote_denom_to_evm_prefix() { + assert_eq!(convert_remote_denom_to_evm_prefix("abcd"), "".to_string()); + assert_eq!(convert_remote_denom_to_evm_prefix("0x"), "".to_string()); + assert_eq!( + convert_remote_denom_to_evm_prefix("evm0x"), + "evm".to_string() + ); + } #[test] fn test_parse_voucher_denom_without_sanity_checks() { From 6e2f83af49c0384f018b7c6c92f7c232e415ee20 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Thu, 27 Jul 2023 13:56:10 +0700 Subject: [PATCH 11/28] optimized handle_ibc_packet_receive_native_remote_chain --- contracts/cw-ics20-latest/src/ibc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/cw-ics20-latest/src/ibc.rs b/contracts/cw-ics20-latest/src/ibc.rs index 097f95a..c164c41 100644 --- a/contracts/cw-ics20-latest/src/ibc.rs +++ b/contracts/cw-ics20-latest/src/ibc.rs @@ -379,7 +379,7 @@ fn handle_ibc_packet_receive_native_remote_chain( .collect(); let transfer_fee_to_admin = - collect_transfer_fee_msgs(CONFIG.load(storage)?.fee_receiver.into_string(), storage)?; + collect_transfer_fee_msgs(config.fee_receiver.into_string(), storage)?; let mut res = IbcReceiveResponse::new() .set_ack(ack_success()) .add_messages(transfer_fee_to_admin) From 517f59c80985b437ae5e83f9edb485018f9e72a0 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Thu, 27 Jul 2023 14:12:29 +0700 Subject: [PATCH 12/28] refactored build ibc send packet --- contracts/cw-ics20-latest/src/contract.rs | 42 +++++++--------- contracts/cw-ics20-latest/src/ibc.rs | 61 ++++++++++++++++------- 2 files changed, 62 insertions(+), 41 deletions(-) diff --git a/contracts/cw-ics20-latest/src/contract.rs b/contracts/cw-ics20-latest/src/contract.rs index db19dde..3260323 100644 --- a/contracts/cw-ics20-latest/src/contract.rs +++ b/contracts/cw-ics20-latest/src/contract.rs @@ -1,7 +1,7 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - from_binary, to_binary, Addr, Binary, Deps, DepsMut, Empty, Env, IbcEndpoint, IbcMsg, IbcQuery, + from_binary, to_binary, Addr, Binary, Deps, DepsMut, Empty, Env, IbcEndpoint, IbcQuery, MessageInfo, Order, PortIdResponse, Response, StdResult, Storage, }; use cw2::set_contract_version; @@ -12,8 +12,8 @@ use oraiswap::router::RouterController; use crate::error::ContractError; use crate::ibc::{ - collect_transfer_fee_msgs, parse_ibc_wasm_port_id, parse_voucher_denom, process_deduct_fee, - Ics20Packet, + build_ibc_send_packet, collect_transfer_fee_msgs, parse_ibc_wasm_port_id, parse_voucher_denom, + process_deduct_fee, }; use crate::msg::{ AllowMsg, AllowedInfo, AllowedResponse, ChannelResponse, ConfigResponse, DeletePairMsg, @@ -336,16 +336,6 @@ pub fn execute_transfer_back_to_remote_chain( mapping.pair_mapping.asset_info_decimals, )?; - // build ics20 packet - let packet = Ics20Packet::new( - amount_remote.clone(), - ibc_denom.clone(), // we use ibc denom in form // so that when it is sent back to remote chain, it gets parsed correctly and burned - sender.as_str(), - &msg.remote_address, - msg.memo, - ); - packet.validate()?; - // now this is processed in ack // // because we are transferring back, we reduce the channel's balance reduce_channel_balance( @@ -357,25 +347,29 @@ pub fn execute_transfer_back_to_remote_chain( )?; // prepare ibc message - let msg = IbcMsg::SendPacket { - channel_id: msg.local_channel_id, - data: to_binary(&packet)?, - timeout: timeout.into(), - }; + let ibc_msg = build_ibc_send_packet( + amount_remote, + &ibc_denom, // we use ibc denom in form // so that when it is sent back to remote chain, it gets parsed correctly and burned + sender.as_str(), + &msg.remote_address, + msg.memo, + &msg.local_channel_id, + timeout.into(), + )?; let mut cosmos_msgs = collect_transfer_fee_msgs(config.fee_receiver.into_string(), deps.storage)?; - cosmos_msgs.push(msg.into()); + cosmos_msgs.push(ibc_msg.into()); // send response let res = Response::new() .add_messages(cosmos_msgs) .add_attribute("action", "transfer") .add_attribute("type", "transfer_back_to_remote_chain") - .add_attribute("sender", &packet.sender) - .add_attribute("receiver", &packet.receiver) - .add_attribute("denom", &packet.denom) - .add_attribute("amount", &packet.amount.to_string()); + .add_attribute("sender", sender.as_str()) + .add_attribute("receiver", &msg.remote_address) + .add_attribute("denom", &ibc_denom) + .add_attribute("amount", &amount_remote.to_string()); Ok(res) } @@ -696,7 +690,7 @@ mod test { use std::ops::Sub; use super::*; - use crate::ibc::ibc_packet_receive; + use crate::ibc::{ibc_packet_receive, Ics20Packet}; use crate::state::{Ratio, TOKEN_FEE_ACCUMULATOR}; use crate::test_helpers::*; diff --git a/contracts/cw-ics20-latest/src/ibc.rs b/contracts/cw-ics20-latest/src/ibc.rs index c164c41..dfac4b3 100644 --- a/contracts/cw-ics20-latest/src/ibc.rs +++ b/contracts/cw-ics20-latest/src/ibc.rs @@ -5,9 +5,9 @@ use cosmwasm_std::{ attr, coin, entry_point, from_binary, to_binary, Addr, Api, BankMsg, Binary, CosmosMsg, Decimal, Deps, DepsMut, Env, IbcBasicResponse, IbcChannel, IbcChannelCloseMsg, IbcChannelConnectMsg, IbcChannelOpenMsg, IbcEndpoint, IbcMsg, IbcOrder, IbcPacket, - IbcPacketAckMsg, IbcPacketReceiveMsg, IbcPacketTimeoutMsg, IbcReceiveResponse, Order, - QuerierWrapper, Reply, Response, StdError, StdResult, Storage, SubMsg, SubMsgResult, Uint128, - WasmMsg, + IbcPacketAckMsg, IbcPacketReceiveMsg, IbcPacketTimeoutMsg, IbcReceiveResponse, IbcTimeout, + Order, QuerierWrapper, Reply, Response, StdError, StdResult, Storage, SubMsg, SubMsgResult, + Uint128, WasmMsg, }; use cw20_ics20_msg::receiver::DestinationInfo; use oraiswap::asset::AssetInfo; @@ -594,14 +594,6 @@ pub fn build_ibc_msg( mapping.1.asset_info_decimals, )?; - // build ics20 packet - let packet = Ics20Packet::new( - remote_amount.clone(), - mapping.0.clone(), // we use ibc denom in form // so that when it is sent back to remote chain, it gets parsed correctly and burned - env.contract.address.as_str(), - &remote_address, - Some(destination.receiver), - ); // because we are transferring back, we reduce the channel's balance reduce_channel_balance( storage, @@ -611,6 +603,18 @@ pub fn build_ibc_msg( false, ) .map_err(|err| StdError::generic_err(err.to_string()))?; + + // prepare ibc message + let msg = build_ibc_send_packet( + remote_amount, + &mapping.0, + env.contract.address.as_str(), + remote_address, + Some(destination.receiver), + local_channel_id, + timeout.into(), + )?; + reply_args.channel = local_channel_id.to_string(); reply_args.ibc_data = Some(IbcSingleStepData { ibc_denom: mapping.0, @@ -619,12 +623,6 @@ pub fn build_ibc_msg( // keep track of the reply. We need to keep a seperate value because if using REPLY, it could be overriden by the channel increase later on in reply SINGLE_STEP_REPLY_ARGS.save(storage, &reply_args)?; - // prepare ibc message - let msg = IbcMsg::SendPacket { - channel_id: local_channel_id.to_string(), - data: to_binary(&packet)?, - timeout: timeout.into(), - }; return Ok(msg.into()); } // 2nd case, where destination network is not evm, but it is still supported on our channel (eg: cw20 ATOM mapped with native ATOM on Cosmos), then we call @@ -1044,3 +1042,32 @@ pub fn denom_to_asset_info(querier: &QuerierWrapper, denom: &str) -> AssetInfo { denom: denom.to_string(), } } + +pub fn build_ibc_send_packet( + amount: Uint128, + denom: &str, + sender: &str, + receiver: &str, + memo: Option, + src_channel: &str, + timeout: IbcTimeout, +) -> StdResult { + // build ics20 packet + let packet = Ics20Packet::new( + amount.clone(), + denom, // we use ibc denom in form // so that when it is sent back to remote chain, it gets parsed correctly and burned + sender, + receiver, + memo, + ); + packet + .validate() + .map_err(|err| StdError::generic_err(err.to_string()))?; + + // prepare ibc message + Ok(IbcMsg::SendPacket { + channel_id: src_channel.to_string(), + data: to_binary(&packet)?, + timeout: timeout.into(), + }) +} From abe2cd37d74663400a2ac05afc8c01446f0778f2 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Thu, 27 Jul 2023 16:49:53 +0700 Subject: [PATCH 13/28] added helper functions, more test cases & check cosmos based dest info --- packages/cw20-ics20-msg/Cargo.toml | 4 +- packages/cw20-ics20-msg/src/amount.rs | 35 ++++++++++- packages/cw20-ics20-msg/src/helper.rs | 39 ++++++++++++ packages/cw20-ics20-msg/src/lib.rs | 1 + packages/cw20-ics20-msg/src/receiver.rs | 83 +++++++++++++++++++++++-- 5 files changed, 154 insertions(+), 8 deletions(-) create mode 100644 packages/cw20-ics20-msg/src/helper.rs diff --git a/packages/cw20-ics20-msg/Cargo.toml b/packages/cw20-ics20-msg/Cargo.toml index e755457..128632f 100644 --- a/packages/cw20-ics20-msg/Cargo.toml +++ b/packages/cw20-ics20-msg/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cw20-ics20-msg" -version = "0.0.2" +version = "0.0.3" authors = ["Oraichain Labs"] edition = "2021" description = "Definition and types for the cw20-ics20-msg interface" @@ -16,4 +16,6 @@ cosmwasm-std = { version = "1.1.9", default-features = false } cw-storage-plus = "1.0.1" cw20 = "1.0.1" schemars = "0.8.1" +bech32 = "0.8.1" +oraiswap = "1.0.1" serde = { version = "1.0.103", default-features = false, features = ["derive"] } diff --git a/packages/cw20-ics20-msg/src/amount.rs b/packages/cw20-ics20-msg/src/amount.rs index 34335b0..1b0ab32 100644 --- a/packages/cw20-ics20-msg/src/amount.rs +++ b/packages/cw20-ics20-msg/src/amount.rs @@ -1,6 +1,8 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Coin, Decimal, StdError, StdResult, Uint128}; -use cw20::Cw20Coin; +use cosmwasm_std::{ + to_binary, BankMsg, Binary, Coin, CosmosMsg, Decimal, StdError, StdResult, Uint128, WasmMsg, +}; +use cw20::{Cw20Coin, Cw20ExecuteMsg}; use std::convert::TryInto; #[cw_serde] @@ -73,6 +75,35 @@ impl Amount { Amount::Cw20(c) => c.amount.is_zero(), } } + + pub fn send_amount(&self, recipient: String, msg: Option) -> CosmosMsg { + match self.to_owned() { + Amount::Native(coin) => BankMsg::Send { + to_address: recipient, + amount: vec![coin], + } + .into(), + Amount::Cw20(coin) => { + let mut msg_cw20 = Cw20ExecuteMsg::Transfer { + recipient: recipient.clone(), + amount: coin.amount, + }; + if let Some(msg) = msg { + msg_cw20 = Cw20ExecuteMsg::Send { + contract: recipient, + amount: coin.amount, + msg, + }; + } + WasmMsg::Execute { + contract_addr: coin.address, + msg: to_binary(&msg_cw20).unwrap(), + funds: vec![], + } + .into() + } + } + } } fn mul_ratio_decimal(amount: Uint128, ratio: Decimal) -> StdResult { diff --git a/packages/cw20-ics20-msg/src/helper.rs b/packages/cw20-ics20-msg/src/helper.rs new file mode 100644 index 0000000..9d54342 --- /dev/null +++ b/packages/cw20-ics20-msg/src/helper.rs @@ -0,0 +1,39 @@ +use cosmwasm_std::{Addr, QuerierWrapper, StdError, StdResult}; +use cw20::{Cw20QueryMsg, TokenInfoResponse}; +use oraiswap::asset::AssetInfo; + +pub fn get_prefix_decode_bech32(address: &str) -> StdResult { + let decode_result = bech32::decode(address); + if decode_result.is_err() { + return Err(StdError::generic_err(format!( + "Cannot decode remote sender: {}", + address + ))); + } + Ok(decode_result.unwrap().0) +} + +pub fn parse_asset_info_denom(asset_info: AssetInfo) -> String { + match asset_info { + AssetInfo::Token { contract_addr } => format!("cw20:{}", contract_addr.to_string()), + AssetInfo::NativeToken { denom } => denom, + } +} + +pub fn parse_ibc_wasm_port_id(contract_addr: String) -> String { + format!("wasm.{}", contract_addr) +} + +pub fn denom_to_asset_info(querier: &QuerierWrapper, denom: &str) -> AssetInfo { + if querier + .query_wasm_smart::(denom.clone(), &Cw20QueryMsg::TokenInfo {}) + .is_ok() + { + return AssetInfo::Token { + contract_addr: Addr::unchecked(denom), + }; + } + AssetInfo::NativeToken { + denom: denom.to_string(), + } +} diff --git a/packages/cw20-ics20-msg/src/lib.rs b/packages/cw20-ics20-msg/src/lib.rs index babc670..3027078 100644 --- a/packages/cw20-ics20-msg/src/lib.rs +++ b/packages/cw20-ics20-msg/src/lib.rs @@ -3,4 +3,5 @@ Shared msgs for the cw20-ics20 and other contracts that interact with it */ pub mod amount; +pub mod helper; pub mod receiver; diff --git a/packages/cw20-ics20-msg/src/receiver.rs b/packages/cw20-ics20-msg/src/receiver.rs index 41ccd64..6d52a5b 100644 --- a/packages/cw20-ics20-msg/src/receiver.rs +++ b/packages/cw20-ics20-msg/src/receiver.rs @@ -1,5 +1,7 @@ use cosmwasm_schema::cw_serde; +use crate::helper::get_prefix_decode_bech32; + #[cw_serde] pub struct DestinationInfo { pub receiver: String, @@ -29,11 +31,7 @@ impl DestinationInfo { } pub fn is_receiver_evm_based(&self) -> (bool, Self) { - let mut new_destination: DestinationInfo = DestinationInfo { - receiver: self.receiver.clone(), - destination_channel: self.destination_channel.clone(), - destination_denom: self.destination_denom.clone(), - }; + let mut new_destination: DestinationInfo = DestinationInfo { ..self.clone() }; match self.receiver.split_once("0x") { Some((evm_prefix, address)) => { // has to have evm_prefix, otherwise we would not be able to know the real denom @@ -51,6 +49,20 @@ impl DestinationInfo { None => (false, new_destination), } } + + pub fn is_receiver_cosmos_based(&self) -> (bool, Self) { + let mut new_destination: DestinationInfo = DestinationInfo { ..self.clone() }; + match get_prefix_decode_bech32(&new_destination.receiver).ok() { + None => (false, new_destination), + Some(prefix) => { + if prefix.is_empty() { + return (false, new_destination); + } + new_destination.destination_channel = prefix.to_string(); + (true, new_destination) + } + } + } } #[test] @@ -75,6 +87,56 @@ fn test_is_evm_based() { ); } +#[test] +fn test_is_cosmos_based() { + let d1 = DestinationInfo::from_str("foo"); + assert_eq!(false, d1.is_receiver_cosmos_based().0); + + let d1 = DestinationInfo::from_str("channel-15/foo:usdt"); + assert_eq!(false, d1.is_receiver_cosmos_based().0); + + let d1 = + DestinationInfo::from_str("channel-15/cosmos14n3tx8s5ftzhlxvq0w5962v60vd82h30sythlz:usdt"); + let result = d1.is_receiver_cosmos_based(); + assert_eq!(true, result.0); + assert_eq!("cosmos", result.1.destination_channel); + + let d1 = + DestinationInfo::from_str("channel-15/akash1g4h64yjt0fvzv5v2j8tyfnpe5kmnetejjpn5xp:usdt"); + let result = d1.is_receiver_cosmos_based(); + assert_eq!(true, result.0); + assert_eq!("akash", result.1.destination_channel); + + let d1 = + DestinationInfo::from_str("channel-15/bostrom1g4h64yjt0fvzv5v2j8tyfnpe5kmnetejuf2qpu:usdt"); + let result = d1.is_receiver_cosmos_based(); + assert_eq!(true, result.0); + assert_eq!("bostrom", result.1.destination_channel); +} + +#[test] +fn test_destination_info_from_str() { + let d1 = DestinationInfo::from_str("cosmos14n3tx8s5ftzhlxvq0w5962v60vd82h30sythlz"); + assert_eq!(d1.destination_channel, ""); + assert_eq!(d1.receiver, "cosmos14n3tx8s5ftzhlxvq0w5962v60vd82h30sythlz"); + assert_eq!(d1.destination_denom, ""); + + let d1 = DestinationInfo::from_str("cosmos14n3tx8s5ftzhlxvq0w5962v60vd82h30sythlz:foo"); + assert_eq!(d1.destination_channel, ""); + assert_eq!(d1.receiver, "cosmos14n3tx8s5ftzhlxvq0w5962v60vd82h30sythlz"); + assert_eq!(d1.destination_denom, "foo"); + + let d1 = DestinationInfo::from_str("foo/cosmos14n3tx8s5ftzhlxvq0w5962v60vd82h30sythlz"); + assert_eq!(d1.destination_channel, "foo"); + assert_eq!(d1.receiver, "cosmos14n3tx8s5ftzhlxvq0w5962v60vd82h30sythlz"); + assert_eq!(d1.destination_denom, ""); + + let d1 = DestinationInfo::from_str("foo/cosmos14n3tx8s5ftzhlxvq0w5962v60vd82h30sythlz:bar"); + assert_eq!(d1.destination_channel, "foo"); + assert_eq!(d1.receiver, "cosmos14n3tx8s5ftzhlxvq0w5962v60vd82h30sythlz"); + assert_eq!(d1.destination_denom, "bar"); +} + #[test] fn test_parse_destination_info() { // swap to orai then orai to atom, then use swapped amount to transfer ibc to destination @@ -140,4 +202,15 @@ fn test_parse_destination_info() { destination_denom: "usdt".to_string() } ); + // ibc hash case + let d7 = DestinationInfo::from_str("channel-5/trx-mainnet0x73Ddc880916021EFC4754Cb42B53db6EAB1f9D64:ibc/A2E2EEC9057A4A1C2C0A6A4C78B0239118DF5F278830F50B4A6BDD7A66506B78"); + assert_eq!( + d7, + DestinationInfo { + receiver: "trx-mainnet0x73Ddc880916021EFC4754Cb42B53db6EAB1f9D64".to_string(), + destination_channel: "channel-5".to_string(), + destination_denom: + "ibc/A2E2EEC9057A4A1C2C0A6A4C78B0239118DF5F278830F50B4A6BDD7A66506B78".to_string() + } + ); } From afc6cef69af9ab06dfa80a0ad4a74ef1caca7868 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Thu, 27 Jul 2023 16:51:35 +0700 Subject: [PATCH 14/28] refactored code & used new cw20-ics20 package --- Cargo.lock | 6 +- README.md | 2 + contracts/cw-ics20-latest/Cargo.toml | 2 +- contracts/cw-ics20-latest/src/contract.rs | 4 +- contracts/cw-ics20-latest/src/ibc.rs | 112 +++++---------------- contracts/cw-ics20-latest/src/ibc_tests.rs | 11 +- 6 files changed, 37 insertions(+), 100 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 62e1d76..5e2256a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -377,14 +377,16 @@ dependencies = [ [[package]] name = "cw20-ics20-msg" -version = "0.0.2" +version = "0.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e04064b936344caa428fa764b4f92859c9619cc0cfc6b3d155d26f8f0a45b13f" +checksum = "ef63faf224f73304e73679494041bf5c31274aad4109d7ce38c1259e6e9e9551" dependencies = [ + "bech32", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 1.0.1", "cw20", + "oraiswap", "schemars", "serde", ] diff --git a/README.md b/README.md index 8c406da..94684d6 100755 --- a/README.md +++ b/README.md @@ -26,3 +26,5 @@ This is really important because by using the CosmosMsg, we force the `allow_con If we use CosmosMsg, then the acknowledgement packet will fail entirely, and it will be retried by the relayer as long as we fix the `allow_contract`. Normally, if it is a `ibctransfer` application developed as a submodule in Cosmos SDK, then the refund part must not fail, and we can trust that it will not fail. However, the `allow_contract` can be developed by anyone, and can be replaced => cannot be trusted. + +# build packages \ No newline at end of file diff --git a/contracts/cw-ics20-latest/Cargo.toml b/contracts/cw-ics20-latest/Cargo.toml index 495decc..4d45689 100644 --- a/contracts/cw-ics20-latest/Cargo.toml +++ b/contracts/cw-ics20-latest/Cargo.toml @@ -22,7 +22,7 @@ cosmwasm-schema = "1.1.9" cw-utils = "0.16.0" cw2 = "1.0.1" cw20 = "1.0.1" -cw20-ics20-msg = "0.0.2" +cw20-ics20-msg = "0.0.3" oraiswap = "1.0.1" cosmwasm-std = { version = "1.1.0", features = ["stargate"] } cw-storage-plus = "1.0.1" diff --git a/contracts/cw-ics20-latest/src/contract.rs b/contracts/cw-ics20-latest/src/contract.rs index 3260323..216c7fe 100644 --- a/contracts/cw-ics20-latest/src/contract.rs +++ b/contracts/cw-ics20-latest/src/contract.rs @@ -6,14 +6,14 @@ use cosmwasm_std::{ }; use cw2::set_contract_version; use cw20::{Cw20Coin, Cw20ReceiveMsg}; +use cw20_ics20_msg::helper::parse_ibc_wasm_port_id; use cw_storage_plus::Bound; use oraiswap::asset::AssetInfo; use oraiswap::router::RouterController; use crate::error::ContractError; use crate::ibc::{ - build_ibc_send_packet, collect_transfer_fee_msgs, parse_ibc_wasm_port_id, parse_voucher_denom, - process_deduct_fee, + build_ibc_send_packet, collect_transfer_fee_msgs, parse_voucher_denom, process_deduct_fee, }; use crate::msg::{ AllowMsg, AllowedInfo, AllowedResponse, ChannelResponse, ConfigResponse, DeletePairMsg, diff --git a/contracts/cw-ics20-latest/src/ibc.rs b/contracts/cw-ics20-latest/src/ibc.rs index dfac4b3..ae7935f 100644 --- a/contracts/cw-ics20-latest/src/ibc.rs +++ b/contracts/cw-ics20-latest/src/ibc.rs @@ -2,12 +2,14 @@ use std::ops::Mul; use cosmwasm_schema::cw_serde; use cosmwasm_std::{ - attr, coin, entry_point, from_binary, to_binary, Addr, Api, BankMsg, Binary, CosmosMsg, - Decimal, Deps, DepsMut, Env, IbcBasicResponse, IbcChannel, IbcChannelCloseMsg, - IbcChannelConnectMsg, IbcChannelOpenMsg, IbcEndpoint, IbcMsg, IbcOrder, IbcPacket, - IbcPacketAckMsg, IbcPacketReceiveMsg, IbcPacketTimeoutMsg, IbcReceiveResponse, IbcTimeout, - Order, QuerierWrapper, Reply, Response, StdError, StdResult, Storage, SubMsg, SubMsgResult, - Uint128, WasmMsg, + attr, coin, entry_point, from_binary, to_binary, Addr, Api, Binary, CosmosMsg, Decimal, Deps, + DepsMut, Env, IbcBasicResponse, IbcChannel, IbcChannelCloseMsg, IbcChannelConnectMsg, + IbcChannelOpenMsg, IbcEndpoint, IbcMsg, IbcOrder, IbcPacket, IbcPacketAckMsg, + IbcPacketReceiveMsg, IbcPacketTimeoutMsg, IbcReceiveResponse, IbcTimeout, Order, + QuerierWrapper, Reply, Response, StdError, StdResult, Storage, SubMsg, SubMsgResult, Uint128, +}; +use cw20_ics20_msg::helper::{ + denom_to_asset_info, get_prefix_decode_bech32, parse_asset_info_denom, }; use cw20_ics20_msg::receiver::DestinationInfo; use oraiswap::asset::AssetInfo; @@ -20,7 +22,6 @@ use crate::state::{ SingleStepReplyArgs, ALLOW_LIST, CHANNEL_INFO, CONFIG, RELAYER_FEE, REPLY_ARGS, SINGLE_STEP_REPLY_ARGS, TOKEN_FEE, TOKEN_FEE_ACCUMULATOR, }; -use cw20::{Cw20ExecuteMsg, Cw20QueryMsg, TokenInfoResponse}; use cw20_ics20_msg::amount::{convert_local_to_remote, convert_remote_to_local, Amount}; pub const ICS20_VERSION: &str = "ics20-1"; @@ -414,7 +415,7 @@ pub fn get_follow_up_msgs( let destination: DestinationInfo = DestinationInfo::from_str(memo); if is_follow_up_msgs_only_send_amount(&memo, &destination.destination_denom) { return Ok(( - vec![send_amount(to_send, receiver.to_string(), None)], + vec![to_send.send_amount(receiver.to_string(), None)], "".to_string(), )); } @@ -447,16 +448,16 @@ pub fn get_follow_up_msgs( config.default_timeout, ); - let mut ibc_error_msg = String::from(""); // by default, the receiver is the original address sent in ics20packet let mut to = Some(api.addr_validate(receiver)?); - if let Some(ibc_msg) = ibc_msg.as_ref().ok() { + let ibc_error_msg = if let Some(ibc_msg) = ibc_msg.as_ref().ok() { cosmos_msgs.push(ibc_msg.to_owned()); // if there's an ibc msg => swap receiver is None so the receiver is this ibc wasm address to = None; + String::from("") } else { - ibc_error_msg = ibc_msg.unwrap_err().to_string(); - } + ibc_msg.unwrap_err().to_string() + }; build_swap_msgs( minimum_receive, &config.swap_router_contract, @@ -469,7 +470,7 @@ pub fn get_follow_up_msgs( // fallback case. If there's no cosmos messages then we return send amount if cosmos_msgs.is_empty() { return Ok(( - vec![send_amount(to_send, receiver.to_string(), None)], + vec![to_send.send_amount(receiver.to_string(), None)], ibc_error_msg, )); } @@ -574,11 +575,11 @@ pub fn build_ibc_msg( .prefix(receiver_asset_info.to_string()) .range(storage, None, None, Order::Ascending) .collect::>>()?; - let (is_evm_based, destination) = destination.is_receiver_evm_based(); + let (is_evm_based, evm_destination) = destination.is_receiver_evm_based(); if is_evm_based { let mapping = pair_mappings .into_iter() - .find(|(key, _)| key.contains(&destination.destination_channel)) + .find(|(key, _)| key.contains(&evm_destination.destination_channel)) .ok_or(StdError::generic_err("cannot find pair mappings"))?; // also deduct fee here because of round trip // TODO: add relayer fee here? @@ -610,7 +611,7 @@ pub fn build_ibc_msg( &mapping.0, env.contract.address.as_str(), remote_address, - Some(destination.receiver), + Some(evm_destination.receiver), local_channel_id, timeout.into(), )?; @@ -631,9 +632,9 @@ pub fn build_ibc_msg( // by using ibc transfer, the contract must actually owns native ibc tokens, which is not possible if it's oraibridge tokens // we do not need to reduce channel balance because this transfer is not on our contract channel, but on destination channel let ibc_msg = IbcMsg::Transfer { - channel_id: destination.destination_channel, - to_address: destination.receiver, - amount: coin(amount.u128(), destination.destination_denom), + channel_id: destination.destination_channel.clone(), + to_address: destination.receiver.clone(), + amount: coin(amount.u128(), destination.destination_denom.clone()), timeout: timeout.into(), }; Ok(ibc_msg.into()) @@ -665,7 +666,7 @@ pub fn handle_follow_up_failure( reply_args.local_amount, ); // we send refund to the local receiver of the single-step tx because the funds are currently in this contract - let send = send_amount(refund_amount, reply_args.receiver, None); + let send = refund_amount.send_amount(reply_args.receiver, None); response = response .add_submessage(SubMsg::reply_on_error(send, REFUND_FAILURE_ID)) .set_data(ack_fail(err.clone())) @@ -778,15 +779,8 @@ pub fn deduct_relayer_fee( return Ok((amount, Uint128::from(0u64))); } - let decode_result = bech32::decode(remote_address); - if decode_result.is_err() { - return Err(StdError::generic_err(format!( - "Cannot decode remote sender: {}", - remote_address - ))); - } + let mut prefix = get_prefix_decode_bech32(remote_address)?; // this is bech32 prefix of sender from other chains. Should not error because we are in the cosmos ecosystem. Every address should have prefix - let mut prefix = decode_result.unwrap().0; // evm case, need to filter remote token denom since prefix is always oraib if prefix.eq("oraib") { prefix = convert_remote_denom_to_evm_prefix(remote_token_denom); @@ -844,11 +838,7 @@ pub fn collect_transfer_fee_msgs( if fee_info.1.is_zero() { return None; } - Some(send_amount( - Amount::from_parts(fee_info.0, fee_info.1), - receiver.clone(), - None, - )) + Some(Amount::from_parts(fee_info.0, fee_info.1).send_amount(receiver.clone(), None)) }) .ok() }) @@ -969,7 +959,7 @@ fn on_packet_failure( pair_mapping.asset_info_decimals, )?, ); - let cosmos_msg = send_amount(to_send, msg.sender.clone(), None); + let cosmos_msg = to_send.send_amount(msg.sender.clone(), None); // used submsg here & reply on error. This means that if the refund process fails => tokens will be locked in this IBC Wasm contract. We will manually handle that case. No retry // similar event messages like ibctransfer module let submsg = SubMsg::reply_on_error(cosmos_msg, ACK_FAILURE_ID); @@ -989,60 +979,6 @@ fn on_packet_failure( // send ack fail to custom contract for refund } -pub fn send_amount(amount: Amount, recipient: String, msg: Option) -> CosmosMsg { - match amount { - Amount::Native(coin) => BankMsg::Send { - to_address: recipient, - amount: vec![coin], - } - .into(), - Amount::Cw20(coin) => { - let mut msg_cw20 = Cw20ExecuteMsg::Transfer { - recipient: recipient.clone(), - amount: coin.amount, - }; - if let Some(msg) = msg { - msg_cw20 = Cw20ExecuteMsg::Send { - contract: recipient, - amount: coin.amount, - msg, - }; - } - WasmMsg::Execute { - contract_addr: coin.address, - msg: to_binary(&msg_cw20).unwrap(), - funds: vec![], - } - .into() - } - } -} - -pub fn parse_asset_info_denom(asset_info: AssetInfo) -> String { - match asset_info { - AssetInfo::Token { contract_addr } => format!("cw20:{}", contract_addr.to_string()), - AssetInfo::NativeToken { denom } => denom, - } -} - -pub fn parse_ibc_wasm_port_id(contract_addr: String) -> String { - format!("wasm.{}", contract_addr) -} - -pub fn denom_to_asset_info(querier: &QuerierWrapper, denom: &str) -> AssetInfo { - if querier - .query_wasm_smart::(denom.clone(), &Cw20QueryMsg::TokenInfo {}) - .is_ok() - { - return AssetInfo::Token { - contract_addr: Addr::unchecked(denom), - }; - } - AssetInfo::NativeToken { - denom: denom.to_string(), - } -} - pub fn build_ibc_send_packet( amount: Uint128, denom: &str, diff --git a/contracts/cw-ics20-latest/src/ibc_tests.rs b/contracts/cw-ics20-latest/src/ibc_tests.rs index a4f4358..0eef9d7 100644 --- a/contracts/cw-ics20-latest/src/ibc_tests.rs +++ b/contracts/cw-ics20-latest/src/ibc_tests.rs @@ -9,8 +9,8 @@ mod test { ack_fail, build_ibc_msg, build_swap_msgs, check_gas_limit, convert_remote_denom_to_evm_prefix, deduct_fee, deduct_relayer_fee, deduct_token_fee, handle_follow_up_failure, ibc_packet_receive, is_follow_up_msgs_only_send_amount, - parse_voucher_denom, parse_voucher_denom_without_sanity_checks, send_amount, Ics20Ack, - Ics20Packet, REFUND_FAILURE_ID, + parse_voucher_denom, parse_voucher_denom_without_sanity_checks, Ics20Ack, Ics20Packet, + REFUND_FAILURE_ID, }; use crate::ibc::{build_swap_operations, get_follow_up_msgs}; use crate::test_helpers::*; @@ -998,11 +998,8 @@ mod test { result, Response::new() .add_submessage(SubMsg::reply_on_error( - send_amount( - Amount::from_parts(native_denom.to_string(), amount.clone()), - single_step_reply_args.receiver.clone(), - None - ), + Amount::from_parts(native_denom.to_string(), amount.clone()) + .send_amount(single_step_reply_args.receiver.clone(), None), REFUND_FAILURE_ID )) .set_data(ack_fail(err.to_string())) From b24739d5404a0300e4975dd025f0e391a2a5b5a4 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Thu, 27 Jul 2023 18:11:08 +0700 Subject: [PATCH 15/28] added cosmos based txs without test cases --- contracts/cw-ics20-latest/src/ibc.rs | 172 ++++++++++++++------- contracts/cw-ics20-latest/src/ibc_tests.rs | 8 +- 2 files changed, 123 insertions(+), 57 deletions(-) diff --git a/contracts/cw-ics20-latest/src/ibc.rs b/contracts/cw-ics20-latest/src/ibc.rs index ae7935f..277a549 100644 --- a/contracts/cw-ics20-latest/src/ibc.rs +++ b/contracts/cw-ics20-latest/src/ibc.rs @@ -6,7 +6,8 @@ use cosmwasm_std::{ DepsMut, Env, IbcBasicResponse, IbcChannel, IbcChannelCloseMsg, IbcChannelConnectMsg, IbcChannelOpenMsg, IbcEndpoint, IbcMsg, IbcOrder, IbcPacket, IbcPacketAckMsg, IbcPacketReceiveMsg, IbcPacketTimeoutMsg, IbcReceiveResponse, IbcTimeout, Order, - QuerierWrapper, Reply, Response, StdError, StdResult, Storage, SubMsg, SubMsgResult, Uint128, + QuerierWrapper, Reply, Response, StdError, StdResult, Storage, SubMsg, SubMsgResult, Timestamp, + Uint128, }; use cw20_ics20_msg::helper::{ denom_to_asset_info, get_prefix_decode_bech32, parse_asset_info_denom, @@ -543,6 +544,7 @@ pub fn build_swap_msgs( Ok(()) } +// TODO: update unit tests for this function pub fn build_ibc_msg( storage: &mut dyn Storage, env: Env, @@ -561,83 +563,145 @@ pub fn build_ibc_msg( )); } let timeout = env.block.time.plus_seconds(default_timeout); - let mut reply_args = SingleStepReplyArgs { + let reply_args = SingleStepReplyArgs { channel: destination.destination_channel.clone(), refund_asset_info: receiver_asset_info.clone(), ibc_data: None, receiver: local_receiver.to_string(), local_amount: amount, }; - // use sender from ICS20Packet as receiver when transferring back let pair_mappings: Vec<(String, MappingMetadata)> = ics20_denoms() .idx .asset_info .prefix(receiver_asset_info.to_string()) .range(storage, None, None, Order::Ascending) .collect::>>()?; + let (is_evm_based, evm_destination) = destination.is_receiver_evm_based(); if is_evm_based { let mapping = pair_mappings .into_iter() - .find(|(key, _)| key.contains(&evm_destination.destination_channel)) + .find(|(key, _)| { + // eg: 'wasm.orai195269awwnt5m6c843q6w7hp8rt0k7syfu9de4h0wz384slshuzps8y7ccm/channel-29/eth-mainnet0x4c11249814f11b9346808179Cf06e71ac328c1b5' + // parse to get eth-mainnet0x... + // then collect eth-mainnet prefix, and compare with dest channel + convert_remote_denom_to_evm_prefix( + parse_voucher_denom_without_sanity_checks(key).unwrap_or_default(), + ) + .eq(&evm_destination.destination_channel) + }) .ok_or(StdError::generic_err("cannot find pair mappings"))?; - // also deduct fee here because of round trip - // TODO: add relayer fee here? - let (new_deducted_amount, _) = deduct_token_fee( + let msg = process_ibc_msg( storage, - parse_voucher_denom_without_sanity_checks(&mapping.0)?, // denom mapping in the form port/channel/denom - amount, - &parse_asset_info_denom(receiver_asset_info.clone()), - )?; - let remote_amount = convert_local_to_remote( - new_deducted_amount, - mapping.1.remote_decimals, - mapping.1.asset_info_decimals, - )?; - - // because we are transferring back, we reduce the channel's balance - reduce_channel_balance( - storage, - &local_channel_id.clone(), - &mapping.0.clone(), - remote_amount, - false, - ) - .map_err(|err| StdError::generic_err(err.to_string()))?; - - // prepare ibc message - let msg = build_ibc_send_packet( - remote_amount, - &mapping.0, + mapping, + receiver_asset_info, + local_channel_id, env.contract.address.as_str(), - remote_address, + remote_address, // use sender from ICS20Packet as receiver when transferring back because we have the actual receiver in memo for evm cases Some(evm_destination.receiver), - local_channel_id, - timeout.into(), + amount, + timeout, + reply_args, )?; - - reply_args.channel = local_channel_id.to_string(); - reply_args.ibc_data = Some(IbcSingleStepData { - ibc_denom: mapping.0, - remote_amount, - }); - // keep track of the reply. We need to keep a seperate value because if using REPLY, it could be overriden by the channel increase later on in reply - SINGLE_STEP_REPLY_ARGS.save(storage, &reply_args)?; - return Ok(msg.into()); } // 2nd case, where destination network is not evm, but it is still supported on our channel (eg: cw20 ATOM mapped with native ATOM on Cosmos), then we call - // final case, where the destination token is from a remote chain that we dont have a pair mapping with. - // we use ibc transfer so that attackers cannot manipulate the data to send to oraibridge without reducing the channel balance - // by using ibc transfer, the contract must actually owns native ibc tokens, which is not possible if it's oraibridge tokens - // we do not need to reduce channel balance because this transfer is not on our contract channel, but on destination channel - let ibc_msg = IbcMsg::Transfer { - channel_id: destination.destination_channel.clone(), - to_address: destination.receiver.clone(), - amount: coin(amount.u128(), destination.destination_denom.clone()), - timeout: timeout.into(), - }; - Ok(ibc_msg.into()) + let (is_cosmos_based, cosmos_destination) = destination.is_receiver_cosmos_based(); + if is_cosmos_based { + let mapping = pair_mappings.into_iter().find(|(key, _)| { + get_prefix_decode_bech32( + parse_voucher_denom_without_sanity_checks(key).unwrap_or_default(), + ) + .unwrap_or_default() + .eq(&cosmos_destination.destination_channel) + }); + if let Some(mapping) = mapping { + let msg = process_ibc_msg( + storage, + mapping, + receiver_asset_info, + local_channel_id, + env.contract.address.as_str(), + &cosmos_destination.receiver, // now we use dest receiver since cosmos based universal swap wont be sent to oraibridge, so the receiver is the correct receive addr + None, // no need memo because it is not used in the remote cosmos based chain + amount, + timeout, + reply_args, + )?; + return Ok(msg.into()); + } + + // final case, where the destination token is from a remote chain that we dont have a pair mapping with. + // we use ibc transfer so that attackers cannot manipulate the data to send to oraibridge without reducing the channel balance + // by using ibc transfer, the contract must actually owns native ibc tokens, which is not possible if it's oraibridge tokens + // we do not need to reduce channel balance because this transfer is not on our contract channel, but on destination channel + let ibc_msg = IbcMsg::Transfer { + channel_id: destination.destination_channel.clone(), + to_address: destination.receiver.clone(), + amount: coin(amount.u128(), destination.destination_denom.clone()), + timeout: timeout.into(), + }; + return Ok(ibc_msg.into()); + } + Err(StdError::generic_err( + "The destination info is neither evm or cosmos based", + )) +} + +// TODO: write unit tests for this function. Write unit tests for relayer fee & cosmos based universal swap in simulate js +pub fn process_ibc_msg( + storage: &mut dyn Storage, + pair_mapping: (String, MappingMetadata), + receiver_asset_info: AssetInfo, + local_channel_id: &str, + ibc_msg_sender: &str, + ibc_msg_receiver: &str, + memo: Option, + amount: Uint128, + timeout: Timestamp, + mut reply_args: SingleStepReplyArgs, +) -> StdResult { + let (new_deducted_amount, _) = deduct_token_fee( + storage, + parse_voucher_denom_without_sanity_checks(&pair_mapping.0)?, // denom mapping in the form port/channel/denom + amount, + &parse_asset_info_denom(receiver_asset_info.clone()), + )?; + let remote_amount = convert_local_to_remote( + new_deducted_amount, + pair_mapping.1.remote_decimals, + pair_mapping.1.asset_info_decimals, + )?; + + // because we are transferring back, we reduce the channel's balance + reduce_channel_balance( + storage, + local_channel_id.clone(), + &pair_mapping.0.clone(), + remote_amount, + false, + ) + .map_err(|err| StdError::generic_err(err.to_string()))?; + + // prepare ibc message + let msg = build_ibc_send_packet( + remote_amount, + &pair_mapping.0, + ibc_msg_sender, + ibc_msg_receiver, + memo, + local_channel_id, + timeout.into(), + )?; + + reply_args.channel = local_channel_id.to_string(); + reply_args.ibc_data = Some(IbcSingleStepData { + ibc_denom: pair_mapping.0.to_string(), + remote_amount, + }); + // keep track of the reply. We need to keep a seperate value because if using REPLY, it could be overriden by the channel increase later on in reply + SINGLE_STEP_REPLY_ARGS.save(storage, &reply_args)?; + Ok(msg) } pub fn handle_follow_up_failure( diff --git a/contracts/cw-ics20-latest/src/ibc_tests.rs b/contracts/cw-ics20-latest/src/ibc_tests.rs index 0eef9d7..f1f5db5 100644 --- a/contracts/cw-ics20-latest/src/ibc_tests.rs +++ b/contracts/cw-ics20-latest/src/ibc_tests.rs @@ -709,6 +709,7 @@ mod test { let send_channel = "channel-9"; let receive_channel = "channel-1"; let allowed = "foobar"; + let pair_mapping_denom = "trx-mainnet0xa614f803B6FD780986A42c78Ec9c7f77e6DeD13C"; let allowed_gas = 777666; let mut deps = setup(&[send_channel], &[(allowed, allowed_gas)]); let receiver_asset_info = AssetInfo::NativeToken { @@ -752,6 +753,7 @@ mod test { // not evm based case, should be successful & cosmos msg is ibc transfer destination.destination_channel = "channel-10".to_string(); + destination.receiver = "cosmos1zedxv25ah8fksmg2lzrndrpkvsjqgk4zt5ff7n".to_string(); let result = build_ibc_msg( deps.as_mut().storage, env.clone(), @@ -768,7 +770,7 @@ mod test { result, CosmosMsg::Ibc(IbcMsg::Transfer { channel_id: "channel-10".to_string(), - to_address: "0x1234".to_string(), + to_address: destination.receiver, amount: coin(10u128, "atom"), timeout: mock_env().block.time.plus_seconds(timeout).into() }) @@ -793,7 +795,7 @@ mod test { // add a pair mapping so we can test the happy case evm based happy case let update = UpdatePairMsg { local_channel_id: "mars-channel".to_string(), - denom: "trx-mainnet".to_string(), + denom: pair_mapping_denom.to_string(), asset_info: receiver_asset_info.clone(), remote_decimals, asset_info_decimals, @@ -806,7 +808,7 @@ mod test { execute(deps.as_mut(), mock_env(), info, msg.clone()).unwrap(); let pair_mapping_key = format!( "wasm.{}/{}/{}", - "cosmos2contract", update.local_channel_id, "trx-mainnet" + "cosmos2contract", update.local_channel_id, pair_mapping_denom ); increase_channel_balance( deps.as_mut().storage, From de1c428bede3eaf0ae2f61892dc543a0d417a3a2 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Fri, 28 Jul 2023 09:57:59 +0700 Subject: [PATCH 16/28] added test cases for process ibc msg --- contracts/cw-ics20-latest/src/ibc.rs | 2 +- contracts/cw-ics20-latest/src/ibc_tests.rs | 190 +++++++++++++++++---- 2 files changed, 159 insertions(+), 33 deletions(-) diff --git a/contracts/cw-ics20-latest/src/ibc.rs b/contracts/cw-ics20-latest/src/ibc.rs index 277a549..7cd9542 100644 --- a/contracts/cw-ics20-latest/src/ibc.rs +++ b/contracts/cw-ics20-latest/src/ibc.rs @@ -648,7 +648,7 @@ pub fn build_ibc_msg( )) } -// TODO: write unit tests for this function. Write unit tests for relayer fee & cosmos based universal swap in simulate js +// TODO: Write unit tests for relayer fee & cosmos based universal swap in simulate js pub fn process_ibc_msg( storage: &mut dyn Storage, pair_mapping: (String, MappingMetadata), diff --git a/contracts/cw-ics20-latest/src/ibc_tests.rs b/contracts/cw-ics20-latest/src/ibc_tests.rs index f1f5db5..bae1e01 100644 --- a/contracts/cw-ics20-latest/src/ibc_tests.rs +++ b/contracts/cw-ics20-latest/src/ibc_tests.rs @@ -1,6 +1,6 @@ #[cfg(test)] mod test { - use cosmwasm_std::{attr, coin, Addr, CosmosMsg, Response, StdError}; + use cosmwasm_std::{attr, coin, Addr, CosmosMsg, IbcTimeout, Response, StdError}; use cw20_ics20_msg::receiver::DestinationInfo; use oraiswap::asset::AssetInfo; use oraiswap::router::SwapOperation; @@ -9,8 +9,8 @@ mod test { ack_fail, build_ibc_msg, build_swap_msgs, check_gas_limit, convert_remote_denom_to_evm_prefix, deduct_fee, deduct_relayer_fee, deduct_token_fee, handle_follow_up_failure, ibc_packet_receive, is_follow_up_msgs_only_send_amount, - parse_voucher_denom, parse_voucher_denom_without_sanity_checks, Ics20Ack, Ics20Packet, - REFUND_FAILURE_ID, + parse_voucher_denom, parse_voucher_denom_without_sanity_checks, process_ibc_msg, Ics20Ack, + Ics20Packet, REFUND_FAILURE_ID, }; use crate::ibc::{build_swap_operations, get_follow_up_msgs}; use crate::test_helpers::*; @@ -21,9 +21,9 @@ mod test { use crate::error::ContractError; use crate::state::{ - get_key_ics20_ibc_denom, increase_channel_balance, ChannelState, IbcSingleStepData, Ratio, - SingleStepReplyArgs, CHANNEL_REVERSE_STATE, RELAYER_FEE, SINGLE_STEP_REPLY_ARGS, TOKEN_FEE, - TOKEN_FEE_ACCUMULATOR, + get_key_ics20_ibc_denom, increase_channel_balance, ChannelState, IbcSingleStepData, + MappingMetadata, Ratio, SingleStepReplyArgs, CHANNEL_REVERSE_STATE, RELAYER_FEE, + SINGLE_STEP_REPLY_ARGS, TOKEN_FEE, TOKEN_FEE_ACCUMULATOR, }; use cw20::{Cw20Coin, Cw20ExecuteMsg}; use cw20_ics20_msg::amount::{convert_local_to_remote, Amount}; @@ -705,7 +705,8 @@ mod test { } #[test] - fn test_get_ibc_msg() { + fn test_get_ibc_msg_evm_case() { + // setup let send_channel = "channel-9"; let receive_channel = "channel-1"; let allowed = "foobar"; @@ -751,31 +752,6 @@ mod test { StdError::generic_err("Destination channel empty in build ibc msg") ); - // not evm based case, should be successful & cosmos msg is ibc transfer - destination.destination_channel = "channel-10".to_string(); - destination.receiver = "cosmos1zedxv25ah8fksmg2lzrndrpkvsjqgk4zt5ff7n".to_string(); - let result = build_ibc_msg( - deps.as_mut().storage, - env.clone(), - receiver_asset_info.clone(), - local_receiver, - receive_channel, - amount, - remote_address, - &destination, - timeout, - ) - .unwrap(); - assert_eq!( - result, - CosmosMsg::Ibc(IbcMsg::Transfer { - channel_id: "channel-10".to_string(), - to_address: destination.receiver, - amount: coin(10u128, "atom"), - timeout: mock_env().block.time.plus_seconds(timeout).into() - }) - ); - // evm based case, error getting pair mapping destination.receiver = "trx-mainnet0x73Ddc880916021EFC4754Cb42B53db6EAB1f9D64".to_string(); let err = build_ibc_msg( @@ -858,6 +834,58 @@ mod test { assert_eq!(reply_args.refund_asset_info, receiver_asset_info) } + // fn test_get_ibc_msg_cosmos_based_case() { + // let send_channel = "channel-9"; + // let receive_channel = "channel-1"; + // let allowed = "foobar"; + // let pair_mapping_denom = "trx-mainnet0xa614f803B6FD780986A42c78Ec9c7f77e6DeD13C"; + // let allowed_gas = 777666; + // let mut deps = setup(&[send_channel], &[(allowed, allowed_gas)]); + // let receiver_asset_info = AssetInfo::NativeToken { + // denom: "orai".to_string(), + // }; + // let amount = Uint128::from(10u128); + // let remote_decimals = 18; + // let asset_info_decimals = 6; + // let remote_amount = + // convert_local_to_remote(amount, remote_decimals, asset_info_decimals).unwrap(); + // let remote_address = "eth-mainnet0x1235"; + // let mut env = mock_env(); + // env.contract.address = Addr::unchecked("addr"); + // let mut destination = DestinationInfo { + // receiver: "0x1234".to_string(), + // destination_channel: "channel-10".to_string(), + // destination_denom: "atom".to_string(), + // }; + // let timeout = 1000u64; + // let local_receiver = "local_receiver"; + + // // cosmos based case, should be successful & cosmos msg is ibc transfer + // destination.destination_channel = "channel-10".to_string(); + // destination.receiver = "cosmos1zedxv25ah8fksmg2lzrndrpkvsjqgk4zt5ff7n".to_string(); + // let result = build_ibc_msg( + // deps.as_mut().storage, + // env.clone(), + // receiver_asset_info.clone(), + // local_receiver, + // receive_channel, + // amount, + // remote_address, + // &destination, + // timeout, + // ) + // .unwrap(); + // assert_eq!( + // result, + // CosmosMsg::Ibc(IbcMsg::Transfer { + // channel_id: "channel-10".to_string(), + // to_address: destination.receiver, + // amount: coin(10u128, "atom"), + // timeout: mock_env().block.time.plus_seconds(timeout).into() + // }) + // ); + // } + #[test] fn test_follow_up_msgs() { let send_channel = "channel-9"; @@ -1232,4 +1260,102 @@ mod test { Uint128::from(10u64) ); } + + #[test] + fn test_process_ibc_msg() { + // setup + let mut deps = mock_dependencies(); + let amount = Uint128::from(1000u64); + let storage = deps.as_mut().storage; + let ibc_denom = "foo/bar/cosmos"; + let pair_mapping = ( + ibc_denom.to_string(), + MappingMetadata { + asset_info: AssetInfo::NativeToken { + denom: "orai".to_string(), + }, + remote_decimals: 18, + asset_info_decimals: 6, + }, + ); + let receiver_asset_info = AssetInfo::Token { + contract_addr: Addr::unchecked("usdt"), + }; + let local_channel_id = "channel"; + let ibc_msg_sender = "sender"; + let ibc_msg_receiver = "receiver"; + let memo = None; + let timeout = Timestamp::from_seconds(10u64); + let reply_args: SingleStepReplyArgs = SingleStepReplyArgs { + channel: local_channel_id.to_string(), + refund_asset_info: receiver_asset_info.clone(), + ibc_data: None, + local_amount: amount.clone(), + receiver: ibc_msg_receiver.to_string(), + }; + let remote_amount = convert_local_to_remote(amount.clone(), 18, 6).unwrap(); + + CHANNEL_REVERSE_STATE + .save( + storage, + (local_channel_id, ibc_denom), + &ChannelState { + outstanding: remote_amount.clone(), + total_sent: Uint128::from(100u128), + }, + ) + .unwrap(); + + // action + let result = process_ibc_msg( + storage, + pair_mapping, + receiver_asset_info, + local_channel_id, + ibc_msg_sender, + ibc_msg_receiver, + memo, + amount, + timeout, + reply_args, + ) + .unwrap(); + + // assert + // channel balance should reduce to 0 + assert_eq!( + CHANNEL_REVERSE_STATE + .load(storage, (local_channel_id, ibc_denom)) + .unwrap() + .outstanding, + Uint128::from(0u64) + ); + // reply args should have ibc data now + assert_eq!( + SINGLE_STEP_REPLY_ARGS + .load(storage) + .unwrap() + .ibc_data + .unwrap(), + IbcSingleStepData { + ibc_denom: ibc_denom.to_string(), + remote_amount + } + ); + assert_eq!( + result, + IbcMsg::SendPacket { + channel_id: local_channel_id.to_string(), + data: to_binary(&Ics20Packet { + amount: remote_amount.clone(), + denom: ibc_denom.to_string(), + receiver: ibc_msg_receiver.to_string(), + sender: ibc_msg_sender.to_string(), + memo: None + }) + .unwrap(), + timeout: IbcTimeout::with_timestamp(timeout) + } + ); + } } From e7eacb92383f2e120d441262c58eee300d2d244c Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Fri, 28 Jul 2023 10:41:02 +0700 Subject: [PATCH 17/28] added more tests for build ibc msg --- contracts/cw-ics20-latest/src/ibc_tests.rs | 193 +++++++++++++++------ 1 file changed, 142 insertions(+), 51 deletions(-) diff --git a/contracts/cw-ics20-latest/src/ibc_tests.rs b/contracts/cw-ics20-latest/src/ibc_tests.rs index bae1e01..e60374e 100644 --- a/contracts/cw-ics20-latest/src/ibc_tests.rs +++ b/contracts/cw-ics20-latest/src/ibc_tests.rs @@ -834,57 +834,148 @@ mod test { assert_eq!(reply_args.refund_asset_info, receiver_asset_info) } - // fn test_get_ibc_msg_cosmos_based_case() { - // let send_channel = "channel-9"; - // let receive_channel = "channel-1"; - // let allowed = "foobar"; - // let pair_mapping_denom = "trx-mainnet0xa614f803B6FD780986A42c78Ec9c7f77e6DeD13C"; - // let allowed_gas = 777666; - // let mut deps = setup(&[send_channel], &[(allowed, allowed_gas)]); - // let receiver_asset_info = AssetInfo::NativeToken { - // denom: "orai".to_string(), - // }; - // let amount = Uint128::from(10u128); - // let remote_decimals = 18; - // let asset_info_decimals = 6; - // let remote_amount = - // convert_local_to_remote(amount, remote_decimals, asset_info_decimals).unwrap(); - // let remote_address = "eth-mainnet0x1235"; - // let mut env = mock_env(); - // env.contract.address = Addr::unchecked("addr"); - // let mut destination = DestinationInfo { - // receiver: "0x1234".to_string(), - // destination_channel: "channel-10".to_string(), - // destination_denom: "atom".to_string(), - // }; - // let timeout = 1000u64; - // let local_receiver = "local_receiver"; - - // // cosmos based case, should be successful & cosmos msg is ibc transfer - // destination.destination_channel = "channel-10".to_string(); - // destination.receiver = "cosmos1zedxv25ah8fksmg2lzrndrpkvsjqgk4zt5ff7n".to_string(); - // let result = build_ibc_msg( - // deps.as_mut().storage, - // env.clone(), - // receiver_asset_info.clone(), - // local_receiver, - // receive_channel, - // amount, - // remote_address, - // &destination, - // timeout, - // ) - // .unwrap(); - // assert_eq!( - // result, - // CosmosMsg::Ibc(IbcMsg::Transfer { - // channel_id: "channel-10".to_string(), - // to_address: destination.receiver, - // amount: coin(10u128, "atom"), - // timeout: mock_env().block.time.plus_seconds(timeout).into() - // }) - // ); - // } + #[test] + fn test_get_ibc_msg_cosmos_based_case() { + // setup + let send_channel = "channel-9"; + let allowed = "foobar"; + let allowed_gas = 777666; + let mut deps = setup(&[send_channel], &[(allowed, allowed_gas)]); + let amount = Uint128::from(1000u64); + let pair_mapping_denom = "cosmos1zedxv25ah8fksmg2lzrndrpkvsjqgk4zt5ff7n"; + let receiver_asset_info = AssetInfo::Token { + contract_addr: Addr::unchecked("usdt"), + }; + let local_channel_id = "channel"; + let local_receiver = "receiver"; + let timeout = 10u64; + let remote_amount = convert_local_to_remote(amount.clone(), 18, 6).unwrap(); + let destination = DestinationInfo { + receiver: "cosmos1zedxv25ah8fksmg2lzrndrpkvsjqgk4zt5ff7n".to_string(), + destination_channel: "channel-10".to_string(), + destination_denom: "atom".to_string(), + }; + let env = mock_env(); + let remote_address = "foobar"; + let ibc_denom = format!("foo/bar/{}", pair_mapping_denom); + let remote_decimals = 18; + let asset_info_decimals = 6; + + CHANNEL_REVERSE_STATE + .save( + deps.as_mut().storage, + (local_channel_id, ibc_denom.as_str()), + &ChannelState { + outstanding: remote_amount.clone(), + total_sent: Uint128::from(100u128), + }, + ) + .unwrap(); + + // cosmos based case but no mapping found. should be successful & cosmos msg is ibc transfer + let result = build_ibc_msg( + deps.as_mut().storage, + env.clone(), + receiver_asset_info.clone(), + local_receiver, + local_channel_id, + amount, + remote_address, + &destination, + timeout, + ) + .unwrap(); + assert_eq!( + result, + CosmosMsg::Ibc(IbcMsg::Transfer { + channel_id: "channel-10".to_string(), + to_address: destination.receiver.clone(), + amount: coin(1000u128, "atom"), + timeout: mock_env().block.time.plus_seconds(timeout).into() + }) + ); + + // cosmos based case with mapping found. Should be successful & cosmos msg is ibc send packet + // add a pair mapping so we can test the happy case evm based happy case + let update = UpdatePairMsg { + local_channel_id: "mars-channel".to_string(), + denom: pair_mapping_denom.to_string(), + asset_info: receiver_asset_info.clone(), + remote_decimals, + asset_info_decimals, + }; + + let msg = ExecuteMsg::UpdateMappingPair(update.clone()); + + let info = mock_info("gov", &coins(1234567, "ucosm")); + execute(deps.as_mut(), mock_env(), info, msg.clone()).unwrap(); + + CHANNEL_REVERSE_STATE + .save( + deps.as_mut().storage, + (local_channel_id, "wasm.cosmos2contract/mars-channel/cosmos1zedxv25ah8fksmg2lzrndrpkvsjqgk4zt5ff7n"), + &ChannelState { + outstanding: remote_amount.clone(), + total_sent: Uint128::from(100u128), + }, + ) + .unwrap(); + + // now we get ibc msg + let result = build_ibc_msg( + deps.as_mut().storage, + env.clone(), + receiver_asset_info.clone(), + local_receiver, + local_channel_id, + amount, + remote_address, + &destination, + timeout, + ) + .unwrap(); + assert_eq!(format!("{:?}", result).contains("SendPacket"), true); + } + + #[test] + fn test_get_ibc_msg_neither_cosmos_or_evm_based_case() { + // setup + let send_channel = "channel-9"; + let allowed = "foobar"; + let allowed_gas = 777666; + let mut deps = setup(&[send_channel], &[(allowed, allowed_gas)]); + let amount = Uint128::from(1000u64); + let receiver_asset_info = AssetInfo::Token { + contract_addr: Addr::unchecked("usdt"), + }; + let local_channel_id = "channel"; + let local_receiver = "receiver"; + let timeout = 10u64; + let destination = DestinationInfo { + receiver: "foo".to_string(), + destination_channel: "channel-10".to_string(), + destination_denom: "atom".to_string(), + }; + let env = mock_env(); + let remote_address = "foobar"; + // cosmos based case but no mapping found. should be successful & cosmos msg is ibc transfer + let result = build_ibc_msg( + deps.as_mut().storage, + env.clone(), + receiver_asset_info.clone(), + local_receiver, + local_channel_id, + amount, + remote_address, + &destination, + timeout, + ) + .unwrap_err(); + assert_eq!( + result, + StdError::generic_err("The destination info is neither evm or cosmos based") + ) + } #[test] fn test_follow_up_msgs() { From adc983f9185b15ccb375f422f6edb823a29646e5 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Fri, 28 Jul 2023 10:47:36 +0700 Subject: [PATCH 18/28] passed all test cases --- contracts/cw-ics20-latest/src/ibc.rs | 1 - contracts/cw-ics20-latest/src/ibc_tests.rs | 25 ++++++++++++++++++++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/contracts/cw-ics20-latest/src/ibc.rs b/contracts/cw-ics20-latest/src/ibc.rs index 7cd9542..58aa90c 100644 --- a/contracts/cw-ics20-latest/src/ibc.rs +++ b/contracts/cw-ics20-latest/src/ibc.rs @@ -544,7 +544,6 @@ pub fn build_swap_msgs( Ok(()) } -// TODO: update unit tests for this function pub fn build_ibc_msg( storage: &mut dyn Storage, env: Env, diff --git a/contracts/cw-ics20-latest/src/ibc_tests.rs b/contracts/cw-ics20-latest/src/ibc_tests.rs index e60374e..e4e3ddf 100644 --- a/contracts/cw-ics20-latest/src/ibc_tests.rs +++ b/contracts/cw-ics20-latest/src/ibc_tests.rs @@ -754,6 +754,7 @@ mod test { // evm based case, error getting pair mapping destination.receiver = "trx-mainnet0x73Ddc880916021EFC4754Cb42B53db6EAB1f9D64".to_string(); + destination.destination_channel = send_channel.to_string(); let err = build_ibc_msg( deps.as_mut().storage, env.clone(), @@ -860,6 +861,11 @@ mod test { let ibc_denom = format!("foo/bar/{}", pair_mapping_denom); let remote_decimals = 18; let asset_info_decimals = 6; + let remote_channel = "mars-channel"; + let pair_mapping_key = format!( + "wasm.cosmos2contract/{}/{}", + remote_channel, pair_mapping_denom + ); CHANNEL_REVERSE_STATE .save( @@ -913,7 +919,7 @@ mod test { CHANNEL_REVERSE_STATE .save( deps.as_mut().storage, - (local_channel_id, "wasm.cosmos2contract/mars-channel/cosmos1zedxv25ah8fksmg2lzrndrpkvsjqgk4zt5ff7n"), + (local_channel_id, &pair_mapping_key), &ChannelState { outstanding: remote_amount.clone(), total_sent: Uint128::from(100u128), @@ -934,7 +940,22 @@ mod test { timeout, ) .unwrap(); - assert_eq!(format!("{:?}", result).contains("SendPacket"), true); + + assert_eq!( + result, + CosmosMsg::Ibc(IbcMsg::SendPacket { + channel_id: local_channel_id.to_string(), + data: to_binary(&Ics20Packet::new( + remote_amount.clone(), + pair_mapping_key.clone(), + env.contract.address.as_str(), + &destination.receiver, + None, + )) + .unwrap(), + timeout: env.block.time.plus_seconds(timeout).into() + }) + ); } #[test] From 118f0a87a14e114b1b838c096d5f91a470cc863a Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Fri, 28 Jul 2023 12:07:18 +0700 Subject: [PATCH 19/28] fixed deduct relayer fee formula --- contracts/cw-ics20-latest/src/ibc.rs | 11 +++++++++-- contracts/cw-ics20-latest/src/ibc_tests.rs | 6 ++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/contracts/cw-ics20-latest/src/ibc.rs b/contracts/cw-ics20-latest/src/ibc.rs index 58aa90c..5ec6da3 100644 --- a/contracts/cw-ics20-latest/src/ibc.rs +++ b/contracts/cw-ics20-latest/src/ibc.rs @@ -775,10 +775,11 @@ pub fn process_deduct_fee( let (_, token_fee) = deduct_token_fee(storage, remote_token_denom, amount, local_token_denom)?; // simulate for relayer fee let offer_asset_info = denom_to_asset_info(querier, local_token_denom); + let offer_amount = Uint128::from(10u32.pow(decimals as u32)); let token_price = swap_router_contract .simulate_swap( querier, - Uint128::from(1u64).checked_mul(Uint128::from(10 * decimals as u32))?, + offer_amount, vec![SwapOperation::OraiSwap { offer_asset_info, // always swap with orai. If it does not share a pool with ORAI => ignore, no fee @@ -794,6 +795,7 @@ pub fn process_deduct_fee( remote_sender, remote_token_denom, amount, + offer_amount, local_token_denom, token_price, )?; @@ -835,6 +837,7 @@ pub fn deduct_relayer_fee( remote_address: &str, remote_token_denom: &str, amount: Uint128, // local amount + offer_amount: Uint128, // offer amount of token that swaps to ORAI local_token_denom: &str, // local denom token_price: Uint128, ) -> StdResult<(Uint128, Uint128)> { @@ -854,7 +857,11 @@ pub fn deduct_relayer_fee( return Ok((amount, Uint128::from(0u64))); } let relayer_fee = relayer_fee.unwrap(); - let required_fee_needed = relayer_fee.checked_div(token_price).unwrap_or_default(); + let required_fee_needed = relayer_fee + .checked_mul(offer_amount) + .unwrap_or_default() + .checked_div(token_price) + .unwrap_or_default(); // accumulate fee so that we can collect it later after everything // we share the same accumulator because it's the same data structure, and we are accumulating so it's fine TOKEN_FEE_ACCUMULATOR.update( diff --git a/contracts/cw-ics20-latest/src/ibc_tests.rs b/contracts/cw-ics20-latest/src/ibc_tests.rs index e4e3ddf..fd41300 100644 --- a/contracts/cw-ics20-latest/src/ibc_tests.rs +++ b/contracts/cw-ics20-latest/src/ibc_tests.rs @@ -1292,6 +1292,7 @@ mod test { let storage = deps.as_mut().storage; let token_fee_denom = "cosmos"; let remote_address = "cosmos1zedxv25ah8fksmg2lzrndrpkvsjqgk4zt5ff7n"; + let offer_amount = Uint128::from(10u32.pow(0 as u32)); let token_price = Uint128::from(10u64); // token price empty case. Should return zero fee let result = deduct_relayer_fee( @@ -1299,6 +1300,7 @@ mod test { remote_address, token_fee_denom, amount, + offer_amount.clone(), "local_token_denom", Uint128::from(0u64), ) @@ -1312,6 +1314,7 @@ mod test { "foobar", token_fee_denom, amount, + offer_amount.clone(), "local_token_denom", token_price, ) @@ -1326,6 +1329,7 @@ mod test { remote_address, token_fee_denom, amount, + offer_amount.clone(), "local_token_denom", token_price, ) @@ -1349,6 +1353,7 @@ mod test { "oraib1603j3e4juddh7cuhfquxspl0p0nsun047wz3rl", "foo0x", amount, + offer_amount.clone(), "local_token_denom", token_price, ) @@ -1364,6 +1369,7 @@ mod test { remote_address, token_fee_denom, amount, + offer_amount.clone(), "local_token_denom", token_price, ) From c8857999722da52887dbcb28692598c9f2531dd9 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Fri, 28 Jul 2023 12:48:26 +0700 Subject: [PATCH 20/28] fixed u32 overflow error deduct relayer fee --- contracts/cw-ics20-latest/src/contract.rs | 6 ++-- contracts/cw-ics20-latest/src/ibc.rs | 35 ++++++++++++----------- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/contracts/cw-ics20-latest/src/contract.rs b/contracts/cw-ics20-latest/src/contract.rs index 216c7fe..0759a81 100644 --- a/contracts/cw-ics20-latest/src/contract.rs +++ b/contracts/cw-ics20-latest/src/contract.rs @@ -303,7 +303,7 @@ pub fn execute_transfer_back_to_remote_chain( .ok_or(ContractError::MappingPairNotFound {})?; // if found mapping, then deduct fee based on mapping - let new_deducted_amount = process_deduct_fee( + let (new_deducted_amount, token_fee, relayer_fee) = process_deduct_fee( deps.storage, &deps.querier, &msg.remote_address, @@ -369,7 +369,9 @@ pub fn execute_transfer_back_to_remote_chain( .add_attribute("sender", sender.as_str()) .add_attribute("receiver", &msg.remote_address) .add_attribute("denom", &ibc_denom) - .add_attribute("amount", &amount_remote.to_string()); + .add_attribute("amount", &amount_remote.to_string()) + .add_attribute("token_fee", token_fee) + .add_attribute("relayer_fee", relayer_fee); Ok(res) } diff --git a/contracts/cw-ics20-latest/src/ibc.rs b/contracts/cw-ics20-latest/src/ibc.rs index 5ec6da3..7df0aec 100644 --- a/contracts/cw-ics20-latest/src/ibc.rs +++ b/contracts/cw-ics20-latest/src/ibc.rs @@ -348,19 +348,17 @@ fn handle_ibc_packet_receive_native_remote_chain( }; REPLY_ARGS.save(storage, &reply_args)?; - let new_deducted_to_send = Amount::from_parts( - to_send.denom(), - process_deduct_fee( - storage, - querier, - &msg.sender, - &msg.denom, - to_send.amount(), - pair_mapping.asset_info_decimals, - &to_send.denom(), - &config.swap_router_contract, - )?, - ); + let (new_deducted_amount, token_fee, relayer_fee) = process_deduct_fee( + storage, + querier, + &msg.sender, + &msg.denom, + to_send.amount(), + pair_mapping.asset_info_decimals, + &to_send.denom(), + &config.swap_router_contract, + )?; + let new_deducted_to_send = Amount::from_parts(to_send.denom(), new_deducted_amount); // after receiving the cw20 amount, we try to do fee swapping for the user if needed so he / she can create txs on the network let (submsgs, ibc_error_msg) = get_follow_up_msgs( @@ -391,7 +389,9 @@ fn handle_ibc_packet_receive_native_remote_chain( .add_attribute("receiver", msg.receiver.clone()) .add_attribute("denom", denom) .add_attribute("amount", msg.amount.to_string()) - .add_attribute("success", "true"); + .add_attribute("success", "true") + .add_attribute("token_fee", token_fee) + .add_attribute("relayer_fee", relayer_fee); if !ibc_error_msg.is_empty() { res = res.add_attribute("ibc_error_msg", ibc_error_msg); } @@ -771,11 +771,11 @@ pub fn process_deduct_fee( decimals: u8, local_token_denom: &str, // local denom swap_router_contract: &RouterController, -) -> StdResult { +) -> StdResult<(Uint128, Uint128, Uint128)> { let (_, token_fee) = deduct_token_fee(storage, remote_token_denom, amount, local_token_denom)?; // simulate for relayer fee let offer_asset_info = denom_to_asset_info(querier, local_token_denom); - let offer_amount = Uint128::from(10u32.pow(decimals as u32)); + let offer_amount = Uint128::from(10u64.pow(decimals as u32) as u64); let token_price = swap_router_contract .simulate_swap( querier, @@ -809,7 +809,7 @@ pub fn process_deduct_fee( "Not enough transfer amount to cover the token and relayer fees", )); } - Ok(new_amount) + Ok((new_amount, token_fee, relayer_fee)) } pub fn deduct_token_fee( @@ -857,6 +857,7 @@ pub fn deduct_relayer_fee( return Ok((amount, Uint128::from(0u64))); } let relayer_fee = relayer_fee.unwrap(); + println!("offer amount: {:?}", offer_amount); let required_fee_needed = relayer_fee .checked_mul(offer_amount) .unwrap_or_default() From 209413e990d30938449a56ae76bef6e183baed83 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Fri, 28 Jul 2023 14:46:54 +0700 Subject: [PATCH 21/28] fixed relayer fee wrong local token denom --- Cargo.lock | 2 -- contracts/cw-ics20-latest/Cargo.toml | 2 +- contracts/cw-ics20-latest/src/contract.rs | 4 +-- contracts/cw-ics20-latest/src/ibc.rs | 39 ++++++++++++++-------- contracts/cw-ics20-latest/src/ibc_tests.rs | 21 +++++++----- packages/cw20-ics20-msg/src/helper.rs | 26 +++++++++------ 6 files changed, 58 insertions(+), 36 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5e2256a..b213d3f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -378,8 +378,6 @@ dependencies = [ [[package]] name = "cw20-ics20-msg" version = "0.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef63faf224f73304e73679494041bf5c31274aad4109d7ce38c1259e6e9e9551" dependencies = [ "bech32", "cosmwasm-schema", diff --git a/contracts/cw-ics20-latest/Cargo.toml b/contracts/cw-ics20-latest/Cargo.toml index 4d45689..f7eede8 100644 --- a/contracts/cw-ics20-latest/Cargo.toml +++ b/contracts/cw-ics20-latest/Cargo.toml @@ -22,7 +22,7 @@ cosmwasm-schema = "1.1.9" cw-utils = "0.16.0" cw2 = "1.0.1" cw20 = "1.0.1" -cw20-ics20-msg = "0.0.3" +cw20-ics20-msg = { path = "../../packages/cw20-ics20-msg" } oraiswap = "1.0.1" cosmwasm-std = { version = "1.1.0", features = ["stargate"] } cw-storage-plus = "1.0.1" diff --git a/contracts/cw-ics20-latest/src/contract.rs b/contracts/cw-ics20-latest/src/contract.rs index 0759a81..232cd61 100644 --- a/contracts/cw-ics20-latest/src/contract.rs +++ b/contracts/cw-ics20-latest/src/contract.rs @@ -306,11 +306,11 @@ pub fn execute_transfer_back_to_remote_chain( let (new_deducted_amount, token_fee, relayer_fee) = process_deduct_fee( deps.storage, &deps.querier, + deps.api, &msg.remote_address, &msg.remote_denom, - amount.amount(), + amount, mapping.pair_mapping.asset_info_decimals, - &amount.denom(), &config.swap_router_contract, )?; diff --git a/contracts/cw-ics20-latest/src/ibc.rs b/contracts/cw-ics20-latest/src/ibc.rs index 7df0aec..6df1589 100644 --- a/contracts/cw-ics20-latest/src/ibc.rs +++ b/contracts/cw-ics20-latest/src/ibc.rs @@ -27,6 +27,7 @@ use cw20_ics20_msg::amount::{convert_local_to_remote, convert_remote_to_local, A pub const ICS20_VERSION: &str = "ics20-1"; pub const ICS20_ORDERING: IbcOrder = IbcOrder::Unordered; +pub const ORAIBRIDGE_PREFIX: &str = "oraib"; /// The format for sending an ics20 packet. /// Proto defined here: https://github.com/cosmos/cosmos-sdk/blob/v0.42.0/proto/ibc/applications/transfer/v1/transfer.proto#L11-L20 @@ -351,11 +352,11 @@ fn handle_ibc_packet_receive_native_remote_chain( let (new_deducted_amount, token_fee, relayer_fee) = process_deduct_fee( storage, querier, + api, &msg.sender, &msg.denom, - to_send.amount(), + to_send.clone(), pair_mapping.asset_info_decimals, - &to_send.denom(), &config.swap_router_contract, )?; let new_deducted_to_send = Amount::from_parts(to_send.denom(), new_deducted_amount); @@ -421,7 +422,7 @@ pub fn get_follow_up_msgs( )); } // successful case. We dont care if this msg is going to be successful or not because it does not affect our ibc receive flow (just submsgs) - let receiver_asset_info = denom_to_asset_info(querier, &destination.destination_denom); + let receiver_asset_info = denom_to_asset_info(querier, api, &destination.destination_denom)?; let swap_operations = build_swap_operations( receiver_asset_info.clone(), initial_receive_asset_info.clone(), @@ -765,16 +766,21 @@ pub fn check_gas_limit(deps: Deps, amount: &Amount) -> Result, Contr pub fn process_deduct_fee( storage: &mut dyn Storage, querier: &QuerierWrapper, + api: &dyn Api, remote_sender: &str, remote_token_denom: &str, - amount: Uint128, // local amount + local_amount: Amount, // local amount decimals: u8, - local_token_denom: &str, // local denom swap_router_contract: &RouterController, ) -> StdResult<(Uint128, Uint128, Uint128)> { - let (_, token_fee) = deduct_token_fee(storage, remote_token_denom, amount, local_token_denom)?; + let (_, token_fee) = deduct_token_fee( + storage, + remote_token_denom, + local_amount.amount(), + &local_amount.denom(), + )?; // simulate for relayer fee - let offer_asset_info = denom_to_asset_info(querier, local_token_denom); + let offer_asset_info = denom_to_asset_info(querier, api, &local_amount.raw_denom())?; let offer_amount = Uint128::from(10u64.pow(decimals as u32) as u64); let token_price = swap_router_contract .simulate_swap( @@ -792,14 +798,16 @@ pub fn process_deduct_fee( .unwrap_or_default(); let (_, relayer_fee) = deduct_relayer_fee( storage, + api, remote_sender, remote_token_denom, - amount, + local_amount.amount(), offer_amount, - local_token_denom, + &local_amount.denom(), token_price, )?; - let new_amount = amount + let new_amount = local_amount + .amount() .checked_sub(token_fee) .unwrap_or_default() .checked_sub(relayer_fee) @@ -834,6 +842,7 @@ pub fn deduct_token_fee( pub fn deduct_relayer_fee( storage: &mut dyn Storage, + api: &dyn Api, remote_address: &str, remote_token_denom: &str, amount: Uint128, // local amount @@ -841,28 +850,32 @@ pub fn deduct_relayer_fee( local_token_denom: &str, // local denom token_price: Uint128, ) -> StdResult<(Uint128, Uint128)> { + api.debug(format!("token price: {}", token_price).as_str()); if token_price.is_zero() { return Ok((amount, Uint128::from(0u64))); } - let mut prefix = get_prefix_decode_bech32(remote_address)?; // this is bech32 prefix of sender from other chains. Should not error because we are in the cosmos ecosystem. Every address should have prefix // evm case, need to filter remote token denom since prefix is always oraib - if prefix.eq("oraib") { + let mut prefix = get_prefix_decode_bech32(remote_address)?; + api.debug(format!("prefix: {}", prefix).as_str()); + if prefix.eq(ORAIBRIDGE_PREFIX) { prefix = convert_remote_denom_to_evm_prefix(remote_token_denom); } + api.debug(format!("prefix after evm prefix: {}", prefix).as_str()); let relayer_fee = RELAYER_FEE.may_load(storage, &prefix)?; + api.debug(format!("relayer fee: {}", relayer_fee.unwrap_or_default()).as_str()); // no need to deduct fee if no fee is found in the mapping if relayer_fee.is_none() { return Ok((amount, Uint128::from(0u64))); } let relayer_fee = relayer_fee.unwrap(); - println!("offer amount: {:?}", offer_amount); let required_fee_needed = relayer_fee .checked_mul(offer_amount) .unwrap_or_default() .checked_div(token_price) .unwrap_or_default(); + api.debug(format!("required fee needed: {}", required_fee_needed).as_str()); // accumulate fee so that we can collect it later after everything // we share the same accumulator because it's the same data structure, and we are accumulating so it's fine TOKEN_FEE_ACCUMULATOR.update( diff --git a/contracts/cw-ics20-latest/src/ibc_tests.rs b/contracts/cw-ics20-latest/src/ibc_tests.rs index fd41300..2965fdb 100644 --- a/contracts/cw-ics20-latest/src/ibc_tests.rs +++ b/contracts/cw-ics20-latest/src/ibc_tests.rs @@ -1289,14 +1289,15 @@ mod test { fn test_deduct_relayer_fee() { let mut deps = mock_dependencies(); let amount = Uint128::from(1000u64); - let storage = deps.as_mut().storage; + let deps_mut = deps.as_mut(); let token_fee_denom = "cosmos"; let remote_address = "cosmos1zedxv25ah8fksmg2lzrndrpkvsjqgk4zt5ff7n"; let offer_amount = Uint128::from(10u32.pow(0 as u32)); let token_price = Uint128::from(10u64); // token price empty case. Should return zero fee let result = deduct_relayer_fee( - storage, + deps_mut.storage, + deps_mut.api, remote_address, token_fee_denom, amount, @@ -1310,7 +1311,8 @@ mod test { // remote address is wrong (dont follow bech32 form) assert_eq!( deduct_relayer_fee( - storage, + deps_mut.storage, + deps_mut.api, "foobar", token_fee_denom, amount, @@ -1325,7 +1327,8 @@ mod test { // no relayer fee case assert_eq!( deduct_relayer_fee( - storage, + deps_mut.storage, + deps_mut.api, remote_address, token_fee_denom, amount, @@ -1340,16 +1343,17 @@ mod test { // oraib prefix case. RELAYER_FEE - .save(storage, token_fee_denom, &Uint128::from(100u64)) + .save(deps_mut.storage, token_fee_denom, &Uint128::from(100u64)) .unwrap(); RELAYER_FEE - .save(storage, "foo", &Uint128::from(1000u64)) + .save(deps_mut.storage, "foo", &Uint128::from(1000u64)) .unwrap(); assert_eq!( deduct_relayer_fee( - storage, + deps_mut.storage, + deps_mut.api, "oraib1603j3e4juddh7cuhfquxspl0p0nsun047wz3rl", "foo0x", amount, @@ -1365,7 +1369,8 @@ mod test { // normal case with remote address assert_eq!( deduct_relayer_fee( - storage, + deps_mut.storage, + deps_mut.api, remote_address, token_fee_denom, amount, diff --git a/packages/cw20-ics20-msg/src/helper.rs b/packages/cw20-ics20-msg/src/helper.rs index 9d54342..8399dec 100644 --- a/packages/cw20-ics20-msg/src/helper.rs +++ b/packages/cw20-ics20-msg/src/helper.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{Addr, QuerierWrapper, StdError, StdResult}; +use cosmwasm_std::{Api, QuerierWrapper, StdError, StdResult}; use cw20::{Cw20QueryMsg, TokenInfoResponse}; use oraiswap::asset::AssetInfo; @@ -24,16 +24,22 @@ pub fn parse_ibc_wasm_port_id(contract_addr: String) -> String { format!("wasm.{}", contract_addr) } -pub fn denom_to_asset_info(querier: &QuerierWrapper, denom: &str) -> AssetInfo { - if querier +pub fn denom_to_asset_info( + querier: &QuerierWrapper, + api: &dyn Api, + denom: &str, +) -> StdResult { + let info = if querier .query_wasm_smart::(denom.clone(), &Cw20QueryMsg::TokenInfo {}) .is_ok() { - return AssetInfo::Token { - contract_addr: Addr::unchecked(denom), - }; - } - AssetInfo::NativeToken { - denom: denom.to_string(), - } + AssetInfo::Token { + contract_addr: api.addr_validate(denom)?, + } + } else { + AssetInfo::NativeToken { + denom: denom.to_string(), + } + }; + Ok(info) } From 45366d4533262d6ff0cd13859aecd8e609bd625a Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Fri, 28 Jul 2023 14:49:46 +0700 Subject: [PATCH 22/28] renamed ratio nominator to numerator --- README.md | 3 +++ contracts/cw-ics20-latest/src/contract.rs | 8 ++++---- contracts/cw-ics20-latest/src/ibc.rs | 2 +- contracts/cw-ics20-latest/src/ibc_tests.rs | 10 +++++----- contracts/cw-ics20-latest/src/state.rs | 2 +- 5 files changed, 14 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 94684d6..34c7486 100755 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # IBC transfer flow: Let's assume the network that has the contract cw20-ics20 deployed is network B, the other network is A. + +In the source code, we call the network having cw20-ics20 deployed local chain, other networks are remote chains. + In the cw-ics20-latest contract, there are couple transfer flows in the code below: ## Network A transfers native tokens to B first (A->B, where native token is not IBC token) diff --git a/contracts/cw-ics20-latest/src/contract.rs b/contracts/cw-ics20-latest/src/contract.rs index 232cd61..acb1324 100644 --- a/contracts/cw-ics20-latest/src/contract.rs +++ b/contracts/cw-ics20-latest/src/contract.rs @@ -1191,11 +1191,11 @@ mod test { let cw20_raw_denom = token_addr.as_str(); let local_channel = "channel-1234"; let ratio = Ratio { - nominator: 1, + numerator: 1, denominator: 10, }; let fee_amount = - Uint128::from(amount) * Decimal::from_ratio(ratio.nominator, ratio.denominator); + Uint128::from(amount) * Decimal::from_ratio(ratio.numerator, ratio.denominator); let mut deps = setup(&[remote_channel, local_channel], &[]); TOKEN_FEE .save(deps.as_mut().storage, denom, &ratio) @@ -1379,14 +1379,14 @@ mod test { TokenFee { token_denom: "orai".to_string(), ratio: Ratio { - nominator: 1, + numerator: 1, denominator: 10, }, }, TokenFee { token_denom: "atom".to_string(), ratio: Ratio { - nominator: 1, + numerator: 1, denominator: 5, }, }, diff --git a/contracts/cw-ics20-latest/src/ibc.rs b/contracts/cw-ics20-latest/src/ibc.rs index 6df1589..6f9f0b0 100644 --- a/contracts/cw-ics20-latest/src/ibc.rs +++ b/contracts/cw-ics20-latest/src/ibc.rs @@ -899,7 +899,7 @@ pub fn deduct_fee(token_fee: Ratio, amount: Uint128) -> Uint128 { return Uint128::from(0u64); } amount.mul(Decimal::from_ratio( - token_fee.nominator, + token_fee.numerator, token_fee.denominator, )) } diff --git a/contracts/cw-ics20-latest/src/ibc_tests.rs b/contracts/cw-ics20-latest/src/ibc_tests.rs index 2965fdb..4e1628f 100644 --- a/contracts/cw-ics20-latest/src/ibc_tests.rs +++ b/contracts/cw-ics20-latest/src/ibc_tests.rs @@ -481,7 +481,7 @@ mod test { deps.as_mut().storage, denom, &Ratio { - nominator: 1, + numerator: 1, denominator: 10, }, ) @@ -1199,7 +1199,7 @@ mod test { assert_eq!( deduct_fee( Ratio { - nominator: 1, + numerator: 1, denominator: 0, }, Uint128::from(1000u64) @@ -1209,7 +1209,7 @@ mod test { assert_eq!( deduct_fee( Ratio { - nominator: 1, + numerator: 1, denominator: 1, }, Uint128::from(1000u64) @@ -1219,7 +1219,7 @@ mod test { assert_eq!( deduct_fee( Ratio { - nominator: 1, + numerator: 1, denominator: 100, }, Uint128::from(1000u64) @@ -1268,7 +1268,7 @@ mod test { storage, token_fee_denom, &Ratio { - nominator: 1, + numerator: 1, denominator: 100, }, ) diff --git a/contracts/cw-ics20-latest/src/state.rs b/contracts/cw-ics20-latest/src/state.rs index f76452d..b9aeb24 100644 --- a/contracts/cw-ics20-latest/src/state.rs +++ b/contracts/cw-ics20-latest/src/state.rs @@ -113,7 +113,7 @@ pub struct RelayerFee { #[cw_serde] pub struct Ratio { - pub nominator: u64, + pub numerator: u64, pub denominator: u64, } From 03b0a612b54631e50142bb8e4f8d803e97ea78b2 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Fri, 28 Jul 2023 16:29:35 +0700 Subject: [PATCH 23/28] added fee relayer receiver config & test cases --- contracts/cw-ics20-latest/src/contract.rs | 43 ++++++++++++++++++---- contracts/cw-ics20-latest/src/ibc.rs | 32 ++++++++++------ contracts/cw-ics20-latest/src/ibc_tests.rs | 19 +++++++++- contracts/cw-ics20-latest/src/msg.rs | 8 +++- contracts/cw-ics20-latest/src/state.rs | 8 +++- 5 files changed, 85 insertions(+), 25 deletions(-) diff --git a/contracts/cw-ics20-latest/src/contract.rs b/contracts/cw-ics20-latest/src/contract.rs index acb1324..57a8a45 100644 --- a/contracts/cw-ics20-latest/src/contract.rs +++ b/contracts/cw-ics20-latest/src/contract.rs @@ -13,7 +13,7 @@ use oraiswap::router::RouterController; use crate::error::ContractError; use crate::ibc::{ - build_ibc_send_packet, collect_transfer_fee_msgs, parse_voucher_denom, process_deduct_fee, + build_ibc_send_packet, collect_fee_msgs, parse_voucher_denom, process_deduct_fee, }; use crate::msg::{ AllowMsg, AllowedInfo, AllowedResponse, ChannelResponse, ConfigResponse, DeletePairMsg, @@ -23,7 +23,7 @@ use crate::msg::{ use crate::state::{ get_key_ics20_ibc_denom, ics20_denoms, reduce_channel_balance, AllowInfo, Config, MappingMetadata, RelayerFee, TokenFee, ADMIN, ALLOW_LIST, CHANNEL_INFO, CHANNEL_REVERSE_STATE, - CONFIG, RELAYER_FEE, TOKEN_FEE, + CONFIG, RELAYER_FEE, RELAYER_FEE_ACCUMULATOR, TOKEN_FEE, TOKEN_FEE_ACCUMULATOR, }; use cw20_ics20_msg::amount::{convert_local_to_remote, Amount}; use cw_utils::{maybe_addr, nonpayable, one_coin}; @@ -47,7 +47,8 @@ pub fn instantiate( default_gas_limit: msg.default_gas_limit, fee_denom: "orai".to_string(), swap_router_contract: RouterController(msg.swap_router_contract), - fee_receiver: admin, + token_fee_receiver: admin.clone(), + relayer_fee_receiver: admin, }; CONFIG.save(deps.storage, &cfg)?; @@ -91,6 +92,7 @@ pub fn execute( admin, token_fee, fee_receiver, + relayer_fee_receiver, relayer_fee, } => update_config( deps, @@ -102,6 +104,7 @@ pub fn execute( admin, token_fee, fee_receiver, + relayer_fee_receiver, relayer_fee, ), } @@ -117,6 +120,7 @@ pub fn update_config( admin: Option, token_fee: Option>, fee_receiver: Option, + relayer_fee_receiver: Option, relayer_fee: Option>, ) -> Result { ADMIN.assert_admin(deps.as_ref(), &info.sender)?; @@ -141,7 +145,10 @@ pub fn update_config( config.swap_router_contract = RouterController(swap_router_contract); } if let Some(fee_receiver) = fee_receiver { - config.fee_receiver = deps.api.addr_validate(&fee_receiver)?; + config.token_fee_receiver = deps.api.addr_validate(&fee_receiver)?; + } + if let Some(relayer_fee_receiver) = relayer_fee_receiver { + config.relayer_fee_receiver = deps.api.addr_validate(&relayer_fee_receiver)?; } config.default_gas_limit = default_gas_limit; Ok(config) @@ -357,9 +364,17 @@ pub fn execute_transfer_back_to_remote_chain( timeout.into(), )?; - let mut cosmos_msgs = - collect_transfer_fee_msgs(config.fee_receiver.into_string(), deps.storage)?; + let mut cosmos_msgs = collect_fee_msgs( + deps.storage, + config.token_fee_receiver.into_string(), + TOKEN_FEE_ACCUMULATOR, + )?; cosmos_msgs.push(ibc_msg.into()); + cosmos_msgs.append(&mut collect_fee_msgs( + deps.storage, + config.relayer_fee_receiver.into_string(), + RELAYER_FEE_ACCUMULATOR, + )?); // send response let res = Response::new() @@ -487,7 +502,8 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result StdResult { fee_denom: cfg.fee_denom, swap_router_contract: cfg.swap_router_contract.addr(), gov_contract: admin.into(), + relayer_fee_receiver: cfg.relayer_fee_receiver, + token_fee_receiver: cfg.token_fee_receiver, }; Ok(res) } @@ -1395,7 +1413,8 @@ mod test { prefix: "foo".to_string(), fee: Uint128::from(1000000u64), }]), - fee_receiver: None, + fee_receiver: Some("token_fee_receiver".to_string()), + relayer_fee_receiver: Some("relayer_fee_receiver".to_string()), }; // unauthorized case let unauthorized_info = mock_info(&String::from("somebody"), &[]); @@ -1416,6 +1435,14 @@ mod test { assert_eq!(config.default_timeout, 1); assert_eq!(config.fee_denom, "hehe".to_string()); assert_eq!(config.swap_router_contract, "new_router".to_string()); + assert_eq!( + config.relayer_fee_receiver, + Addr::unchecked("relayer_fee_receiver") + ); + assert_eq!( + config.token_fee_receiver, + Addr::unchecked("token_fee_receiver") + ); assert_eq!( TOKEN_FEE .range(deps.as_ref().storage, None, None, Order::Ascending) diff --git a/contracts/cw-ics20-latest/src/ibc.rs b/contracts/cw-ics20-latest/src/ibc.rs index 6f9f0b0..b72cb22 100644 --- a/contracts/cw-ics20-latest/src/ibc.rs +++ b/contracts/cw-ics20-latest/src/ibc.rs @@ -13,6 +13,7 @@ use cw20_ics20_msg::helper::{ denom_to_asset_info, get_prefix_decode_bech32, parse_asset_info_denom, }; use cw20_ics20_msg::receiver::DestinationInfo; +use cw_storage_plus::Map; use oraiswap::asset::AssetInfo; use oraiswap::router::{RouterController, SwapOperation}; @@ -20,8 +21,8 @@ use crate::error::{ContractError, Never}; use crate::state::{ get_key_ics20_ibc_denom, ics20_denoms, increase_channel_balance, reduce_channel_balance, undo_reduce_channel_balance, ChannelInfo, IbcSingleStepData, MappingMetadata, Ratio, ReplyArgs, - SingleStepReplyArgs, ALLOW_LIST, CHANNEL_INFO, CONFIG, RELAYER_FEE, REPLY_ARGS, - SINGLE_STEP_REPLY_ARGS, TOKEN_FEE, TOKEN_FEE_ACCUMULATOR, + SingleStepReplyArgs, ALLOW_LIST, CHANNEL_INFO, CONFIG, RELAYER_FEE, RELAYER_FEE_ACCUMULATOR, + REPLY_ARGS, SINGLE_STEP_REPLY_ARGS, TOKEN_FEE, TOKEN_FEE_ACCUMULATOR, }; use cw20_ics20_msg::amount::{convert_local_to_remote, convert_remote_to_local, Amount}; @@ -379,11 +380,19 @@ fn handle_ibc_packet_receive_native_remote_chain( .map(|msg| SubMsg::reply_on_error(msg, FOLLOW_UP_ID)) .collect(); - let transfer_fee_to_admin = - collect_transfer_fee_msgs(config.fee_receiver.into_string(), storage)?; + let mut fee_msgs = collect_fee_msgs( + storage, + config.token_fee_receiver.into_string(), + TOKEN_FEE_ACCUMULATOR, + )?; + fee_msgs.append(&mut collect_fee_msgs( + storage, + config.relayer_fee_receiver.to_string(), + RELAYER_FEE_ACCUMULATOR, + )?); let mut res = IbcReceiveResponse::new() .set_ack(ack_success()) - .add_messages(transfer_fee_to_admin) + .add_messages(fee_msgs) .add_submessages(submsgs) .add_attribute("action", "receive_native") .add_attribute("sender", msg.sender.clone()) @@ -878,7 +887,7 @@ pub fn deduct_relayer_fee( api.debug(format!("required fee needed: {}", required_fee_needed).as_str()); // accumulate fee so that we can collect it later after everything // we share the same accumulator because it's the same data structure, and we are accumulating so it's fine - TOKEN_FEE_ACCUMULATOR.update( + RELAYER_FEE_ACCUMULATOR.update( storage, local_token_denom, |prev_fee| -> StdResult { @@ -911,11 +920,12 @@ pub fn convert_remote_denom_to_evm_prefix(remote_denom: &str) -> String { } } -pub fn collect_transfer_fee_msgs( - receiver: String, +pub fn collect_fee_msgs( storage: &mut dyn Storage, + receiver: String, + fee_accumulator: Map<&str, Uint128>, ) -> StdResult> { - let cosmos_msgs = TOKEN_FEE_ACCUMULATOR + let cosmos_msgs = fee_accumulator .range(storage, None, None, Order::Ascending) .filter_map(|data| { data.map(|fee_info| { @@ -929,11 +939,11 @@ pub fn collect_transfer_fee_msgs( .flatten() .collect::>(); // we reset all the accumulator keys to zero so that it wont accumulate more in the next txs. This action will be reverted if the fee payment txs fail. - TOKEN_FEE_ACCUMULATOR + fee_accumulator .keys(storage, None, None, Order::Ascending) .collect::, StdError>>()? .into_iter() - .for_each(|key| TOKEN_FEE_ACCUMULATOR.remove(storage, &key)); + .for_each(|key| fee_accumulator.remove(storage, &key)); Ok(cosmos_msgs) } diff --git a/contracts/cw-ics20-latest/src/ibc_tests.rs b/contracts/cw-ics20-latest/src/ibc_tests.rs index 4e1628f..ca5433a 100644 --- a/contracts/cw-ics20-latest/src/ibc_tests.rs +++ b/contracts/cw-ics20-latest/src/ibc_tests.rs @@ -23,7 +23,7 @@ mod test { use crate::state::{ get_key_ics20_ibc_denom, increase_channel_balance, ChannelState, IbcSingleStepData, MappingMetadata, Ratio, SingleStepReplyArgs, CHANNEL_REVERSE_STATE, RELAYER_FEE, - SINGLE_STEP_REPLY_ARGS, TOKEN_FEE, TOKEN_FEE_ACCUMULATOR, + RELAYER_FEE_ACCUMULATOR, SINGLE_STEP_REPLY_ARGS, TOKEN_FEE, TOKEN_FEE_ACCUMULATOR, }; use cw20::{Cw20Coin, Cw20ExecuteMsg}; use cw20_ics20_msg::amount::{convert_local_to_remote, Amount}; @@ -317,7 +317,8 @@ mod test { mock_env(), MigrateMsg { default_gas_limit: Some(def_limit), - fee_receiver: "receiver".to_string(), + token_fee_receiver: "receiver".to_string(), + relayer_fee_receiver: "relayer_fee_receiver".to_string(), default_timeout: 100u64, fee_denom: "orai".to_string(), swap_router_contract: "foobar".to_string(), @@ -1366,6 +1367,13 @@ mod test { Uint128::from(100u64) ); + assert_eq!( + RELAYER_FEE_ACCUMULATOR + .load(deps_mut.storage, "local_token_denom") + .unwrap(), + Uint128::from(100u64) + ); + // normal case with remote address assert_eq!( deduct_relayer_fee( @@ -1382,6 +1390,13 @@ mod test { .1, Uint128::from(10u64) ); + + assert_eq!( + RELAYER_FEE_ACCUMULATOR + .load(deps_mut.storage, "local_token_denom") + .unwrap(), + Uint128::from(110u64) + ); } #[test] diff --git a/contracts/cw-ics20-latest/src/msg.rs b/contracts/cw-ics20-latest/src/msg.rs index 7f71143..7f1f465 100644 --- a/contracts/cw-ics20-latest/src/msg.rs +++ b/contracts/cw-ics20-latest/src/msg.rs @@ -1,5 +1,5 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Binary, IbcEndpoint}; +use cosmwasm_std::{Addr, Binary, IbcEndpoint}; use cw20::Cw20ReceiveMsg; use oraiswap::asset::AssetInfo; @@ -33,7 +33,8 @@ pub struct MigrateMsg { pub default_gas_limit: Option, pub fee_denom: String, pub swap_router_contract: String, - pub fee_receiver: String, + pub token_fee_receiver: String, + pub relayer_fee_receiver: String, } #[cw_serde] @@ -57,6 +58,7 @@ pub enum ExecuteMsg { token_fee: Option>, relayer_fee: Option>, fee_receiver: Option, + relayer_fee_receiver: Option, }, } @@ -191,6 +193,8 @@ pub struct ConfigResponse { pub fee_denom: String, pub swap_router_contract: String, pub gov_contract: String, + pub token_fee_receiver: Addr, + pub relayer_fee_receiver: Addr, } #[cw_serde] diff --git a/contracts/cw-ics20-latest/src/state.rs b/contracts/cw-ics20-latest/src/state.rs index b9aeb24..1112b9a 100644 --- a/contracts/cw-ics20-latest/src/state.rs +++ b/contracts/cw-ics20-latest/src/state.rs @@ -39,9 +39,12 @@ pub const TOKEN_FEE: Map<&str, Ratio> = Map::new("token_fee"); // decimals of relayer fee should always be 10^6 because we use ORAI as relayer fee pub const RELAYER_FEE: Map<&str, Uint128> = Map::new("relayer_fee"); -// shared accumulator fee for token & relayer +// accumulated token fee pub const TOKEN_FEE_ACCUMULATOR: Map<&str, Uint128> = Map::new("token_fee_accumulator"); +// accumulated relayer fee +pub const RELAYER_FEE_ACCUMULATOR: Map<&str, Uint128> = Map::new("relayer_fee_accumulator"); + // MappingMetadataIndexex structs keeps a list of indexers pub struct MappingMetadataIndexex<'a> { // token.identifier @@ -81,7 +84,8 @@ pub struct Config { pub default_gas_limit: Option, pub fee_denom: String, pub swap_router_contract: RouterController, - pub fee_receiver: Addr, + pub token_fee_receiver: Addr, + pub relayer_fee_receiver: Addr, } #[cw_serde] From 85c9ca3f88410bf8b8210f6ecd3a088c72eaca9d Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Fri, 28 Jul 2023 16:39:28 +0700 Subject: [PATCH 24/28] commented out api debug relayer fee --- contracts/cw-ics20-latest/src/ibc.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/cw-ics20-latest/src/ibc.rs b/contracts/cw-ics20-latest/src/ibc.rs index b72cb22..c11b2bf 100644 --- a/contracts/cw-ics20-latest/src/ibc.rs +++ b/contracts/cw-ics20-latest/src/ibc.rs @@ -859,7 +859,7 @@ pub fn deduct_relayer_fee( local_token_denom: &str, // local denom token_price: Uint128, ) -> StdResult<(Uint128, Uint128)> { - api.debug(format!("token price: {}", token_price).as_str()); + // api.debug(format!("token price: {}", token_price).as_str()); if token_price.is_zero() { return Ok((amount, Uint128::from(0u64))); } @@ -867,13 +867,13 @@ pub fn deduct_relayer_fee( // this is bech32 prefix of sender from other chains. Should not error because we are in the cosmos ecosystem. Every address should have prefix // evm case, need to filter remote token denom since prefix is always oraib let mut prefix = get_prefix_decode_bech32(remote_address)?; - api.debug(format!("prefix: {}", prefix).as_str()); + // api.debug(format!("prefix: {}", prefix).as_str()); if prefix.eq(ORAIBRIDGE_PREFIX) { prefix = convert_remote_denom_to_evm_prefix(remote_token_denom); } - api.debug(format!("prefix after evm prefix: {}", prefix).as_str()); + // api.debug(format!("prefix after evm prefix: {}", prefix).as_str()); let relayer_fee = RELAYER_FEE.may_load(storage, &prefix)?; - api.debug(format!("relayer fee: {}", relayer_fee.unwrap_or_default()).as_str()); + // api.debug(format!("relayer fee: {}", relayer_fee.unwrap_or_default()).as_str()); // no need to deduct fee if no fee is found in the mapping if relayer_fee.is_none() { return Ok((amount, Uint128::from(0u64))); @@ -884,7 +884,7 @@ pub fn deduct_relayer_fee( .unwrap_or_default() .checked_div(token_price) .unwrap_or_default(); - api.debug(format!("required fee needed: {}", required_fee_needed).as_str()); + // api.debug(format!("required fee needed: {}", required_fee_needed).as_str()); // accumulate fee so that we can collect it later after everything // we share the same accumulator because it's the same data structure, and we are accumulating so it's fine RELAYER_FEE_ACCUMULATOR.update( From 7ddbcd5607e418e9cc2ffb75e88f075b6bafb5f5 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Fri, 28 Jul 2023 17:13:23 +0700 Subject: [PATCH 25/28] deduct relayer fee swap 10^7 for certainty --- contracts/cw-ics20-latest/src/ibc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/cw-ics20-latest/src/ibc.rs b/contracts/cw-ics20-latest/src/ibc.rs index c11b2bf..8ad5b68 100644 --- a/contracts/cw-ics20-latest/src/ibc.rs +++ b/contracts/cw-ics20-latest/src/ibc.rs @@ -790,7 +790,7 @@ pub fn process_deduct_fee( )?; // simulate for relayer fee let offer_asset_info = denom_to_asset_info(querier, api, &local_amount.raw_denom())?; - let offer_amount = Uint128::from(10u64.pow(decimals as u32) as u64); + let offer_amount = Uint128::from(10u64.pow((decimals + 1) as u32) as u64); // +1 to make sure the offer amount is large enough to swap successfully let token_price = swap_router_contract .simulate_swap( querier, From cc7a89ac38ec31e02015d37aaabf45d4a48c7036 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Tue, 1 Aug 2023 10:00:12 +0700 Subject: [PATCH 26/28] query config added token fees & relayer fees --- contracts/cw-ics20-latest/src/contract.rs | 53 ++++++++++++----------- contracts/cw-ics20-latest/src/msg.rs | 10 ++++- 2 files changed, 37 insertions(+), 26 deletions(-) diff --git a/contracts/cw-ics20-latest/src/contract.rs b/contracts/cw-ics20-latest/src/contract.rs index 57a8a45..f0e32d8 100644 --- a/contracts/cw-ics20-latest/src/contract.rs +++ b/contracts/cw-ics20-latest/src/contract.rs @@ -18,7 +18,8 @@ use crate::ibc::{ use crate::msg::{ AllowMsg, AllowedInfo, AllowedResponse, ChannelResponse, ConfigResponse, DeletePairMsg, ExecuteMsg, InitMsg, ListAllowedResponse, ListChannelsResponse, ListMappingResponse, - MigrateMsg, PairQuery, PortResponse, QueryMsg, TransferBackMsg, UpdatePairMsg, + MigrateMsg, PairQuery, PortResponse, QueryMsg, RelayerFeeResponse, TransferBackMsg, + UpdatePairMsg, }; use crate::state::{ get_key_ics20_ibc_denom, ics20_denoms, reduce_channel_balance, AllowInfo, Config, @@ -591,6 +592,24 @@ fn query_config(deps: Deps) -> StdResult { gov_contract: admin.into(), relayer_fee_receiver: cfg.relayer_fee_receiver, token_fee_receiver: cfg.token_fee_receiver, + token_fees: TOKEN_FEE + .range(deps.storage, None, None, Order::Ascending) + .map(|data_result| { + data_result.map(|data| TokenFee { + token_denom: data.0, + ratio: data.1, + }) + }) + .collect::>>()?, + relayer_fees: RELAYER_FEE + .range(deps.storage, None, None, Order::Ascending) + .map(|data_result| { + data_result.map(|data| RelayerFeeResponse { + prefix: data.0, + amount: data.1, + }) + }) + .collect::>>()?, }; Ok(res) } @@ -1443,30 +1462,14 @@ mod test { config.token_fee_receiver, Addr::unchecked("token_fee_receiver") ); - assert_eq!( - TOKEN_FEE - .range(deps.as_ref().storage, None, None, Order::Ascending) - .count(), - 2usize - ); - assert_eq!( - TOKEN_FEE - .load(deps.as_ref().storage, "orai") - .unwrap() - .denominator, - 10 - ); - assert_eq!( - TOKEN_FEE - .load(deps.as_ref().storage, "atom") - .unwrap() - .denominator, - 5 - ); - assert_eq!( - RELAYER_FEE.load(deps.as_ref().storage, "foo").unwrap(), - Uint128::from(1000000u64), - ); + assert_eq!(config.token_fees.len(), 2usize); + assert_eq!(config.token_fees[0].ratio.denominator, 5); + assert_eq!(config.token_fees[0].token_denom, "atom".to_string()); + assert_eq!(config.token_fees[1].ratio.denominator, 10); + assert_eq!(config.token_fees[1].token_denom, "orai".to_string()); + assert_eq!(config.relayer_fees.len(), 1); + assert_eq!(config.relayer_fees[0].prefix, "foo".to_string()); + assert_eq!(config.relayer_fees[0].amount, Uint128::from(1000000u64)); } #[test] diff --git a/contracts/cw-ics20-latest/src/msg.rs b/contracts/cw-ics20-latest/src/msg.rs index 7f1f465..83e17fa 100644 --- a/contracts/cw-ics20-latest/src/msg.rs +++ b/contracts/cw-ics20-latest/src/msg.rs @@ -1,5 +1,5 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Addr, Binary, IbcEndpoint}; +use cosmwasm_std::{Addr, Binary, IbcEndpoint, Uint128}; use cw20::Cw20ReceiveMsg; use oraiswap::asset::AssetInfo; @@ -195,6 +195,14 @@ pub struct ConfigResponse { pub gov_contract: String, pub token_fee_receiver: Addr, pub relayer_fee_receiver: Addr, + pub token_fees: Vec, + pub relayer_fees: Vec, +} + +#[cw_serde] +pub struct RelayerFeeResponse { + pub prefix: String, + pub amount: Uint128, } #[cw_serde] From 36ed9ca0c331cf519ddf6a946871d5923495c717 Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Tue, 1 Aug 2023 11:32:38 +0700 Subject: [PATCH 27/28] renamed update pair msg for better understanding --- contracts/cw-ics20-latest/src/contract.rs | 33 ++++++++++--------- contracts/cw-ics20-latest/src/ibc_tests.rs | 12 +++---- .../cw-ics20-latest/src/integration_tests.rs | 4 +-- contracts/cw-ics20-latest/src/msg.rs | 4 +-- 4 files changed, 28 insertions(+), 25 deletions(-) diff --git a/contracts/cw-ics20-latest/src/contract.rs b/contracts/cw-ics20-latest/src/contract.rs index f0e32d8..b764c92 100644 --- a/contracts/cw-ics20-latest/src/contract.rs +++ b/contracts/cw-ics20-latest/src/contract.rs @@ -457,16 +457,19 @@ pub fn execute_update_mapping_pair( deps.storage, &ibc_denom, &MappingMetadata { - asset_info: mapping_pair_msg.asset_info.clone(), + asset_info: mapping_pair_msg.local_asset_info.clone(), remote_decimals: mapping_pair_msg.remote_decimals, - asset_info_decimals: mapping_pair_msg.asset_info_decimals, + asset_info_decimals: mapping_pair_msg.local_asset_info_decimals, }, )?; let res = Response::new() .add_attribute("action", "execute_update_mapping_pair") .add_attribute("denom", mapping_pair_msg.denom) - .add_attribute("new_asset_info", mapping_pair_msg.asset_info.to_string()); + .add_attribute( + "new_asset_info", + mapping_pair_msg.local_asset_info.to_string(), + ); Ok(res) } @@ -797,9 +800,9 @@ mod test { let mut update = UpdatePairMsg { local_channel_id: "mars-channel".to_string(), denom: "earth".to_string(), - asset_info: asset_info.clone(), + local_asset_info: asset_info.clone(), remote_decimals: 18, - asset_info_decimals: 18, + local_asset_info_decimals: 18, }; // works with proper funds @@ -816,7 +819,7 @@ mod test { // add another pair with a different asset info update.denom = "moon".to_string(); - update.asset_info = AssetInfo::NativeToken { + update.local_asset_info = AssetInfo::NativeToken { denom: "orai".to_string(), }; msg = ExecuteMsg::UpdateMappingPair(update.clone()); @@ -880,9 +883,9 @@ mod test { let mut update = UpdatePairMsg { local_channel_id: "mars-channel".to_string(), denom: "earth".to_string(), - asset_info: asset_info.clone(), + local_asset_info: asset_info.clone(), remote_decimals: 18, - asset_info_decimals: 18, + local_asset_info_decimals: 18, }; // works with proper funds @@ -929,7 +932,7 @@ mod test { assert_ne!(response.pairs.first().unwrap().key, "foobar".to_string()); // update existing key case must pass - update.asset_info = asset_info_second.clone(); + update.local_asset_info = asset_info_second.clone(); msg = ExecuteMsg::UpdateMappingPair(update.clone()); let info = mock_info("gov", &coins(1234567, "ucosm")); @@ -968,9 +971,9 @@ mod test { let update = UpdatePairMsg { local_channel_id: "mars-channel".to_string(), denom: "earth".to_string(), - asset_info: cw20_denom.clone(), + local_asset_info: cw20_denom.clone(), remote_decimals: 18, - asset_info_decimals: 18, + local_asset_info_decimals: 18, }; // works with proper funds @@ -1241,9 +1244,9 @@ mod test { let pair = UpdatePairMsg { local_channel_id: local_channel.to_string(), denom: denom.to_string(), - asset_info: asset_info.clone(), + local_asset_info: asset_info.clone(), remote_decimals: 18u8, - asset_info_decimals: 18u8, + local_asset_info_decimals: 18u8, }; let _ = execute( @@ -1377,11 +1380,11 @@ mod test { let pair = UpdatePairMsg { local_channel_id: "not_registered_channel".to_string(), denom: denom.to_string(), - asset_info: AssetInfo::Token { + local_asset_info: AssetInfo::Token { contract_addr: Addr::unchecked("random_cw20_denom".to_string()), }, remote_decimals: 18u8, - asset_info_decimals: 18u8, + local_asset_info_decimals: 18u8, }; execute( diff --git a/contracts/cw-ics20-latest/src/ibc_tests.rs b/contracts/cw-ics20-latest/src/ibc_tests.rs index ca5433a..2610bfd 100644 --- a/contracts/cw-ics20-latest/src/ibc_tests.rs +++ b/contracts/cw-ics20-latest/src/ibc_tests.rs @@ -491,9 +491,9 @@ mod test { let pair = UpdatePairMsg { local_channel_id: send_channel.to_string(), denom: denom.to_string(), - asset_info: asset_info.clone(), + local_asset_info: asset_info.clone(), remote_decimals: 18u8, - asset_info_decimals: 18u8, + local_asset_info_decimals: 18u8, }; let _ = execute( @@ -774,9 +774,9 @@ mod test { let update = UpdatePairMsg { local_channel_id: "mars-channel".to_string(), denom: pair_mapping_denom.to_string(), - asset_info: receiver_asset_info.clone(), + local_asset_info: receiver_asset_info.clone(), remote_decimals, - asset_info_decimals, + local_asset_info_decimals: asset_info_decimals, }; // works with proper funds @@ -907,9 +907,9 @@ mod test { let update = UpdatePairMsg { local_channel_id: "mars-channel".to_string(), denom: pair_mapping_denom.to_string(), - asset_info: receiver_asset_info.clone(), + local_asset_info: receiver_asset_info.clone(), remote_decimals, - asset_info_decimals, + local_asset_info_decimals: asset_info_decimals, }; let msg = ExecuteMsg::UpdateMappingPair(update.clone()); diff --git a/contracts/cw-ics20-latest/src/integration_tests.rs b/contracts/cw-ics20-latest/src/integration_tests.rs index 5e55d87..4ff6f54 100644 --- a/contracts/cw-ics20-latest/src/integration_tests.rs +++ b/contracts/cw-ics20-latest/src/integration_tests.rs @@ -102,9 +102,9 @@ fn initialize_basic_data_for_testings() -> (App, Addr, Addr, IbcEndpoint, String let update_allow_msg = ExecuteMsg::UpdateMappingPair(UpdatePairMsg { local_channel_id: local_channel_id.clone(), denom: native_denom.to_string(), - asset_info: asset_info.clone(), + local_asset_info: asset_info.clone(), remote_decimals, - asset_info_decimals, + local_asset_info_decimals: asset_info_decimals, }); router .execute_contract( diff --git a/contracts/cw-ics20-latest/src/msg.rs b/contracts/cw-ics20-latest/src/msg.rs index 83e17fa..b161174 100644 --- a/contracts/cw-ics20-latest/src/msg.rs +++ b/contracts/cw-ics20-latest/src/msg.rs @@ -68,9 +68,9 @@ pub struct UpdatePairMsg { /// native denom of the remote chain. Eg: orai pub denom: String, /// asset info of the local chain. - pub asset_info: AssetInfo, + pub local_asset_info: AssetInfo, pub remote_decimals: u8, - pub asset_info_decimals: u8, + pub local_asset_info_decimals: u8, } #[cw_serde] From 6e1520fbc3de16c942282ecb7a88b9ca7e39e67e Mon Sep 17 00:00:00 2001 From: ducphamle2 Date: Tue, 1 Aug 2023 19:00:59 +0700 Subject: [PATCH 28/28] finished cosmos based single step flow --- Cargo.lock | 1 - contracts/cw-ics20-latest/Cargo.toml | 1 - contracts/cw-ics20-latest/src/contract.rs | 6 +- contracts/cw-ics20-latest/src/ibc.rs | 194 +++++++++++++------- contracts/cw-ics20-latest/src/ibc_tests.rs | 203 +++++++++++++-------- packages/cw20-ics20-msg/src/helper.rs | 6 + packages/cw20-ics20-msg/src/receiver.rs | 39 ++-- 7 files changed, 291 insertions(+), 159 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b213d3f..1964793 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -230,7 +230,6 @@ dependencies = [ name = "cw-ics20" version = "1.0.2" dependencies = [ - "bech32", "cosmwasm-schema", "cosmwasm-std", "cw-controllers", diff --git a/contracts/cw-ics20-latest/Cargo.toml b/contracts/cw-ics20-latest/Cargo.toml index f7eede8..7ebf4a7 100644 --- a/contracts/cw-ics20-latest/Cargo.toml +++ b/contracts/cw-ics20-latest/Cargo.toml @@ -31,7 +31,6 @@ schemars = "0.8.1" semver = "1" serde = { version = "1.0.103", default-features = false, features = ["derive"] } thiserror = { version = "1.0.23" } -bech32 = "0.8.1" [dev-dependencies] cw-multi-test = "0.16.0" diff --git a/contracts/cw-ics20-latest/src/contract.rs b/contracts/cw-ics20-latest/src/contract.rs index b764c92..6e487f1 100644 --- a/contracts/cw-ics20-latest/src/contract.rs +++ b/contracts/cw-ics20-latest/src/contract.rs @@ -24,7 +24,8 @@ use crate::msg::{ use crate::state::{ get_key_ics20_ibc_denom, ics20_denoms, reduce_channel_balance, AllowInfo, Config, MappingMetadata, RelayerFee, TokenFee, ADMIN, ALLOW_LIST, CHANNEL_INFO, CHANNEL_REVERSE_STATE, - CONFIG, RELAYER_FEE, RELAYER_FEE_ACCUMULATOR, TOKEN_FEE, TOKEN_FEE_ACCUMULATOR, + CONFIG, RELAYER_FEE, RELAYER_FEE_ACCUMULATOR, REPLY_ARGS, SINGLE_STEP_REPLY_ARGS, TOKEN_FEE, + TOKEN_FEE_ACCUMULATOR, }; use cw20_ics20_msg::amount::{convert_local_to_remote, Amount}; use cw_utils::{maybe_addr, nonpayable, one_coin}; @@ -510,6 +511,9 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result Binary { } // pub const RECEIVE_ID: u64 = 1337; -pub const FOLLOW_UP_ID: u64 = 1339; +pub const NATIVE_RECEIVE_ID: u64 = 1338; +pub const FOLLOW_UP_ERROR_ID: u64 = 1339; +pub const IBC_TRANSFER_NATIVE_ERROR_ID: u64 = 1341; pub const REFUND_FAILURE_ID: u64 = 1340; pub const ACK_FAILURE_ID: u64 = 64023; #[entry_point] pub fn reply(deps: DepsMut, _env: Env, reply: Reply) -> Result { match reply.id { - FOLLOW_UP_ID => match reply.result { + NATIVE_RECEIVE_ID => match reply.result { + SubMsgResult::Ok(_) => Ok(Response::new()), + SubMsgResult::Err(err) => { + // Important design note: with ibcv2 and wasmd 0.22 we can implement this all much easier. + // No reply needed... the receive function and submessage should return error on failure and all + // state gets reverted with a proper app-level message auto-generated + + // Since we need compatibility with Juno (Jan 2022), we need to ensure that optimisitic + // state updates in ibc_packet_receive get reverted in the (unlikely) chance of an + // error while sending the token + + // However, this requires passing some state between the ibc_packet_receive function and + // the reply handler. We do this with a singleton, with is "okay" for IBC as there is no + // reentrancy on these functions (cannot be called by another contract). This pattern + // should not be used for ExecuteMsg handlers + let reply_args = REPLY_ARGS.load(deps.storage)?; + // after getting args, we should always clear old data just in case + REPLY_ARGS.remove(deps.storage); + undo_increase_channel_balance( + deps.storage, + &reply_args.channel, + &reply_args.denom, + reply_args.amount, + false, + )?; + + Ok(Response::new() + .set_data(ack_fail(err.clone())) + .add_attribute("error_transferring_ibc_tokens_to_cw20", err) + .add_attributes(vec![ + attr("undo_increase_channel", reply_args.channel), + attr("undo_increase_channel_ibc_denom", reply_args.denom), + attr("undo_increase_channel_amount", reply_args.amount), + ])) + } + }, + FOLLOW_UP_ERROR_ID => match reply.result { SubMsgResult::Ok(_) => Ok(Response::new()), SubMsgResult::Err(err) => { let reply_args = SINGLE_STEP_REPLY_ARGS.load(deps.storage)?; + SINGLE_STEP_REPLY_ARGS.remove(deps.storage); // only refund, not undo reduce balance handle_follow_up_failure(deps.storage, reply_args, err) } @@ -120,6 +160,12 @@ pub fn reply(deps: DepsMut, _env: Env, reply: Reply) -> Result match reply.result { + SubMsgResult::Ok(_) => Ok(Response::new()), + SubMsgResult::Err(err) => Ok(Response::new() + .set_data(ack_fail(err.clone())) + .add_attribute("error_trying_to_transfer_ibc_native_with_error", err)), + }, _ => Err(ContractError::UnknownReplyId { id: reply.id }), } } @@ -242,8 +288,8 @@ pub fn parse_voucher_denom<'a>( // Returns local denom if the denom is an encoded voucher from the expected endpoint // Otherwise, error -pub fn parse_voucher_denom_without_sanity_checks<'a>(voucher_denom: &'a str) -> StdResult<&'a str> { - let split_denom: Vec<&str> = voucher_denom.splitn(3, '/').collect(); +pub fn parse_ibc_denom_without_sanity_checks<'a>(ibc_denom: &'a str) -> StdResult<&'a str> { + let split_denom: Vec<&str> = ibc_denom.splitn(3, '/').collect(); if split_denom.len() != 3 { return Err(StdError::generic_err( @@ -253,6 +299,19 @@ pub fn parse_voucher_denom_without_sanity_checks<'a>(voucher_denom: &'a str) -> Ok(split_denom[2]) } +// Returns +// Otherwise, error +pub fn parse_ibc_channel_without_sanity_checks<'a>(ibc_denom: &'a str) -> StdResult<&'a str> { + let split_denom: Vec<&str> = ibc_denom.splitn(3, '/').collect(); + + if split_denom.len() != 3 { + return Err(StdError::generic_err( + ContractError::NoForeignTokens {}.to_string(), + )); + } + Ok(split_denom[1]) +} + // this does the work of ibc_packet_receive, we wrap it to turn errors into acknowledgements fn do_ibc_packet_receive( deps: DepsMut, @@ -375,10 +434,6 @@ fn handle_ibc_packet_receive_native_remote_chain( &msg.memo.clone().unwrap_or_default(), packet.dest.channel_id.as_str(), )?; - let submsgs: Vec = submsgs - .into_iter() - .map(|msg| SubMsg::reply_on_error(msg, FOLLOW_UP_ID)) - .collect(); let mut fee_msgs = collect_fee_msgs( storage, @@ -420,15 +475,16 @@ pub fn get_follow_up_msgs( receiver: &str, memo: &str, initial_dest_channel_id: &str, // channel id on Oraichain receiving the token from other chain -) -> Result<(Vec, String), ContractError> { +) -> Result<(Vec, String), ContractError> { let config = CONFIG.load(storage)?; - let mut cosmos_msgs: Vec = vec![]; + let mut sub_msgs: Vec = vec![]; let destination: DestinationInfo = DestinationInfo::from_str(memo); + let send_only_sub_msg = SubMsg::reply_on_error( + to_send.send_amount(receiver.to_string(), None), + NATIVE_RECEIVE_ID, + ); if is_follow_up_msgs_only_send_amount(&memo, &destination.destination_denom) { - return Ok(( - vec![to_send.send_amount(receiver.to_string(), None)], - "".to_string(), - )); + return Ok((vec![send_only_sub_msg], "".to_string())); } // successful case. We dont care if this msg is going to be successful or not because it does not affect our ibc receive flow (just submsgs) let receiver_asset_info = denom_to_asset_info(querier, api, &destination.destination_denom)?; @@ -443,8 +499,18 @@ pub fn get_follow_up_msgs( querier, to_send.amount().clone(), swap_operations.clone(), - )?; - minimum_receive = response.amount; + ); + if response.is_err() { + return Ok(( + vec![send_only_sub_msg], + format!( + "Cannot simulate swap with ops: {:?} with error: {:?}", + swap_operations, + response.unwrap_err().to_string() + ), + )); + } + minimum_receive = response.unwrap().amount; } let ibc_msg = build_ibc_msg( @@ -462,7 +528,7 @@ pub fn get_follow_up_msgs( // by default, the receiver is the original address sent in ics20packet let mut to = Some(api.addr_validate(receiver)?); let ibc_error_msg = if let Some(ibc_msg) = ibc_msg.as_ref().ok() { - cosmos_msgs.push(ibc_msg.to_owned()); + sub_msgs.push(ibc_msg.to_owned()); // if there's an ibc msg => swap receiver is None so the receiver is this ibc wasm address to = None; String::from("") @@ -475,17 +541,14 @@ pub fn get_follow_up_msgs( to_send.amount(), initial_receive_asset_info, to, - &mut cosmos_msgs, + &mut sub_msgs, swap_operations, )?; - // fallback case. If there's no cosmos messages then we return send amount - if cosmos_msgs.is_empty() { - return Ok(( - vec![to_send.send_amount(receiver.to_string(), None)], - ibc_error_msg, - )); + // fallback case. If there's no cosmos messages or ibc error msg is not empty then we return send amount + if sub_msgs.is_empty() { + return Ok((vec![send_only_sub_msg], ibc_error_msg)); } - return Ok((cosmos_msgs, ibc_error_msg)); + return Ok((sub_msgs, ibc_error_msg)); } pub fn is_follow_up_msgs_only_send_amount(memo: &str, destination_denom: &str) -> bool { @@ -533,22 +596,25 @@ pub fn build_swap_msgs( amount: Uint128, initial_receive_asset_info: AssetInfo, to: Option, - cosmos_msgs: &mut Vec, + sub_msgs: &mut Vec, operations: Vec, ) -> StdResult<()> { // the swap msg must be executed before other msgs because we need the ask token amount to create ibc msg => insert in first index if operations.len() == 0 { return Ok(()); } - cosmos_msgs.insert( + sub_msgs.insert( 0, - swap_router_contract.execute_operations( - initial_receive_asset_info, - amount, - operations, - Some(minimum_receive), - to, - )?, + SubMsg::reply_on_error( + swap_router_contract.execute_operations( + initial_receive_asset_info, + amount, + operations, + Some(minimum_receive), + to, + )?, + NATIVE_RECEIVE_ID, + ), ); Ok(()) @@ -564,7 +630,7 @@ pub fn build_ibc_msg( remote_address: &str, destination: &DestinationInfo, default_timeout: u64, -) -> StdResult { +) -> StdResult { // if there's no dest channel then we stop, no need to transfer ibc if destination.destination_channel.is_empty() { return Err(StdError::generic_err( @@ -595,12 +661,12 @@ pub fn build_ibc_msg( // parse to get eth-mainnet0x... // then collect eth-mainnet prefix, and compare with dest channel convert_remote_denom_to_evm_prefix( - parse_voucher_denom_without_sanity_checks(key).unwrap_or_default(), + parse_ibc_denom_without_sanity_checks(key).unwrap_or_default(), ) .eq(&evm_destination.destination_channel) }) .ok_or(StdError::generic_err("cannot find pair mappings"))?; - let msg = process_ibc_msg( + let msg: CosmosMsg = process_ibc_msg( storage, mapping, receiver_asset_info, @@ -611,46 +677,52 @@ pub fn build_ibc_msg( amount, timeout, reply_args, - )?; - return Ok(msg.into()); + )? + .into(); + return Ok(SubMsg::reply_on_error(msg, NATIVE_RECEIVE_ID)); } // 2nd case, where destination network is not evm, but it is still supported on our channel (eg: cw20 ATOM mapped with native ATOM on Cosmos), then we call - let (is_cosmos_based, cosmos_destination) = destination.is_receiver_cosmos_based(); + let is_cosmos_based = destination.is_receiver_cosmos_based(); if is_cosmos_based { + // eg: wasm.orai195269awwnt5m6c843q6w7hp8rt0k7syfu9de4h0wz384slshuzps8y7ccm/channel-124/uatom + // for cosmos-based networks, each will have its own channel id => we filter using channel id, no need to check for denom let mapping = pair_mappings.into_iter().find(|(key, _)| { - get_prefix_decode_bech32( - parse_voucher_denom_without_sanity_checks(key).unwrap_or_default(), - ) - .unwrap_or_default() - .eq(&cosmos_destination.destination_channel) + parse_ibc_channel_without_sanity_checks(key) + .unwrap_or_default() + .eq(&destination.destination_channel) }); if let Some(mapping) = mapping { - let msg = process_ibc_msg( + let msg: CosmosMsg = process_ibc_msg( storage, mapping, receiver_asset_info, - local_channel_id, + &destination.destination_channel, env.contract.address.as_str(), - &cosmos_destination.receiver, // now we use dest receiver since cosmos based universal swap wont be sent to oraibridge, so the receiver is the correct receive addr + &destination.receiver, // now we use dest receiver since cosmos based universal swap wont be sent to oraibridge, so the receiver is the correct receive addr None, // no need memo because it is not used in the remote cosmos based chain amount, timeout, reply_args, - )?; - return Ok(msg.into()); + )? + .into(); + return Ok(SubMsg::reply_on_error(msg, NATIVE_RECEIVE_ID)); } // final case, where the destination token is from a remote chain that we dont have a pair mapping with. // we use ibc transfer so that attackers cannot manipulate the data to send to oraibridge without reducing the channel balance // by using ibc transfer, the contract must actually owns native ibc tokens, which is not possible if it's oraibridge tokens // we do not need to reduce channel balance because this transfer is not on our contract channel, but on destination channel - let ibc_msg = IbcMsg::Transfer { + let ibc_msg: CosmosMsg = IbcMsg::Transfer { channel_id: destination.destination_channel.clone(), to_address: destination.receiver.clone(), amount: coin(amount.u128(), destination.destination_denom.clone()), timeout: timeout.into(), - }; - return Ok(ibc_msg.into()); + } + .into(); + return Ok(SubMsg::reply_on_error( + ibc_msg, + IBC_TRANSFER_NATIVE_ERROR_ID, + )); } Err(StdError::generic_err( "The destination info is neither evm or cosmos based", @@ -662,7 +734,7 @@ pub fn process_ibc_msg( storage: &mut dyn Storage, pair_mapping: (String, MappingMetadata), receiver_asset_info: AssetInfo, - local_channel_id: &str, + src_channel: &str, ibc_msg_sender: &str, ibc_msg_receiver: &str, memo: Option, @@ -672,7 +744,7 @@ pub fn process_ibc_msg( ) -> StdResult { let (new_deducted_amount, _) = deduct_token_fee( storage, - parse_voucher_denom_without_sanity_checks(&pair_mapping.0)?, // denom mapping in the form port/channel/denom + parse_ibc_denom_without_sanity_checks(&pair_mapping.0)?, // denom mapping in the form port/channel/denom amount, &parse_asset_info_denom(receiver_asset_info.clone()), )?; @@ -685,7 +757,7 @@ pub fn process_ibc_msg( // because we are transferring back, we reduce the channel's balance reduce_channel_balance( storage, - local_channel_id.clone(), + src_channel.clone(), &pair_mapping.0.clone(), remote_amount, false, @@ -699,11 +771,11 @@ pub fn process_ibc_msg( ibc_msg_sender, ibc_msg_receiver, memo, - local_channel_id, + src_channel, timeout.into(), )?; - reply_args.channel = local_channel_id.to_string(); + reply_args.channel = src_channel.to_string(); reply_args.ibc_data = Some(IbcSingleStepData { ibc_denom: pair_mapping.0.to_string(), remote_amount, diff --git a/contracts/cw-ics20-latest/src/ibc_tests.rs b/contracts/cw-ics20-latest/src/ibc_tests.rs index 2610bfd..44fbdc2 100644 --- a/contracts/cw-ics20-latest/src/ibc_tests.rs +++ b/contracts/cw-ics20-latest/src/ibc_tests.rs @@ -9,8 +9,9 @@ mod test { ack_fail, build_ibc_msg, build_swap_msgs, check_gas_limit, convert_remote_denom_to_evm_prefix, deduct_fee, deduct_relayer_fee, deduct_token_fee, handle_follow_up_failure, ibc_packet_receive, is_follow_up_msgs_only_send_amount, - parse_voucher_denom, parse_voucher_denom_without_sanity_checks, process_ibc_msg, Ics20Ack, - Ics20Packet, REFUND_FAILURE_ID, + parse_ibc_channel_without_sanity_checks, parse_ibc_denom_without_sanity_checks, + parse_voucher_denom, process_ibc_msg, Ics20Ack, Ics20Packet, IBC_TRANSFER_NATIVE_ERROR_ID, + NATIVE_RECEIVE_ID, REFUND_FAILURE_ID, }; use crate::ibc::{build_swap_operations, get_follow_up_msgs}; use crate::test_helpers::*; @@ -641,7 +642,7 @@ mod test { }; let native_denom = "foobar"; let to: Option = None; - let mut cosmos_msgs: Vec = vec![]; + let mut cosmos_msgs: Vec = vec![]; let mut operations: Vec = vec![]; build_swap_msgs( minimum_receive.clone(), @@ -691,16 +692,19 @@ mod test { format!("{:?}", cosmos_msgs[0]).contains("execute_swap_operations") ); assert_eq!( - CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: swap_router_contract.to_string(), - msg: to_binary(&oraiswap::router::ExecuteMsg::ExecuteSwapOperations { - operations: operations, - minimum_receive: Some(minimum_receive), - to - }) - .unwrap(), - funds: coins(amount.u128(), native_denom) - }), + SubMsg::reply_on_error( + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: swap_router_contract.to_string(), + msg: to_binary(&oraiswap::router::ExecuteMsg::ExecuteSwapOperations { + operations: operations, + minimum_receive: Some(minimum_receive), + to + }) + .unwrap(), + funds: coins(amount.u128(), native_denom) + }), + NATIVE_RECEIVE_ID + ), cosmos_msgs[0] ); } @@ -813,18 +817,21 @@ mod test { assert_eq!( result, - CosmosMsg::Ibc(IbcMsg::SendPacket { - channel_id: receive_channel.to_string(), - data: to_binary(&Ics20Packet::new( - remote_amount.clone(), - pair_mapping_key.clone(), - env.contract.address.as_str(), - &remote_address, - Some(destination.receiver), - )) - .unwrap(), - timeout: env.block.time.plus_seconds(timeout).into() - }) + SubMsg::reply_on_error( + CosmosMsg::Ibc(IbcMsg::SendPacket { + channel_id: receive_channel.to_string(), + data: to_binary(&Ics20Packet::new( + remote_amount.clone(), + pair_mapping_key.clone(), + env.contract.address.as_str(), + &remote_address, + Some(destination.receiver), + )) + .unwrap(), + timeout: env.block.time.plus_seconds(timeout).into() + }), + NATIVE_RECEIVE_ID + ) ); let reply_args = SINGLE_STEP_REPLY_ARGS.load(deps.as_mut().storage).unwrap(); let ibc_data = reply_args.ibc_data.unwrap(); @@ -839,7 +846,7 @@ mod test { #[test] fn test_get_ibc_msg_cosmos_based_case() { // setup - let send_channel = "channel-9"; + let send_channel = "channel-10"; let allowed = "foobar"; let allowed_gas = 777666; let mut deps = setup(&[send_channel], &[(allowed, allowed_gas)]); @@ -854,7 +861,7 @@ mod test { let remote_amount = convert_local_to_remote(amount.clone(), 18, 6).unwrap(); let destination = DestinationInfo { receiver: "cosmos1zedxv25ah8fksmg2lzrndrpkvsjqgk4zt5ff7n".to_string(), - destination_channel: "channel-10".to_string(), + destination_channel: send_channel.to_string(), destination_denom: "atom".to_string(), }; let env = mock_env(); @@ -862,10 +869,9 @@ mod test { let ibc_denom = format!("foo/bar/{}", pair_mapping_denom); let remote_decimals = 18; let asset_info_decimals = 6; - let remote_channel = "mars-channel"; let pair_mapping_key = format!( "wasm.cosmos2contract/{}/{}", - remote_channel, pair_mapping_denom + send_channel, pair_mapping_denom ); CHANNEL_REVERSE_STATE @@ -879,6 +885,17 @@ mod test { ) .unwrap(); + CHANNEL_REVERSE_STATE + .save( + deps.as_mut().storage, + (send_channel, pair_mapping_key.as_str()), + &ChannelState { + outstanding: remote_amount.clone(), + total_sent: Uint128::from(100u128), + }, + ) + .unwrap(); + // cosmos based case but no mapping found. should be successful & cosmos msg is ibc transfer let result = build_ibc_msg( deps.as_mut().storage, @@ -894,18 +911,21 @@ mod test { .unwrap(); assert_eq!( result, - CosmosMsg::Ibc(IbcMsg::Transfer { - channel_id: "channel-10".to_string(), - to_address: destination.receiver.clone(), - amount: coin(1000u128, "atom"), - timeout: mock_env().block.time.plus_seconds(timeout).into() - }) + SubMsg::reply_on_error( + CosmosMsg::Ibc(IbcMsg::Transfer { + channel_id: send_channel.to_string(), + to_address: destination.receiver.clone(), + amount: coin(1000u128, "atom"), + timeout: mock_env().block.time.plus_seconds(timeout).into() + }), + IBC_TRANSFER_NATIVE_ERROR_ID + ) ); // cosmos based case with mapping found. Should be successful & cosmos msg is ibc send packet // add a pair mapping so we can test the happy case evm based happy case let update = UpdatePairMsg { - local_channel_id: "mars-channel".to_string(), + local_channel_id: send_channel.to_string(), denom: pair_mapping_denom.to_string(), local_asset_info: receiver_asset_info.clone(), remote_decimals, @@ -944,18 +964,21 @@ mod test { assert_eq!( result, - CosmosMsg::Ibc(IbcMsg::SendPacket { - channel_id: local_channel_id.to_string(), - data: to_binary(&Ics20Packet::new( - remote_amount.clone(), - pair_mapping_key.clone(), - env.contract.address.as_str(), - &destination.receiver, - None, - )) - .unwrap(), - timeout: env.block.time.plus_seconds(timeout).into() - }) + SubMsg::reply_on_error( + CosmosMsg::Ibc(IbcMsg::SendPacket { + channel_id: send_channel.to_string(), + data: to_binary(&Ics20Packet::new( + remote_amount.clone(), + pair_mapping_key.clone(), + env.contract.address.as_str(), + &destination.receiver, + None, + )) + .unwrap(), + timeout: env.block.time.plus_seconds(timeout).into() + }), + NATIVE_RECEIVE_ID + ) ); } @@ -1035,15 +1058,18 @@ mod test { assert_eq!( result.0, - vec![CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: env.contract.address.to_string(), - msg: to_binary(&Cw20ExecuteMsg::Transfer { - recipient: receiver.to_string(), - amount: amount.clone() - }) - .unwrap(), - funds: vec![] - })] + vec![SubMsg::reply_on_error( + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: env.contract.address.to_string(), + msg: to_binary(&Cw20ExecuteMsg::Transfer { + recipient: receiver.to_string(), + amount: amount.clone() + }) + .unwrap(), + funds: vec![] + }), + NATIVE_RECEIVE_ID + )] ); // 2nd case, destination denom is empty => destination is collected from memo @@ -1067,15 +1093,18 @@ mod test { assert_eq!( result.0, - vec![CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: env.contract.address.to_string(), - msg: to_binary(&Cw20ExecuteMsg::Transfer { - recipient: receiver.to_string(), - amount: amount.clone() - }) - .unwrap(), - funds: vec![] - })] + vec![SubMsg::reply_on_error( + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: env.contract.address.to_string(), + msg: to_binary(&Cw20ExecuteMsg::Transfer { + recipient: receiver.to_string(), + amount: amount.clone() + }) + .unwrap(), + funds: vec![] + }), + NATIVE_RECEIVE_ID + )] ); // 3rd case, cosmos msgs empty case, also send amount @@ -1101,15 +1130,18 @@ mod test { assert_eq!( result.0, - vec![CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: env.contract.address.to_string(), - msg: to_binary(&Cw20ExecuteMsg::Transfer { - recipient: receiver.to_string(), - amount: amount.clone() - }) - .unwrap(), - funds: vec![] - })] + vec![SubMsg::reply_on_error( + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: env.contract.address.to_string(), + msg: to_binary(&Cw20ExecuteMsg::Transfer { + recipient: receiver.to_string(), + amount: amount.clone() + }) + .unwrap(), + funds: vec![] + }), + NATIVE_RECEIVE_ID + )] ); } @@ -1240,17 +1272,28 @@ mod test { } #[test] - fn test_parse_voucher_denom_without_sanity_checks() { + fn test_parse_ibc_denom_without_sanity_checks() { + assert_eq!(parse_ibc_denom_without_sanity_checks("foo").is_err(), true); + assert_eq!( + parse_ibc_denom_without_sanity_checks("foo/bar").is_err(), + true + ); + let result = parse_ibc_denom_without_sanity_checks("foo/bar/helloworld").unwrap(); + assert_eq!(result, "helloworld"); + } + + #[test] + fn test_parse_ibc_channel_without_sanity_checks() { assert_eq!( - parse_voucher_denom_without_sanity_checks("foo").is_err(), + parse_ibc_channel_without_sanity_checks("foo").is_err(), true ); assert_eq!( - parse_voucher_denom_without_sanity_checks("foo/bar").is_err(), + parse_ibc_channel_without_sanity_checks("foo/bar").is_err(), true ); - let result = parse_voucher_denom_without_sanity_checks("foo/bar/helloworld").unwrap(); - assert_eq!(result, "helloworld"); + let result = parse_ibc_channel_without_sanity_checks("foo/bar/helloworld").unwrap(); + assert_eq!(result, "bar"); } #[test] diff --git a/packages/cw20-ics20-msg/src/helper.rs b/packages/cw20-ics20-msg/src/helper.rs index 8399dec..0a8e919 100644 --- a/packages/cw20-ics20-msg/src/helper.rs +++ b/packages/cw20-ics20-msg/src/helper.rs @@ -43,3 +43,9 @@ pub fn denom_to_asset_info( }; Ok(info) } + +#[test] +fn test_get_prefix_decode_bech32() { + let result = get_prefix_decode_bech32("cosmos1g4h64yjt0fvzv5v2j8tyfnpe5kmnetejl67nlm").unwrap(); + assert_eq!(result, "cosmos".to_string()); +} diff --git a/packages/cw20-ics20-msg/src/receiver.rs b/packages/cw20-ics20-msg/src/receiver.rs index 6d52a5b..e25acaa 100644 --- a/packages/cw20-ics20-msg/src/receiver.rs +++ b/packages/cw20-ics20-msg/src/receiver.rs @@ -50,16 +50,14 @@ impl DestinationInfo { } } - pub fn is_receiver_cosmos_based(&self) -> (bool, Self) { - let mut new_destination: DestinationInfo = DestinationInfo { ..self.clone() }; - match get_prefix_decode_bech32(&new_destination.receiver).ok() { - None => (false, new_destination), + pub fn is_receiver_cosmos_based(&self) -> bool { + match get_prefix_decode_bech32(&self.receiver).ok() { + None => false, Some(prefix) => { if prefix.is_empty() { - return (false, new_destination); + return false; } - new_destination.destination_channel = prefix.to_string(); - (true, new_destination) + true } } } @@ -90,28 +88,29 @@ fn test_is_evm_based() { #[test] fn test_is_cosmos_based() { let d1 = DestinationInfo::from_str("foo"); - assert_eq!(false, d1.is_receiver_cosmos_based().0); + assert_eq!(false, d1.is_receiver_cosmos_based()); let d1 = DestinationInfo::from_str("channel-15/foo:usdt"); - assert_eq!(false, d1.is_receiver_cosmos_based().0); + assert_eq!(false, d1.is_receiver_cosmos_based()); let d1 = DestinationInfo::from_str("channel-15/cosmos14n3tx8s5ftzhlxvq0w5962v60vd82h30sythlz:usdt"); let result = d1.is_receiver_cosmos_based(); - assert_eq!(true, result.0); - assert_eq!("cosmos", result.1.destination_channel); + assert_eq!(true, result); let d1 = DestinationInfo::from_str("channel-15/akash1g4h64yjt0fvzv5v2j8tyfnpe5kmnetejjpn5xp:usdt"); let result = d1.is_receiver_cosmos_based(); - assert_eq!(true, result.0); - assert_eq!("akash", result.1.destination_channel); + assert_eq!(true, result); let d1 = DestinationInfo::from_str("channel-15/bostrom1g4h64yjt0fvzv5v2j8tyfnpe5kmnetejuf2qpu:usdt"); let result = d1.is_receiver_cosmos_based(); - assert_eq!(true, result.0); - assert_eq!("bostrom", result.1.destination_channel); + assert_eq!(true, result); + + let d1 = DestinationInfo::from_str("channel-124/cosmos1g4h64yjt0fvzv5v2j8tyfnpe5kmnetejl67nlm:orai17l2zk3arrx0a0fyuneyx8raln68622a2lrsz8ph75u7gw9tgz3esayqryf"); + let result = d1.is_receiver_cosmos_based(); + assert_eq!(true, result); } #[test] @@ -213,4 +212,14 @@ fn test_parse_destination_info() { "ibc/A2E2EEC9057A4A1C2C0A6A4C78B0239118DF5F278830F50B4A6BDD7A66506B78".to_string() } ); + let d8 = DestinationInfo::from_str("channel-124/cosmos1g4h64yjt0fvzv5v2j8tyfnpe5kmnetejl67nlm:orai17l2zk3arrx0a0fyuneyx8raln68622a2lrsz8ph75u7gw9tgz3esayqryf"); + assert_eq!( + d8, + DestinationInfo { + receiver: "cosmos1g4h64yjt0fvzv5v2j8tyfnpe5kmnetejl67nlm".to_string(), + destination_channel: "channel-124".to_string(), + destination_denom: "orai17l2zk3arrx0a0fyuneyx8raln68622a2lrsz8ph75u7gw9tgz3esayqryf" + .to_string(), + } + ) }