From 12a7a484ce86ae5d50cbf54e80575d4ff0777003 Mon Sep 17 00:00:00 2001
From: Jeremy Liu <31809888+NotJeremyLiu@users.noreply.github.com>
Date: Wed, 23 Oct 2024 07:54:48 -0700
Subject: [PATCH 1/5] add execute action

---
 contracts/entry-point/src/contract.rs |  20 ++-
 contracts/entry-point/src/error.rs    |  13 ++
 contracts/entry-point/src/execute.rs  | 219 +++++++++++++++++++-------
 packages/skip/src/entry_point.rs      |   7 +
 4 files changed, 198 insertions(+), 61 deletions(-)

diff --git a/contracts/entry-point/src/contract.rs b/contracts/entry-point/src/contract.rs
index 45f0bca4..ad5587fc 100644
--- a/contracts/entry-point/src/contract.rs
+++ b/contracts/entry-point/src/contract.rs
@@ -1,8 +1,8 @@
 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_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 +174,22 @@ 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,
+        ),
     }
 }
 
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..69294e92 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,
@@ -128,54 +128,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 +488,117 @@ 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<Asset>,
+    timeout_timestamp: u64,
+    action: Action,
+    exact_out: bool,
+    min_asset: Option<Asset>,
+) -> ContractResult<Response> {
+    // 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)
+}
+
+////////////////////////
+/// 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<Response, ContractError> {
+    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 +624,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 +639,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<SwapExactAssetOut>,
+    mut remaining_asset: &mut Asset,
+    mut response: Response,
+) -> Result<Response, ContractError> {
+    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");
+    }
+
+    Ok(response)
+}
 
 // SWAP MESSAGE HELPER FUNCTIONS
 
diff --git a/packages/skip/src/entry_point.rs b/packages/skip/src/entry_point.rs
index 21e42c28..ebc4edb6 100644
--- a/packages/skip/src/entry_point.rs
+++ b/packages/skip/src/entry_point.rs
@@ -66,6 +66,13 @@ pub enum ExecuteMsg {
         post_swap_action: Action,
         exact_out: bool,
     },
+    Action {
+        sent_asset: Option<Asset>,
+        timeout_timestamp: u64,
+        action: Action,
+        exact_out: bool,
+        min_asset: Option<Asset>,
+    },
 }
 
 /// This structure describes a CW20 hook message.

From 471dc4129539d61ee07f2a1cc220aef9fbf140ad Mon Sep 17 00:00:00 2001
From: Jeremy Liu <31809888+NotJeremyLiu@users.noreply.github.com>
Date: Wed, 23 Oct 2024 21:23:45 -0700
Subject: [PATCH 2/5] add action and recover logic

---
 contracts/entry-point/src/contract.rs | 23 +++++++++++-
 contracts/entry-point/src/execute.rs  | 54 ++++++++++++++++++++++++++-
 packages/skip/src/entry_point.rs      |  8 ++++
 3 files changed, 81 insertions(+), 4 deletions(-)

diff --git a/contracts/entry-point/src/contract.rs b/contracts/entry-point/src/contract.rs
index ad5587fc..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_action, 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},
@@ -190,6 +191,24 @@ pub fn execute(
             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/execute.rs b/contracts/entry-point/src/execute.rs
index 69294e92..d6a5a005 100644
--- a/contracts/entry-point/src/execute.rs
+++ b/contracts/entry-point/src/execute.rs
@@ -567,6 +567,56 @@ pub fn execute_action(
     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<Asset>,
+    timeout_timestamp: u64,
+    action: Action,
+    exact_out: bool,
+    min_asset: Option<Asset>,
+    recovery_addr: Addr,
+) -> ContractResult<Response> {
+    let mut assets: Vec<Asset> = 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 ///
 ////////////////////////
@@ -658,7 +708,7 @@ fn handle_ibc_transfer_fees(
     deps: &DepsMut,
     ibc_info: &IbcInfo,
     fee_swap: &Option<SwapExactAssetOut>,
-    mut remaining_asset: &mut Asset,
+    remaining_asset: &mut Asset,
     mut response: Response,
 ) -> Result<Response, ContractError> {
     let ibc_fee_coin = ibc_info
@@ -674,7 +724,7 @@ fn handle_ibc_transfer_fees(
 
         // 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)?;
+            verify_and_create_fee_swap_msg(deps, fee_swap, remaining_asset, &ibc_fee_coin)?;
 
         // Add the fee swap message to the response
         response = response
diff --git a/packages/skip/src/entry_point.rs b/packages/skip/src/entry_point.rs
index ebc4edb6..e965d42b 100644
--- a/packages/skip/src/entry_point.rs
+++ b/packages/skip/src/entry_point.rs
@@ -73,6 +73,14 @@ pub enum ExecuteMsg {
         exact_out: bool,
         min_asset: Option<Asset>,
     },
+    ActionWithRecover {
+        sent_asset: Option<Asset>,
+        timeout_timestamp: u64,
+        action: Action,
+        exact_out: bool,
+        min_asset: Option<Asset>,
+        recovery_addr: Addr,
+    },
 }
 
 /// This structure describes a CW20 hook message.

From 080a41be1c1f32be3888d27336675db9a5747d69 Mon Sep 17 00:00:00 2001
From: Jeremy Liu <31809888+NotJeremyLiu@users.noreply.github.com>
Date: Wed, 23 Oct 2024 21:26:40 -0700
Subject: [PATCH 3/5] add cw20 logic

---
 contracts/entry-point/src/execute.rs | 32 ++++++++++++++++++++++++++++
 packages/skip/src/entry_point.rs     | 13 +++++++++++
 2 files changed, 45 insertions(+)

diff --git a/contracts/entry-point/src/execute.rs b/contracts/entry-point/src/execute.rs
index d6a5a005..9897563d 100644
--- a/contracts/entry-point/src/execute.rs
+++ b/contracts/entry-point/src/execute.rs
@@ -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,
+        ),
     }
 }
 
diff --git a/packages/skip/src/entry_point.rs b/packages/skip/src/entry_point.rs
index e965d42b..630fb9e9 100644
--- a/packages/skip/src/entry_point.rs
+++ b/packages/skip/src/entry_point.rs
@@ -101,6 +101,19 @@ pub enum Cw20HookMsg {
         post_swap_action: Action,
         affiliates: Vec<Affiliate>,
     },
+    Action {
+        timeout_timestamp: u64,
+        action: Action,
+        exact_out: bool,
+        min_asset: Option<Asset>,
+    },
+    ActionWithRecover {
+        timeout_timestamp: u64,
+        action: Action,
+        exact_out: bool,
+        min_asset: Option<Asset>,
+        recovery_addr: Addr,
+    },
 }
 
 /////////////

From cff2398ada6eaca5692cffbc292ca80a351e558f Mon Sep 17 00:00:00 2001
From: Jeremy Liu <31809888+NotJeremyLiu@users.noreply.github.com>
Date: Wed, 23 Oct 2024 21:31:18 -0700
Subject: [PATCH 4/5] update schema

---
 schema/raw/execute.json         | 106 ++++++++++++++++++++++++++++++++
 schema/skip-go-entry-point.json | 106 ++++++++++++++++++++++++++++++++
 2 files changed, 212 insertions(+)

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": {

From 74ad8f6bb68fd11eb189fc20bc1277650cb90b25 Mon Sep 17 00:00:00 2001
From: Jeremy Liu <31809888+NotJeremyLiu@users.noreply.github.com>
Date: Thu, 24 Oct 2024 07:24:00 -0700
Subject: [PATCH 5/5] add tests for execute action

---
 .../entry-point/tests/test_execute_action.rs  | 498 ++++++++++++++++++
 1 file changed, 498 insertions(+)
 create mode 100644 contracts/entry-point/tests/test_execute_action.rs

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<Coin>,
+    sent_asset: Option<Asset>,
+    action: Action,
+    exact_out: bool,
+    min_asset: Option<Asset>,
+    expected_messages: Vec<SubMsg>,
+    expected_error: Option<ContractError>,
+}
+
+// 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] = &params.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());
+        }
+    }
+}