diff --git a/Cargo.lock b/Cargo.lock index 72335784..defa2e4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -231,12 +231,11 @@ dependencies = [ [[package]] name = "cosmwasm-crypto" -version = "1.5.5" +version = "1.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd50718a2b6830ce9eb5d465de5a018a12e71729d66b70807ce97e6dd14f931d" +checksum = "58535cbcd599b3c193e3967c8292fe1dbbb5de7c2a2d87380661091dd4744044" dependencies = [ "digest 0.10.7", - "ecdsa", "ed25519-zebra", "k256", "rand_core 0.6.4", @@ -1185,9 +1184,9 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "k256" -version = "0.13.1" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cadb76004ed8e97623117f3df85b17aaa6626ab0b0831e6573f104df16cd1bcc" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" dependencies = [ "cfg-if", "ecdsa", @@ -2008,6 +2007,21 @@ dependencies = [ "thiserror", ] +[[package]] +name = "skip-go-swap-adapter-mantra-dex" +version = "0.3.0" +dependencies = [ + "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-osmosis-poolmanager" version = "0.3.0" diff --git a/contracts/adapters/swap/mantra-dex/Cargo.toml b/contracts/adapters/swap/mantra-dex/Cargo.toml new file mode 100644 index 00000000..b946a74f --- /dev/null +++ b/contracts/adapters/swap/mantra-dex/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "skip-go-swap-adapter-mantra-dex" +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] +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/mantra-dex/README.md b/contracts/adapters/swap/mantra-dex/README.md new file mode 100644 index 00000000..dc379b80 --- /dev/null +++ b/contracts/adapters/swap/mantra-dex/README.md @@ -0,0 +1,110 @@ +# MANTRA Dex Adapter Contract + +The MANTRA Dex swap adapter contract is responsible for: +1. Taking the standardized entry point swap operations message format and converting it to MANTRA dex pool swaps message format. +2. Swapping by dispatching swaps to MANTRA Dex pool manager. +3. Providing query methods that can be called by the entry point contract (generally, to any external actor) to simulate multi-hop swaps that either specify an exact amount in (estimating how much would be received from the swap) or an exact amount out (estimating how much is required to get the specified amount out). + +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 MANTRA Dex swap adapter contract using the Entrypoint contract address provided in the instantiation message. + +``` json +{ + "entry_point_contract_address": "mantra...", + "mantra_pool_manager_address": "mantra..." +} +``` + +## ExecuteMsg + +### `swap` + +Swaps the coin sent using the operations provided. + +``` json +{ + "swap": { + "operations": [ + { + "pool": "o.uom.uusdc", + "denom_in": "uusdc", + "denom_out": "uom" + }, + { + "pool": "o.uom.uatom", + "denom_in": "uom", + "denom_out": "uatom" + } + ] + } +} +``` + +## QueryMsg + +### `simulate_swap_exact_coin_out` + +Returns the coin in required to receive the `coin_out` specified in the call (swapped through the `swap_operatons` provided) + +Query: +``` json +{ + "simulate_swap_exact_coin_out": { + "coin_out": { + "denom": "uom", + "amount": "200000" + }, + "swap_operations": [ + { + "pool": "o.uom.uusdc", + "denom_in": "uusdc", + "denom_out": "uom" + } + ] + } +} +``` + +Response: +``` json +{ + "denom": "uusdc", + "amount": "100" +} +``` + +### `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": "uusdc", + "amount": "100" + }, + "swap_operations": [ + { + "pool": "o.uom.uusdc", + "denom_in": "uusdc", + "denom_out": "uom" + } + ] + } +} +``` + +Response: +``` json +{ + "denom": "uom", + "amount": "100000" +} +``` \ No newline at end of file diff --git a/contracts/adapters/swap/mantra-dex/src/bin/white-whale-schema.rs b/contracts/adapters/swap/mantra-dex/src/bin/white-whale-schema.rs new file mode 100644 index 00000000..4f4733f0 --- /dev/null +++ b/contracts/adapters/swap/mantra-dex/src/bin/white-whale-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/mantra-dex/src/contract.rs b/contracts/adapters/swap/mantra-dex/src/contract.rs new file mode 100644 index 00000000..4ac50def --- /dev/null +++ b/contracts/adapters/swap/mantra-dex/src/contract.rs @@ -0,0 +1,626 @@ +use crate::state::MANTRA_DEX_POOL_MANAGER_ADDRESS; +use crate::{ + error::{ContractError, ContractResult}, + state::ENTRY_POINT_CONTRACT_ADDRESS, +}; +use cosmwasm_std::{ + ensure, entry_point, to_json_binary, wasm_execute, Binary, Decimal, Deps, DepsMut, Env, + MessageInfo, Response, Uint128, +}; +use cw2::set_contract_version; +use cw_utils::one_coin; +use skip::swap::MantraDexInstantiateMsg; +use skip::{ + asset::Asset, + swap::{ + get_ask_denom_for_routes, ExecuteMsg, MigrateMsg, QueryMsg, Route, + SimulateSmartSwapExactAssetInResponse, SimulateSwapExactAssetInResponse, + SimulateSwapExactAssetOutResponse, SwapOperation, + }, +}; + +use crate::pool_manager::{ + ExecuteMsg as MantraPoolManagerExecuteMsg, QueryMsg as MantraQueryMsg, + ReverseSimulationResponse, SimulationResponse, SwapOperation as MantraSwapOperation, + MAX_ALLOWED_SLIPPAGE, +}; + +/////////////// +/// MIGRATE /// +/////////////// + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> ContractResult { + unimplemented!() +} + +/////////////////// +/// INSTANTIATE /// +/////////////////// + +// Contract name and version used for migration. +const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: MantraDexInstantiateMsg, +) -> 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)?; + + // Validate entry point contract address + let checked_mantra_pool_manager_address = + deps.api.addr_validate(&msg.mantra_pool_manager_address)?; + + // Store MANTRA dex pool manager address + MANTRA_DEX_POOL_MANAGER_ADDRESS.save(deps.storage, &checked_mantra_pool_manager_address)?; + + Ok(Response::new() + .add_attribute("action", "instantiate") + .add_attribute( + "entry_point_contract_address", + checked_entry_point_contract_address.to_string(), + ) + .add_attribute( + "mantra_pool_manager_address", + checked_mantra_pool_manager_address.to_string(), + )) +} + +/////////////// +/// EXECUTE /// +/////////////// + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> ContractResult { + match msg { + ExecuteMsg::Swap { operations } => execute_swap(deps, info, operations), + _ => { + unimplemented!() + } + } +} + +fn execute_swap( + deps: DepsMut, + info: MessageInfo, + operations: Vec, +) -> ContractResult { + // Get entry point contract address from storage + let entry_point_contract_address = ENTRY_POINT_CONTRACT_ADDRESS.load(deps.storage)?; + + // Enforce the caller is the entry point contract + if info.sender != entry_point_contract_address { + return Err(ContractError::Unauthorized); + } + + // Get coin in from the message info, error if there is not exactly one coin sent + let coin_in = one_coin(&info)?; + + // sanity check + ensure!( + coin_in.amount != Uint128::zero(), + ContractError::NoOfferAssetAmount + ); + + // Create a response object to return + let response: Response = Response::new().add_attribute("action", "execute_swap"); + + // map SwapOperation into MantraSwapOperation + let mantra_swap_operations: Vec = operations + .iter() + .map(|op| MantraSwapOperation::MantraSwap { + token_in_denom: op.denom_in.clone(), + token_out_denom: op.denom_out.clone(), + pool_identifier: op.pool.clone(), + }) + .collect(); + + ensure!( + !mantra_swap_operations.is_empty(), + ContractError::SwapOperationsEmpty + ); + + let msg = MantraPoolManagerExecuteMsg::ExecuteSwapOperations { + operations: mantra_swap_operations, + minimum_receive: None, + receiver: Some(entry_point_contract_address.to_string()), + max_spread: Some(MAX_ALLOWED_SLIPPAGE.parse::()?), + }; + + // Create swap message on MANTRA dex pool manager + let mantra_dex_pool_manager = MANTRA_DEX_POOL_MANAGER_ADDRESS.load(deps.storage)?; + + Ok(response + .add_message(wasm_execute(mantra_dex_pool_manager, &msg, vec![coin_in])?) + .add_attribute("action", "swap")) +} + +///////////// +/// 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::SimulateSwapExactAssetOut { + asset_out, + swap_operations, + } => to_json_binary(&query_simulate_swap_exact_asset_out( + deps, + asset_out, + 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::SimulateSwapExactAssetOutWithMetadata { + asset_out, + swap_operations, + include_spot_price, + } => to_json_binary(&query_simulate_swap_exact_asset_out_with_metadata( + deps, + asset_out, + 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 { + asset_in, + routes, + 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, + )?) + } + } + .map_err(From::from) +} + +/// Queries the MANTRA dex pool manager 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 (asset_out, _) = simulate_swap_exact_asset_in(deps, asset_in, swap_operations, false)?; + + // Return the asset out + Ok(asset_out) +} + +/// Queries the MANTRA dex pool manager to simulate a multi-hop swap exact amount out +fn query_simulate_swap_exact_asset_out( + deps: Deps, + asset_out: Asset, + swap_operations: Vec, +) -> ContractResult { + // Error if swap operations is empty + let Some(last_op) = swap_operations.last() else { + return Err(ContractError::SwapOperationsEmpty); + }; + + // Ensure asset_out's denom is the same as the last swap operation's denom out + if asset_out.denom() != last_op.denom_out { + return Err(ContractError::CoinOutDenomMismatch); + } + + let (asset_in, _) = simulate_swap_exact_asset_out(deps, asset_out, swap_operations, false)?; + + // Return the asset in needed + Ok(asset_in) +} + +fn query_simulate_smart_swap_exact_asset_in( + deps: Deps, + ask_denom: String, + routes: Vec, +) -> ContractResult { + let (asset_out, _) = simulate_smart_swap_exact_asset_in(deps, ask_denom, routes, false)?; + + Ok(asset_out) +} + +/// Queries the MANTRA dex pool manager 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); + } + + // Determine if we should request the simulation responses from simulate_swap_exact_asset_in + let mut include_sim_resps = false; + if include_spot_price { + include_sim_resps = true; + } + + // Simulate the swap exact amount in + let (asset_out, sim_resps) = simulate_swap_exact_asset_in( + deps, + asset_in.clone(), + swap_operations.clone(), + include_sim_resps, + )?; + + // Create the response + let mut response = SimulateSwapExactAssetInResponse { + asset_out, + spot_price: None, + }; + + // Include the spot price in the response if requested + if include_spot_price { + response.spot_price = Some(calculate_spot_price_from_simulation_responses( + deps, + asset_in, + swap_operations, + sim_resps, + )?) + } + + Ok(response) +} + +/// Queries the MANTRA dex pool manager to simulate a multi-hop swap exact amount out with metadata +fn query_simulate_swap_exact_asset_out_with_metadata( + deps: Deps, + asset_out: Asset, + swap_operations: Vec, + include_spot_price: bool, +) -> ContractResult { + // Error if swap operations is empty + let Some(last_op) = swap_operations.last() else { + return Err(ContractError::SwapOperationsEmpty); + }; + + // Ensure asset_out's denom is the same as the last swap operation's denom out + if asset_out.denom() != last_op.denom_out { + return Err(ContractError::CoinOutDenomMismatch); + } + + // Determine if we should request the simulation responses from simulate_swap_exact_asset_out + let mut include_sim_resps = false; + if include_spot_price { + include_sim_resps = true; + } + + // Simulate the swap exact amount out + let (asset_in, sim_resps) = simulate_swap_exact_asset_out( + deps, + asset_out.clone(), + swap_operations.clone(), + include_sim_resps, + )?; + + // Create the response + let mut response = SimulateSwapExactAssetOutResponse { + asset_in, + spot_price: None, + }; + + // Include the spot price in the response if requested + if include_spot_price { + response.spot_price = Some(calculate_spot_price_from_reverse_simulation_responses( + deps, + asset_out, + swap_operations, + sim_resps, + )?) + } + + Ok(response) +} + +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, simulation_responses) = + simulate_smart_swap_exact_asset_in(deps, ask_denom, routes.clone(), include_spot_price)?; + + let mut response = SimulateSmartSwapExactAssetInResponse { + asset_out, + spot_price: None, + }; + + if include_spot_price { + response.spot_price = Some(calculate_weighted_spot_price_from_simulation_responses( + deps, + asset_in, + routes, + simulation_responses, + )?) + } + + Ok(response) +} + +fn assert_max_spread(return_amount: Uint128, spread_amount: Uint128) -> ContractResult<()> { + let max_spread = MAX_ALLOWED_SLIPPAGE.parse::()?; + if Decimal::from_ratio(spread_amount, return_amount + spread_amount) > max_spread { + return Err(ContractError::MaxSpreadAssertion {}); + } + Ok(()) +} + +/// Simulates a swap exact amount in request, returning the asset out and optionally the reverse simulation responses +fn simulate_swap_exact_asset_in( + deps: Deps, + asset_in: Asset, + swap_operations: Vec, + include_responses: bool, +) -> ContractResult<(Asset, Vec)> { + let mantra_pool_manager = MANTRA_DEX_POOL_MANAGER_ADDRESS.load(deps.storage)?; + + let (asset_out, responses) = swap_operations.iter().try_fold( + (asset_in, Vec::new()), + |(asset_out, mut responses), operation| -> Result<_, ContractError> { + let offer_asset = match asset_out { + Asset::Native(coin) => coin, + Asset::Cw20(_) => unimplemented!("CW20 not supported"), + }; + + // Query mantra's pool manager to get the simulation response + let res: SimulationResponse = deps.querier.query_wasm_smart( + &mantra_pool_manager, + &MantraQueryMsg::Simulation { + offer_asset: offer_asset.clone(), + ask_asset_denom: operation.denom_out.clone(), + pool_identifier: operation.pool.clone(), + }, + )?; + + // Assert the operation does not exceed the max spread limit + assert_max_spread(res.return_amount, res.spread_amount)?; + + if include_responses { + responses.push(res.clone()); + } + + Ok(( + Asset::new(deps.api, &operation.denom_out, res.return_amount), + responses, + )) + }, + )?; + + Ok((asset_out, responses)) +} + +/// Simulates a swap exact amount out request, returning the asset in needed and optionally the reverse simulation responses +fn simulate_swap_exact_asset_out( + deps: Deps, + asset_out: Asset, + swap_operations: Vec, + include_responses: bool, +) -> ContractResult<(Asset, Vec)> { + let mantra_pool_manager = MANTRA_DEX_POOL_MANAGER_ADDRESS.load(deps.storage)?; + + let (asset_in, responses) = swap_operations.iter().rev().try_fold( + (asset_out, Vec::new()), + |(asset_in_needed, mut responses), operation| -> Result<_, ContractError> { + let ask_asset = match asset_in_needed { + Asset::Native(coin) => coin, + Asset::Cw20(_) => unimplemented!("CW20 not supported"), + }; + + // Query the mantra's pool manager to get the reverse simulation response + let res: ReverseSimulationResponse = deps.querier.query_wasm_smart( + &mantra_pool_manager, + &MantraQueryMsg::ReverseSimulation { + ask_asset: ask_asset.clone(), + offer_asset_denom: operation.denom_in.to_string(), + pool_identifier: operation.pool.to_string(), + }, + )?; + + // Assert the operation does not exceed the max spread limit + assert_max_spread(res.offer_amount, res.spread_amount)?; + + if include_responses { + responses.push(res.clone()); + } + + Ok(( + Asset::new( + deps.api, + &operation.denom_in, + res.offer_amount.checked_add(Uint128::one())?, + ), + responses, + )) + }, + )?; + + Ok((asset_in, responses)) +} + +fn simulate_smart_swap_exact_asset_in( + deps: Deps, + ask_denom: String, + routes: Vec, + include_responses: bool, +) -> ContractResult<(Asset, Vec>)> { + let mut asset_out = Asset::new(deps.api, &ask_denom, Uint128::zero()); + let mut simulation_responses = Vec::new(); + + for route in &routes { + let (route_asset_out, route_simulation_responses) = simulate_swap_exact_asset_in( + deps, + route.offer_asset.clone(), + route.operations.clone(), + include_responses, + )?; + + asset_out.add(route_asset_out.amount())?; + + if include_responses { + simulation_responses.push(route_simulation_responses); + } + } + + Ok((asset_out, simulation_responses)) +} + +fn calculate_weighted_spot_price_from_simulation_responses( + deps: Deps, + asset_in: Asset, + routes: Vec, + simulation_responses: Vec>, +) -> ContractResult { + let spot_price = routes.into_iter().zip(simulation_responses).try_fold( + Decimal::zero(), + |curr_spot_price, (route, res)| -> ContractResult { + let route_spot_price = calculate_spot_price_from_simulation_responses( + deps, + asset_in.clone(), + route.operations, + res, + )?; + + let weight = Decimal::from_ratio(route.offer_asset.amount(), asset_in.amount()); + + Ok(curr_spot_price + (route_spot_price * weight)) + }, + )?; + + Ok(spot_price) +} + +/// Calculates the spot price using simulation responses +fn calculate_spot_price_from_simulation_responses( + deps: Deps, + asset_in: Asset, + swap_operations: Vec, + simulation_responses: Vec, +) -> ContractResult { + let (_, spot_price) = swap_operations.iter().zip(simulation_responses).try_fold( + (asset_in, Decimal::one()), + |(asset_out, curr_spot_price), (op, res)| -> Result<_, ContractError> { + // Calculate the amount out without slippage + let amount_out_without_slippage = res + .return_amount + .checked_add(res.spread_amount)? + .checked_add(res.swap_fee_amount)? + .checked_add(res.protocol_fee_amount)? + .checked_add(res.burn_fee_amount)? + .checked_add(res.extra_fees_amount)?; + + Ok(( + Asset::new(deps.api, &op.denom_out, res.return_amount), + curr_spot_price.checked_mul(Decimal::from_ratio( + amount_out_without_slippage, + asset_out.amount(), + ))?, + )) + }, + )?; + + Ok(spot_price) +} + +/// Calculates the spot price using reverse simulaation responses +fn calculate_spot_price_from_reverse_simulation_responses( + deps: Deps, + asset_out: Asset, + swap_operations: Vec, + reverse_simulation_responses: Vec, +) -> ContractResult { + let (_, spot_price) = swap_operations + .iter() + .rev() + .zip(reverse_simulation_responses) + .try_fold( + (asset_out, Decimal::one()), + |(asset_in_needed, curr_spot_price), (op, res)| -> Result<_, ContractError> { + let amount_out_without_slippage = asset_in_needed + .amount() + .checked_add(res.spread_amount)? + .checked_add(res.swap_fee_amount)? + .checked_add(res.protocol_fee_amount)? + .checked_add(res.burn_fee_amount)? + .checked_add(res.extra_fees_amount)?; + + Ok(( + Asset::new( + deps.api, + &op.denom_in, + res.offer_amount.checked_add(Uint128::one())?, + ), + curr_spot_price.checked_mul(Decimal::from_ratio( + amount_out_without_slippage, + res.offer_amount, + ))?, + )) + }, + )?; + + Ok(spot_price) +} diff --git a/contracts/adapters/swap/mantra-dex/src/error.rs b/contracts/adapters/swap/mantra-dex/src/error.rs new file mode 100644 index 00000000..84d8b0e9 --- /dev/null +++ b/contracts/adapters/swap/mantra-dex/src/error.rs @@ -0,0 +1,38 @@ +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, +} diff --git a/contracts/adapters/swap/mantra-dex/src/fee.rs b/contracts/adapters/swap/mantra-dex/src/fee.rs new file mode 100644 index 00000000..12853a6b --- /dev/null +++ b/contracts/adapters/swap/mantra-dex/src/fee.rs @@ -0,0 +1,243 @@ +use std::fmt::{Display, Formatter}; + +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Decimal, Decimal256, StdError, StdResult, Uint128, Uint256}; + +#[cw_serde] +pub struct Fee { + pub share: Decimal, +} + +impl Fee { + /// Computes the fee for the given amount + pub fn compute(&self, amount: Uint256) -> StdResult { + Ok(Decimal256::from_ratio(amount, Uint256::one()) + .checked_mul(self.to_decimal_256()) + .map_err(|e| StdError::generic_err(e.to_string()))? + .to_uint_floor()) + } + + /// Converts a Fee to a Decimal256 + pub fn to_decimal_256(&self) -> Decimal256 { + Decimal256::from(self.share) + } + + /// Checks that the given [Fee] is valid, i.e. it's lower or equal to 100% + pub fn is_valid(&self) -> StdResult<()> { + if self.share >= Decimal::percent(100) { + return Err(StdError::generic_err("Invalid fee")); + } + Ok(()) + } +} + +impl Display for Fee { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}%", self.share * Decimal::percent(100)) + } +} + +/// Represents the fee structure for transactions within a pool. +/// +/// +/// # Fields +/// - `protocol_fee`: The fee percentage charged by the protocol on each transaction to support +/// operational and developmental needs. +/// - `swap_fee`: The fee percentage allocated to liquidity providers as a reward for supplying +/// liquidity to the pool, incentivizing participation and ensuring pool health. +/// - `burn_fee`: A fee percentage that is burned on each transaction, helping manage the token +/// economy by reducing supply over time, potentially increasing token value. +/// - `extra_fees`: A vector of custom fees allowing for extensible and adaptable fee structures +/// to meet diverse and evolving needs. Validation ensures that the total of all fees does not +/// exceed 100%, maintaining fairness and avoiding overcharging. +#[cw_serde] +pub struct PoolFee { + /// Fee percentage charged on each transaction for the protocol's benefit. + pub protocol_fee: Fee, + + /// Fee percentage allocated to liquidity providers on each swap. + pub swap_fee: Fee, + + /// Fee percentage that is burned on each transaction. Burning a portion of the transaction fee + /// helps in reducing the overall token supply. + pub burn_fee: Fee, + + /// A list of custom, additional fees that can be defined for specific use cases or additional + /// functionalities. This vector enables the flexibility to introduce new fees without altering + /// the core fee structure. Total of all fees, including custom ones, is validated to not exceed + /// 100%, ensuring a balanced and fair fee distribution. + pub extra_fees: Vec, +} + +impl PoolFee { + /// Validates the PoolFee structure to ensure the sum of all fees does not exceed 20%. + pub fn is_valid(&self) -> StdResult<()> { + let mut total_share = Decimal::zero(); + + // Validate predefined fees and accumulate their shares + let predefined_fees = [&self.protocol_fee, &self.swap_fee, &self.burn_fee]; + + for fee in predefined_fees.iter().copied() { + fee.is_valid()?; // Validates the fee is not >= 100% + total_share += fee.share; + } + + // Validate extra fees and accumulate their shares + for fee in &self.extra_fees { + fee.is_valid()?; // Validates the fee is not >= 100% + total_share += fee.share; + } + + // Check if the total share exceeds 20% + if total_share > Decimal::percent(20) { + return Err(StdError::generic_err("Total fees cannot exceed 20%")); + } + + Ok(()) + } + + /// Computes and applies all defined fees to a given amount. + /// Returns the total amount of fees deducted. + pub fn compute_and_apply_fees(&self, amount: Uint256) -> StdResult { + let mut total_fee_amount = Uint256::zero(); + + // Compute protocol fee + let protocol_fee_amount = self.protocol_fee.compute(amount)?; + total_fee_amount = total_fee_amount.checked_add(protocol_fee_amount)?; + + // Compute swap fee + let swap_fee_amount = self.swap_fee.compute(amount)?; + total_fee_amount = total_fee_amount.checked_add(swap_fee_amount)?; + + // Compute burn fee + let burn_fee_amount = self.burn_fee.compute(amount)?; + total_fee_amount = total_fee_amount.checked_add(burn_fee_amount)?; + + // Compute extra fees + for extra_fee in &self.extra_fees { + let extra_fee_amount = extra_fee.compute(amount)?; + total_fee_amount = total_fee_amount.checked_add(extra_fee_amount)?; + } + + // Convert the total fee amount to Uint128 (or handle potential conversion failure) + Uint128::try_from(total_fee_amount) + .map_err(|_| StdError::generic_err("Fee conversion error")) + } +} + +#[cfg(test)] +mod tests { + use cosmwasm_std::{Decimal, StdError, Uint128, Uint256}; + use test_case::test_case; + + use crate::fee::{Fee, PoolFee}; + + #[test] + fn valid_fee() { + let fee = Fee { + share: Decimal::from_ratio(9u128, 10u128), + }; + let res = fee.is_valid(); + match res { + Ok(_) => (), + Err(_) => panic!("this fee shouldn't fail"), + } + + let fee = Fee { + share: Decimal::from_ratio(Uint128::new(2u128), Uint128::new(100u128)), + }; + let res = fee.is_valid(); + match res { + Ok(_) => (), + Err(_) => panic!("this fee shouldn't fail"), + } + + let fee = Fee { + share: Decimal::zero(), + }; + let res = fee.is_valid(); + match res { + Ok(_) => (), + Err(_) => panic!("this fee shouldn't fail"), + } + } + + #[test] + fn invalid_fee() { + let fee = Fee { + share: Decimal::one(), + }; + assert_eq!(fee.is_valid(), Err(StdError::generic_err("Invalid fee"))); + + let fee = Fee { + share: Decimal::from_ratio(Uint128::new(2u128), Uint128::new(1u128)), + }; + assert_eq!(fee.is_valid(), Err(StdError::generic_err("Invalid fee"))); + } + + #[test_case( + Decimal::permille(1), Decimal::permille(2), Decimal::permille(1), Uint256::from(1000u128), Uint128::from(4u128); "low fee scenario" + )] + #[test_case( + Decimal::percent(1), Decimal::percent(2), Decimal::zero(), Uint256::from(1000u128), Uint128::from(30u128); "higher fee scenario" + )] + fn pool_fee_application( + protocol_fee_share: Decimal, + swap_fee_share: Decimal, + burn_fee_share: Decimal, + amount: Uint256, + expected_fee_deducted: Uint128, + ) { + let protocol_fee = Fee { + share: protocol_fee_share, + }; + let swap_fee = Fee { + share: swap_fee_share, + }; + let burn_fee = Fee { + share: burn_fee_share, + }; + let extra_fees = vec![]; // Assuming no extra fees for simplicity + + let pool_fee = PoolFee { + protocol_fee, + swap_fee, + burn_fee, + extra_fees, + }; + + let total_fee_deducted = pool_fee.compute_and_apply_fees(amount).unwrap(); + assert_eq!( + total_fee_deducted, expected_fee_deducted, + "The total deducted fees did not match the expected value." + ); + } + + #[test] + fn pool_fee_exceeds_limit() { + let protocol_fee = Fee { + share: Decimal::percent(10), + }; + let swap_fee = Fee { + share: Decimal::percent(5), + }; + let burn_fee = Fee { + share: Decimal::percent(5), + }; + let extra_fees = vec![Fee { + share: Decimal::percent(1), + }]; // Sum is 21% + + let pool_fee = PoolFee { + protocol_fee, + swap_fee, + burn_fee, + extra_fees, + }; + + assert_eq!( + pool_fee.is_valid(), + Err(StdError::generic_err("Total fees cannot exceed 20%")) + ); + } +} diff --git a/contracts/adapters/swap/mantra-dex/src/lib.rs b/contracts/adapters/swap/mantra-dex/src/lib.rs new file mode 100644 index 00000000..9320b3bd --- /dev/null +++ b/contracts/adapters/swap/mantra-dex/src/lib.rs @@ -0,0 +1,5 @@ +pub mod contract; +pub mod error; +pub mod fee; +pub mod pool_manager; +pub mod state; diff --git a/contracts/adapters/swap/mantra-dex/src/pool_manager.rs b/contracts/adapters/swap/mantra-dex/src/pool_manager.rs new file mode 100644 index 00000000..cf7b0545 --- /dev/null +++ b/contracts/adapters/swap/mantra-dex/src/pool_manager.rs @@ -0,0 +1,392 @@ +use std::fmt; + +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, Coin, Decimal, Uint128}; + +use crate::fee::PoolFee; + +pub const MAX_ALLOWED_SLIPPAGE: &str = "0.5"; + +/// The type of swap operation to perform. +#[cw_serde] +pub enum SwapOperation { + /// A swap operation that uses the MantraSwap router. + MantraSwap { + /// The token denom to swap in. + token_in_denom: String, + /// The token denom returning from the swap. + token_out_denom: String, + /// The identifier of the pool to use for the swap. + pool_identifier: String, + }, +} + +impl SwapOperation { + /// Retrieves the `token_in_denom` used for this swap operation. + pub fn get_input_asset_info(&self) -> &String { + match self { + SwapOperation::MantraSwap { token_in_denom, .. } => token_in_denom, + } + } + + pub fn get_target_asset_info(&self) -> String { + match self { + SwapOperation::MantraSwap { + token_out_denom, .. + } => token_out_denom.clone(), + } + } + + pub fn get_pool_identifer(&self) -> String { + match self { + SwapOperation::MantraSwap { + pool_identifier, .. + } => pool_identifier.clone(), + } + } +} + +impl fmt::Display for SwapOperation { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + SwapOperation::MantraSwap { + token_in_denom, + token_out_denom, + pool_identifier, + } => write!( + f, + "MantraSwap {{ token_in_info: {token_in_denom}, token_out_info: {token_out_denom}, pool_identifier: {pool_identifier} }}" + ), + } + } +} + +/// Contains the pool information +#[cw_serde] +pub struct PoolInfo { + /// The identifier for the pool. + pub pool_identifier: String, + /// The asset denoms for the pool. + pub asset_denoms: Vec, + /// The LP denom of the pool. + pub lp_denom: String, + /// The decimals for the given asset denoms, provided in the same order as asset_denoms. + pub asset_decimals: Vec, + /// The total amount of assets in the pool. + pub assets: Vec, + /// The type of pool to create. + pub pool_type: PoolType, + /// The fees for the pool. + pub pool_fees: PoolFee, +} + +/// Possible pool types, it can be either a constant product (xyk) pool or a stable swap pool. +#[cw_serde] +pub enum PoolType { + /// A stable swap pool. + StableSwap { + /// The amount of amplification to perform on the constant product part of the swap formula. + amp: u64, + }, + /// xyk pool + ConstantProduct, +} + +impl PoolType { + /// Gets a string representation of the pair type + pub fn get_label(&self) -> &str { + match self { + PoolType::ConstantProduct => "ConstantProduct", + PoolType::StableSwap { .. } => "StableSwap", + } + } +} + +/// The contract configuration. +#[cw_serde] +pub struct Config { + /// The address where the collected fees go to. + pub fee_collector_addr: Addr, + /// The address of the farm manager contract. + pub farm_manager_addr: Addr, + /// How much it costs to create a pool. It helps prevent spamming of new pools. + pub pool_creation_fee: Coin, + // Whether or not swaps, deposits, and withdrawals are enabled + pub feature_toggle: FeatureToggle, +} + +#[cw_serde] +pub struct InstantiateMsg { + /// The address where the collected fees go to. + pub fee_collector_addr: String, + /// The address of the farm manager contract. + pub farm_manager_addr: String, + /// How much it costs to create a pool. It helps prevent spamming of new pools. + pub pool_creation_fee: Coin, +} + +#[cw_serde] +pub struct MigrateMsg {} + +#[cw_serde] +pub enum ExecuteMsg { + /// Creates a new pool. + CreatePool { + /// The asset denoms for the pool. + asset_denoms: Vec, + /// The decimals for the given asset denoms, provided in the same order as `asset_denoms`. + asset_decimals: Vec, + /// The fees for the pool. + pool_fees: PoolFee, + /// The type of pool to create. + pool_type: PoolType, + /// The identifier for the pool. + pool_identifier: Option, + }, + /// Provides liquidity to the pool + ProvideLiquidity { + /// A percentage value representing the acceptable slippage for the operation. + /// When provided, if the slippage exceeds this value, the liquidity provision will not be + /// executed. + slippage_tolerance: Option, + /// The maximum allowable spread between the bid and ask prices for the pool. + /// When provided, if the spread exceeds this value, the liquidity provision will not be + /// executed. + max_spread: Option, + /// The receiver of the LP + receiver: Option, + /// The identifier for the pool to provide liquidity for. + pool_identifier: String, + /// The amount of time in seconds to unlock tokens if taking part on the farms. If not passed, + /// the tokens will not be locked and the LP tokens will be returned to the user. + unlocking_duration: Option, + /// The identifier of the position to lock the LP tokens in the farm manager, if any. + lock_position_identifier: Option, + }, + /// Swap an offer asset to the other + Swap { + /// The return asset of the swap. + ask_asset_denom: String, + /// The belief price of the swap. + belief_price: Option, + /// The maximum spread to incur when performing the swap. If the spread exceeds this value, + /// the swap will not be executed. Max 50%. + max_spread: Option, + /// The recipient of the output tokens. If not provided, the tokens will be sent to the sender + /// of the message. + receiver: Option, + /// The identifier for the pool to swap in. + pool_identifier: String, + }, + /// Withdraws liquidity from the pool. + WithdrawLiquidity { pool_identifier: String }, + /// Execute multiple [`SwapOperation`]s to allow for multi-hop swaps. + ExecuteSwapOperations { + /// The operations that should be performed in sequence. + /// + /// The amount in each swap will be the output from the previous swap. + /// + /// The first swap will use whatever funds are sent in the MessageInfo. + operations: Vec, + /// The minimum amount of the output (i.e., final swap operation token) required for the message to succeed. + minimum_receive: Option, + /// The (optional) recipient of the output tokens. + /// + /// If left unspecified, tokens will be sent to the sender of the message. + receiver: Option, + /// The maximum spread to incur when performing the swap. If the spread exceeds this value, + /// the swap will not be executed. Max 50%. + max_spread: Option, + }, + /// Updates the configuration of the contract. + /// If a field is not specified (i.e., set to `None`), it will not be modified. + UpdateConfig { + /// The new fee collector contract address. + fee_collector_addr: Option, + /// The new farm manager contract address. + farm_manager_addr: Option, + /// The new fee that must be paid when a pool is created. + pool_creation_fee: Option, + /// The new feature toggles of the contract, allowing fine-tuned + /// control over which operations are allowed. + feature_toggle: Option, + }, +} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + /// Retrieves the contract's config. + #[returns(ConfigResponse)] + Config {}, + /// Retrieves the decimals for the given asset. + #[returns(AssetDecimalsResponse)] + AssetDecimals { + /// The pool identifier to do the query for. + pool_identifier: String, + /// The queried denom in the given pool_identifier. + denom: String, + }, + /// Simulates a swap. + #[returns(SimulationResponse)] + Simulation { + /// The offer asset to swap. + offer_asset: Coin, + /// The ask asset denom to get. + ask_asset_denom: String, + /// The pool identifier to swap in. + pool_identifier: String, + }, + /// Simulates a reverse swap, i.e. given the ask asset, how much of the offer asset is needed + /// to perform the swap. + #[returns(ReverseSimulationResponse)] + ReverseSimulation { + /// The ask asset to get after the swap. + ask_asset: Coin, + /// The offer asset denom to input. + offer_asset_denom: String, + /// The pool identifier to swap in. + pool_identifier: String, + }, + /// Simulates swap operations. + #[returns(SimulateSwapOperationsResponse)] + SimulateSwapOperations { + /// The amount to swap. + offer_amount: Uint128, + /// The operations to perform. + operations: Vec, + }, + /// Simulates a reverse swap operations, i.e. given the ask asset, how much of the offer asset + /// is needed to perform the swap. + #[returns(ReverseSimulateSwapOperationsResponse)] + ReverseSimulateSwapOperations { + /// The amount to get after the swap. + ask_amount: Uint128, + /// The operations to perform. + operations: Vec, + }, + /// Retrieves the pool information for the given pool identifier. + #[returns(PoolsResponse)] + Pools { + /// An optional parameter specifying the pool identifier to do the query for. If not + /// provided, it will return all pools based on the pagination parameters. + pool_identifier: Option, + /// An optional parameter specifying what pool (identifier) to start searching after. + start_after: Option, + /// The amount of pools to return. If unspecified, will default to a value specified by + /// the contract. + limit: Option, + }, +} + +/// The response for the `Config` query. +#[cw_serde] +pub struct ConfigResponse { + /// The contract configuration. + pub config: Config, +} + +/// The response for the `Pools` query. +#[cw_serde] +pub struct PoolsResponse { + /// The pools information responses. + pub pools: Vec, +} + +#[cw_serde] +pub struct PoolInfoResponse { + /// The pool information for the given pool identifier. + pub pool_info: PoolInfo, + /// The total LP tokens in the pool. + pub total_share: Coin, +} + +/// The response for the `AssetDecimals` query. +#[cw_serde] +pub struct AssetDecimalsResponse { + /// The pool identifier to do the query for. + pub pool_identifier: String, + /// The queried denom in the given pool_identifier. + pub denom: String, + /// The decimals for the requested denom. + pub decimals: u8, +} + +/// SimulationResponse returns swap simulation response +#[cw_serde] +pub struct SimulationResponse { + /// The return amount of the ask asset given the offer amount. + pub return_amount: Uint128, + /// The spread amount of the swap. + pub spread_amount: Uint128, + /// The swap fee amount of the swap. + pub swap_fee_amount: Uint128, + /// The protocol fee amount of the swap. + pub protocol_fee_amount: Uint128, + /// The burn fee amount of the swap. + pub burn_fee_amount: Uint128, + /// The extra fees amount of the swap. + pub extra_fees_amount: Uint128, +} + +/// ReverseSimulationResponse returns reverse swap simulation response +#[cw_serde] +pub struct ReverseSimulationResponse { + /// The amount of the offer asset needed to get the ask amount. + pub offer_amount: Uint128, + /// The spread amount of the swap. + pub spread_amount: Uint128, + /// The swap fee amount of the swap. + pub swap_fee_amount: Uint128, + /// The protocol fee amount of the swap. + pub protocol_fee_amount: Uint128, + /// The burn fee amount of the swap. + pub burn_fee_amount: Uint128, + /// The extra fees amount of the swap. + pub extra_fees_amount: Uint128, +} + +/// Pool feature toggle, can control whether swaps, deposits, and withdrawals are enabled. +#[cw_serde] +pub struct FeatureToggle { + /// Whether or not swaps are enabled + pub withdrawals_enabled: bool, + /// Whether or not deposits are enabled + pub deposits_enabled: bool, + /// Whether or not swaps are enabled + pub swaps_enabled: bool, +} + +/// The response for the `SimulateSwapOperations` query. +#[cw_serde] +pub struct SimulateSwapOperationsResponse { + /// The return amount of the ask asset after the swap operations. + pub return_amount: Uint128, + /// The spreads of the swap. + pub spreads: Vec, + /// The swap fees of the swap. + pub swap_fees: Vec, + /// The protocol fees of the swap. + pub protocol_fees: Vec, + /// The burn fees of the swap. + pub burn_fees: Vec, + /// The extra fees of the swap. + pub extra_fees: Vec, +} + +/// The response for the `ReverseSimulateSwapOperations` query. +#[cw_serde] +pub struct ReverseSimulateSwapOperationsResponse { + /// The amount of the initial token needed to get the final token after the swap operations. + pub offer_amount: Uint128, + /// The spreads of the swap. + pub spreads: Vec, + /// The swap fees of the swap. + pub swap_fees: Vec, + /// The protocol fees of the swap. + pub protocol_fees: Vec, + /// The burn fees of the swap. + pub burn_fees: Vec, + /// The extra fees of the swap. + pub extra_fees: Vec, +} diff --git a/contracts/adapters/swap/mantra-dex/src/state.rs b/contracts/adapters/swap/mantra-dex/src/state.rs new file mode 100644 index 00000000..de466f68 --- /dev/null +++ b/contracts/adapters/swap/mantra-dex/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 MANTRA_DEX_POOL_MANAGER_ADDRESS: Item = + Item::new("mantra_dex_pool_manager_address"); diff --git a/contracts/adapters/swap/mantra-dex/tests/test_execute_swap.rs b/contracts/adapters/swap/mantra-dex/tests/test_execute_swap.rs new file mode 100644 index 00000000..52a0c41d --- /dev/null +++ b/contracts/adapters/swap/mantra-dex/tests/test_execute_swap.rs @@ -0,0 +1,226 @@ +use cosmwasm_std::{ + testing::{mock_dependencies, mock_env, mock_info}, + to_json_binary, Addr, Coin, Decimal, + ReplyOn::Never, + SubMsg, WasmMsg, +}; +use skip::swap::{ExecuteMsg, SwapOperation}; +use skip_go_swap_adapter_mantra_dex::error::{ContractError, ContractResult}; +use skip_go_swap_adapter_mantra_dex::state::{ + ENTRY_POINT_CONTRACT_ADDRESS, MANTRA_DEX_POOL_MANAGER_ADDRESS, +}; +use test_case::test_case; + +use skip_go_swap_adapter_mantra_dex::pool_manager::{ + ExecuteMsg as MantraPoolManagerExecuteMsg, SwapOperation as MantraSwapOperation, +}; + +/* +Test Cases: + +Expect Success + - One Swap Operation + - Multiple Swap Operations + - No Swap Operations (This is prevented in the entry point contract; and will not add any swap messages to the response) + +Expect Error + - Unauthorized Caller (Only the stored entry point contract can call this function) + - No Coin Sent + - More Than One Coin Sent + + */ + +// 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: "mantra_pool_manager".to_string(), + msg: to_json_binary(&MantraPoolManagerExecuteMsg::ExecuteSwapOperations { + operations: vec![MantraSwapOperation::MantraSwap { + pool_identifier: "pool_1".to_string(), + token_in_denom: "os".to_string(), + token_out_denom: "ua".to_string(), + }], + minimum_receive: None, + receiver: Some("entry_point".to_string()), + max_spread: Some(Decimal::percent(50)), + })?, + funds: vec![Coin::new(100, "os")], + }.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: "mantra_pool_manager".to_string(), + msg: to_json_binary(&MantraPoolManagerExecuteMsg::ExecuteSwapOperations { + operations: vec![MantraSwapOperation::MantraSwap { + pool_identifier: "pool_1".to_string(), + token_in_denom: "os".to_string(), + token_out_denom: "ua".to_string(), + }, + MantraSwapOperation::MantraSwap { + pool_identifier: "pool_2".to_string(), + token_in_denom: "ua".to_string(), + token_out_denom: "un".to_string(), + } + ], + minimum_receive: None, + receiver: Some("entry_point".to_string()), + max_spread: Some(Decimal::percent(50)), + })?, + funds: vec![Coin::new(100, "os")], + }.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![], + 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"), + Coin::new(100, "os"), + ], + swap_operations: vec![], + 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, "un"), + ], + swap_operations: vec![], + 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(); + + // 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"))?; + MANTRA_DEX_POOL_MANAGER_ADDRESS.save( + deps.as_mut().storage, + &Addr::unchecked("mantra_pool_manager"), + )?; + + // Call execute_swap with the given test parameters + let res = skip_go_swap_adapter_mantra_dex::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/mantra-dex/tests/test_instantiate.rs b/contracts/adapters/swap/mantra-dex/tests/test_instantiate.rs new file mode 100644 index 00000000..eecf78f1 --- /dev/null +++ b/contracts/adapters/swap/mantra-dex/tests/test_instantiate.rs @@ -0,0 +1,42 @@ +use cosmwasm_std::{ + testing::{mock_dependencies, mock_env, mock_info}, + Addr, +}; +use skip::swap::MantraDexInstantiateMsg; +use skip_go_swap_adapter_mantra_dex::state::{ + ENTRY_POINT_CONTRACT_ADDRESS, MANTRA_DEX_POOL_MANAGER_ADDRESS, +}; + +#[test] +fn test_instantiate() { + let mut deps = mock_dependencies(); + let env = mock_env(); + + let sender = Addr::unchecked("sender"); + + // Create mock info with entry point contract address + let info = mock_info(&sender.to_string(), &[]); + + // Call execute_swap with the given test parameters + let res = skip_go_swap_adapter_mantra_dex::contract::instantiate( + deps.as_mut(), + env, + info, + MantraDexInstantiateMsg { + entry_point_contract_address: "entry_point".to_string(), + mantra_pool_manager_address: "pool_manager".to_string(), + }, + ); + + assert!(res.is_ok()); + + let entry_point = ENTRY_POINT_CONTRACT_ADDRESS + .load(deps.as_ref().storage) + .unwrap(); + let pool_manager = MANTRA_DEX_POOL_MANAGER_ADDRESS + .load(deps.as_ref().storage) + .unwrap(); + + assert_eq!(entry_point, Addr::unchecked("entry_point")); + assert_eq!(pool_manager, Addr::unchecked("pool_manager")); +} diff --git a/packages/skip/src/swap.rs b/packages/skip/src/swap.rs index 5e1c82cb..5ec97ac5 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 MantraDexInstantiateMsg { + pub entry_point_contract_address: String, + pub mantra_pool_manager_address: String, +} + #[cw_serde] pub struct AstrovaultAdapterInstantiateMsg { pub entry_point_contract_address: String,