diff --git a/Cargo.lock b/Cargo.lock index 8c31787..824968a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,6 +63,8 @@ dependencies = [ "coreum-wasm-sdk", "cosmwasm-std", "cw-multi-test", + "cw20", + "dex", "schemars", "serde", ] @@ -698,6 +700,7 @@ dependencies = [ "cw20", "cw20-base", "dex", + "dex-factory", "dex-pool", "dex-stake", "itertools", diff --git a/Cargo.toml b/Cargo.toml index 16d95e0..d4fb32b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ dex = { path = "./packages/dex", default-features = false } dex-factory = { path = "./contracts/factory", default-features = false } dex-pool = { path = "./contracts/pool", default-features = false } dex-stake = { path = "./contracts/stake", default-features = false } +dex-fee-splitter = { path = "./contracts/fee_splitter", default-features = false } itertools = "0.10" proptest = "1.0" schemars = "0.8" diff --git a/contracts/fee_splitter/Cargo.toml b/contracts/fee_splitter/Cargo.toml index e8aa330..6cdbf28 100644 --- a/contracts/fee_splitter/Cargo.toml +++ b/contracts/fee_splitter/Cargo.toml @@ -31,6 +31,6 @@ anyhow = { workspace = true } bindings-test = { workspace = true } cw-multi-test = { workspace = true } cw20-base = { workspace = true } -# dex-factory = { workspace = true } +dex-factory = { workspace = true } dex-pool = { workspace = true } dex-stake = { workspace = true } diff --git a/contracts/fee_splitter/src/contract.rs b/contracts/fee_splitter/src/contract.rs index 03ee6ba..3fe199d 100644 --- a/contracts/fee_splitter/src/contract.rs +++ b/contracts/fee_splitter/src/contract.rs @@ -4,17 +4,13 @@ use cosmwasm_std::{ DepsMut, Env, MessageInfo, StdError, StdResult, WasmMsg, }; use cw20::{BalanceResponse, Cw20ExecuteMsg, Cw20QueryMsg}; -use cw_storage_plus::Item; use crate::{ error::ContractError, msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, - state::Config, + state::{Config, CONFIG}, }; -/// Saves factory settings -pub const CONFIG: Item = Item::new("config"); - pub type Response = cosmwasm_std::Response; pub type SubMsg = cosmwasm_std::SubMsg; @@ -38,7 +34,7 @@ pub fn instantiate( .iter() .map(|&(_, weight)| weight) .fold(Decimal::zero(), |acc, x| acc + x) - .le(&Decimal::from_ratio(1u32, 1u32)); + .eq(&Decimal::percent(100u64)); if !is_weights_valid { return Err(ContractError::InvalidWeights {}); @@ -50,7 +46,7 @@ pub fn instantiate( CONFIG.save(deps.storage, &config)?; - Ok(Response::new().add_attribute("initialized", "contract")) + Ok(Response::new().add_attribute("initialized", "fee_splitter contract")) } #[cfg_attr(not(feature = "library"), entry_point)] @@ -74,7 +70,7 @@ fn execute_send_tokens( native_denoms: Vec, cw20_addresses: Vec, ) -> Result { - let config = query_config(deps.as_ref())?; + let config = CONFIG.load(deps.storage)?; let contract_address = env.contract.address.to_string(); // gather balances of native tokens, either from function parameter or all @@ -163,12 +159,3 @@ pub fn query_config(deps: Deps) -> StdResult { Ok(resp) } - -#[cfg(test)] -mod tests { - #[test] - #[ignore] - fn instantiate_with_invalid_weights_should_throw_error() { - todo!() - } -} diff --git a/contracts/fee_splitter/src/state.rs b/contracts/fee_splitter/src/state.rs index ca0d945..0497f1f 100644 --- a/contracts/fee_splitter/src/state.rs +++ b/contracts/fee_splitter/src/state.rs @@ -1,5 +1,6 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::Decimal; +use cw_storage_plus::Item; #[cw_serde] pub struct Config { @@ -7,3 +8,5 @@ pub struct Config { // Weights must sum up to 1.0 pub addresses: Vec<(String, Decimal)>, } + +pub const CONFIG: Item = Item::new("config"); diff --git a/contracts/fee_splitter/src/testing.rs b/contracts/fee_splitter/src/testing.rs index 8b13789..487c77e 100644 --- a/contracts/fee_splitter/src/testing.rs +++ b/contracts/fee_splitter/src/testing.rs @@ -1 +1,218 @@ +use bindings_test::mock_coreum_deps; +use cosmwasm_std::{ + from_json, + testing::{mock_env, mock_info, MOCK_CONTRACT_ADDR}, + to_json_binary, Attribute, BankMsg, Coin, CosmosMsg, Decimal, ReplyOn, Uint128, WasmMsg, +}; +use cw20::Cw20ExecuteMsg; +use crate::{ + contract::{execute, instantiate, query, SubMsg}, + error::ContractError, + msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, + state::Config, +}; + +const SENDER: &str = "addr0000"; +const FIRST_RECIPIENT: &str = "address0000"; +const SECOND_RECIPIENT: &str = "address0001"; +const ATOM: &str = "ATOM"; +const TIA: &str = "TIA"; +const USDT: &str = "USDT"; +const CW20_ASSET_ONE: &str = "asset0000"; +const CW20_ASSET_TWO: &str = "asset0001"; + +#[test] +fn init_works() { + let mut deps = mock_coreum_deps(&[]); + let env = mock_env(); + let info = mock_info(SENDER, &[]); + + let first_addr_percent = (FIRST_RECIPIENT.to_string(), Decimal::percent(50u64)); + let second_addr_percent = (SECOND_RECIPIENT.to_string(), Decimal::percent(50u64)); + let msg = InstantiateMsg { + addresses: vec![first_addr_percent.clone(), second_addr_percent.clone()], + cw20_contracts: vec![USDT.to_string()], + }; + + let res = instantiate(deps.as_mut(), env, info, msg).unwrap(); + + assert_eq!( + res.attributes, + vec![Attribute { + key: "initialized".to_string(), + value: "fee_splitter contract".to_string(), + }] + ); +} + +#[test] +fn fails_to_init_because_weights_above_limit() { + let mut deps = mock_coreum_deps(&[]); + let env = mock_env(); + let info = mock_info(SENDER, &[]); + + let first_addr_percent = (FIRST_RECIPIENT.to_string(), Decimal::percent(50u64)); + let second_addr_percent = (SECOND_RECIPIENT.to_string(), Decimal::percent(60u64)); + let msg = InstantiateMsg { + addresses: vec![first_addr_percent.clone(), second_addr_percent.clone()], + cw20_contracts: vec![USDT.to_string()], + }; + + let res = instantiate(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(res, ContractError::InvalidWeights {}); +} + +#[test] +fn fails_to_init_because_weights_below_limit() { + let mut deps = mock_coreum_deps(&[]); + let env = mock_env(); + let info = mock_info(SENDER, &[]); + + let first_addr_percent = (FIRST_RECIPIENT.to_string(), Decimal::percent(20u64)); + let second_addr_percent = (SECOND_RECIPIENT.to_string(), Decimal::percent(20u64)); + let msg = InstantiateMsg { + addresses: vec![first_addr_percent.clone(), second_addr_percent.clone()], + cw20_contracts: vec![USDT.to_string()], + }; + + let res = instantiate(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(res, ContractError::InvalidWeights {}); +} + +#[test] +fn should_send_tokens_in_correct_amount() { + let mut deps = mock_coreum_deps(&[]); + + deps.querier.with_token_balances(&[( + &String::from(CW20_ASSET_ONE), + &[(&String::from(MOCK_CONTRACT_ADDR), &Uint128::new(100_000))], + )]); + + deps.querier.with_balance(&[( + &String::from(MOCK_CONTRACT_ADDR), + &[ + Coin { + denom: ATOM.to_string(), + amount: Uint128::new(100_000), + }, + Coin { + denom: TIA.to_string(), + amount: Uint128::new(100_000), + }, + ], + )]); + + let env = mock_env(); + + let info = mock_info(SENDER, &[]); + let msg = InstantiateMsg { + addresses: vec![ + (FIRST_RECIPIENT.to_string(), Decimal::percent(60u64)), + (SECOND_RECIPIENT.to_string(), Decimal::percent(40u64)), + ], + cw20_contracts: vec![CW20_ASSET_ONE.to_string(), CW20_ASSET_TWO.to_string()], + }; + + let fee_splitter_instance = instantiate(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); + + assert_eq!( + fee_splitter_instance.attributes, + vec![Attribute { + key: "initialized".to_string(), + value: "fee_splitter contract".to_string(), + }] + ); + + let msg = ExecuteMsg::SendTokens { + native_denoms: vec![ATOM.to_string(), TIA.to_string()], + cw20_addresses: vec![CW20_ASSET_ONE.to_string()], + }; + + let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + + assert_eq!( + res.messages, + vec![ + SubMsg { + id: 0, + msg: CosmosMsg::Bank(BankMsg::Send { + to_address: FIRST_RECIPIENT.to_string(), + amount: vec![ + Coin { + denom: ATOM.to_string(), + amount: Uint128::new(60_000), + }, + Coin { + denom: TIA.to_string(), + amount: Uint128::new(60_000), + } + ] + }), + gas_limit: None, + reply_on: ReplyOn::Never + }, + SubMsg { + id: 0, + msg: CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: CW20_ASSET_ONE.to_string(), + msg: to_json_binary(&Cw20ExecuteMsg::Transfer { + recipient: FIRST_RECIPIENT.to_string(), + amount: Uint128::new(60_000), + }) + .unwrap(), + funds: vec![] + }), + gas_limit: None, + reply_on: ReplyOn::Never + }, + SubMsg { + id: 0, + msg: CosmosMsg::Bank(BankMsg::Send { + to_address: SECOND_RECIPIENT.to_string(), + amount: vec![ + Coin { + denom: ATOM.to_string(), + amount: Uint128::new(40_000), + }, + Coin { + denom: TIA.to_string(), + amount: Uint128::new(40_000), + } + ] + }), + gas_limit: None, + reply_on: ReplyOn::Never + }, + SubMsg { + id: 0, + msg: CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: CW20_ASSET_ONE.to_string(), + msg: to_json_binary(&Cw20ExecuteMsg::Transfer { + recipient: SECOND_RECIPIENT.to_string(), + amount: Uint128::new(40_000), + }) + .unwrap(), + funds: vec![] + }), + gas_limit: None, + reply_on: ReplyOn::Never + }, + ] + ); + + let msg = QueryMsg::Config {}; + + let query_result = query(deps.as_ref(), env, msg).unwrap(); + let config_res: Config = from_json(query_result).unwrap(); + + assert_eq!( + config_res, + Config { + addresses: vec![ + (FIRST_RECIPIENT.to_string(), Decimal::percent(60)), + (SECOND_RECIPIENT.to_string(), Decimal::percent(40)) + ], + } + ); +} diff --git a/packages/bindings-test/Cargo.toml b/packages/bindings-test/Cargo.toml index b80f292..21f261a 100644 --- a/packages/bindings-test/Cargo.toml +++ b/packages/bindings-test/Cargo.toml @@ -14,5 +14,6 @@ cosmwasm-std = { workspace = true } cw-multi-test = { workspace = true } schemars = { workspace = true } serde = { workspace = true } - +dex = { workspace = true } +cw20 = { workspace = true } diff --git a/packages/bindings-test/src/lib.rs b/packages/bindings-test/src/lib.rs index 9ea7c7b..3fae80d 100644 --- a/packages/bindings-test/src/lib.rs +++ b/packages/bindings-test/src/lib.rs @@ -1,5 +1,6 @@ mod multitest; +mod testing; -pub use multitest::{ - mock_coreum_deps, CoreumApp, CoreumAppWrapped, CoreumDeps, CoreumModule, BLOCK_TIME, -}; +pub use multitest::{CoreumApp, CoreumAppWrapped, CoreumModule, BLOCK_TIME}; + +pub use testing::{mock_coreum_deps, CoreumDeps}; diff --git a/packages/bindings-test/src/multitest.rs b/packages/bindings-test/src/multitest.rs index 9881c44..33668a6 100644 --- a/packages/bindings-test/src/multitest.rs +++ b/packages/bindings-test/src/multitest.rs @@ -1,7 +1,6 @@ use std::{ cmp::max, fmt::Debug, - marker::PhantomData, ops::{Deref, DerefMut}, }; @@ -14,9 +13,9 @@ use coreum_wasm_sdk::{ core::{CoreumMsg, CoreumQueries}, }; use cosmwasm_std::{ - testing::{MockApi, MockQuerier, MockStorage}, + testing::{MockApi, MockStorage}, to_json_binary, Addr, Api, BalanceResponse, BankMsg, BankQuery, Binary, BlockInfo, CustomQuery, - Empty, OwnedDeps, Querier, QuerierWrapper, QueryRequest, Storage, + Empty, Querier, QuerierWrapper, QueryRequest, Storage, }; use cw_multi_test::{ App, AppResponse, BankKeeper, BankSudo, BasicAppBuilder, CosmosRouter, Module, WasmKeeper, @@ -26,17 +25,6 @@ use cw_multi_test::{ /// (when we increment block.height, use this multiplier for block.time) pub const BLOCK_TIME: u64 = 5; -pub type CoreumDeps = OwnedDeps; - -pub fn mock_coreum_deps() -> CoreumDeps { - CoreumDeps { - storage: MockStorage::default(), - api: MockApi::default(), - querier: MockQuerier::default(), - custom_query_type: PhantomData, - } -} - pub struct CoreumModule {} impl Module for CoreumModule { diff --git a/packages/bindings-test/src/testing.rs b/packages/bindings-test/src/testing.rs new file mode 100644 index 0000000..bd9b8ed --- /dev/null +++ b/packages/bindings-test/src/testing.rs @@ -0,0 +1,188 @@ +use std::{collections::HashMap, marker::PhantomData}; + +use coreum_wasm_sdk::core::CoreumQueries; +use cosmwasm_std::{ + from_json, + testing::{MockApi, MockQuerier, MockStorage, MOCK_CONTRACT_ADDR}, + to_json_binary, Addr, Coin, Decimal, OwnedDeps, Querier, QuerierResult, QueryRequest, + SystemError, SystemResult, Uint128, WasmQuery, +}; +use dex::factory::{ + ConfigResponse, FeeInfoResponse, + QueryMsg::{Config, FeeInfo}, +}; + +use cw20::{BalanceResponse, Cw20QueryMsg, TokenInfoResponse}; + +pub type CoreumDeps = OwnedDeps; + +pub fn mock_coreum_deps(contract_balance: &[Coin]) -> CoreumDeps { + let custom_qurier = + WhelpMockQuerier::new(MockQuerier::new(&[(MOCK_CONTRACT_ADDR, contract_balance)])); + + CoreumDeps { + storage: MockStorage::default(), + api: MockApi::default(), + querier: custom_qurier, + custom_query_type: PhantomData, + } +} + +pub struct WhelpMockQuerier { + base: MockQuerier, + token_querier: TokenQuerier, +} + +#[derive(Clone, Default)] +pub struct TokenQuerier { + // This lets us iterate over all pools that match the first string + balances: HashMap>, +} + +impl TokenQuerier { + pub fn new(balances: &[(&String, &[(&String, &Uint128)])]) -> Self { + TokenQuerier { + balances: balances_to_map(balances), + } + } +} + +pub(crate) fn balances_to_map( + balances: &[(&String, &[(&String, &Uint128)])], +) -> HashMap> { + let mut balances_map: HashMap> = HashMap::new(); + for (contract_addr, balances) in balances.iter() { + let mut contract_balances_map: HashMap = HashMap::new(); + for (addr, balance) in balances.iter() { + contract_balances_map.insert(addr.to_string(), **balance); + } + + balances_map.insert(contract_addr.to_string(), contract_balances_map); + } + balances_map +} +impl Querier for WhelpMockQuerier { + fn raw_query(&self, bin_request: &[u8]) -> QuerierResult { + // MockQuerier doesn't support Custom, so we ignore it completely + let request: QueryRequest = match from_json(bin_request) { + Ok(v) => v, + Err(e) => { + return SystemResult::Err(SystemError::InvalidRequest { + error: format!("Parsing query request: {}", e), + request: bin_request.into(), + }) + } + }; + self.handle_query(&request) + } +} + +impl WhelpMockQuerier { + pub fn handle_query(&self, request: &QueryRequest) -> QuerierResult { + match &request { + QueryRequest::Wasm(WasmQuery::Smart { contract_addr, msg }) => { + if contract_addr == "factory" { + match from_json(msg).unwrap() { + FeeInfo { .. } => SystemResult::Ok( + to_json_binary(&FeeInfoResponse { + fee_address: Some(Addr::unchecked("fee_address")), + total_fee_bps: 30, + protocol_fee_bps: 1660, + }) + .into(), + ), + Config {} => SystemResult::Ok( + to_json_binary(&ConfigResponse { + owner: Addr::unchecked("owner"), + pool_configs: vec![], + fee_address: Some(Addr::unchecked("fee_address")), + max_referral_commission: Decimal::one(), + only_owner_can_create_pools: true, + trading_starts: None, + }) + .into(), + ), + _ => panic!("DO NOT ENTER HERE"), + } + } else { + match from_json(msg).unwrap() { + Cw20QueryMsg::TokenInfo {} => { + let balances: &HashMap = + match self.token_querier.balances.get(contract_addr) { + Some(balances) => balances, + None => { + return SystemResult::Err(SystemError::Unknown {}); + } + }; + + let mut total_supply = Uint128::zero(); + + for balance in balances { + total_supply += *balance.1; + } + + SystemResult::Ok( + to_json_binary(&TokenInfoResponse { + name: "mAPPL".to_string(), + symbol: "mAPPL".to_string(), + decimals: 6, + total_supply, + }) + .into(), + ) + } + Cw20QueryMsg::Balance { address } => { + let balances: &HashMap = + match self.token_querier.balances.get(contract_addr) { + Some(balances) => balances, + None => { + return SystemResult::Err(SystemError::Unknown {}); + } + }; + + let balance = match balances.get(&address) { + Some(v) => v, + None => { + return SystemResult::Err(SystemError::Unknown {}); + } + }; + + SystemResult::Ok( + to_json_binary(&BalanceResponse { balance: *balance }).into(), + ) + } + _ => panic!("DO NOT ENTER HERE"), + } + } + } + QueryRequest::Wasm(WasmQuery::Raw { contract_addr, .. }) => { + if contract_addr == "factory" { + SystemResult::Ok(to_json_binary(&Vec::::new()).into()) + } else { + panic!("DO NOT ENTER HERE"); + } + } + _ => self.base.handle_query(request), + } + } +} + +impl WhelpMockQuerier { + pub fn new(base: MockQuerier) -> Self { + WhelpMockQuerier { + base, + token_querier: TokenQuerier::default(), + } + } + + // Configure the mint whitelist mock querier + pub fn with_token_balances(&mut self, balances: &[(&String, &[(&String, &Uint128)])]) { + self.token_querier = TokenQuerier::new(balances); + } + + pub fn with_balance(&mut self, balances: &[(&String, &[Coin])]) { + for (addr, balance) in balances { + self.base.update_balance(addr.to_string(), balance.to_vec()); + } + } +}