From 390fb8f7198896d597919f332053b0583077d47e Mon Sep 17 00:00:00 2001 From: fsoares Date: Tue, 24 Sep 2024 17:18:18 +0100 Subject: [PATCH 1/5] add astrovault swap venue adapter --- Cargo.lock | 51 +- Cargo.toml | 1 + contracts/adapters/swap/astrovault/Cargo.toml | 34 ++ contracts/adapters/swap/astrovault/README.md | 96 +++ .../astrovault/src/bin/astrovault-schema.rs | 10 + .../adapters/swap/astrovault/src/contract.rs | 552 ++++++++++++++++++ .../adapters/swap/astrovault/src/error.rs | 44 ++ contracts/adapters/swap/astrovault/src/lib.rs | 3 + .../adapters/swap/astrovault/src/state.rs | 6 + .../astrovault/tests/test_execute_receive.rs | 214 +++++++ .../astrovault/tests/test_execute_swap.rs | 346 +++++++++++ .../tests/test_execute_transfer_funds_back.rs | 240 ++++++++ packages/skip/src/swap.rs | 6 + 13 files changed, 1602 insertions(+), 1 deletion(-) create mode 100644 contracts/adapters/swap/astrovault/Cargo.toml create mode 100644 contracts/adapters/swap/astrovault/README.md create mode 100644 contracts/adapters/swap/astrovault/src/bin/astrovault-schema.rs create mode 100644 contracts/adapters/swap/astrovault/src/contract.rs create mode 100644 contracts/adapters/swap/astrovault/src/error.rs create mode 100644 contracts/adapters/swap/astrovault/src/lib.rs create mode 100644 contracts/adapters/swap/astrovault/src/state.rs create mode 100644 contracts/adapters/swap/astrovault/tests/test_execute_receive.rs create mode 100644 contracts/adapters/swap/astrovault/tests/test_execute_swap.rs create mode 100644 contracts/adapters/swap/astrovault/tests/test_execute_transfer_funds_back.rs diff --git a/Cargo.lock b/Cargo.lock index 15db2d07..539ed1fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -70,6 +70,23 @@ dependencies = [ "thiserror", ] +[[package]] +name = "astrovault" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f00162d5a60a1463f5f35f7f5c5a60b01a7ab391693176028757559d90118a39" +dependencies = [ + "bigint", + "cosmwasm-schema", + "cosmwasm-std", + "cw20 1.1.2", + "cw721 0.16.0", + "schemars", + "serde", + "sha2 0.10.8", + "thiserror", +] + [[package]] name = "autocfg" version = "1.3.0" @@ -100,6 +117,16 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" +[[package]] +name = "bigint" +version = "4.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0e8c8a600052b52482eff2cf4d810e462fdff1f656ac1ecb6232132a1ed7def" +dependencies = [ + "byteorder", + "crunchy 0.1.6", +] + [[package]] name = "block-buffer" version = "0.9.0" @@ -280,6 +307,12 @@ dependencies = [ "libc", ] +[[package]] +name = "crunchy" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2f4a431c5c9f662e1200b7c7f02c34e91361150e382089a8f2dec3ba680cbda" + [[package]] name = "crunchy" version = "0.2.2" @@ -1797,6 +1830,22 @@ dependencies = [ "thiserror", ] +[[package]] +name = "skip-go-swap-adapter-astrovault" +version = "0.3.0" +dependencies = [ + "astrovault", + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw20 1.1.2", + "skip", + "test-case", + "thiserror", +] + [[package]] name = "skip-go-swap-adapter-dexter" version = "0.3.0" @@ -2174,7 +2223,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" dependencies = [ "byteorder", - "crunchy", + "crunchy 0.2.2", "hex", "static_assertions", ] diff --git a/Cargo.toml b/Cargo.toml index ee9b8d8a..e43369ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ keywords = ["cosmwasm"] [workspace.dependencies] astroport = "2.9" +astrovault = "0.1.8" dexter = "1.4.0" dexter-vault = "1.1.0" dexter-stable-pool = "1.1.1" diff --git a/contracts/adapters/swap/astrovault/Cargo.toml b/contracts/adapters/swap/astrovault/Cargo.toml new file mode 100644 index 00000000..8bd1cb32 --- /dev/null +++ b/contracts/adapters/swap/astrovault/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "skip-go-swap-adapter-astrovault" +version = { workspace = true } +rust-version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +license = { workspace = true } +homepage = { workspace = true } +repository = { workspace = true } +documentation = { workspace = true } +keywords = { workspace = true } + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +astrovault = { workspace = true } +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +cw2 = { workspace = true } +cw20 = { workspace = true } +cw-storage-plus = { workspace = true } +cw-utils = { workspace = true } +skip = { workspace = true } +thiserror = { workspace = true } + +[dev-dependencies] +test-case = { workspace = true } \ No newline at end of file diff --git a/contracts/adapters/swap/astrovault/README.md b/contracts/adapters/swap/astrovault/README.md new file mode 100644 index 00000000..95c79fc6 --- /dev/null +++ b/contracts/adapters/swap/astrovault/README.md @@ -0,0 +1,96 @@ +# Neutron Astrovault Swap Adapter Contract + +The Neutron Astrovault swap adapter contract is responsible for: + +1. Taking the standardized entry point swap operations message format and converting it to Astrovault pool swaps message format. +2. Swapping by dispatching swaps to Astrovault router contract. +3. Providing query methods that can be called by the entry point contract (generally, to any external actor) to simulate multi-hop swaps that specify an exact amount in (estimating how much would be received from the swap) + +Note: Swap adapter contracts expect to be called by an entry point contract that provides basic validation and minimum amount out safety guarantees for the caller. There are no slippage guarantees provided by swap adapter contracts. + +WARNING: Do not send funds directly to the contract without calling one of its functions. Funds sent directly to the contract do not trigger any contract logic that performs validation / safety checks (as the Cosmos SDK handles direct fund transfers in the `Bank` module and not the `Wasm` module). There are no explicit recovery mechanisms for accidentally sent funds. + +## InstantiateMsg + +Instantiates a new Neutron Astrovault swap adapter contract using the Entrypoint contract address provided in the instantiation message. + +```json +{ + "entry_point_contract_address": "neutron...", + "astrovault_router_address": "neutron..." +} +``` + +## ExecuteMsg + +### `swap` + +Swaps the coin sent using the operations provided. + +```json +{ + "swap": { + "operations": [ + { + "pool": "neutron...", + "denom_in": "ibc/B559A80D62249C8AA07A380E2A2BEA6E5CA9A6F079C912C3A9E9B494105E4F81", + "denom_out": "ibc/C4CFF46FD6DE35CA4CF4CE031E643C8FDC9BA4B99AE598E9B0ED98FE3A2319F9" + }, + { + "pool": "neutron...", + "denom_in": "ibc/C4CFF46FD6DE35CA4CF4CE031E643C8FDC9BA4B99AE598E9B0ED98FE3A2319F9", + "denom_out": "untrn" + } + ] + } +} +``` + +### `transfer_funds_back` + +Transfers all contract funds to the address provided, called by the swap adapter contract to send back the entry point contract the assets received from swapping. + +Note: This function can be called by anyone as the contract is assumed to have no balance before/after it's called by the entry point contract. Do not send funds directly to this contract without calling a function. + +```json +{ + "transfer_funds_back": { + "caller": "neutron..." + } +} +``` + +## QueryMsg + +### `simulate_swap_exact_coin_in` + +Returns the coin out that would be received from swapping the `coin_in` specified in the call (swapped through the `swap_operatons` provided) + +Query: + +```json +{ + "simulate_swap_exact_coin_in": { + "coin_in": { + "denom": "untrn", + "amount": "1000000" + }, + "swap_operations": [ + { + "pool": "neutron...", + "denom_in": "untrn", + "denom_out": "ibc/C4CFF46FD6DE35CA4CF4CE031E643C8FDC9BA4B99AE598E9B0ED98FE3A2319F9" + } + ] + } +} +``` + +Response: + +```json +{ + "denom": "ibc/C4CFF46FD6DE35CA4CF4CE031E643C8FDC9BA4B99AE598E9B0ED98FE3A2319F9", + "amount": "1000" +} +``` diff --git a/contracts/adapters/swap/astrovault/src/bin/astrovault-schema.rs b/contracts/adapters/swap/astrovault/src/bin/astrovault-schema.rs new file mode 100644 index 00000000..4f4733f0 --- /dev/null +++ b/contracts/adapters/swap/astrovault/src/bin/astrovault-schema.rs @@ -0,0 +1,10 @@ +use cosmwasm_schema::write_api; +use skip::swap::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + execute: ExecuteMsg, + query: QueryMsg + } +} diff --git a/contracts/adapters/swap/astrovault/src/contract.rs b/contracts/adapters/swap/astrovault/src/contract.rs new file mode 100644 index 00000000..dfc1d194 --- /dev/null +++ b/contracts/adapters/swap/astrovault/src/contract.rs @@ -0,0 +1,552 @@ +use crate::{ + error::{ContractError, ContractResult}, + state::{ASTROVAULT_CASHBACK_ADDRESS, ASTROVAULT_ROUTER_ADDRESS, ENTRY_POINT_CONTRACT_ADDRESS}, +}; +use astrovault::{ + nft_booster::handle_msg, + router::{ + self, + handle_msg::RouterReceiveMsg, + query_msg::{ConfigResponse, QueryRouteSwapSimulation, RoutePoolType}, + state::HopV2, + }, +}; +use cosmwasm_std::{ + entry_point, from_json, to_json_binary, Addr, BankMsg, Binary, CosmosMsg, Deps, DepsMut, Env, + MessageInfo, QueryRequest, Response, Uint128, WasmMsg, WasmQuery, +}; +use cw2::{ensure_from_older_version, set_contract_version}; +use cw20::{Cw20Coin, Cw20Contract, Cw20ReceiveMsg}; +use cw_utils::one_coin; +use skip::{ + asset::Asset, + error::SkipError, + swap::{ + get_ask_denom_for_routes, AstrovaultAdapterInstantiateMsg, Cw20HookMsg, ExecuteMsg, + MigrateMsg, QueryMsg, Route, SimulateSmartSwapExactAssetInResponse, + SimulateSwapExactAssetInResponse, SwapOperation, + }, +}; + +// Contract name and version used for migration. +const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +/////////////// +/// MIGRATE /// +/////////////// + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> ContractResult { + ensure_from_older_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + Ok(Response::default()) +} + +/////////////////// +/// INSTANTIATE /// +/////////////////// + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: AstrovaultAdapterInstantiateMsg, +) -> ContractResult { + // Set contract version + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + // Validate entry point contract address + let checked_entry_point_contract_address = + deps.api.addr_validate(&msg.entry_point_contract_address)?; + + // Store the entry point contract address + ENTRY_POINT_CONTRACT_ADDRESS.save(deps.storage, &checked_entry_point_contract_address)?; + + let astrovault_router_contract_address = deps + .api + .addr_validate(&msg.astrovault_router_contract_address)?; + ASTROVAULT_ROUTER_ADDRESS.save(deps.storage, &astrovault_router_contract_address)?; + + // query router configs to get cashback address if available + let router_config: ConfigResponse = + deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: astrovault_router_contract_address.to_string(), + msg: to_json_binary(&router::query_msg::QueryMsg::Config {})?, + }))?; + + if let Some(cashback) = router_config.cashback { + // this is needed so the grvt8 won by the swaps executed by this adapter can be sent back to the router address + ASTROVAULT_CASHBACK_ADDRESS.save(deps.storage, &deps.api.addr_validate(&cashback)?)?; + } + + Ok(Response::new() + .add_attribute("action", "instantiate") + .add_attribute( + "entry_point_contract_address", + checked_entry_point_contract_address.to_string(), + ) + .add_attribute( + "astrovault_router_contract_address", + astrovault_router_contract_address.to_string(), + )) +} + +/////////////// +/// RECEIVE /// +/////////////// + +// Receive is the main entry point for the contract to +// receive cw20 tokens and execute the swap +pub fn receive_cw20( + deps: DepsMut, + env: Env, + mut info: MessageInfo, + cw20_msg: Cw20ReceiveMsg, +) -> ContractResult { + let sent_asset = Asset::Cw20(Cw20Coin { + address: info.sender.to_string(), + amount: cw20_msg.amount, + }); + sent_asset.validate(&deps, &env, &info)?; + + // Set the sender to the originating address that triggered the cw20 send call + // This is later validated / enforced to be the entry point contract address + info.sender = deps.api.addr_validate(&cw20_msg.sender)?; + + match from_json(&cw20_msg.msg)? { + Cw20HookMsg::Swap { operations } => { + execute_swap(deps, env, info, sent_asset.amount(), operations) + } + } +} + +/////////////// +/// EXECUTE /// +/////////////// + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> ContractResult { + match msg { + ExecuteMsg::Receive(cw20_msg) => receive_cw20(deps, env, info, cw20_msg), + ExecuteMsg::Swap { operations } => { + // validate that there's at least one swap operation + if operations.is_empty() { + return Err(ContractError::SwapOperationsEmpty); + } + + let coin = one_coin(&info)?; + + // validate that the one coin is the same as the first swap operation's denom in + if coin.denom != operations.first().unwrap().denom_in { + return Err(ContractError::CoinInDenomMismatch); + } + + execute_swap(deps, env, info, coin.amount, operations) + } + ExecuteMsg::TransferFundsBack { + swapper, + return_denom, + } => Ok(execute_transfer_funds_back( + deps, + env, + info, + swapper, + return_denom, + )?), + _ => { + unimplemented!() + } + } +} + +fn execute_swap( + deps: DepsMut, + env: Env, + info: MessageInfo, + amount_in: Uint128, + operations: Vec, +) -> ContractResult { + let entry_point_contract_address = ENTRY_POINT_CONTRACT_ADDRESS.load(deps.storage)?; + let astrovault_router_contract_address = ASTROVAULT_ROUTER_ADDRESS.load(deps.storage)?; + + // Enforce the caller is the entry point contract + if info.sender != entry_point_contract_address { + return Err(ContractError::Unauthorized); + } + + let hops = convert_operations_to_hops( + deps.as_ref(), + astrovault_router_contract_address.to_string(), + operations.clone(), + )?; + + // Create base astrovault router wasm message + let router_execute_msg = RouterReceiveMsg::RouteV2 { + hops, + minimum_receive: None, + to: None, + }; + + let initial_asset = Asset::new(deps.api, &operations.first().unwrap().denom_in, amount_in); + + // depending if the initial asset is native or cw20, we set the respective msg + let astrovault_router_wasm_msg = match initial_asset { + Asset::Native(native_asset) => CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: astrovault_router_contract_address.to_string(), + funds: vec![native_asset], + msg: to_json_binary(&router::handle_msg::ExecuteMsg::Receive( + cw20::Cw20ReceiveMsg { + sender: env.contract.address.to_string(), + amount: amount_in, + msg: to_json_binary(&router_execute_msg)?, + }, + ))?, + }), + Asset::Cw20(cw20_asset) => CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: cw20_asset.address.to_string(), + funds: vec![], + msg: to_json_binary(&cw20::Cw20ExecuteMsg::Send { + contract: astrovault_router_contract_address.to_string(), + amount: cw20_asset.amount, + msg: to_json_binary(&router_execute_msg)?, + })?, + }), + }; + + let return_denom = match operations.last() { + Some(last_op) => last_op.denom_out.clone(), + None => return Err(ContractError::SwapOperationsEmpty), + }; + + // Create the transfer funds back message + let transfer_funds_back_msg = WasmMsg::Execute { + contract_addr: env.contract.address.to_string(), + msg: to_json_binary(&ExecuteMsg::TransferFundsBack { + swapper: entry_point_contract_address, + return_denom, + })?, + funds: vec![], + }; + + Ok(Response::new() + .add_attribute("action", "execute_swap") + .add_message(astrovault_router_wasm_msg) + .add_message(transfer_funds_back_msg) + .add_attribute("action", "dispatch_swaps_and_transfer_back")) +} + +pub fn execute_transfer_funds_back( + deps: DepsMut, + env: Env, + info: MessageInfo, + swapper: Addr, + return_denom: String, +) -> Result { + // Ensure the caller is the contract itself + if info.sender != env.contract.address { + return Err(SkipError::Unauthorized); + } + + // Create the transfer funds back message + let transfer_funds_back_msg: CosmosMsg = match deps.api.addr_validate(&return_denom) { + Ok(contract_addr) => Asset::new( + deps.api, + contract_addr.as_str(), + Cw20Contract(contract_addr.clone()).balance(&deps.querier, &env.contract.address)?, + ) + .transfer(swapper.as_str()), + Err(_) => CosmosMsg::Bank(BankMsg::Send { + to_address: swapper.to_string(), + amount: deps + .querier + .query_all_balances(env.contract.address.clone())?, + }), + }; + + let mut msgs = vec![transfer_funds_back_msg]; + + // ADDED: Also create the return of cashback funds msg if available + if let Some(cashback_addr) = ASTROVAULT_CASHBACK_ADDRESS.may_load(deps.storage)? { + if return_denom != cashback_addr { + msgs.push( + Asset::new( + deps.api, + cashback_addr.as_str(), + Cw20Contract(cashback_addr.clone()) + .balance(&deps.querier, &env.contract.address)?, + ) + .transfer(ASTROVAULT_ROUTER_ADDRESS.load(deps.storage)?.as_str()), + ) + } + } + + Ok(Response::new() + .add_messages(msgs) + .add_attribute("action", "dispatch_transfer_funds_back_bank_send")) +} + +///////////// +/// QUERY /// +///////////// + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> ContractResult { + match msg { + QueryMsg::SimulateSwapExactAssetIn { + asset_in, + swap_operations, + } => to_json_binary(&query_simulate_swap_exact_asset_in( + deps, + asset_in, + swap_operations, + )?), + QueryMsg::SimulateSwapExactAssetInWithMetadata { + asset_in, + swap_operations, + include_spot_price, + } => to_json_binary(&query_simulate_swap_exact_asset_in_with_metadata( + deps, + asset_in, + swap_operations, + include_spot_price, + )?), + QueryMsg::SimulateSmartSwapExactAssetIn { routes, .. } => { + let ask_denom = get_ask_denom_for_routes(&routes)?; + + to_json_binary(&query_simulate_smart_swap_exact_asset_in( + deps, ask_denom, routes, + )?) + } + QueryMsg::SimulateSmartSwapExactAssetInWithMetadata { + routes, + asset_in, + include_spot_price, + } => { + let ask_denom = get_ask_denom_for_routes(&routes)?; + + to_json_binary(&query_simulate_smart_swap_exact_asset_in_with_metadata( + deps, + asset_in, + ask_denom, + routes, + include_spot_price, + )?) + } + _ => { + unimplemented!() + } + } + .map_err(From::from) +} + +// Queries the astrovault pool contracts to simulate a swap exact amount in +fn query_simulate_swap_exact_asset_in( + deps: Deps, + asset_in: Asset, + swap_operations: Vec, +) -> ContractResult { + // Error if swap operations is empty + let Some(first_op) = swap_operations.first() else { + return Err(ContractError::SwapOperationsEmpty); + }; + + // Ensure asset_in's denom is the same as the first swap operation's denom in + if asset_in.denom() != first_op.denom_in { + return Err(ContractError::CoinInDenomMismatch); + } + + let astrovault_router_contract_address = ASTROVAULT_ROUTER_ADDRESS.load(deps.storage)?; + let hops = convert_operations_to_hops( + deps, + astrovault_router_contract_address.to_string(), + swap_operations, + )?; + + let simulation_response: QueryRouteSwapSimulation = + deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: astrovault_router_contract_address.to_string(), + msg: to_json_binary(&router::query_msg::QueryMsg::RouteSwapSimulation { + amount: asset_in.amount(), + hops, + })?, + }))?; + + Ok(Asset::new( + deps.api, + &simulation_response.to.info.to_string(), + simulation_response.to.amount, + )) +} + +// Queries the astrovault pool contracts to simulate a swap exact amount in with metadata +fn query_simulate_swap_exact_asset_in_with_metadata( + deps: Deps, + asset_in: Asset, + swap_operations: Vec, + include_spot_price: bool, +) -> ContractResult { + // Error if swap operations is empty + let Some(first_op) = swap_operations.first() else { + return Err(ContractError::SwapOperationsEmpty); + }; + + // Ensure asset_in's denom is the same as the first swap operation's denom in + if asset_in.denom() != first_op.denom_in { + return Err(ContractError::CoinInDenomMismatch); + } + + let astrovault_router_contract_address = ASTROVAULT_ROUTER_ADDRESS.load(deps.storage)?; + let hops = convert_operations_to_hops( + deps, + astrovault_router_contract_address.to_string(), + swap_operations, + )?; + + let simulation_response: QueryRouteSwapSimulation = + deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: astrovault_router_contract_address.to_string(), + msg: to_json_binary(&router::query_msg::QueryMsg::RouteSwapSimulation { + amount: asset_in.amount(), + hops, + })?, + }))?; + + // Create the response + let response = SimulateSwapExactAssetInResponse { + asset_out: Asset::new( + deps.api, + &simulation_response.to.info.to_string(), + simulation_response.to.amount, + ), + spot_price: if include_spot_price { + Some(simulation_response.to_spot_price) + } else { + None + }, + }; + + Ok(response) +} + +fn query_simulate_smart_swap_exact_asset_in( + deps: Deps, + ask_denom: String, + routes: Vec, +) -> ContractResult { + simulate_smart_swap_exact_asset_in(deps, ask_denom, routes) +} + +fn simulate_smart_swap_exact_asset_in( + deps: Deps, + ask_denom: String, + routes: Vec, +) -> ContractResult { + let mut asset_out = Asset::new(deps.api, &ask_denom, Uint128::zero()); + + for route in &routes { + let route_asset_out = query_simulate_swap_exact_asset_in( + deps, + route.offer_asset.clone(), + route.operations.clone(), + )?; + + asset_out.add(route_asset_out.amount())?; + } + + Ok(asset_out) +} + +fn query_simulate_smart_swap_exact_asset_in_with_metadata( + deps: Deps, + _asset_in: Asset, + ask_denom: String, + routes: Vec, + _include_spot_price: bool, +) -> ContractResult { + let asset_out = simulate_smart_swap_exact_asset_in(deps, ask_denom, routes.clone())?; + + // TODO: Implement spot price calculation for smart swaps + let response = SimulateSmartSwapExactAssetInResponse { + asset_out, + spot_price: None, + }; + + Ok(response) +} + +pub fn convert_operations_to_hops( + deps: Deps, + astrovault_router_contract_address: String, + operations: Vec, +) -> ContractResult> { + // Create hops for astrovault router + let mut hops = vec![]; + + // Get Pool types for each operation + let route_pools_type: Vec = + deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: astrovault_router_contract_address, + msg: to_json_binary(&router::query_msg::QueryMsg::RoutePoolsType { + route_pools_addr: operations.clone().into_iter().map(|op| op.pool).collect(), + })?, + }))?; + + for (i, operation) in operations.iter().enumerate() { + // depending on the pool type of the operation, we add the respective hop type to the astrovault router swap msg + let pool = astrovault::assets::pools::PoolInfoInput::Addr(operation.pool.clone()); + let from_asset_index = route_pools_type[i] + .pool_asset_infos + .iter() + .position(|x| x.to_string() == operation.denom_in); + + let from_asset_index = match from_asset_index { + Some(index) => index as u32, + None => return Err(ContractError::InvalidPoolAsset), + }; + + match route_pools_type[i].pool_type.as_str() { + "hybrid" => { + hops.push(HopV2::RatioHopInfo { + pool, + from_asset_index, + }); + } + "standard" => { + hops.push(HopV2::StandardHopInfo { + pool, + from_asset_index, + }); + } + "stable" => { + // this type of pool can have more than 2 assets, so we need to find the to_asset_index + let to_asset_index = route_pools_type[i] + .pool_asset_infos + .iter() + .position(|x| x.to_string() == operation.denom_out); + + let to_asset_index = match to_asset_index { + Some(index) => index as u32, + None => return Err(ContractError::InvalidPoolAsset), + }; + + hops.push(HopV2::StableHopInfo { + pool, + from_asset_index, + to_asset_index, + }); + } + _ => { + return Err(ContractError::InvalidPoolType); + } + } + } + + Ok(hops) +} diff --git a/contracts/adapters/swap/astrovault/src/error.rs b/contracts/adapters/swap/astrovault/src/error.rs new file mode 100644 index 00000000..3ae8a3fe --- /dev/null +++ b/contracts/adapters/swap/astrovault/src/error.rs @@ -0,0 +1,44 @@ +use cosmwasm_std::{OverflowError, StdError}; +use skip::error::SkipError; +use thiserror::Error; + +pub type ContractResult = core::result::Result; + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error(transparent)] + Std(#[from] StdError), + + #[error(transparent)] + Overflow(#[from] OverflowError), + + #[error(transparent)] + Skip(#[from] SkipError), + + #[error(transparent)] + Payment(#[from] cw_utils::PaymentError), + + #[error("Unauthorized")] + Unauthorized, + + #[error("swap_operations cannot be empty")] + SwapOperationsEmpty, + + #[error("coin_in denom must match the first swap operation's denom in")] + CoinInDenomMismatch, + + #[error("coin_out denom must match the last swap operation's denom out")] + CoinOutDenomMismatch, + + #[error("Operation exceeds max spread limit")] + MaxSpreadAssertion, + + #[error("Contract has no balance of offer asset")] + NoOfferAssetAmount, + + #[error("InvalidPoolAsset")] + InvalidPoolAsset, + + #[error("InvalidPoolType")] + InvalidPoolType, +} diff --git a/contracts/adapters/swap/astrovault/src/lib.rs b/contracts/adapters/swap/astrovault/src/lib.rs new file mode 100644 index 00000000..3d3e89c8 --- /dev/null +++ b/contracts/adapters/swap/astrovault/src/lib.rs @@ -0,0 +1,3 @@ +pub mod contract; +pub mod error; +pub mod state; diff --git a/contracts/adapters/swap/astrovault/src/state.rs b/contracts/adapters/swap/astrovault/src/state.rs new file mode 100644 index 00000000..63347524 --- /dev/null +++ b/contracts/adapters/swap/astrovault/src/state.rs @@ -0,0 +1,6 @@ +use cosmwasm_std::Addr; +use cw_storage_plus::Item; + +pub const ENTRY_POINT_CONTRACT_ADDRESS: Item = Item::new("entry_point_contract_address"); +pub const ASTROVAULT_ROUTER_ADDRESS: Item = Item::new("astrovault_router_address"); +pub const ASTROVAULT_CASHBACK_ADDRESS: Item = Item::new("astrovault_cashback_address"); diff --git a/contracts/adapters/swap/astrovault/tests/test_execute_receive.rs b/contracts/adapters/swap/astrovault/tests/test_execute_receive.rs new file mode 100644 index 00000000..f77a4ba7 --- /dev/null +++ b/contracts/adapters/swap/astrovault/tests/test_execute_receive.rs @@ -0,0 +1,214 @@ +use astrovault::assets::pools::PoolInfoInput; +use astrovault::router::handle_msg::RouterReceiveMsg; +use astrovault::router::state::HopV2; +use astrovault::{assets::asset::AssetInfo, router::query_msg::RoutePoolType}; +use cosmwasm_std::Uint128; +use cosmwasm_std::{ + testing::{mock_dependencies, mock_env, mock_info}, + to_json_binary, Addr, Coin, QuerierResult, + ReplyOn::Never, + SubMsg, SystemResult, WasmMsg, WasmQuery, +}; +use skip::swap::{Cw20HookMsg, ExecuteMsg, SwapOperation}; +use skip_go_swap_adapter_astrovault::{ + error::{ContractError, ContractResult}, + state::{ASTROVAULT_ROUTER_ADDRESS, ENTRY_POINT_CONTRACT_ADDRESS}, +}; +use test_case::test_case; + +/* +Test Cases: + +Expect Success + - One swap operation (starting with cw20 token) + +Expect Error + - Unauthorized Caller (Only the stored entry point contract can call this function) + */ + +// Define test parameters +struct Params { + sender: String, + caller: String, + info_funds: Vec, + swap_operations: Vec, + expected_messages: Vec, + expected_error: Option, +} + +pub const CW20_ADDR: &str = "neutron10dxyft3nv4vpxh5vrpn0xw8geej8dw3g39g7nqp8mrm307ypssksau29af"; + +// Test execute_swap +#[test_case( + Params { + sender: "entry_point".to_string(), + caller: CW20_ADDR.to_string(), + info_funds: vec![], + swap_operations: vec![ + SwapOperation { + pool: "pool_3".to_string(), + denom_in: CW20_ADDR.to_string(), + denom_out: "ua".to_string(), + interface: None, + } + ], + expected_messages: vec![ + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: CW20_ADDR.to_string(), + msg: to_json_binary(& + cw20::Cw20ExecuteMsg::Send { contract: "astrovault_router".to_string(), amount: Uint128::from(100u128), msg: to_json_binary(&RouterReceiveMsg::RouteV2 { + hops: vec![ + HopV2::RatioHopInfo { pool: + PoolInfoInput::Addr("pool_3".to_string()), from_asset_index: 0 } + ], + minimum_receive: None, + to: None, + })? } + )?, + funds: vec![], + }.into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "swap_contract_address".to_string(), + msg: to_json_binary(&ExecuteMsg::TransferFundsBack { + return_denom: "ua".to_string(), + swapper: Addr::unchecked("entry_point"), + })?, + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "One Swap Operation")] +#[test_case( + Params { + sender: "random".to_string(), + caller: CW20_ADDR.to_string(), + info_funds: vec![], + swap_operations: vec![ + SwapOperation { + pool: "pool_3".to_string(), + denom_in: CW20_ADDR.to_string(), + denom_out: "ua".to_string(), + interface: None, + } + ], + expected_messages: vec![], + expected_error: Some(ContractError::Unauthorized), + }; + "Unauthorized Caller - Expect Error")] + +fn test_execute_receive(params: Params) -> ContractResult<()> { + // Create mock dependencies + let mut deps = mock_dependencies(); + let swap_ops = params.swap_operations.clone(); + + // Create mock wasm handler to handle the swap adapter contract query + let wasm_handler = move |query: &WasmQuery| -> QuerierResult { + match query { + WasmQuery::Smart { contract_addr, .. } => { + // the function queries the balance of the contract address + if contract_addr == &CW20_ADDR.to_string() { + return SystemResult::Ok(cosmwasm_std::ContractResult::Ok( + to_json_binary(&cw20::BalanceResponse { + balance: Uint128::from(100u128), + }) + .unwrap(), + )); + } + if contract_addr == "astrovault_router" { + let mut mock_route_pool_type_query_response = vec![]; + if !swap_ops.is_empty() { + mock_route_pool_type_query_response.push(RoutePoolType { + pool_addr: "pool_3".to_string(), + pool_type: "hybrid".to_string(), + pool_asset_infos: vec![ + AssetInfo::Token { + contract_addr: CW20_ADDR.to_string(), + }, + AssetInfo::NativeToken { + denom: "ua".to_string(), + }, + ], + }); + } + SystemResult::Ok(cosmwasm_std::ContractResult::Ok( + to_json_binary(&mock_route_pool_type_query_response).unwrap(), + )) + } else { + panic!("Unsupported contract: {:?}", query); + } + } + _ => panic!("Unsupported query: {:?}", query), + } + }; + + // Update querier with mock wasm handler + deps.querier.update_wasm(wasm_handler); + + // Create mock env + let mut env = mock_env(); + env.contract.address = Addr::unchecked("swap_contract_address"); + + // 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(¶ms.caller, info_funds); + + // Store the entry point contract address + ENTRY_POINT_CONTRACT_ADDRESS.save(deps.as_mut().storage, &Addr::unchecked("entry_point"))?; + ASTROVAULT_ROUTER_ADDRESS.save(deps.as_mut().storage, &Addr::unchecked("astrovault_router"))?; + + // Call execute_swap with the given test parameters + let res = skip_go_swap_adapter_astrovault::contract::execute( + deps.as_mut(), + env, + info, + ExecuteMsg::Receive(cw20::Cw20ReceiveMsg { + sender: params.sender, + amount: Uint128::from(100u128), + msg: to_json_binary(&Cw20HookMsg::Swap { + operations: params.swap_operations, + })?, + }), + ); + + // Assert the behavior is correct + 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 messages 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()); + } + } + + Ok(()) +} diff --git a/contracts/adapters/swap/astrovault/tests/test_execute_swap.rs b/contracts/adapters/swap/astrovault/tests/test_execute_swap.rs new file mode 100644 index 00000000..9e7972f6 --- /dev/null +++ b/contracts/adapters/swap/astrovault/tests/test_execute_swap.rs @@ -0,0 +1,346 @@ +use astrovault::assets::pools::PoolInfoInput; +use astrovault::router::handle_msg::RouterReceiveMsg; +use astrovault::router::state::HopV2; +use astrovault::{assets::asset::AssetInfo, router::query_msg::RoutePoolType}; +use cosmwasm_std::{ + coin, + testing::{mock_dependencies, mock_env, mock_info}, + to_json_binary, Addr, Coin, QuerierResult, + ReplyOn::Never, + SubMsg, SystemResult, WasmMsg, WasmQuery, +}; +use skip::swap::{ExecuteMsg, SwapOperation}; +use skip_go_swap_adapter_astrovault::{ + error::{ContractError, ContractResult}, + state::{ASTROVAULT_ROUTER_ADDRESS, ENTRY_POINT_CONTRACT_ADDRESS}, +}; +use test_case::test_case; + +/* +Test Cases: + +Expect Success + - One Swap Operation + - Multiple Swap Operations + +Expect Error + - Unauthorized Caller (Only the stored entry point contract can call this function) + - No Coin Sent + - Bad Coin Sent + - More Than One Coin Sent + - No Swap Operations + */ + +// Define test parameters +struct Params { + caller: String, + info_funds: Vec, + swap_operations: Vec, + expected_messages: Vec, + expected_error: Option, +} + +// Test execute_swap +#[test_case( + Params { + caller: "entry_point".to_string(), + info_funds: vec![Coin::new(100, "os")], + swap_operations: vec![ + SwapOperation { + pool: "pool_1".to_string(), + denom_in: "os".to_string(), + denom_out: "ua".to_string(), + interface: None, + } + ], + expected_messages: vec![ + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "astrovault_router".to_string(), + msg: to_json_binary(&astrovault::router::handle_msg::ExecuteMsg::Receive( + cw20::Cw20ReceiveMsg { + sender: "swap_contract_address".to_string(), + amount: cosmwasm_std::Uint128::from(100u128), + msg: to_json_binary(&RouterReceiveMsg::RouteV2 { + hops: vec![ + HopV2::RatioHopInfo { pool: + PoolInfoInput::Addr("pool_1".to_string()), from_asset_index: 0 } + ], + minimum_receive: None, + to: None, + })?, + } + ))?, + funds: vec![coin(100, "os")], + }.into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "swap_contract_address".to_string(), + msg: to_json_binary(&ExecuteMsg::TransferFundsBack { + return_denom: "ua".to_string(), + swapper: Addr::unchecked("entry_point"), + })?, + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "One Swap Operation")] +#[test_case( + Params { + caller: "entry_point".to_string(), + info_funds: vec![Coin::new(100, "os")], + swap_operations: vec![ + SwapOperation { + pool: "pool_1".to_string(), + denom_in: "os".to_string(), + denom_out: "ua".to_string(), + interface: None, + }, + SwapOperation { + pool: "pool_2".to_string(), + denom_in: "ua".to_string(), + denom_out: "un".to_string(), + interface: None, + } + ], + expected_messages: vec![ + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "astrovault_router".to_string(), + msg: to_json_binary(&astrovault::router::handle_msg::ExecuteMsg::Receive( + cw20::Cw20ReceiveMsg { + sender: "swap_contract_address".to_string(), + amount: cosmwasm_std::Uint128::from(100u128), + msg: to_json_binary(&RouterReceiveMsg::RouteV2 { + hops: vec![ + HopV2::RatioHopInfo { pool: + PoolInfoInput::Addr("pool_1".to_string()), from_asset_index: 0 }, + HopV2::StandardHopInfo { pool: + PoolInfoInput::Addr("pool_2".to_string()), from_asset_index: 0 }, + ], + minimum_receive: None, + to: None, + })?, + } + ))?, + funds: vec![coin(100, "os")], + }.into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: WasmMsg::Execute { + contract_addr: "swap_contract_address".to_string(), + msg: to_json_binary(&ExecuteMsg::TransferFundsBack { + return_denom: "un".to_string(), + swapper: Addr::unchecked("entry_point"), + })?, + funds: vec![], + } + .into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "Multiple Swap Operations")] +#[test_case( + Params { + caller: "entry_point".to_string(), + info_funds: vec![Coin::new(100, "os")], + swap_operations: vec![], + expected_messages: vec![], + expected_error: Some(ContractError::SwapOperationsEmpty), + }; + "No Swap Operations")] +#[test_case( + Params { + caller: "entry_point".to_string(), + info_funds: vec![], + swap_operations: vec![ + SwapOperation { + pool: "pool_1".to_string(), + denom_in: "os".to_string(), + denom_out: "ua".to_string(), + interface: None, + } + ], + expected_messages: vec![], + expected_error: Some(ContractError::Payment(cw_utils::PaymentError::NoFunds{})), + }; + "No Coin Sent - Expect Error")] +#[test_case( + Params { + caller: "entry_point".to_string(), + info_funds: vec![ + Coin::new(100, "un"), // should be os + ], + swap_operations: vec![ + SwapOperation { + pool: "pool_1".to_string(), + denom_in: "os".to_string(), + denom_out: "ua".to_string(), + interface: None, + } + ], + expected_messages: vec![], + expected_error: Some(ContractError::CoinInDenomMismatch{}), + }; + "Bad Coin Sent - Expect Error")] +#[test_case( + Params { + caller: "entry_point".to_string(), + info_funds: vec![ + Coin::new(100, "un"), + Coin::new(100, "os"), + ], + swap_operations: vec![ + SwapOperation { + pool: "pool_1".to_string(), + denom_in: "os".to_string(), + denom_out: "ua".to_string(), + interface: None, + } + ], + expected_messages: vec![], + expected_error: Some(ContractError::Payment(cw_utils::PaymentError::MultipleDenoms{})), + }; + "More Than One Coin Sent - Expect Error")] +#[test_case( + Params { + caller: "random".to_string(), + info_funds: vec![ + Coin::new(100, "os"), + ], + swap_operations: vec![ + SwapOperation { + pool: "pool_1".to_string(), + denom_in: "os".to_string(), + denom_out: "ua".to_string(), + interface: None, + } + ], + expected_messages: vec![], + expected_error: Some(ContractError::Unauthorized), + }; + "Unauthorized Caller - Expect Error")] + +fn test_execute_swap(params: Params) -> ContractResult<()> { + // Create mock dependencies + let mut deps = mock_dependencies(); + let swap_ops = params.swap_operations.clone(); + + // Create mock wasm handler to handle the swap adapter contract query + let wasm_handler = move |query: &WasmQuery| -> QuerierResult { + match query { + WasmQuery::Smart { contract_addr, .. } => { + if contract_addr == "astrovault_router" { + let mut mock_route_pool_type_query_response = vec![]; + if !swap_ops.is_empty() { + mock_route_pool_type_query_response.push(RoutePoolType { + pool_addr: "pool_1".to_string(), + pool_type: "hybrid".to_string(), + pool_asset_infos: vec![ + AssetInfo::NativeToken { + denom: "os".to_string(), + }, + AssetInfo::NativeToken { + denom: "ua".to_string(), + }, + ], + }); + } + if swap_ops.len() > 1 { + mock_route_pool_type_query_response.push(RoutePoolType { + pool_addr: "pool_2".to_string(), + pool_type: "standard".to_string(), + pool_asset_infos: vec![ + AssetInfo::NativeToken { + denom: "ua".to_string(), + }, + AssetInfo::NativeToken { + denom: "un".to_string(), + }, + ], + }); + } + + SystemResult::Ok(cosmwasm_std::ContractResult::Ok( + to_json_binary(&mock_route_pool_type_query_response).unwrap(), + )) + } else { + panic!("Unsupported contract: {:?}", query); + } + } + _ => panic!("Unsupported query: {:?}", query), + } + }; + + // Update querier with mock wasm handler + deps.querier.update_wasm(wasm_handler); + + // Create mock env + let mut env = mock_env(); + env.contract.address = Addr::unchecked("swap_contract_address"); + + // 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(¶ms.caller, info_funds); + + // Store the entry point contract address + ENTRY_POINT_CONTRACT_ADDRESS.save(deps.as_mut().storage, &Addr::unchecked("entry_point"))?; + ASTROVAULT_ROUTER_ADDRESS.save(deps.as_mut().storage, &Addr::unchecked("astrovault_router"))?; + + // Call execute_swap with the given test parameters + let res = skip_go_swap_adapter_astrovault::contract::execute( + deps.as_mut(), + env, + info, + ExecuteMsg::Swap { + operations: params.swap_operations.clone(), + }, + ); + + // Assert the behavior is correct + 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 messages 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()); + } + } + + Ok(()) +} diff --git a/contracts/adapters/swap/astrovault/tests/test_execute_transfer_funds_back.rs b/contracts/adapters/swap/astrovault/tests/test_execute_transfer_funds_back.rs new file mode 100644 index 00000000..fbb1898b --- /dev/null +++ b/contracts/adapters/swap/astrovault/tests/test_execute_transfer_funds_back.rs @@ -0,0 +1,240 @@ +use cosmwasm_std::{ + testing::{mock_dependencies_with_balances, mock_env, mock_info}, + to_json_binary, Addr, BankMsg, Coin, QuerierResult, + ReplyOn::Never, + SubMsg, SystemResult, Uint128, WasmQuery, +}; +use skip::{error::SkipError, swap::ExecuteMsg}; +use skip_go_swap_adapter_astrovault::{ + error::{ContractError, ContractResult}, + state::{ASTROVAULT_CASHBACK_ADDRESS, ASTROVAULT_ROUTER_ADDRESS, ENTRY_POINT_CONTRACT_ADDRESS}, +}; +use test_case::test_case; + +/* +Test Cases: + +Expect Success + - One Coin Balance + - Multiple Coin Balance + - No Coin Balance (This will fail at the bank module if attempted) + +Expect Error + - Unauthorized Caller (Only contract itself can call this function) + */ + +// Define test parameters +struct Params { + caller: String, + contract_balance: Vec, + return_denom: String, + expected_messages: Vec, + expected_error: Option, +} + +// Test execute_transfer_funds_back +#[test_case( + Params { + caller: "swap_contract_address".to_string(), + contract_balance: vec![Coin::new(100, "os")], + return_denom: "os".to_string(), + expected_messages: vec![ + SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "swapper".to_string(), + amount: vec![Coin::new(100, "os")], + }.into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: cosmwasm_std::CosmosMsg::Wasm(cosmwasm_std::WasmMsg::Execute { + contract_addr: "astrovault_cashback".to_string(), + msg: to_json_binary(&cw20::Cw20ExecuteMsg::Transfer { + recipient: "astrovault_router".to_string(), + amount: Uint128::from(100u128), + }) + .unwrap(), + funds: vec![], + }), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "Transfers One Coin Balance")] +#[test_case( + Params { + caller: "swap_contract_address".to_string(), + contract_balance: vec![ + Coin::new(100, "os"), + Coin::new(100, "uatom"), + ], + return_denom: "os".to_string(), + expected_messages: vec![ + SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "swapper".to_string(), + amount: vec![ + Coin::new(100, "os"), + Coin::new(100, "uatom") + ], + }.into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: cosmwasm_std::CosmosMsg::Wasm(cosmwasm_std::WasmMsg::Execute { + contract_addr: "astrovault_cashback".to_string(), + msg: to_json_binary(&cw20::Cw20ExecuteMsg::Transfer { + recipient: "astrovault_router".to_string(), + amount: Uint128::from(100u128), + }) + .unwrap(), + funds: vec![], + }), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "Transfers Multiple Coin Balance")] +#[test_case( + Params { + caller: "swap_contract_address".to_string(), + contract_balance: vec![], + return_denom: "os".to_string(), + expected_messages: vec![ + SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "swapper".to_string(), + amount: vec![], + }.into(), + gas_limit: None, + reply_on: Never, + }, + SubMsg { + id: 0, + msg: cosmwasm_std::CosmosMsg::Wasm(cosmwasm_std::WasmMsg::Execute { + contract_addr: "astrovault_cashback".to_string(), + msg: to_json_binary(&cw20::Cw20ExecuteMsg::Transfer { + recipient: "astrovault_router".to_string(), + amount: Uint128::from(100u128), + }) + .unwrap(), + funds: vec![], + }), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: None, + }; + "Transfers No Coin Balance")] +#[test_case( + Params { + caller: "random".to_string(), + contract_balance: vec![], + return_denom: "os".to_string(), + expected_messages: vec![ + SubMsg { + id: 0, + msg: BankMsg::Send { + to_address: "swapper".to_string(), + amount: vec![], + }.into(), + gas_limit: None, + reply_on: Never, + }, + ], + expected_error: Some(ContractError::Skip(SkipError::Unauthorized)), + }; + "Unauthorized Caller")] +fn test_execute_transfer_funds_back(params: Params) -> ContractResult<()> { + // Convert params contract balance to a slice + let contract_balance: &[Coin] = ¶ms.contract_balance; + + // Create mock dependencies + let mut deps = mock_dependencies_with_balances(&[("swap_contract_address", contract_balance)]); + + // Create mock wasm handler to handle the swap adapter contract query + let wasm_handler = move |query: &WasmQuery| -> QuerierResult { + match query { + WasmQuery::Smart { contract_addr, .. } => { + if contract_addr == "astrovault_cashback" { + SystemResult::Ok(cosmwasm_std::ContractResult::Ok( + to_json_binary(&cw20::BalanceResponse { + balance: Uint128::from(100u128), + }) + .unwrap(), + )) + } else { + panic!("Unsupported contract: {:?}", query); + } + } + _ => panic!("Unsupported query: {:?}", query), + } + }; + + // Update querier with mock wasm handler + deps.querier.update_wasm(wasm_handler); + + // Create mock env + let mut env = mock_env(); + env.contract.address = Addr::unchecked("swap_contract_address"); + + // Create mock info + let info = mock_info(¶ms.caller, &[]); + ENTRY_POINT_CONTRACT_ADDRESS.save(deps.as_mut().storage, &Addr::unchecked("entry_point"))?; + ASTROVAULT_ROUTER_ADDRESS.save(deps.as_mut().storage, &Addr::unchecked("astrovault_router"))?; + ASTROVAULT_CASHBACK_ADDRESS.save( + deps.as_mut().storage, + &Addr::unchecked("astrovault_cashback"), + )?; + + // Call execute_swap with the given test parameters + let res = skip_go_swap_adapter_astrovault::contract::execute( + deps.as_mut(), + env, + info, + ExecuteMsg::TransferFundsBack { + return_denom: params.return_denom, + swapper: Addr::unchecked("swapper"), + }, + ); + + // Assert the behavior is correct + 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 messages 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()); + } + } + + Ok(()) +} diff --git a/packages/skip/src/swap.rs b/packages/skip/src/swap.rs index 7dd63895..5e1c82cb 100644 --- a/packages/skip/src/swap.rs +++ b/packages/skip/src/swap.rs @@ -34,6 +34,12 @@ pub struct InstantiateMsg { pub entry_point_contract_address: String, } +#[cw_serde] +pub struct AstrovaultAdapterInstantiateMsg { + pub entry_point_contract_address: String, + pub astrovault_router_contract_address: String, +} + #[cw_serde] pub struct DexterAdapterInstantiateMsg { pub entry_point_contract_address: String, From 84b789c593b195bf5b6816b822776dbc7eabfd2a Mon Sep 17 00:00:00 2001 From: fsoares Date: Tue, 1 Oct 2024 16:06:07 +0100 Subject: [PATCH 2/5] add way to include the spot price on smart swap with metadata query (weighted spot price calc) --- .../adapters/swap/astrovault/src/contract.rs | 82 +++++++++++-------- 1 file changed, 46 insertions(+), 36 deletions(-) diff --git a/contracts/adapters/swap/astrovault/src/contract.rs b/contracts/adapters/swap/astrovault/src/contract.rs index dfc1d194..9c029419 100644 --- a/contracts/adapters/swap/astrovault/src/contract.rs +++ b/contracts/adapters/swap/astrovault/src/contract.rs @@ -12,8 +12,8 @@ use astrovault::{ }, }; use cosmwasm_std::{ - entry_point, from_json, to_json_binary, Addr, BankMsg, Binary, CosmosMsg, Deps, DepsMut, Env, - MessageInfo, QueryRequest, Response, Uint128, WasmMsg, WasmQuery, + entry_point, from_json, to_json_binary, Addr, BankMsg, Binary, CosmosMsg, Decimal, Deps, + DepsMut, Env, MessageInfo, QueryRequest, Response, Uint128, WasmMsg, WasmQuery, }; use cw2::{ensure_from_older_version, set_contract_version}; use cw20::{Cw20Coin, Cw20Contract, Cw20ReceiveMsg}; @@ -362,6 +362,16 @@ fn query_simulate_swap_exact_asset_in( return Err(ContractError::CoinInDenomMismatch); } + let (asset_out, _) = simulate_swap_exact_asset_in(deps, asset_in, swap_operations)?; + + Ok(asset_out) +} + +fn simulate_swap_exact_asset_in( + deps: Deps, + asset_in: Asset, + swap_operations: Vec, +) -> ContractResult<(Asset, Decimal)> { let astrovault_router_contract_address = ASTROVAULT_ROUTER_ADDRESS.load(deps.storage)?; let hops = convert_operations_to_hops( deps, @@ -378,10 +388,13 @@ fn query_simulate_swap_exact_asset_in( })?, }))?; - Ok(Asset::new( - deps.api, - &simulation_response.to.info.to_string(), - simulation_response.to.amount, + Ok(( + Asset::new( + deps.api, + &simulation_response.to.info.to_string(), + simulation_response.to.amount, + ), + simulation_response.to_spot_price, )) } @@ -402,31 +415,13 @@ fn query_simulate_swap_exact_asset_in_with_metadata( return Err(ContractError::CoinInDenomMismatch); } - let astrovault_router_contract_address = ASTROVAULT_ROUTER_ADDRESS.load(deps.storage)?; - let hops = convert_operations_to_hops( - deps, - astrovault_router_contract_address.to_string(), - swap_operations, - )?; - - let simulation_response: QueryRouteSwapSimulation = - deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart { - contract_addr: astrovault_router_contract_address.to_string(), - msg: to_json_binary(&router::query_msg::QueryMsg::RouteSwapSimulation { - amount: asset_in.amount(), - hops, - })?, - }))?; + let (asset_out, spot_price) = simulate_swap_exact_asset_in(deps, asset_in, swap_operations)?; // Create the response let response = SimulateSwapExactAssetInResponse { - asset_out: Asset::new( - deps.api, - &simulation_response.to.info.to_string(), - simulation_response.to.amount, - ), + asset_out, spot_price: if include_spot_price { - Some(simulation_response.to_spot_price) + Some(spot_price) } else { None }, @@ -440,44 +435,59 @@ fn query_simulate_smart_swap_exact_asset_in( ask_denom: String, routes: Vec, ) -> ContractResult { - simulate_smart_swap_exact_asset_in(deps, ask_denom, routes) + let (asset_out, _) = simulate_smart_swap_exact_asset_in(deps, ask_denom, routes)?; + + Ok(asset_out) } fn simulate_smart_swap_exact_asset_in( deps: Deps, ask_denom: String, routes: Vec, -) -> ContractResult { +) -> ContractResult<(Asset, Vec)> { let mut asset_out = Asset::new(deps.api, &ask_denom, Uint128::zero()); + let mut spot_prices = Vec::new(); for route in &routes { - let route_asset_out = query_simulate_swap_exact_asset_in( + let (route_asset_out, spot_price) = simulate_swap_exact_asset_in( deps, route.offer_asset.clone(), route.operations.clone(), )?; asset_out.add(route_asset_out.amount())?; + spot_prices.push(spot_price); } - Ok(asset_out) + Ok((asset_out, spot_prices)) } fn query_simulate_smart_swap_exact_asset_in_with_metadata( deps: Deps, - _asset_in: Asset, + asset_in: Asset, ask_denom: String, routes: Vec, - _include_spot_price: bool, + include_spot_price: bool, ) -> ContractResult { - let asset_out = simulate_smart_swap_exact_asset_in(deps, ask_denom, routes.clone())?; + let (asset_out, spot_prices) = + simulate_smart_swap_exact_asset_in(deps, ask_denom, routes.clone())?; - // TODO: Implement spot price calculation for smart swaps - let response = SimulateSmartSwapExactAssetInResponse { + let mut response = SimulateSmartSwapExactAssetInResponse { asset_out, spot_price: None, }; + if include_spot_price { + let mut spot_price = Decimal::zero(); + for (i, route) in routes.iter().enumerate() { + let weight = Decimal::from_ratio(route.offer_asset.amount(), asset_in.amount()); + let route_spot_price = spot_prices[i]; + spot_price += weight * route_spot_price; + } + + response.spot_price = Some(spot_price); + } + Ok(response) } From 8b86e1af81ff78b2bb964c278e50751a86fd1565 Mon Sep 17 00:00:00 2001 From: Jeremy Liu <31809888+NotJeremyLiu@users.noreply.github.com> Date: Sun, 27 Oct 2024 19:35:52 -0700 Subject: [PATCH 3/5] remove unused import --- contracts/adapters/swap/astrovault/src/contract.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/contracts/adapters/swap/astrovault/src/contract.rs b/contracts/adapters/swap/astrovault/src/contract.rs index 9c029419..7ceb3673 100644 --- a/contracts/adapters/swap/astrovault/src/contract.rs +++ b/contracts/adapters/swap/astrovault/src/contract.rs @@ -2,14 +2,11 @@ use crate::{ error::{ContractError, ContractResult}, state::{ASTROVAULT_CASHBACK_ADDRESS, ASTROVAULT_ROUTER_ADDRESS, ENTRY_POINT_CONTRACT_ADDRESS}, }; -use astrovault::{ - nft_booster::handle_msg, - router::{ - self, - handle_msg::RouterReceiveMsg, - query_msg::{ConfigResponse, QueryRouteSwapSimulation, RoutePoolType}, - state::HopV2, - }, +use astrovault::router::{ + self, + handle_msg::RouterReceiveMsg, + query_msg::{ConfigResponse, QueryRouteSwapSimulation, RoutePoolType}, + state::HopV2, }; use cosmwasm_std::{ entry_point, from_json, to_json_binary, Addr, BankMsg, Binary, CosmosMsg, Decimal, Deps, From bc8fa458a3126bfe626296bbaf3d4110d55b78c5 Mon Sep 17 00:00:00 2001 From: Jeremy Liu <31809888+NotJeremyLiu@users.noreply.github.com> Date: Tue, 29 Oct 2024 15:11:59 -0700 Subject: [PATCH 4/5] remove duplicate validation both of these checks are already validated in the entrypoint contract, removing to reduce gas --- contracts/adapters/swap/astrovault/src/contract.rs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/contracts/adapters/swap/astrovault/src/contract.rs b/contracts/adapters/swap/astrovault/src/contract.rs index 7ceb3673..1b303a8b 100644 --- a/contracts/adapters/swap/astrovault/src/contract.rs +++ b/contracts/adapters/swap/astrovault/src/contract.rs @@ -133,18 +133,7 @@ pub fn execute( match msg { ExecuteMsg::Receive(cw20_msg) => receive_cw20(deps, env, info, cw20_msg), ExecuteMsg::Swap { operations } => { - // validate that there's at least one swap operation - if operations.is_empty() { - return Err(ContractError::SwapOperationsEmpty); - } - let coin = one_coin(&info)?; - - // validate that the one coin is the same as the first swap operation's denom in - if coin.denom != operations.first().unwrap().denom_in { - return Err(ContractError::CoinInDenomMismatch); - } - execute_swap(deps, env, info, coin.amount, operations) } ExecuteMsg::TransferFundsBack { From 5af0f5b24d3cdc4b2ac3f5a8f98c38b2c240c65e Mon Sep 17 00:00:00 2001 From: Jeremy Liu <31809888+NotJeremyLiu@users.noreply.github.com> Date: Tue, 29 Oct 2024 15:17:09 -0700 Subject: [PATCH 5/5] remove tests that test removed validation --- .../astrovault/tests/test_execute_swap.rs | 29 ------------------- 1 file changed, 29 deletions(-) diff --git a/contracts/adapters/swap/astrovault/tests/test_execute_swap.rs b/contracts/adapters/swap/astrovault/tests/test_execute_swap.rs index 9e7972f6..8bf20920 100644 --- a/contracts/adapters/swap/astrovault/tests/test_execute_swap.rs +++ b/contracts/adapters/swap/astrovault/tests/test_execute_swap.rs @@ -26,9 +26,7 @@ Expect Success Expect Error - Unauthorized Caller (Only the stored entry point contract can call this function) - No Coin Sent - - Bad Coin Sent - More Than One Coin Sent - - No Swap Operations */ // Define test parameters @@ -157,15 +155,6 @@ struct Params { expected_error: None, }; "Multiple Swap Operations")] -#[test_case( - Params { - caller: "entry_point".to_string(), - info_funds: vec![Coin::new(100, "os")], - swap_operations: vec![], - expected_messages: vec![], - expected_error: Some(ContractError::SwapOperationsEmpty), - }; - "No Swap Operations")] #[test_case( Params { caller: "entry_point".to_string(), @@ -182,24 +171,6 @@ struct Params { expected_error: Some(ContractError::Payment(cw_utils::PaymentError::NoFunds{})), }; "No Coin Sent - Expect Error")] -#[test_case( - Params { - caller: "entry_point".to_string(), - info_funds: vec![ - Coin::new(100, "un"), // should be os - ], - swap_operations: vec![ - SwapOperation { - pool: "pool_1".to_string(), - denom_in: "os".to_string(), - denom_out: "ua".to_string(), - interface: None, - } - ], - expected_messages: vec![], - expected_error: Some(ContractError::CoinInDenomMismatch{}), - }; - "Bad Coin Sent - Expect Error")] #[test_case( Params { caller: "entry_point".to_string(),