From 301b45af489506f28695efc1a6b1d10479a33d8b Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Fri, 29 Dec 2023 14:07:42 -0800 Subject: [PATCH] Basic Integration Tests --- Cargo.lock | 9 + contracts/external/cw-abc/src/contract.rs | 5 +- contracts/external/dao-abc-factory/Cargo.toml | 14 + .../schema/dao-abc-factory.json | 18 +- .../external/dao-abc-factory/src/contract.rs | 69 ++- contracts/external/dao-abc-factory/src/lib.rs | 3 + contracts/external/dao-abc-factory/src/msg.rs | 6 +- .../src/test_tube/integration_tests.rs | 147 +++++ .../dao-abc-factory/src/test_tube/mod.rs | 6 + .../dao-abc-factory/src/test_tube/test_env.rs | 560 ++++++++++++++++++ packages/dao-testing/Cargo.toml | 1 + packages/dao-testing/src/test_tube/cw_abc.rs | 4 +- .../src/test_tube/dao_abc_factory.rs | 129 ++++ .../src/test_tube/dao_voting_token_staked.rs | 129 ++++ packages/dao-testing/src/test_tube/mod.rs | 6 + 15 files changed, 1083 insertions(+), 23 deletions(-) create mode 100644 contracts/external/dao-abc-factory/src/test_tube/integration_tests.rs create mode 100644 contracts/external/dao-abc-factory/src/test_tube/mod.rs create mode 100644 contracts/external/dao-abc-factory/src/test_tube/test_env.rs create mode 100644 packages/dao-testing/src/test_tube/dao_abc_factory.rs create mode 100644 packages/dao-testing/src/test_tube/dao_voting_token_staked.rs diff --git a/Cargo.lock b/Cargo.lock index 4dcb36da6..a3efdd308 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1675,6 +1675,7 @@ dependencies = [ name = "dao-abc-factory" version = "2.4.0" dependencies = [ + "anyhow", "cosmwasm-schema", "cosmwasm-std", "cosmwasm-storage", @@ -1687,7 +1688,14 @@ dependencies = [ "cw2 1.1.2", "dao-dao-macros", "dao-interface", + "dao-proposal-hook-counter", + "dao-proposal-single", + "dao-testing", "dao-voting 2.4.0", + "dao-voting-token-staked", + "osmosis-std", + "osmosis-test-tube", + "serde", "thiserror", ] @@ -2092,6 +2100,7 @@ dependencies = [ "cw4-group 1.1.2", "cw721-base 0.18.0", "cw721-roles", + "dao-abc-factory", "dao-dao-core", "dao-interface", "dao-pre-propose-multiple", diff --git a/contracts/external/cw-abc/src/contract.rs b/contracts/external/cw-abc/src/contract.rs index 1c3b7e294..02dbfd3b0 100644 --- a/contracts/external/cw-abc/src/contract.rs +++ b/contracts/external/cw-abc/src/contract.rs @@ -86,12 +86,11 @@ pub fn instantiate( // Initialize owner to sender cw_ownable::initialize_owner(deps.storage, deps.api, Some(info.sender.as_str()))?; - // TODO Potential renounce admin? // Tnstantiate cw-token-factory-issuer contract - // Sender is set as contract admin + // Contract is immutable, no admin let issuer_instantiate_msg = SubMsg::reply_always( WasmMsg::Instantiate { - admin: Some(info.sender.to_string()), + admin: None, code_id: token_issuer_code_id, msg: to_json_binary(&IssuerInstantiateMsg::NewToken { subdenom: supply.subdenom.clone(), diff --git a/contracts/external/dao-abc-factory/Cargo.toml b/contracts/external/dao-abc-factory/Cargo.toml index 3d5c2fdcf..accf4f3cf 100644 --- a/contracts/external/dao-abc-factory/Cargo.toml +++ b/contracts/external/dao-abc-factory/Cargo.toml @@ -15,6 +15,11 @@ crate-type = ["cdylib", "rlib"] backtraces = ["cosmwasm-std/backtraces"] # use library feature to disable all instantiate/execute/query exports library = [] +# use test tube feature to enable test-tube integration tests, for example +# cargo test --features "test-tube" +test-tube = [] +# # when writing tests you may wish to enable test-tube as a default feature +# default = ["test-tube"] [dependencies] cosmwasm-std = { workspace = true } @@ -32,4 +37,13 @@ dao-voting = { workspace = true } cw-tokenfactory-issuer = { workspace = true, features = ["library"] } [dev-dependencies] +anyhow = { workspace = true } cw-multi-test = { workspace = true } +cw-tokenfactory-issuer = { workspace = true } +dao-proposal-single = { workspace = true } +dao-proposal-hook-counter = { workspace = true } +dao-testing = { workspace = true, features = ["test-tube"] } +dao-voting-token-staked = { workspace = true } +osmosis-std = { workspace = true } +osmosis-test-tube = { workspace = true } +serde = { workspace = true } diff --git a/contracts/external/dao-abc-factory/schema/dao-abc-factory.json b/contracts/external/dao-abc-factory/schema/dao-abc-factory.json index 9c341ce9e..1e72c7edb 100644 --- a/contracts/external/dao-abc-factory/schema/dao-abc-factory.json +++ b/contracts/external/dao-abc-factory/schema/dao-abc-factory.json @@ -13,14 +13,28 @@ "title": "ExecuteMsg", "oneOf": [ { - "description": "Example Factory Implementation", "type": "object", "required": [ "abc_factory" ], "properties": { "abc_factory": { - "$ref": "#/definitions/InstantiateMsg" + "type": "object", + "required": [ + "code_id", + "instantiate_msg" + ], + "properties": { + "code_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "instantiate_msg": { + "$ref": "#/definitions/InstantiateMsg" + } + }, + "additionalProperties": false } }, "additionalProperties": false diff --git a/contracts/external/dao-abc-factory/src/contract.rs b/contracts/external/dao-abc-factory/src/contract.rs index ecd909e22..b2836742f 100644 --- a/contracts/external/dao-abc-factory/src/contract.rs +++ b/contracts/external/dao-abc-factory/src/contract.rs @@ -1,14 +1,20 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - to_json_binary, Addr, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Order, Reply, Response, - StdResult, SubMsg, WasmMsg, + to_json_binary, Addr, Binary, CosmosMsg, Deps, DepsMut, Empty, Env, MessageInfo, Order, Reply, + Response, StdResult, SubMsg, WasmMsg, }; use cw2::set_contract_version; -use cw_abc::msg::{InstantiateMsg as AbcInstantiateMsg, QueryMsg as AbcQueryMsg}; +use cw_abc::msg::{ + DenomResponse, ExecuteMsg as AbcExecuteMsg, InstantiateMsg as AbcInstantiateMsg, + QueryMsg as AbcQueryMsg, +}; use cw_storage_plus::{Bound, Item, Map}; use cw_utils::parse_reply_instantiate_data; -use dao_interface::{token::TokenFactoryCallback, voting::Query as VotingModuleQueryMsg}; +use dao_interface::{ + state::ModuleInstantiateCallback, token::TokenFactoryCallback, + voting::Query as VotingModuleQueryMsg, +}; use crate::{ error::ContractError, @@ -21,6 +27,7 @@ const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); const INSTANTIATE_ABC_REPLY_ID: u64 = 1; const DAOS: Map = Map::new("daos"); +const CURRENT_DAO: Item = Item::new("current_dao"); const VOTING_MODULE: Item = Item::new("voting_module"); #[cfg_attr(not(feature = "library"), entry_point)] @@ -43,7 +50,10 @@ pub fn execute( msg: ExecuteMsg, ) -> Result { match msg { - ExecuteMsg::AbcFactory(msg) => execute_token_factory_factory(deps, env, info, msg), + ExecuteMsg::AbcFactory { + code_id, + instantiate_msg, + } => execute_token_factory_factory(deps, env, info, code_id, instantiate_msg), } } @@ -51,6 +61,7 @@ pub fn execute_token_factory_factory( deps: DepsMut, _env: Env, info: MessageInfo, + code_id: u64, msg: AbcInstantiateMsg, ) -> Result { // Save voting module address @@ -61,7 +72,8 @@ pub fn execute_token_factory_factory( .querier .query_wasm_smart(info.sender, &VotingModuleQueryMsg::Dao {})?; - DAOS.save(deps.storage, dao, &Empty {})?; + DAOS.save(deps.storage, dao.clone(), &Empty {})?; + CURRENT_DAO.save(deps.storage, &dao)?; // Instantiate new contract, further setup is handled in the // SubMsg reply. @@ -69,7 +81,7 @@ pub fn execute_token_factory_factory( WasmMsg::Instantiate { // No admin as we want the bonding curve contract to be immutable admin: None, - code_id: msg.token_issuer_code_id, + code_id, msg: to_json_binary(&msg)?, funds: vec![], label: "cw_abc".to_string(), @@ -118,11 +130,14 @@ pub fn query_daos( pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result { match msg.id { INSTANTIATE_ABC_REPLY_ID => { + // Load DAO + let dao = CURRENT_DAO.load(deps.storage)?; + // Parse issuer address from instantiate reply let abc_addr = parse_reply_instantiate_data(msg)?.contract_address; // Query for denom - let denom = deps + let denom: DenomResponse = deps .querier .query_wasm_smart(abc_addr.clone(), &AbcQueryMsg::Denom {})?; @@ -131,15 +146,41 @@ pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result Err(ContractError::UnknownReplyId { id: msg.id }), } diff --git a/contracts/external/dao-abc-factory/src/lib.rs b/contracts/external/dao-abc-factory/src/lib.rs index 3915b791e..286ec8c24 100644 --- a/contracts/external/dao-abc-factory/src/lib.rs +++ b/contracts/external/dao-abc-factory/src/lib.rs @@ -3,3 +3,6 @@ mod error; pub mod msg; pub use crate::error::ContractError; + +#[cfg(test)] +mod test_tube; diff --git a/contracts/external/dao-abc-factory/src/msg.rs b/contracts/external/dao-abc-factory/src/msg.rs index 6d6cdf52a..143224330 100644 --- a/contracts/external/dao-abc-factory/src/msg.rs +++ b/contracts/external/dao-abc-factory/src/msg.rs @@ -6,8 +6,10 @@ pub struct InstantiateMsg {} #[cw_serde] pub enum ExecuteMsg { - /// Example Factory Implementation - AbcFactory(AbcInstantiateMsg), + AbcFactory { + instantiate_msg: AbcInstantiateMsg, + code_id: u64, + }, } #[cw_serde] diff --git a/contracts/external/dao-abc-factory/src/test_tube/integration_tests.rs b/contracts/external/dao-abc-factory/src/test_tube/integration_tests.rs new file mode 100644 index 000000000..5b3bd3126 --- /dev/null +++ b/contracts/external/dao-abc-factory/src/test_tube/integration_tests.rs @@ -0,0 +1,147 @@ +use cosmwasm_std::{coins, Addr, Coin, Uint128}; +use cw_ownable::Ownership; +use dao_interface::voting::{DenomResponse, IsActiveResponse, VotingPowerAtHeightResponse}; +use dao_voting_token_staked::msg::{ + ExecuteMsg as VotingTokenExecuteMsg, QueryMsg as VotingTokenQueryMsg, +}; +use osmosis_test_tube::{Account, OsmosisTestApp}; + +use super::test_env::{TestEnv, TestEnvBuilder, RESERVE}; + +#[test] +fn test_full_integration_correct_setup() { + let app = OsmosisTestApp::new(); + let env = TestEnvBuilder::new(); + let TestEnv { + dao, + tf_issuer, + cw_abc, + vp_contract, + .. + } = env.full_dao_setup(&app); + + // Issuer owner should be set to the abc contract + let issuer_admin = tf_issuer + .query::>(&cw_tokenfactory_issuer::msg::QueryMsg::Ownership {}) + .unwrap() + .owner; + assert_eq!( + issuer_admin, + Some(Addr::unchecked(cw_abc.contract_addr.clone())) + ); + + // Abc contract should have DAO as owner + let abc_admin = cw_abc + .query::>(&cw_abc::msg::QueryMsg::Ownership {}) + .unwrap() + .owner; + assert_eq!( + abc_admin, + Some(Addr::unchecked(dao.unwrap().contract_addr.clone())) + ); + + let issuer_denom = tf_issuer + .query::( + &cw_tokenfactory_issuer::msg::QueryMsg::Denom {}, + ) + .unwrap() + .denom; + + let abc_denom = cw_abc + .query::(&cw_abc::msg::QueryMsg::Denom {}) + .unwrap() + .denom; + + let vp_denom = vp_contract + .query::(&VotingTokenQueryMsg::Denom {}) + .unwrap() + .denom; + + // Denoms for contracts should be the same + assert_eq!(issuer_denom, abc_denom); + assert_eq!(issuer_denom, vp_denom); +} + +#[test] +fn test_stake_unstake_new_denom() { + let app = OsmosisTestApp::new(); + let env = TestEnvBuilder::new(); + let TestEnv { + vp_contract, + accounts, + cw_abc, + .. + } = env.full_dao_setup(&app); + + let denom = vp_contract + .query::(&VotingTokenQueryMsg::Denom {}) + .unwrap() + .denom; + + // Buy tokens off of bonding curve + cw_abc + .execute( + &cw_abc::msg::ExecuteMsg::Buy {}, + &coins(100000, RESERVE), + &accounts[0], + ) + .unwrap(); + + // Stake 100 tokens + let stake_msg = VotingTokenExecuteMsg::Stake {}; + vp_contract + .execute(&stake_msg, &[Coin::new(100, denom)], &accounts[0]) + .unwrap(); + + app.increase_time(1); + + // Query voting power + let voting_power: VotingPowerAtHeightResponse = vp_contract + .query(&VotingTokenQueryMsg::VotingPowerAtHeight { + address: accounts[0].address(), + height: None, + }) + .unwrap(); + assert_eq!(voting_power.power, Uint128::new(100)); + + // DAO is active (default threshold is absolute count of 75) + let active = vp_contract + .query::(&VotingTokenQueryMsg::IsActive {}) + .unwrap() + .active; + assert!(active); + + // Unstake 50 tokens + let unstake_msg = VotingTokenExecuteMsg::Unstake { + amount: Uint128::new(50), + }; + vp_contract + .execute(&unstake_msg, &[], &accounts[0]) + .unwrap(); + app.increase_time(1); + let voting_power: VotingPowerAtHeightResponse = vp_contract + .query(&VotingTokenQueryMsg::VotingPowerAtHeight { + address: accounts[0].address(), + height: None, + }) + .unwrap(); + assert_eq!(voting_power.power, Uint128::new(50)); + + // DAO is not active + let active = vp_contract + .query::(&VotingTokenQueryMsg::IsActive {}) + .unwrap() + .active; + assert!(!active); + + // Can't claim before unstaking period (2 seconds) + vp_contract + .execute(&VotingTokenExecuteMsg::Claim {}, &[], &accounts[0]) + .unwrap_err(); + + // Pass time, unstaking duration is set to 2 seconds + app.increase_time(5); + vp_contract + .execute(&VotingTokenExecuteMsg::Claim {}, &[], &accounts[0]) + .unwrap(); +} diff --git a/contracts/external/dao-abc-factory/src/test_tube/mod.rs b/contracts/external/dao-abc-factory/src/test_tube/mod.rs new file mode 100644 index 000000000..fe51e9fb6 --- /dev/null +++ b/contracts/external/dao-abc-factory/src/test_tube/mod.rs @@ -0,0 +1,6 @@ +// Ignore integration tests for code coverage since there will be problems with dynamic linking libosmosistesttube +// and also, tarpaulin will not be able read coverage out of wasm binary anyway +#![cfg(not(tarpaulin))] + +mod integration_tests; +mod test_env; diff --git a/contracts/external/dao-abc-factory/src/test_tube/test_env.rs b/contracts/external/dao-abc-factory/src/test_tube/test_env.rs new file mode 100644 index 000000000..a31a88822 --- /dev/null +++ b/contracts/external/dao-abc-factory/src/test_tube/test_env.rs @@ -0,0 +1,560 @@ +// The code is used in tests but reported as dead code +// see https://github.com/rust-lang/rust/issues/46379 +#![allow(dead_code)] + +use crate::{ + msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, + ContractError, +}; + +use cosmwasm_std::{to_json_binary, Addr, Coin, Decimal, Uint128, WasmMsg}; +use cw_abc::abc::{ + ClosedConfig, CommonsPhaseConfig, CurveType, HatchConfig, MinMax, OpenConfig, ReserveToken, + SupplyToken, +}; +use cw_utils::Duration; +use dao_interface::{ + msg::QueryMsg as DaoQueryMsg, + state::{Admin, ModuleInstantiateInfo, ProposalModule}, +}; +use dao_voting::{ + pre_propose::PreProposeInfo, threshold::PercentageThreshold, threshold::Threshold, +}; +use dao_voting_token_staked::msg::{QueryMsg as TokenVotingQueryMsg, TokenInfo}; + +use dao_testing::test_tube::{ + cw_abc::CwAbc, cw_tokenfactory_issuer::TokenfactoryIssuer, dao_dao_core::DaoCore, + dao_proposal_single::DaoProposalSingle, dao_voting_token_staked::TokenVotingContract, +}; +use dao_voting::threshold::ActiveThreshold; +use osmosis_test_tube::{ + osmosis_std::types::{ + cosmos::bank::v1beta1::QueryAllBalancesRequest, + cosmwasm::wasm::v1::MsgExecuteContractResponse, + }, + Account, Bank, Module, OsmosisTestApp, RunnerError, RunnerExecuteResult, RunnerResult, + SigningAccount, Wasm, +}; +use serde::de::DeserializeOwned; +use std::path::PathBuf; + +pub const DENOM: &str = "ucat"; +pub const JUNO: &str = "ujuno"; + +// Needs to match what's configured for test-tube +pub const RESERVE: &str = "uosmo"; + +pub struct TestEnv<'a> { + pub app: &'a OsmosisTestApp, + pub dao: Option>, + pub proposal_single: Option>, + pub vp_contract: TokenVotingContract<'a>, + pub tf_issuer: TokenfactoryIssuer<'a>, + pub dao_abc_factory: AbcFactoryContract<'a>, + pub accounts: Vec, + pub cw_abc: CwAbc<'a>, +} + +impl<'a> TestEnv<'a> { + pub fn get_tf_issuer_code_id(&self) -> u64 { + self.tf_issuer.code_id + } + + pub fn bank(&self) -> Bank<'_, OsmosisTestApp> { + Bank::new(self.app) + } + + pub fn assert_account_balances( + &self, + account: SigningAccount, + expected_balances: Vec, + ignore_denoms: Vec<&str>, + ) { + let account_balances: Vec = Bank::new(self.app) + .query_all_balances(&QueryAllBalancesRequest { + address: account.address(), + pagination: None, + }) + .unwrap() + .balances + .into_iter() + .map(|coin| Coin::new(coin.amount.parse().unwrap(), coin.denom)) + .filter(|coin| !ignore_denoms.contains(&coin.denom.as_str())) + .collect(); + + assert_eq!(account_balances, expected_balances); + } + + pub fn assert_contract_balances(&self, expected_balances: &[Coin]) { + let contract_balances: Vec = Bank::new(self.app) + .query_all_balances(&QueryAllBalancesRequest { + address: self.vp_contract.contract_addr.clone(), + pagination: None, + }) + .unwrap() + .balances + .into_iter() + .map(|coin| Coin::new(coin.amount.parse().unwrap(), coin.denom)) + .collect(); + + assert_eq!(contract_balances, expected_balances); + } +} + +pub struct TestEnvBuilder { + pub accounts: Vec, + pub instantiate_msg: Option, +} + +impl TestEnvBuilder { + pub fn new() -> Self { + Self { + accounts: vec![], + instantiate_msg: None, + } + } + + // Minimal default setup with just the key contracts + pub fn default_setup(self, app: &'_ OsmosisTestApp) -> TestEnv<'_> { + let accounts = app + .init_accounts(&[Coin::new(1000000000000000u128, "uosmo")], 10) + .unwrap(); + + let issuer_id = TokenfactoryIssuer::upload(app, &accounts[0]).unwrap(); + let abc_id = CwAbc::upload(app, &accounts[0]).unwrap(); + + // Upload and instantiate abc factory + let dao_abc_factory = + AbcFactoryContract::new(app, &InstantiateMsg {}, &accounts[0]).unwrap(); + + let vp_contract = TokenVotingContract::new( + app, + &dao_voting_token_staked::msg::InstantiateMsg { + token_info: TokenInfo::Factory( + to_json_binary(&WasmMsg::Execute { + contract_addr: dao_abc_factory.contract_addr.clone(), + msg: to_json_binary(&ExecuteMsg::AbcFactory { + instantiate_msg: cw_abc::msg::InstantiateMsg { + fees_recipient: accounts[0].address(), + token_issuer_code_id: issuer_id, + supply: SupplyToken { + subdenom: DENOM.to_string(), + metadata: None, + decimals: 6, + max_supply: Some(Uint128::from(1000000000u128)), + }, + reserve: ReserveToken { + denom: RESERVE.to_string(), + decimals: 6, + }, + phase_config: CommonsPhaseConfig { + hatch: HatchConfig { + contribution_limits: MinMax { + min: Uint128::from(10u128), + max: Uint128::from(1000000u128), + }, + initial_raise: MinMax { + min: Uint128::from(10u128), + max: Uint128::from(1000000u128), + }, + entry_fee: Decimal::percent(10u64), + exit_fee: Decimal::percent(10u64), + }, + open: OpenConfig { + entry_fee: Decimal::percent(10u64), + exit_fee: Decimal::percent(10u64), + }, + closed: ClosedConfig {}, + }, + hatcher_allowlist: None, + curve_type: CurveType::Constant { + value: Uint128::one(), + scale: 1, + }, + }, + code_id: abc_id, + }) + .unwrap(), + funds: vec![], + }) + .unwrap(), + ), + unstaking_duration: Some(Duration::Time(2)), + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(75), + }), + }, + &accounts[0], + ) + .unwrap(); + + let issuer_addr = + TokenVotingContract::query(&vp_contract, &TokenVotingQueryMsg::TokenContract {}) + .unwrap(); + + let tf_issuer = TokenfactoryIssuer::new_with_values(app, issuer_id, issuer_addr).unwrap(); + + // The abc contract is the owner of the issuer + let abc_addr = tf_issuer + .query::>( + &cw_tokenfactory_issuer::msg::QueryMsg::Ownership {}, + ) + .unwrap() + .owner; + let cw_abc = CwAbc::new_with_values(app, abc_id, abc_addr.unwrap().to_string()).unwrap(); + + TestEnv { + app, + accounts, + cw_abc, + dao: None, + proposal_single: None, + tf_issuer, + vp_contract, + dao_abc_factory, + } + } + + // Full DAO setup + pub fn full_dao_setup(self, app: &'_ OsmosisTestApp) -> TestEnv<'_> { + let accounts = app + .init_accounts(&[Coin::new(1000000000000000u128, "uosmo")], 10) + .unwrap(); + + // Upload all needed code ids + let issuer_id = TokenfactoryIssuer::upload(app, &accounts[0]).unwrap(); + let vp_contract_id = TokenVotingContract::upload(app, &accounts[0]).unwrap(); + let proposal_single_id = DaoProposalSingle::upload(app, &accounts[0]).unwrap(); + let abc_id = CwAbc::upload(app, &accounts[0]).unwrap(); + + // Upload and instantiate abc factory + let dao_abc_factory = + AbcFactoryContract::new(app, &InstantiateMsg {}, &accounts[0]).unwrap(); + + let msg = dao_interface::msg::InstantiateMsg { + dao_uri: None, + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that makes DAO tooling".to_string(), + image_url: None, + automatically_add_cw20s: false, + automatically_add_cw721s: false, + voting_module_instantiate_info: ModuleInstantiateInfo { + code_id: vp_contract_id, + msg: to_json_binary(&dao_voting_token_staked::msg::InstantiateMsg { + token_info: TokenInfo::Factory( + to_json_binary(&WasmMsg::Execute { + contract_addr: dao_abc_factory.contract_addr.clone(), + msg: to_json_binary(&ExecuteMsg::AbcFactory { + instantiate_msg: cw_abc::msg::InstantiateMsg { + fees_recipient: accounts[0].address(), + token_issuer_code_id: issuer_id, + supply: SupplyToken { + subdenom: DENOM.to_string(), + metadata: None, + decimals: 6, + max_supply: Some(Uint128::from(1000000000u128)), + }, + reserve: ReserveToken { + denom: RESERVE.to_string(), + decimals: 6, + }, + phase_config: CommonsPhaseConfig { + hatch: HatchConfig { + contribution_limits: MinMax { + min: Uint128::from(10u128), + max: Uint128::from(1000000u128), + }, + initial_raise: MinMax { + min: Uint128::from(10u128), + max: Uint128::from(1000000u128), + }, + entry_fee: Decimal::percent(10u64), + exit_fee: Decimal::percent(10u64), + }, + open: OpenConfig { + entry_fee: Decimal::percent(10u64), + exit_fee: Decimal::percent(10u64), + }, + closed: ClosedConfig {}, + }, + hatcher_allowlist: None, + curve_type: CurveType::Constant { + value: Uint128::one(), + scale: 1, + }, + }, + code_id: abc_id, + }) + .unwrap(), + funds: vec![], + }) + .unwrap(), + ), + unstaking_duration: Some(Duration::Time(2)), + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(75), + }), + }) + .unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO Voting Module".to_string(), + }, + proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { + code_id: proposal_single_id, + msg: to_json_binary(&dao_proposal_single::msg::InstantiateMsg { + min_voting_period: None, + threshold: Threshold::ThresholdQuorum { + threshold: PercentageThreshold::Majority {}, + quorum: PercentageThreshold::Percent(Decimal::percent(35)), + }, + max_voting_period: Duration::Time(432000), + allow_revoting: false, + only_members_execute: true, + close_proposal_on_execution_failure: false, + pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: None, + }) + .unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO Proposal Module".to_string(), + }], + initial_items: None, + }; + + // Instantiate DAO + let dao = DaoCore::new(app, &msg, &accounts[0], &[]).unwrap(); + + // Get voting module address, setup vp_contract helper + let vp_addr: Addr = dao.query(&DaoQueryMsg::VotingModule {}).unwrap(); + let vp_contract = + TokenVotingContract::new_with_values(app, vp_contract_id, vp_addr.to_string()).unwrap(); + + // Get proposal module address, setup proposal_single helper + let proposal_modules: Vec = dao + .query(&DaoQueryMsg::ProposalModules { + limit: None, + start_after: None, + }) + .unwrap(); + let proposal_single = DaoProposalSingle::new_with_values( + app, + proposal_single_id, + proposal_modules[0].address.to_string(), + ) + .unwrap(); + + // Get issuer address, setup tf_issuer helper + let issuer_addr = + TokenVotingContract::query(&vp_contract, &TokenVotingQueryMsg::TokenContract {}) + .unwrap(); + let tf_issuer = TokenfactoryIssuer::new_with_values(app, issuer_id, issuer_addr).unwrap(); + + // Get ABC Contract address + // The abc contract is the owner of the issuer + let abc_addr = tf_issuer + .query::>( + &cw_tokenfactory_issuer::msg::QueryMsg::Ownership {}, + ) + .unwrap() + .owner; + let cw_abc = CwAbc::new_with_values(app, abc_id, abc_addr.unwrap().to_string()).unwrap(); + + TestEnv { + app, + dao: Some(dao), + cw_abc, + vp_contract, + proposal_single: Some(proposal_single), + tf_issuer, + accounts, + dao_abc_factory, + } + } + + pub fn upload_issuer(self, app: &'_ OsmosisTestApp, signer: &SigningAccount) -> u64 { + TokenfactoryIssuer::upload(app, signer).unwrap() + } + + pub fn set_accounts(mut self, accounts: Vec) -> Self { + self.accounts = accounts; + self + } + + pub fn with_account(mut self, account: SigningAccount) -> Self { + self.accounts.push(account); + self + } + + pub fn with_instantiate_msg(mut self, msg: InstantiateMsg) -> Self { + self.instantiate_msg = Some(msg); + self + } +} + +#[derive(Debug)] +pub struct AbcFactoryContract<'a> { + pub app: &'a OsmosisTestApp, + pub contract_addr: String, + pub code_id: u64, +} + +impl<'a> AbcFactoryContract<'a> { + pub fn new( + app: &'a OsmosisTestApp, + instantiate_msg: &InstantiateMsg, + signer: &SigningAccount, + ) -> Result { + let wasm = Wasm::new(app); + + let code_id = wasm + .store_code(&Self::get_wasm_byte_code(), None, signer)? + .data + .code_id; + + let contract_addr = wasm + .instantiate( + code_id, + &instantiate_msg, + Some(&signer.address()), + None, + &[], + signer, + )? + .data + .address; + + Ok(Self { + app, + code_id, + contract_addr, + }) + } + + pub fn new_with_values( + app: &'a OsmosisTestApp, + code_id: u64, + contract_addr: String, + ) -> Result { + Ok(Self { + app, + code_id, + contract_addr, + }) + } + + /// uploads contract and returns a code ID + pub fn upload(app: &OsmosisTestApp, signer: &SigningAccount) -> Result { + let wasm = Wasm::new(app); + + let code_id = wasm + .store_code(&Self::get_wasm_byte_code(), None, signer)? + .data + .code_id; + + Ok(code_id) + } + + pub fn instantiate( + app: &'a OsmosisTestApp, + code_id: u64, + instantiate_msg: &InstantiateMsg, + signer: &SigningAccount, + ) -> Result { + let wasm = Wasm::new(app); + let contract_addr = wasm + .instantiate( + code_id, + &instantiate_msg, + Some(&signer.address()), + None, + &[], + signer, + )? + .data + .address; + + Ok(Self { + app, + code_id, + contract_addr, + }) + } + + pub fn execute( + &self, + msg: &ExecuteMsg, + funds: &[Coin], + signer: &SigningAccount, + ) -> RunnerExecuteResult { + let wasm = Wasm::new(self.app); + wasm.execute(&self.contract_addr, msg, funds, signer) + } + + pub fn query(&self, msg: &QueryMsg) -> RunnerResult + where + T: ?Sized + DeserializeOwned, + { + let wasm = Wasm::new(self.app); + wasm.query(&self.contract_addr, msg) + } + + fn get_wasm_byte_code() -> Vec { + let manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let byte_code = std::fs::read( + manifest_path + .join("..") + .join("..") + .join("..") + .join("artifacts") + .join("dao_abc_factory.wasm"), + ); + match byte_code { + Ok(byte_code) => byte_code, + // On arm processors, the above path is not found, so we try the following path + Err(_) => std::fs::read( + manifest_path + .join("..") + .join("..") + .join("..") + .join("artifacts") + .join("dao_abc_factory-aarch64.wasm"), + ) + .unwrap(), + } + } + + pub fn execute_error(err: ContractError) -> RunnerError { + RunnerError::ExecuteError { + msg: format!( + "failed to execute message; message index: 0: {}: execute wasm contract failed", + err + ), + } + } + + pub fn execute_submessage_error(err: ContractError) -> RunnerError { + RunnerError::ExecuteError { + msg: format!( + "failed to execute message; message index: 0: dispatch: submessages: reply: {}: execute wasm contract failed", + err + ), + } + } +} + +pub fn assert_contract_err(expected: ContractError, actual: RunnerError) { + match actual { + RunnerError::ExecuteError { msg } => { + if !msg.contains(&expected.to_string()) { + panic!( + "assertion failed:\n\n must contain \t: \"{}\",\n actual \t: \"{}\"\n", + expected, msg + ) + } + } + _ => panic!("unexpected error, expect execute error but got: {}", actual), + }; +} diff --git a/packages/dao-testing/Cargo.toml b/packages/dao-testing/Cargo.toml index d128f6226..a86b65db6 100644 --- a/packages/dao-testing/Cargo.toml +++ b/packages/dao-testing/Cargo.toml @@ -43,6 +43,7 @@ cw20-stake = { workspace = true } cw721-base = { workspace = true } cw721-roles = { workspace = true } cw-tokenfactory-issuer = { workspace = true } +dao-abc-factory = { workspace = true } dao-dao-core = { workspace = true, features = ["library"] } dao-interface = { workspace = true } dao-pre-propose-multiple = { workspace = true } diff --git a/packages/dao-testing/src/test_tube/cw_abc.rs b/packages/dao-testing/src/test_tube/cw_abc.rs index 59e90039d..c0b17e29d 100644 --- a/packages/dao-testing/src/test_tube/cw_abc.rs +++ b/packages/dao-testing/src/test_tube/cw_abc.rs @@ -154,7 +154,7 @@ impl<'a> CwAbc<'a> { .join("..") .join("..") .join("artifacts") - .join("cw_tokenfactory_issuer.wasm"), + .join("cw_abc.wasm"), ); match byte_code { Ok(byte_code) => byte_code, @@ -164,7 +164,7 @@ impl<'a> CwAbc<'a> { .join("..") .join("..") .join("artifacts") - .join("cw_tokenfactory_issuer-aarch64.wasm"), + .join("cw_abc-aarch64.wasm"), ) .unwrap(), } diff --git a/packages/dao-testing/src/test_tube/dao_abc_factory.rs b/packages/dao-testing/src/test_tube/dao_abc_factory.rs new file mode 100644 index 000000000..d000548c4 --- /dev/null +++ b/packages/dao-testing/src/test_tube/dao_abc_factory.rs @@ -0,0 +1,129 @@ +use cosmwasm_std::Coin; +use dao_abc_factory::{ + msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, + ContractError, +}; +use osmosis_test_tube::{ + osmosis_std::types::cosmwasm::wasm::v1::MsgExecuteContractResponse, Account, Module, + OsmosisTestApp, RunnerError, RunnerExecuteResult, SigningAccount, Wasm, +}; +use serde::de::DeserializeOwned; +use std::fmt::Debug; +use std::path::PathBuf; + +#[derive(Debug)] +pub struct AbcFactoryContract<'a> { + pub app: &'a OsmosisTestApp, + pub code_id: u64, + pub contract_addr: String, +} + +impl<'a> AbcFactoryContract<'a> { + pub fn new( + app: &'a OsmosisTestApp, + instantiate_msg: &InstantiateMsg, + signer: &SigningAccount, + ) -> Result { + let wasm = Wasm::new(app); + let token_creation_fee = Coin::new(10000000, "uosmo"); + + let code_id = wasm + .store_code(&Self::get_wasm_byte_code(), None, signer)? + .data + .code_id; + + let contract_addr = wasm + .instantiate( + code_id, + &instantiate_msg, + Some(&signer.address()), + None, + &[token_creation_fee], + signer, + )? + .data + .address; + + Ok(Self { + app, + code_id, + contract_addr, + }) + } + + pub fn new_with_values( + app: &'a OsmosisTestApp, + code_id: u64, + contract_addr: String, + ) -> Result { + Ok(Self { + app, + code_id, + contract_addr, + }) + } + + /// uploads contract and returns a code ID + pub fn upload(app: &OsmosisTestApp, signer: &SigningAccount) -> Result { + let wasm = Wasm::new(app); + + let code_id = wasm + .store_code(&Self::get_wasm_byte_code(), None, signer)? + .data + .code_id; + + Ok(code_id) + } + + // executes + pub fn execute( + &self, + execute_msg: &ExecuteMsg, + funds: &[Coin], + signer: &SigningAccount, + ) -> RunnerExecuteResult { + let wasm = Wasm::new(self.app); + wasm.execute(&self.contract_addr, execute_msg, funds, signer) + } + + // queries + pub fn query(&self, query_msg: &QueryMsg) -> Result + where + T: DeserializeOwned, + { + let wasm = Wasm::new(self.app); + wasm.query(&self.contract_addr, query_msg) + } + + fn get_wasm_byte_code() -> Vec { + let manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let byte_code = std::fs::read( + manifest_path + .join("..") + .join("..") + .join("artifacts") + .join("dao_abc_factory.wasm"), + ); + match byte_code { + Ok(byte_code) => byte_code, + // On arm processors, the above path is not found, so we try the following path + Err(_) => std::fs::read( + manifest_path + .join("..") + .join("..") + .join("artifacts") + .join("dao_abc_factory-aarch64.wasm"), + ) + .unwrap(), + } + } + + pub fn execute_error(err: ContractError) -> RunnerError { + RunnerError::ExecuteError { + msg: format!( + "failed to execute message; message index: 0: {}: execute wasm contract failed", + err + ), + } + } +} diff --git a/packages/dao-testing/src/test_tube/dao_voting_token_staked.rs b/packages/dao-testing/src/test_tube/dao_voting_token_staked.rs new file mode 100644 index 000000000..df1f6b7cc --- /dev/null +++ b/packages/dao-testing/src/test_tube/dao_voting_token_staked.rs @@ -0,0 +1,129 @@ +use cosmwasm_std::Coin; +use dao_voting_token_staked::{ + msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, + ContractError, +}; +use osmosis_test_tube::{ + osmosis_std::types::cosmwasm::wasm::v1::MsgExecuteContractResponse, Account, Module, + OsmosisTestApp, RunnerError, RunnerExecuteResult, SigningAccount, Wasm, +}; +use serde::de::DeserializeOwned; +use std::fmt::Debug; +use std::path::PathBuf; + +#[derive(Debug)] +pub struct TokenVotingContract<'a> { + pub app: &'a OsmosisTestApp, + pub code_id: u64, + pub contract_addr: String, +} + +impl<'a> TokenVotingContract<'a> { + pub fn new( + app: &'a OsmosisTestApp, + instantiate_msg: &InstantiateMsg, + signer: &SigningAccount, + ) -> Result { + let wasm = Wasm::new(app); + let token_creation_fee = Coin::new(10000000, "uosmo"); + + let code_id = wasm + .store_code(&Self::get_wasm_byte_code(), None, signer)? + .data + .code_id; + + let contract_addr = wasm + .instantiate( + code_id, + &instantiate_msg, + Some(&signer.address()), + None, + &[token_creation_fee], + signer, + )? + .data + .address; + + Ok(Self { + app, + code_id, + contract_addr, + }) + } + + pub fn new_with_values( + app: &'a OsmosisTestApp, + code_id: u64, + contract_addr: String, + ) -> Result { + Ok(Self { + app, + code_id, + contract_addr, + }) + } + + /// uploads contract and returns a code ID + pub fn upload(app: &OsmosisTestApp, signer: &SigningAccount) -> Result { + let wasm = Wasm::new(app); + + let code_id = wasm + .store_code(&Self::get_wasm_byte_code(), None, signer)? + .data + .code_id; + + Ok(code_id) + } + + // executes + pub fn execute( + &self, + execute_msg: &ExecuteMsg, + funds: &[Coin], + signer: &SigningAccount, + ) -> RunnerExecuteResult { + let wasm = Wasm::new(self.app); + wasm.execute(&self.contract_addr, execute_msg, funds, signer) + } + + // queries + pub fn query(&self, query_msg: &QueryMsg) -> Result + where + T: DeserializeOwned, + { + let wasm = Wasm::new(self.app); + wasm.query(&self.contract_addr, query_msg) + } + + fn get_wasm_byte_code() -> Vec { + let manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let byte_code = std::fs::read( + manifest_path + .join("..") + .join("..") + .join("artifacts") + .join("dao_voting_token_staked.wasm"), + ); + match byte_code { + Ok(byte_code) => byte_code, + // On arm processors, the above path is not found, so we try the following path + Err(_) => std::fs::read( + manifest_path + .join("..") + .join("..") + .join("artifacts") + .join("dao_voting_token_staked-aarch64.wasm"), + ) + .unwrap(), + } + } + + pub fn execute_error(err: ContractError) -> RunnerError { + RunnerError::ExecuteError { + msg: format!( + "failed to execute message; message index: 0: {}: execute wasm contract failed", + err + ), + } + } +} diff --git a/packages/dao-testing/src/test_tube/mod.rs b/packages/dao-testing/src/test_tube/mod.rs index 016c51488..e609fa47e 100644 --- a/packages/dao-testing/src/test_tube/mod.rs +++ b/packages/dao-testing/src/test_tube/mod.rs @@ -14,6 +14,9 @@ pub mod cw_tokenfactory_issuer; #[cfg(feature = "test-tube")] pub mod cw721_base; +#[cfg(feature = "test-tube")] +pub mod dao_abc_factory; + #[cfg(feature = "test-tube")] pub mod dao_dao_core; @@ -22,3 +25,6 @@ pub mod dao_proposal_single; #[cfg(feature = "test-tube")] pub mod dao_test_custom_factory; + +#[cfg(feature = "test-tube")] +pub mod dao_voting_token_staked;