diff --git a/Cargo.lock b/Cargo.lock index 64e251f..38ba51d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -524,7 +524,7 @@ checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" [[package]] name = "multicall" -version = "1.2.1" +version = "1.2.2" dependencies = [ "bech32", "cosmwasm-schema", diff --git a/contracts/multicall/Cargo.toml b/contracts/multicall/Cargo.toml index c45a2cb..1251500 100644 --- a/contracts/multicall/Cargo.toml +++ b/contracts/multicall/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "multicall" -version = "1.2.1" +version = "1.2.2" authors = ["0xsquid"] edition = "2021" @@ -31,5 +31,6 @@ enum-repr = { workspace = true } prost = { workspace = true } cw20 = "1.1.0" + ibc-tracking = { version = "1.2.0", path = "../../packages/ibc-tracking" } shared = { version = "1.2.0", path = "../../packages/shared" } diff --git a/contracts/multicall/README.md b/contracts/multicall/README.md index 4dfe4d8..590d21a 100644 --- a/contracts/multicall/README.md +++ b/contracts/multicall/README.md @@ -17,7 +17,8 @@ Executes a set of cosmos messages specified in the calls array "msg": {}, "actions": [] } - ] + ], + "fallback_address": "", } } ``` @@ -106,13 +107,13 @@ Converts specified field into [`Binary`] type ### `field_to_proto_binary` Converts specified field into [`Binary`] type encoded using [`prost::Message::encode`] method. -Note: since the type of the message should be known to the contract at the moment only ibc transfer is supported. +Note: since the type of the message should be known to the contract at the moment only ibc transfer and osmosis swap exact amount in is supported. ```json { "field_to_proto_binary": { "replacer": "/path/to/field/for/replacement", - "proto_msg_type": "ibc_transfer" + "proto_msg_type": "ibc_transfer" | "osmosis_swap_exact_amt_in" } } ``` @@ -155,9 +156,9 @@ Replacer is basically a path from the root of the `msg` object to a field. For e `replacer` value is equals to a path to `amount` field inside bank message. This field will be replaced with fetched contract balance of `uosmo` coin. Numbers in the path are equal to an index in the array. ## Fallback address -Fallback address is an optional field that could be set for: +Fallback address is a field that must be set for: - ibc error recovery when `ibc_tracking` action is enabled -- local funds recovery in case of contract execution failure, e.g. multicall contract was trying to perform a swap and failed because of price change - so instead of forwarding dex error multicall contract will recover all owned funds to the specified fallback address. Note: if fallback address is not set or there is no funds to recover - the contract will forward an error. +- local funds recovery in case of contract execution failure or any funds left on the contract balance after successful execution, e.g. multicall contract was trying to perform a swap and failed because of price change - so instead of forwarding dex error multicall contract will recover all owned funds to the specified fallback address. Note: if fallback address is not set or there is no funds to recover - the contract will forward an error. ## Example calls @@ -311,4 +312,4 @@ Second - since `msg` field in a wasm call must be a base64 encoded Binary object } ``` -Note: since there is no updates for wasm message field it can be alredy specified as a binary value. Here it is shown for explanaition purposes. \ No newline at end of file +Note: since there is no updates for wasm message field it can be alredy specified as a binary value. Here it is shown for explanaition purposes. diff --git a/contracts/multicall/src/call.rs b/contracts/multicall/src/call.rs index a21f0c7..5f5f72c 100644 --- a/contracts/multicall/src/call.rs +++ b/contracts/multicall/src/call.rs @@ -7,6 +7,7 @@ use ibc_tracking::{ msg::{CwIbcMessage, MsgTransfer}, state::{store_ibc_transfer_reply_state, IbcTransferReplyState}, }; +use osmosis_std::types::osmosis::gamm::v1beta1::MsgSwapExactAmountIn; use shared::{util::json_pointer, SerializableJson}; use std::str::FromStr; @@ -23,7 +24,7 @@ impl Call { storage: &mut dyn Storage, querier: &QuerierWrapper, env: &Env, - fallback_address: &Option, + fallback_address: &str, ) -> Result, ContractError> { let mut cosmos_msg = self.msg.0.clone(); let mut reply_id = MsgReplyId::ProcessCall.repr(); @@ -95,9 +96,7 @@ impl Call { } => { reply_id = MsgReplyId::IbcTransferTracking.repr(); - let Some(local_fallback_address) = fallback_address.clone() else { - return Err(ContractError::FallbackAddressMustBeSetForIbcTracking {}); - }; + let local_fallback_address = fallback_address.to_owned(); let amount = if let Some(pointer) = amount_pointer { let amount_field = json_pointer(&mut cosmos_msg, pointer).ok_or( @@ -153,11 +152,15 @@ impl Call { .map_err(|_| ContractError::ProtoSerializationError {})? .into(); - let mut bytes = Vec::new(); - prost::Message::encode(&ibc, &mut bytes) + self.encode_proto_msg(&ibc)? + } + ProtoMessageType::OsmosisSwapExactAmtIn => { + let swap: MsgSwapExactAmountIn = binary_field + .clone() + .deserialize_into::() .map_err(|_| ContractError::ProtoSerializationError {})?; - Binary(bytes) + self.encode_proto_msg(&swap)? } }; @@ -188,4 +191,12 @@ impl Call { *field = serde_cw_value::Value::String(value.to_owned()); Ok(()) } + + fn encode_proto_msg(&self, msg: &T) -> Result { + let mut bytes = Vec::new(); + prost::Message::encode(msg, &mut bytes) + .map_err(|_| ContractError::ProtoSerializationError {})?; + + Ok(Binary(bytes)) + } } diff --git a/contracts/multicall/src/commands.rs b/contracts/multicall/src/commands.rs index 5c8d042..6ac9372 100644 --- a/contracts/multicall/src/commands.rs +++ b/contracts/multicall/src/commands.rs @@ -29,7 +29,7 @@ pub fn handle_multicall( deps: DepsMut, env: &Env, calls: &[Call], - fallback_address: &Option, + fallback_address: &str, ) -> Result, ContractError> { if multicall_state_exists(deps.storage)? { return Err(ContractError::ContractLocked { @@ -37,7 +37,7 @@ pub fn handle_multicall( }); } - let state = MulticallState::new(calls.to_owned().as_mut(), fallback_address.clone())?; + let state = MulticallState::new(calls.to_owned().as_mut(), fallback_address.to_owned())?; store_multicall_state(deps.storage, &state)?; Ok(Response::new().add_submessage(SubMsg::reply_on_error( @@ -76,7 +76,19 @@ pub fn handle_call( let Some(call) = state.next_call() else { // if there is no calls left then finish the execution here remove_multicall_state(deps.storage)?; - return Ok(Response::new().add_attribute("multicall_execution", "success")); + + let mut response: Response = Response::new().add_attribute("multicall_execution", "success"); + + // query contracts balance for any leftover funds after calls execution and if anything left then transfer it to the fallback address + let leftover_funds = deps.querier.query_all_balances(env.contract.address.as_str())?; + if !leftover_funds.is_empty() { + response = response.add_message(BankMsg::Send { + to_address: fallback_address, + amount: leftover_funds, + }).add_attribute("leftover_funds", "recovered"); + } + + return Ok(response); }; let submsg = call.try_into_msg(deps.storage, &deps.querier, env, &fallback_address)?; @@ -145,9 +157,7 @@ pub fn handle_execution_fallback_reply( let state = load_multicall_state(deps.storage)?; remove_multicall_state(deps.storage)?; - let Some(fallback_address) = state.fallback_address else { - return Err(ContractError::RecoveryError { msg: "Fallback address is not set for local funds recovery".to_owned(), origin_err }); - }; + let fallback_address = state.fallback_address; let recover_funds = deps .querier diff --git a/contracts/multicall/src/error.rs b/contracts/multicall/src/error.rs index c49b260..a1f1a0e 100644 --- a/contracts/multicall/src/error.rs +++ b/contracts/multicall/src/error.rs @@ -36,9 +36,6 @@ pub enum ContractError { #[error("Calls list is empty")] EmptyCallsList {}, - #[error("Fallback address must be set for Ibc tracking")] - FallbackAddressMustBeSetForIbcTracking {}, - #[error("Invalid call action argument: {msg}")] InvalidCallActionArgument { msg: String }, diff --git a/contracts/multicall/src/msg.rs b/contracts/multicall/src/msg.rs index a6c5140..bc3b1b0 100644 --- a/contracts/multicall/src/msg.rs +++ b/contracts/multicall/src/msg.rs @@ -19,7 +19,7 @@ pub enum ExecuteMsg { /// onchain calls to perform calls: Vec, /// fallback address for failed/timeout rejected ibc transfers - fallback_address: Option, + fallback_address: String, }, /// ## Description /// Internal action, can be called only by the contract itself @@ -145,4 +145,6 @@ pub struct ReplaceInfo { pub enum ProtoMessageType { /// ibc message type IbcTransfer, + /// osmosis gamm swap exact amount in type + OsmosisSwapExactAmtIn, } diff --git a/contracts/multicall/src/state.rs b/contracts/multicall/src/state.rs index 606a15b..1fb5a88 100644 --- a/contracts/multicall/src/state.rs +++ b/contracts/multicall/src/state.rs @@ -22,16 +22,13 @@ pub struct MulticallState { /// onchain calls to perform pub calls: Vec, /// fallback address for failed/timeout rejected ibc transfers - pub fallback_address: Option, + pub fallback_address: String, } impl MulticallState { /// ## Description /// Creates new instance of [`MulticallState`] struct - pub fn new( - calls: &mut [Call], - fallback_address: Option, - ) -> Result { + pub fn new(calls: &mut [Call], fallback_address: String) -> Result { calls.iter_mut().for_each(|call| call.actions.sort()); let state = Self { @@ -60,17 +57,6 @@ impl MulticallState { return Err(ContractError::EmptyCallsList {}); } - // if ibc tracking is required then fallback address must be set - let has_ibc_tracking = self - .calls - .iter() - .flat_map(|call| call.actions.clone()) - .any(|action| matches!(action, CallAction::IbcTracking { .. })); - - if has_ibc_tracking && self.fallback_address.is_none() { - return Err(ContractError::FallbackAddressMustBeSetForIbcTracking {}); - } - for call in self.calls.iter() { let ibc_tracking_count = call .actions diff --git a/contracts/multicall/src/tests.rs b/contracts/multicall/src/tests.rs index f8f4dc8..f8331c6 100644 --- a/contracts/multicall/src/tests.rs +++ b/contracts/multicall/src/tests.rs @@ -13,29 +13,12 @@ use self::mock_querier::mock_dependencies; #[test] fn test_multicall_state() { - let state = MulticallState::new(&mut [], None); + let state = MulticallState::new(&mut [], "addr0000".to_owned()); match state { Err(ContractError::EmptyCallsList {}) => (), _ => panic!("expecting ContractError::EmptyCallsList"), }; - let state = MulticallState::new( - &mut [Call { - msg: Value::String("msg".to_owned()).into(), - actions: vec![CallAction::IbcTracking { - channel: "channel-0".to_owned(), - denom: "usquid".to_owned(), - amount: Some(Uint128::from(1u128)), - amount_pointer: None, - }], - }], - None, - ); - match state { - Err(ContractError::FallbackAddressMustBeSetForIbcTracking {}) => (), - _ => panic!("expecting ContractError::FallbackAddressMustBeSetForIbcTracking"), - }; - let state = MulticallState::new( &mut [Call { msg: Value::String("msg".to_owned()).into(), @@ -46,7 +29,7 @@ fn test_multicall_state() { amount_pointer: None, }], }], - Some("addr0000".to_owned()), + "addr0000".to_owned(), ); match state { Err(ContractError::EitherAmountOfPointerMustBeSet {}) => (), @@ -71,7 +54,7 @@ fn test_multicall_state() { }, ], }], - Some("addr0000".to_owned()), + "addr0000".to_owned(), ); match state { Err(ContractError::InvalidCallActionArgument { msg }) => { @@ -80,7 +63,7 @@ fn test_multicall_state() { "Only one or none IbcTracking call action entries allowed per call".to_owned() ) } - _ => panic!("expecting ContractError::FallbackAddressMustBeSetForIbcTracking"), + _ => panic!("expecting ContractError::InvalidCallActionArgument"), }; let state = MulticallState::new( @@ -91,7 +74,7 @@ fn test_multicall_state() { replacer: "".to_owned(), }], }], - Some("addr0000".to_owned()), + "addr0000".to_owned(), ); match state { Err(ContractError::InvalidReplacer {}) => (), @@ -106,7 +89,7 @@ fn test_multicall_state() { replacer: "funds/0/amount".to_owned(), }], }], - Some("addr0000".to_owned()), + "addr0000".to_owned(), ); match state { Err(ContractError::InvalidReplacer {}) => (), @@ -123,7 +106,7 @@ fn test_multicall_state() { amount_pointer: Some("invalid/replacer".to_owned()), }], }], - Some("addr0000".to_owned()), + "addr0000".to_owned(), ); match state { Err(ContractError::InvalidReplacer {}) => (), @@ -147,7 +130,7 @@ fn test_multicall_state() { ], }], }], - Some("addr0000".to_owned()), + "addr0000".to_owned(), ); match state { Err(ContractError::InvalidReplacer {}) => (), @@ -171,7 +154,7 @@ fn test_multicall_state() { ], }], }], - Some("addr0000".to_owned()), + "addr0000".to_owned(), ); match state { Err(ContractError::InvalidReplacer {}) => (), @@ -239,7 +222,7 @@ fn test_multicall_state() { actions: vec![], }, ], - Some("addr0000".to_owned()), + "addr0000".to_owned(), ); assert_eq!(valid_state.is_ok(), true); @@ -355,7 +338,7 @@ fn test_call_into_msg() { }; let bank_send_msg = bank_send_call - .try_into_msg(deps.storage, &deps.querier, &env, &None) + .try_into_msg(deps.storage, &deps.querier, &env, "addr0000") .unwrap(); assert_eq!(bank_send_msg.id, MsgReplyId::ProcessCall.repr()); @@ -415,7 +398,7 @@ fn test_call_into_msg() { }; let wasm_msg = wasm_call - .try_into_msg(deps.storage, &deps.querier, &env, &None) + .try_into_msg(deps.storage, &deps.querier, &env, "addr0000") .unwrap(); assert_eq!(wasm_msg.id, MsgReplyId::ProcessCall.repr()); @@ -482,7 +465,7 @@ fn test_call_into_msg() { }; let custom_query_msg = custom_query_call - .try_into_msg(deps.storage, &deps.querier, &env, &None) + .try_into_msg(deps.storage, &deps.querier, &env, "addr0000") .unwrap(); assert_eq!(custom_query_msg.id, MsgReplyId::ProcessCall.repr()); @@ -535,12 +518,7 @@ fn test_call_into_msg() { }; let ibc_msg = ibc_call - .try_into_msg( - deps.storage, - &deps.querier, - &env, - &Some("addr0004".to_owned()), - ) + .try_into_msg(deps.storage, &deps.querier, &env, "addr0004") .unwrap(); assert_eq!(ibc_msg.id, MsgReplyId::IbcTransferTracking.repr()); @@ -600,12 +578,7 @@ fn test_call_into_msg() { }; let ibc_msg = ibc_call - .try_into_msg( - deps.storage, - &deps.querier, - &env, - &Some("addr0004".to_owned()), - ) + .try_into_msg(deps.storage, &deps.querier, &env, "addr0004") .unwrap(); assert_eq!(ibc_msg.id, MsgReplyId::IbcTransferTracking.repr()); @@ -653,7 +626,7 @@ fn test_call_into_msg() { }; let invalid_replacer_msg_err = invalid_replacer_call - .try_into_msg(deps.storage, &deps.querier, &env, &None) + .try_into_msg(deps.storage, &deps.querier, &env, "addr0004") .unwrap_err(); match invalid_replacer_msg_err { @@ -689,7 +662,7 @@ fn test_call_into_msg() { }; let zero_balance_msg_err = zero_balance_call - .try_into_msg(deps.storage, &deps.querier, &env, &None) + .try_into_msg(deps.storage, &deps.querier, &env, "addr0004") .unwrap_err(); match zero_balance_msg_err { @@ -699,52 +672,6 @@ fn test_call_into_msg() { _ => panic!("unexpected error"), } - - let no_fallback_ibc_call = Call { - msg: serde_json_wasm::from_str( - r#" - { - "stargate": { - "type_url": "/ibc.applications.transfer.v1.MsgTransfer", - "value": { - "source_port": "transfer", - "source_channel": "channel-3", - "token": { - "denom": "usquid", - "amount": "111111" - }, - "sender": "osmo1vmpds4p8grwz54dygeljhq9vffssw5caydyj3heqd02f2seckk3smlug7w", - "receiver": "axelar15t9awn6jnxheckur5vc6dqv6pqlpph0hw24vwf", - "timeout_timestamp": 1693856646000000000, - "memo": "{\"ibc_callback\":\"addr0000\"}" - } - } - } - "#, - ) - .unwrap(), - actions: vec![ - CallAction::IbcTracking { - channel: "channel-3".to_owned(), - denom: "usquid".to_owned(), - amount: Some(Uint128::from(111111u128)), - amount_pointer: None, - }, - CallAction::FieldToProtoBinary { - replacer: "/stargate/value".to_owned(), - proto_msg_type: ProtoMessageType::IbcTransfer, - }, - ], - }; - - let no_fallback_ibc_msg_err = no_fallback_ibc_call - .try_into_msg(deps.storage, &deps.querier, &env, &None) - .unwrap_err(); - - match no_fallback_ibc_msg_err { - ContractError::FallbackAddressMustBeSetForIbcTracking {} => (), - _ => panic!("unexpected error"), - }; } #[cfg(test)]