diff --git a/contracts/entry-point/src/contract.rs b/contracts/entry-point/src/contract.rs index 45f0bca4..bfbb1f9e 100644 --- a/contracts/entry-point/src/contract.rs +++ b/contracts/entry-point/src/contract.rs @@ -1,8 +1,9 @@ use crate::{ error::{ContractError, ContractResult}, execute::{ - execute_post_swap_action, execute_swap_and_action, execute_swap_and_action_with_recover, - execute_user_swap, receive_cw20, + execute_action, execute_action_with_recover, execute_post_swap_action, + execute_swap_and_action, execute_swap_and_action_with_recover, execute_user_swap, + receive_cw20, }, query::{query_ibc_transfer_adapter_contract, query_swap_venue_adapter_contract}, reply::{reply_swap_and_action_with_recover, RECOVER_REPLY_ID}, @@ -174,6 +175,40 @@ pub fn execute( post_swap_action, exact_out, ), + ExecuteMsg::Action { + sent_asset, + timeout_timestamp, + action, + exact_out, + min_asset, + } => execute_action( + deps, + env, + info, + sent_asset, + timeout_timestamp, + action, + exact_out, + min_asset, + ), + ExecuteMsg::ActionWithRecover { + sent_asset, + timeout_timestamp, + action, + exact_out, + min_asset, + recovery_addr, + } => execute_action_with_recover( + deps, + env, + info, + sent_asset, + timeout_timestamp, + action, + exact_out, + min_asset, + recovery_addr, + ), } } diff --git a/contracts/entry-point/src/error.rs b/contracts/entry-point/src/error.rs index 2755b462..8f3b9f6a 100644 --- a/contracts/entry-point/src/error.rs +++ b/contracts/entry-point/src/error.rs @@ -71,4 +71,17 @@ pub enum ContractError { #[error("Reply id: {0} not valid")] ReplyIdError(u64), + + ////////////////// + /// ACTION /// + ////////////////// + + #[error("No Minimum Asset Provided with Exact Out Action")] + NoMinAssetProvided, + + #[error("Sent Asset and Min Asset Denoms Do Not Match with Exact Out Action")] + ActionDenomMismatch, + + #[error("Remaining Asset Less Than Min Asset with Exact Out Action")] + RemainingAssetLessThanMinAsset, } diff --git a/contracts/entry-point/src/execute.rs b/contracts/entry-point/src/execute.rs index c72be832..9897563d 100644 --- a/contracts/entry-point/src/execute.rs +++ b/contracts/entry-point/src/execute.rs @@ -17,7 +17,7 @@ use cw_utils::one_coin; use skip::{ asset::{get_current_asset_available, Asset}, entry_point::{Action, Affiliate, Cw20HookMsg, ExecuteMsg}, - ibc::{ExecuteMsg as IbcTransferExecuteMsg, IbcTransfer}, + ibc::{ExecuteMsg as IbcTransferExecuteMsg, IbcInfo, IbcTransfer}, swap::{ validate_swap_operations, ExecuteMsg as SwapExecuteMsg, QueryMsg as SwapQueryMsg, Swap, SwapExactAssetOut, @@ -78,6 +78,38 @@ pub fn receive_cw20( post_swap_action, affiliates, ), + Cw20HookMsg::Action { + timeout_timestamp, + action, + exact_out, + min_asset, + } => execute_action( + deps, + env, + info, + Some(sent_asset), + timeout_timestamp, + action, + exact_out, + min_asset, + ), + Cw20HookMsg::ActionWithRecover { + timeout_timestamp, + action, + exact_out, + min_asset, + recovery_addr, + } => execute_action_with_recover( + deps, + env, + info, + Some(sent_asset), + timeout_timestamp, + action, + exact_out, + min_asset, + recovery_addr, + ), } } @@ -128,54 +160,8 @@ pub fn execute_swap_and_action( // by either creating a fee swap message or deducting the ibc fees from // the remaining asset received amount. if let Action::IbcTransfer { ibc_info, fee_swap } = &post_swap_action { - let ibc_fee_coin = ibc_info - .fee - .as_ref() - .map(|fee| fee.one_coin()) - .transpose()?; - - if let Some(fee_swap) = fee_swap { - let ibc_fee_coin = ibc_fee_coin - .clone() - .ok_or(ContractError::FeeSwapWithoutIbcFees)?; - - // NOTE: this call mutates remaining_asset by deducting ibc_fee_coin's amount from it - let fee_swap_msg = verify_and_create_fee_swap_msg( - &deps, - fee_swap, - &mut remaining_asset, - &ibc_fee_coin, - )?; - - // Add the fee swap message to the response - response = response - .add_message(fee_swap_msg) - .add_attribute("action", "dispatch_fee_swap"); - } else if let Some(ibc_fee_coin) = &ibc_fee_coin { - if remaining_asset.denom() != ibc_fee_coin.denom { - return Err(ContractError::IBCFeeDenomDiffersFromAssetReceived); - } - - // Deduct the ibc_fee_coin amount from the remaining asset amount - remaining_asset.sub(ibc_fee_coin.amount)?; - } - - // Dispatch the ibc fee bank send to the ibc transfer adapter contract if needed - if let Some(ibc_fee_coin) = ibc_fee_coin { - // Get the ibc transfer adapter contract address - let ibc_transfer_contract_address = IBC_TRANSFER_CONTRACT_ADDRESS.load(deps.storage)?; - - // Create the ibc fee bank send message - let ibc_fee_msg = BankMsg::Send { - to_address: ibc_transfer_contract_address.to_string(), - amount: vec![ibc_fee_coin], - }; - - // Add the ibc fee message to the response - response = response - .add_message(ibc_fee_msg) - .add_attribute("action", "dispatch_ibc_fee_bank_send"); - } + response = + handle_ibc_transfer_fees(&deps, ibc_info, fee_swap, &mut remaining_asset, response)?; } // Set a boolean to determine if the user swap is exact out or not @@ -534,24 +520,167 @@ pub fn execute_post_swap_action( ) .add_attribute("post_swap_action_denom_out", transfer_out_asset.denom()); - match post_swap_action { + // Dispatch the action message + response = validate_and_dispatch_action( + deps, + post_swap_action, + transfer_out_asset, + timeout_timestamp, + response, + )?; + + Ok(response) +} + +// Dispatches an action +#[allow(clippy::too_many_arguments)] +pub fn execute_action( + deps: DepsMut, + env: Env, + info: MessageInfo, + sent_asset: Option, + timeout_timestamp: u64, + action: Action, + exact_out: bool, + min_asset: Option, +) -> ContractResult { + // Create a response object to return + let mut response: Response = Response::new().add_attribute("action", "execute_action"); + + // Validate and unwrap the sent asset + let sent_asset = match sent_asset { + Some(sent_asset) => { + sent_asset.validate(&deps, &env, &info)?; + sent_asset + } + None => one_coin(&info)?.into(), + }; + + // Error if the current block time is greater than the timeout timestamp + if env.block.time.nanos() > timeout_timestamp { + return Err(ContractError::Timeout); + } + + // Already validated at entrypoints (both direct and cw20_receive) + let mut remaining_asset = sent_asset; + + // If the post swap action is an IBC transfer, then handle the ibc fees + // by either creating a fee swap message or deducting the ibc fees from + // the remaining asset received amount. + if let Action::IbcTransfer { ibc_info, fee_swap } = &action { + response = + handle_ibc_transfer_fees(&deps, ibc_info, fee_swap, &mut remaining_asset, response)?; + } + + // Validate and determine the asset to be used for the action + let action_asset = if exact_out { + let min_asset = min_asset.ok_or(ContractError::NoMinAssetProvided)?; + + // Ensure remaining_asset and min_asset have the same denom + if remaining_asset.denom() != min_asset.denom() { + return Err(ContractError::ActionDenomMismatch); + } + + // Ensure remaining_asset is greater than or equal to min_asset + if remaining_asset.amount() < min_asset.amount() { + return Err(ContractError::RemainingAssetLessThanMinAsset); + } + + min_asset + } else { + remaining_asset.clone() + }; + + // Dispatch the action message + response = + validate_and_dispatch_action(deps, action, action_asset, timeout_timestamp, response)?; + + // Return the response + Ok(response) +} + +// Entrypoint that catches all errors in Action and recovers +// the original funds sent to the contract to a recover address. +#[allow(clippy::too_many_arguments)] +pub fn execute_action_with_recover( + deps: DepsMut, + env: Env, + info: MessageInfo, + sent_asset: Option, + timeout_timestamp: u64, + action: Action, + exact_out: bool, + min_asset: Option, + recovery_addr: Addr, +) -> ContractResult { + let mut assets: Vec = info.funds.iter().cloned().map(Asset::Native).collect(); + + if let Some(asset) = &sent_asset { + if let Asset::Cw20(_) = asset { + assets.push(asset.clone()); + } + } + + // Store all parameters into a temporary storage. + RECOVER_TEMP_STORAGE.save( + deps.storage, + &RecoverTempStorage { + assets, + recovery_addr, + }, + )?; + + // Then call ExecuteMsg::Action using a SubMsg. + let sub_msg = SubMsg::reply_always( + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: env.contract.address.to_string(), + msg: to_json_binary(&ExecuteMsg::Action { + sent_asset, + timeout_timestamp, + action, + exact_out, + min_asset, + })?, + funds: info.funds, + }), + RECOVER_REPLY_ID, + ); + + Ok(Response::new().add_submessage(sub_msg)) +} + +//////////////////////// +/// HELPER FUNCTIONS /// +//////////////////////// + +// ACTION HELPER FUNCTIONS + +// Validates and adds an action message to the response +fn validate_and_dispatch_action( + deps: DepsMut, + action: Action, + action_asset: Asset, + timeout_timestamp: u64, + mut response: Response, +) -> Result { + match action { Action::Transfer { to_address } => { // Error if the destination address is not a valid address on the current chain deps.api.addr_validate(&to_address)?; // Create the transfer message - let transfer_msg = transfer_out_asset.transfer(&to_address); + let transfer_msg = action_asset.transfer(&to_address); // Add the transfer message to the response response = response .add_message(transfer_msg) - .add_attribute("action", "dispatch_post_swap_transfer"); + .add_attribute("action", "dispatch_action_transfer"); } Action::IbcTransfer { ibc_info, .. } => { // Validates recover address, errors if invalid deps.api.addr_validate(&ibc_info.recover_address)?; - let transfer_out_coin = match transfer_out_asset { + let transfer_out_coin = match action_asset { Asset::Native(coin) => coin, _ => return Err(ContractError::NonNativeIbcTransfer), }; @@ -577,7 +706,7 @@ pub fn execute_post_swap_action( // Add the IBC transfer message to the response response = response .add_message(ibc_transfer_msg) - .add_attribute("action", "dispatch_post_swap_ibc_transfer"); + .add_attribute("action", "dispatch_action_ibc_transfer"); } Action::ContractCall { contract_address, @@ -592,21 +721,75 @@ pub fn execute_post_swap_action( } // Create the contract call message - let contract_call_msg = transfer_out_asset.into_wasm_msg(contract_address, msg)?; + let contract_call_msg = action_asset.into_wasm_msg(contract_address, msg)?; // Add the contract call message to the response response = response .add_message(contract_call_msg) - .add_attribute("action", "dispatch_post_swap_contract_call"); + .add_attribute("action", "dispatch_action_contract_call"); } }; Ok(response) } -//////////////////////// -/// HELPER FUNCTIONS /// -//////////////////////// +// IBC FEE HELPER FUNCTIONS + +// Creates the fee swap and ibc transfer messages and adds them to the response +fn handle_ibc_transfer_fees( + deps: &DepsMut, + ibc_info: &IbcInfo, + fee_swap: &Option, + remaining_asset: &mut Asset, + mut response: Response, +) -> Result { + let ibc_fee_coin = ibc_info + .fee + .as_ref() + .map(|fee| fee.one_coin()) + .transpose()?; + + if let Some(fee_swap) = fee_swap { + let ibc_fee_coin = ibc_fee_coin + .clone() + .ok_or(ContractError::FeeSwapWithoutIbcFees)?; + + // NOTE: this call mutates remaining_asset by deducting ibc_fee_coin's amount from it + let fee_swap_msg = + verify_and_create_fee_swap_msg(deps, fee_swap, remaining_asset, &ibc_fee_coin)?; + + // Add the fee swap message to the response + response = response + .add_message(fee_swap_msg) + .add_attribute("action", "dispatch_fee_swap"); + } else if let Some(ibc_fee_coin) = &ibc_fee_coin { + if remaining_asset.denom() != ibc_fee_coin.denom { + return Err(ContractError::IBCFeeDenomDiffersFromAssetReceived); + } + + // Deduct the ibc_fee_coin amount from the remaining asset amount + remaining_asset.sub(ibc_fee_coin.amount)?; + } + + // Dispatch the ibc fee bank send to the ibc transfer adapter contract if needed + if let Some(ibc_fee_coin) = ibc_fee_coin { + // Get the ibc transfer adapter contract address + let ibc_transfer_contract_address = IBC_TRANSFER_CONTRACT_ADDRESS.load(deps.storage)?; + + // Create the ibc fee bank send message + let ibc_fee_msg = BankMsg::Send { + to_address: ibc_transfer_contract_address.to_string(), + amount: vec![ibc_fee_coin], + }; + + // Add the ibc fee message to the response + response = response + .add_message(ibc_fee_msg) + .add_attribute("action", "dispatch_ibc_fee_bank_send"); + } + + Ok(response) +} // SWAP MESSAGE HELPER FUNCTIONS diff --git a/contracts/entry-point/tests/test_execute_action.rs b/contracts/entry-point/tests/test_execute_action.rs new file mode 100644 index 00000000..7e761ecd --- /dev/null +++ b/contracts/entry-point/tests/test_execute_action.rs @@ -0,0 +1,498 @@ +use cosmwasm_std::{ + testing::{mock_dependencies_with_balances, mock_env, mock_info}, + to_json_binary, Addr, BankMsg, Coin, ContractResult, QuerierResult, + ReplyOn::Never, + SubMsg, SystemResult, Timestamp, Uint128, WasmMsg, WasmQuery, +}; +use cw20::{BalanceResponse, Cw20Coin, Cw20ExecuteMsg}; +use skip::{ + asset::Asset, + entry_point::{Action, ExecuteMsg}, + ibc::{ExecuteMsg as IbcTransferExecuteMsg, IbcFee, IbcInfo}, +}; +use skip_go_entry_point::{ + error::ContractError, + state::{BLOCKED_CONTRACT_ADDRESSES, IBC_TRANSFER_CONTRACT_ADDRESS}, +}; +use test_case::test_case; + +/* +Test Cases: + +Expect Response + // General + - Native Asset Transfer + - Cw20 Asset Transfer + - Ibc Transfer + - Native Asset Contract Call + - Cw20 Asset Contract Call + + // Exact Out + - Ibc Transfer With Exact Out Set To True + - Ibc Transfer w/ IBC Fees of same denom as min coin With Exact Out Set To True + +Expect Error + - Remaining Asset Less Than Min Asset - Native + - Remaining Asset Less Than Min Asset - CW20 + - Contract Call Address Blocked + - Ibc Transfer w/ IBC Fees of different denom than min coin no fee swap + */ + +// Define test parameters +struct Params { + info_funds: Vec, + sent_asset: Option, + action: Action, + exact_out: bool, + min_asset: Option, + expected_messages: Vec, + expected_error: Option, +} + +// Test execute_action +#[test_case( + Params { + info_funds: vec![Coin::new(1_000_000, "os")], + sent_asset: Some(Asset::Native(Coin::new(1_000_000, "os"))), + min_asset: None, + action: Action::Transfer { + to_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5".to_string(), + }, + exact_out: false, + expected_messages: vec![SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5".to_string(), + amount: vec![Coin::new(1_000_000, "os")], + } + .into(), + gas_limit: None, + reply_on: Never, + }], + expected_error: None, + }; + "Native Asset Transfer")] +#[test_case( + Params { + info_funds: vec![], + sent_asset: Some(Asset::Cw20(Cw20Coin{ + address: "neutron123".to_string(), + amount: Uint128::new(1_000_000), + })), + min_asset: None, + action: Action::Transfer { + to_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5".to_string(), + }, + exact_out: false, + expected_messages: vec![SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "neutron123".to_string(), + msg: to_json_binary(&Cw20ExecuteMsg::Transfer { + recipient: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5".to_string(), + amount: Uint128::new(1_000_000), + }).unwrap(), + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }], + expected_error: None, + }; + "Cw20 Asset Transfer")] +#[test_case( + Params { + info_funds: vec![Coin::new(1_000_000, "os")], + sent_asset: Some(Asset::Native(Coin::new(1_000_000, "os"))), + min_asset: None, + action: Action::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: None, + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + fee_swap: None, + }, + exact_out: false, + expected_messages: vec![SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "ibc_transfer_adapter".to_string(), + msg: to_json_binary(&IbcTransferExecuteMsg::IbcTransfer { + info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: None, + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + coin: Coin::new(1_000_000, "os"), + timeout_timestamp: 101, + }) + .unwrap(), + funds: vec![Coin::new(1_000_000, "os")], + } + .into(), + gas_limit: None, + reply_on: Never, + }], + expected_error: None, + }; + "Ibc Transfer")] +#[test_case( + Params { + info_funds: vec![Coin::new(1_000_000, "os")], + sent_asset: Some(Asset::Native(Coin::new(1_000_000, "os"))), + min_asset: None, + action: Action::ContractCall { + contract_address: "contract_call".to_string(), + msg: to_json_binary(&"contract_call_msg").unwrap(), + }, + exact_out: false, + expected_messages: vec![SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "contract_call".to_string(), + msg: to_json_binary(&"contract_call_msg").unwrap(), + funds: vec![Coin::new(1_000_000, "os")], + } + .into(), + gas_limit: None, + reply_on: Never, + }], + expected_error: None, + }; + "Native Asset Contract Call")] +#[test_case( + Params { + info_funds: vec![], + sent_asset: Some(Asset::Cw20(Cw20Coin{ + address: "neutron123".to_string(), + amount: Uint128::new(1_000_000), + })), + min_asset: None, + action: Action::ContractCall { + contract_address: "contract_call".to_string(), + msg: to_json_binary(&"contract_call_msg").unwrap(), + }, + exact_out: false, + expected_messages: vec![SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "neutron123".to_string(), + msg: to_json_binary(&Cw20ExecuteMsg::Send { + contract: "contract_call".to_string(), + amount: Uint128::new(1_000_000), + msg: to_json_binary(&"contract_call_msg").unwrap(), + }).unwrap(), + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }], + expected_error: None, + }; + "Cw20 Asset Contract Call")] +#[test_case( + Params { + info_funds: vec![Coin::new(1_000_000, "os")], + sent_asset: Some(Asset::Native(Coin::new(1_000_000, "os"))), + min_asset: Some(Asset::Native(Coin::new(900_000, "os"))), + action: Action::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: None, + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + fee_swap: None, + }, + exact_out: true, + expected_messages: vec![SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "ibc_transfer_adapter".to_string(), + msg: to_json_binary(&IbcTransferExecuteMsg::IbcTransfer { + info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: None, + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + coin: Coin::new(900_000, "os"), + timeout_timestamp: 101, + }) + .unwrap(), + funds: vec![Coin::new(900_000, "os")], + } + .into(), + gas_limit: None, + reply_on: Never, + }], + expected_error: None, + }; + "Ibc Transfer With Exact Out Set To True")] +#[test_case( + Params { + info_funds: vec![Coin::new(1_200_000, "os")], + sent_asset: Some(Asset::Native(Coin::new(1_200_000, "os"))), + min_asset: Some(Asset::Native(Coin::new(900_000, "os"))), + action: Action::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: Some(IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "os")], + timeout_fee: vec![Coin::new(100_000, "os")], + }), + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + fee_swap: None, + }, + exact_out: true, + expected_messages: vec![ + SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "ibc_transfer_adapter".to_string(), + amount: vec![Coin::new(200_000, "os")], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "ibc_transfer_adapter".to_string(), + msg: to_json_binary(&IbcTransferExecuteMsg::IbcTransfer { + info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: Some(IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "os")], + timeout_fee: vec![Coin::new(100_000, "os")], + }), + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + coin: Coin::new(900_000, "os"), + timeout_timestamp: 101, + }) + .unwrap(), + funds: vec![Coin::new(900_000, "os")], + } + .into(), + gas_limit: None, + reply_on: Never, + }], + expected_error: None, + }; + "Ibc Transfer w/ IBC Fees of same denom as min coin With Exact Out Set To True")] +#[test_case( + Params { + info_funds: vec![Coin::new(1_000_000, "os")], + sent_asset: Some(Asset::Native(Coin::new(1_000_000, "os"))), + min_asset: Some(Asset::Native(Coin::new(900_000, "os"))), + action: Action::IbcTransfer { + ibc_info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: Some(IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "un")], + timeout_fee: vec![Coin::new(100_000, "un")], + }), + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + fee_swap: None, + }, + exact_out: true, + expected_messages: vec![SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "ibc_transfer_adapter".to_string(), + msg: to_json_binary(&IbcTransferExecuteMsg::IbcTransfer { + info: IbcInfo { + source_channel: "channel-0".to_string(), + receiver: "receiver".to_string(), + memo: "".to_string(), + fee: Some(IbcFee { + recv_fee: vec![], + ack_fee: vec![Coin::new(100_000, "un")], + timeout_fee: vec![Coin::new(100_000, "un")], + }), + recover_address: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5" + .to_string(), + }, + coin: Coin::new(900_000, "os"), + timeout_timestamp: 101, + }) + .unwrap(), + funds: vec![Coin::new(900_000, "os")], + } + .into(), + gas_limit: None, + reply_on: Never, + }], + expected_error: Some(ContractError::IBCFeeDenomDiffersFromAssetReceived), + }; + "Ibc Transfer w/ IBC Fees of different denom than min coin no fee swap - Expect Error")] +#[test_case( + Params { + info_funds: vec![Coin::new(1_000_000, "os")], + sent_asset: Some(Asset::Native(Coin::new(1_000_000, "os"))), + min_asset: Some(Asset::Native(Coin::new(1_100_000, "os"))), + action: Action::ContractCall { + contract_address: "entry_point".to_string(), + msg: to_json_binary(&"contract_call_msg").unwrap(), + }, + exact_out: true, + expected_messages: vec![], + expected_error: Some(ContractError::RemainingAssetLessThanMinAsset), + }; + "Remaining Asset Less Than Min Asset Native - Expect Error")] +#[test_case( + Params { + info_funds: vec![], + sent_asset: Some(Asset::Cw20(Cw20Coin{ + address: "neutron123".to_string(), + amount: Uint128::new(1_000_000), + })), + min_asset: Some(Asset::Cw20(Cw20Coin{ + address: "neutron123".to_string(), + amount: Uint128::new(2_100_000), + })), + action: Action::ContractCall { + contract_address: "entry_point".to_string(), + msg: to_json_binary(&"contract_call_msg").unwrap(), + }, + exact_out: true, + expected_messages: vec![], + expected_error: Some(ContractError::RemainingAssetLessThanMinAsset), + }; + "Remaining Asset Less Than Min Asset CW20 - Expect Error")] +#[test_case( + Params { + info_funds: vec![Coin::new(1_000_000, "os")], + sent_asset: Some(Asset::Native(Coin::new(1_000_000, "os"))), + min_asset: None, + action: Action::ContractCall { + contract_address: "entry_point".to_string(), + msg: to_json_binary(&"contract_call_msg").unwrap(), + }, + exact_out: false, + expected_messages: vec![], + expected_error: Some(ContractError::ContractCallAddressBlocked), + }; + "Contract Call Address Blocked - Expect Error")] +fn test_execute_post_swap_action(params: Params) { + // Create mock dependencies + let mut deps = mock_dependencies_with_balances(&[( + "entry_point", + &[Coin::new(1_000_000, "os"), Coin::new(1_000_000, "un")], + )]); + + // Create mock wasm handler to handle the swap adapter contract query + let wasm_handler = |query: &WasmQuery| -> QuerierResult { + match query { + WasmQuery::Smart { .. } => SystemResult::Ok(ContractResult::Ok( + to_json_binary(&BalanceResponse { + balance: Uint128::from(1_000_000u128), + }) + .unwrap(), + )), + _ => panic!("Unsupported query: {:?}", query), + } + }; + + // Update querier with mock wasm handler + deps.querier.update_wasm(wasm_handler); + + // Create mock env with parameters that make testing easier + let mut env = mock_env(); + env.contract.address = Addr::unchecked("entry_point"); + env.block.time = Timestamp::from_nanos(100); + + // Convert info funds vector into a slice of Coin objects + let info_funds: &[Coin] = ¶ms.info_funds; + + // Create mock info with entry point contract address + let info = mock_info("actioner", info_funds); + + // Store the ibc transfer adapter contract address + let ibc_transfer_adapter = Addr::unchecked("ibc_transfer_adapter"); + IBC_TRANSFER_CONTRACT_ADDRESS + .save(deps.as_mut().storage, &ibc_transfer_adapter) + .unwrap(); + + // Store the entry point contract address in the blocked contract addresses map + BLOCKED_CONTRACT_ADDRESSES + .save(deps.as_mut().storage, &Addr::unchecked("entry_point"), &()) + .unwrap(); + + // Call execute_post_swap_action with the given test parameters + let res = skip_go_entry_point::contract::execute( + deps.as_mut(), + env, + info, + ExecuteMsg::Action { + sent_asset: params.sent_asset, + timeout_timestamp: 101, + action: params.action, + exact_out: params.exact_out, + min_asset: params.min_asset, + }, + ); + + match res { + Ok(res) => { + // Assert the test did not expect an error + assert!( + params.expected_error.is_none(), + "expected test to error with {:?}, but it succeeded", + params.expected_error + ); + + // Assert the number of messages in the response is correct + assert_eq!( + res.messages.len(), + params.expected_messages.len(), + "expected {:?} messages, but got {:?}", + params.expected_messages.len(), + res.messages.len() + ); + + // Assert the messages in the response are correct + assert_eq!(res.messages, params.expected_messages,); + } + Err(err) => { + // Assert the test expected an error + assert!( + params.expected_error.is_some(), + "expected test to succeed, but it errored with {:?}", + err + ); + + // Assert the error is correct + assert_eq!(err, params.expected_error.unwrap()); + } + } +} diff --git a/packages/skip/src/entry_point.rs b/packages/skip/src/entry_point.rs index 21e42c28..630fb9e9 100644 --- a/packages/skip/src/entry_point.rs +++ b/packages/skip/src/entry_point.rs @@ -66,6 +66,21 @@ pub enum ExecuteMsg { post_swap_action: Action, exact_out: bool, }, + Action { + sent_asset: Option, + timeout_timestamp: u64, + action: Action, + exact_out: bool, + min_asset: Option, + }, + ActionWithRecover { + sent_asset: Option, + timeout_timestamp: u64, + action: Action, + exact_out: bool, + min_asset: Option, + recovery_addr: Addr, + }, } /// This structure describes a CW20 hook message. @@ -86,6 +101,19 @@ pub enum Cw20HookMsg { post_swap_action: Action, affiliates: Vec, }, + Action { + timeout_timestamp: u64, + action: Action, + exact_out: bool, + min_asset: Option, + }, + ActionWithRecover { + timeout_timestamp: u64, + action: Action, + exact_out: bool, + min_asset: Option, + recovery_addr: Addr, + }, } ///////////// diff --git a/schema/raw/execute.json b/schema/raw/execute.json index ac2deb06..32bdfe6c 100644 --- a/schema/raw/execute.json +++ b/schema/raw/execute.json @@ -193,6 +193,112 @@ } }, "additionalProperties": false + }, + { + "type": "object", + "required": [ + "action" + ], + "properties": { + "action": { + "type": "object", + "required": [ + "action", + "exact_out", + "timeout_timestamp" + ], + "properties": { + "action": { + "$ref": "#/definitions/Action" + }, + "exact_out": { + "type": "boolean" + }, + "min_asset": { + "anyOf": [ + { + "$ref": "#/definitions/Asset" + }, + { + "type": "null" + } + ] + }, + "sent_asset": { + "anyOf": [ + { + "$ref": "#/definitions/Asset" + }, + { + "type": "null" + } + ] + }, + "timeout_timestamp": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "action_with_recover" + ], + "properties": { + "action_with_recover": { + "type": "object", + "required": [ + "action", + "exact_out", + "recovery_addr", + "timeout_timestamp" + ], + "properties": { + "action": { + "$ref": "#/definitions/Action" + }, + "exact_out": { + "type": "boolean" + }, + "min_asset": { + "anyOf": [ + { + "$ref": "#/definitions/Asset" + }, + { + "type": "null" + } + ] + }, + "recovery_addr": { + "$ref": "#/definitions/Addr" + }, + "sent_asset": { + "anyOf": [ + { + "$ref": "#/definitions/Asset" + }, + { + "type": "null" + } + ] + }, + "timeout_timestamp": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ], "definitions": { diff --git a/schema/skip-go-entry-point.json b/schema/skip-go-entry-point.json index 2770df5d..519425e7 100644 --- a/schema/skip-go-entry-point.json +++ b/schema/skip-go-entry-point.json @@ -238,6 +238,112 @@ } }, "additionalProperties": false + }, + { + "type": "object", + "required": [ + "action" + ], + "properties": { + "action": { + "type": "object", + "required": [ + "action", + "exact_out", + "timeout_timestamp" + ], + "properties": { + "action": { + "$ref": "#/definitions/Action" + }, + "exact_out": { + "type": "boolean" + }, + "min_asset": { + "anyOf": [ + { + "$ref": "#/definitions/Asset" + }, + { + "type": "null" + } + ] + }, + "sent_asset": { + "anyOf": [ + { + "$ref": "#/definitions/Asset" + }, + { + "type": "null" + } + ] + }, + "timeout_timestamp": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "action_with_recover" + ], + "properties": { + "action_with_recover": { + "type": "object", + "required": [ + "action", + "exact_out", + "recovery_addr", + "timeout_timestamp" + ], + "properties": { + "action": { + "$ref": "#/definitions/Action" + }, + "exact_out": { + "type": "boolean" + }, + "min_asset": { + "anyOf": [ + { + "$ref": "#/definitions/Asset" + }, + { + "type": "null" + } + ] + }, + "recovery_addr": { + "$ref": "#/definitions/Addr" + }, + "sent_asset": { + "anyOf": [ + { + "$ref": "#/definitions/Asset" + }, + { + "type": "null" + } + ] + }, + "timeout_timestamp": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ], "definitions": {