From 4935395b608492b2861116c0e13bb05bb84a5911 Mon Sep 17 00:00:00 2001 From: Noah Saso Date: Wed, 16 Oct 2024 16:51:14 -0400 Subject: [PATCH] made DAO testing suites for all voting modules --- packages/dao-testing/src/suite/base.rs | 107 ++++--- packages/dao-testing/src/suite/cw20_suite.rs | 268 ++++++++++++++++++ packages/dao-testing/src/suite/cw4_suite.rs | 81 +++++- packages/dao-testing/src/suite/cw721_suite.rs | 245 ++++++++++++++++ packages/dao-testing/src/suite/mod.rs | 9 +- packages/dao-testing/src/suite/token_suite.rs | 227 +++++++++++++++ 6 files changed, 879 insertions(+), 58 deletions(-) create mode 100644 packages/dao-testing/src/suite/cw20_suite.rs create mode 100644 packages/dao-testing/src/suite/cw721_suite.rs create mode 100644 packages/dao-testing/src/suite/token_suite.rs diff --git a/packages/dao-testing/src/suite/base.rs b/packages/dao-testing/src/suite/base.rs index 1c136a726..174c2b9a8 100644 --- a/packages/dao-testing/src/suite/base.rs +++ b/packages/dao-testing/src/suite/base.rs @@ -5,10 +5,11 @@ use cw_utils::Duration; use super::*; use crate::contracts::*; -pub struct TestDao { - pub core: Addr, - pub voting_module: Addr, +pub struct TestDao { + pub core_addr: Addr, + pub voting_module_addr: Addr, pub proposal_modules: Vec, + pub x: Extra, } pub struct DaoTestingSuiteBase { @@ -41,7 +42,7 @@ pub struct DaoTestingSuiteBase { pub admin_factory_addr: Addr, } -pub trait DaoTestingSuite { +pub trait DaoTestingSuite { /// get the testing suite base fn base(&self) -> &DaoTestingSuiteBase; @@ -51,8 +52,15 @@ pub trait DaoTestingSuite { /// get the voting module info to instantiate the DAO with fn get_voting_module_info(&self) -> dao_interface::state::ModuleInstantiateInfo; - /// build the DAO - fn dao(&mut self) -> TestDao { + /// get the extra DAO fields + fn get_dao_extra(&self, _dao: &TestDao) -> Extra; + + /// perform additional setup for the DAO after it is created. empty default + /// implementation makes this optional. + fn dao_setup(&mut self, _dao: &mut TestDao) {} + + /// build the DAO. no need to override this. + fn dao(&mut self) -> TestDao { let voting_module_info = self.get_voting_module_info(); let proposal_module_infos = @@ -131,9 +139,25 @@ pub trait DaoTestingSuite { label: "multiple choice proposal module".to_string(), }]; - return self + // create the DAO using the base testing suite + let dao = self .base_mut() .build(voting_module_info, proposal_module_infos); + + // perform additional queries to get extra fields for DAO struct + let x = self.get_dao_extra(&dao); + + let mut dao = TestDao { + core_addr: dao.core_addr, + voting_module_addr: dao.voting_module_addr, + proposal_modules: dao.proposal_modules, + x, + }; + + // perform additional setup after the DAO is created + self.dao_setup(&mut dao); + + dao } /// get the app querier @@ -142,6 +166,7 @@ pub trait DaoTestingSuite { } } +// CONSTRUCTOR impl DaoTestingSuiteBase { pub fn new() -> Self { let mut app = App::default(); @@ -207,12 +232,13 @@ impl DaoTestingSuiteBase { } } +// DAO CREATION impl DaoTestingSuiteBase { pub fn build( &mut self, voting_module_instantiate_info: dao_interface::state::ModuleInstantiateInfo, proposal_modules_instantiate_info: Vec, - ) -> TestDao { + ) -> TestDao { let init = dao_interface::msg::InstantiateMsg { admin: None, name: "DAO DAO".to_string(), @@ -266,63 +292,34 @@ impl DaoTestingSuiteBase { .unwrap(); TestDao { - core, - voting_module, + core_addr: core, + voting_module_addr: voting_module, proposal_modules, + x: Empty::default(), } } pub fn cw4(&mut self) -> DaoTestingSuiteCw4 { DaoTestingSuiteCw4::new(self) } -} - -#[cfg(test)] -mod test { - use cosmwasm_std::Uint128; - use super::*; - - #[test] - fn dao_testing_suite_cw4() { - let mut suite = DaoTestingSuiteBase::new(); - let mut suite = suite.cw4(); - let cw4_dao = suite.dao(); + pub fn cw20(&mut self) -> DaoTestingSuiteCw20 { + DaoTestingSuiteCw20::new(self) + } - let voting_module: Addr = suite - .querier() - .query_wasm_smart( - &cw4_dao.core, - &dao_interface::msg::QueryMsg::VotingModule {}, - ) - .unwrap(); - assert_eq!(voting_module, cw4_dao.voting_module); + pub fn cw721(&mut self) -> DaoTestingSuiteCw721 { + DaoTestingSuiteCw721::new(self) + } - let proposal_modules: Vec = suite - .querier() - .query_wasm_smart( - &cw4_dao.core, - &dao_interface::msg::QueryMsg::ProposalModules { - start_after: None, - limit: None, - }, - ) - .unwrap(); - assert_eq!(proposal_modules.len(), 2); + pub fn token(&mut self) -> DaoTestingSuiteToken { + DaoTestingSuiteToken::new(self) + } +} - let total_weight: dao_interface::voting::TotalPowerAtHeightResponse = suite - .querier() - .query_wasm_smart( - &cw4_dao.core, - &dao_interface::msg::QueryMsg::TotalPowerAtHeight { height: None }, - ) - .unwrap(); - assert_eq!( - total_weight.power, - suite - .members - .iter() - .fold(Uint128::zero(), |acc, m| acc + Uint128::from(m.weight)) - ); +// UTILITIES +impl DaoTestingSuiteBase { + /// advance the block height by one + pub fn advance_block(&mut self) { + self.app.update_block(|b| b.height += 1); } } diff --git a/packages/dao-testing/src/suite/cw20_suite.rs b/packages/dao-testing/src/suite/cw20_suite.rs new file mode 100644 index 000000000..f5d65762e --- /dev/null +++ b/packages/dao-testing/src/suite/cw20_suite.rs @@ -0,0 +1,268 @@ +use cosmwasm_std::{to_json_binary, Addr, Uint128}; +use cw20::Cw20Coin; +use cw_utils::Duration; + +use super::*; + +pub struct DaoTestingSuiteCw20<'a> { + pub base: &'a mut DaoTestingSuiteBase, + + pub initial_balances: Vec, + pub initial_dao_balance: Uint128, + pub unstaking_duration: Option, + pub active_threshold: Option, +} + +pub struct Cw20DaoExtra { + pub cw20_addr: Addr, + pub staking_addr: Addr, +} + +pub type Cw20TestDao = TestDao; + +impl<'a> DaoTestingSuiteCw20<'a> { + pub fn new(base: &'a mut DaoTestingSuiteBase) -> Self { + Self { + base, + + initial_balances: vec![ + Cw20Coin { + address: MEMBER1.to_string(), + amount: Uint128::new(100), + }, + Cw20Coin { + address: MEMBER2.to_string(), + amount: Uint128::new(200), + }, + Cw20Coin { + address: MEMBER3.to_string(), + amount: Uint128::new(300), + }, + Cw20Coin { + address: MEMBER4.to_string(), + amount: Uint128::new(300), + }, + Cw20Coin { + address: MEMBER5.to_string(), + amount: Uint128::new(100), + }, + ], + initial_dao_balance: Uint128::new(10000), + unstaking_duration: Some(Duration::Height(10)), + active_threshold: None, + } + } + + pub fn with_initial_balances(&mut self, initial_balances: Vec) -> &mut Self { + self.initial_balances = initial_balances; + self + } + + pub fn with_initial_dao_balance( + &mut self, + initial_dao_balance: impl Into, + ) -> &mut Self { + self.initial_dao_balance = initial_dao_balance.into(); + self + } + + pub fn with_unstaking_duration(&mut self, unstaking_duration: Option) -> &mut Self { + self.unstaking_duration = unstaking_duration; + self + } + + pub fn with_active_threshold( + &mut self, + active_threshold: Option, + ) -> &mut Self { + self.active_threshold = active_threshold; + self + } + + /// stake tokens + pub fn stake( + &mut self, + dao: &Cw20TestDao, + staker: impl Into, + amount: impl Into, + ) { + self.base + .app + .execute_contract( + Addr::unchecked(staker), + dao.x.cw20_addr.clone(), + &cw20::Cw20ExecuteMsg::Send { + contract: dao.x.staking_addr.to_string(), + amount: amount.into(), + msg: to_json_binary(&cw20_stake::msg::ReceiveMsg::Stake {}).unwrap(), + }, + &[], + ) + .unwrap(); + } + + /// unstake tokens + pub fn unstake( + &mut self, + dao: &Cw20TestDao, + staker: impl Into, + amount: impl Into, + ) { + self.base + .app + .execute_contract( + Addr::unchecked(staker), + dao.x.staking_addr.clone(), + &cw20_stake::msg::ExecuteMsg::Unstake { + amount: amount.into(), + }, + &[], + ) + .unwrap(); + } + + /// stake all initial balances and progress one block + pub fn stake_all_initial(&mut self, dao: &Cw20TestDao) { + for member in self.initial_balances.clone() { + self.stake(&dao, member.address, member.amount); + } + + // staking takes effect at the next block + self.base.advance_block(); + } +} + +impl<'a> DaoTestingSuite for DaoTestingSuiteCw20<'a> { + fn base(&self) -> &DaoTestingSuiteBase { + self.base + } + + fn base_mut(&mut self) -> &mut DaoTestingSuiteBase { + self.base + } + + fn get_voting_module_info(&self) -> dao_interface::state::ModuleInstantiateInfo { + dao_interface::state::ModuleInstantiateInfo { + code_id: self.base.voting_cw20_staked_id, + msg: to_json_binary(&dao_voting_cw20_staked::msg::InstantiateMsg { + token_info: dao_voting_cw20_staked::msg::TokenInfo::New { + code_id: self.base.cw20_base_id, + label: "voting token".to_string(), + name: "Voting Token".to_string(), + symbol: "VOTE".to_string(), + decimals: 6, + initial_balances: self.initial_balances.clone(), + marketing: None, + staking_code_id: self.base.cw20_stake_id, + unstaking_duration: self.unstaking_duration, + initial_dao_balance: Some(self.initial_dao_balance), + }, + active_threshold: self.active_threshold.clone(), + }) + .unwrap(), + admin: Some(dao_interface::state::Admin::CoreModule {}), + funds: vec![], + label: "voting module".to_string(), + } + } + + fn get_dao_extra(&self, dao: &TestDao) -> Cw20DaoExtra { + let cw20_addr: Addr = self + .querier() + .query_wasm_smart( + &dao.voting_module_addr, + &dao_voting_cw20_staked::msg::QueryMsg::TokenContract {}, + ) + .unwrap(); + let staking_addr: Addr = self + .querier() + .query_wasm_smart( + &dao.voting_module_addr, + &dao_voting_cw20_staked::msg::QueryMsg::StakingContract {}, + ) + .unwrap(); + + Cw20DaoExtra { + cw20_addr, + staking_addr, + } + } + + /// stake all initial balances and progress one block + fn dao_setup(&mut self, dao: &mut Cw20TestDao) { + for member in self.initial_balances.clone() { + self.stake(&dao, member.address, member.amount); + } + + // staking takes effect at the next block + self.base.advance_block(); + } +} + +#[cfg(test)] +mod tests { + use cosmwasm_std::Uint128; + + use super::*; + + #[test] + fn dao_testing_suite_cw20() { + let mut suite = DaoTestingSuiteBase::new(); + let mut suite = suite.cw20(); + let dao = suite.dao(); + + let voting_module: Addr = suite + .querier() + .query_wasm_smart( + &dao.core_addr, + &dao_interface::msg::QueryMsg::VotingModule {}, + ) + .unwrap(); + assert_eq!(voting_module, dao.voting_module_addr); + + let proposal_modules: Vec = suite + .querier() + .query_wasm_smart( + &dao.core_addr, + &dao_interface::msg::QueryMsg::ProposalModules { + start_after: None, + limit: None, + }, + ) + .unwrap(); + assert_eq!(proposal_modules.len(), 2); + + let cw20_addr: Addr = suite + .querier() + .query_wasm_smart( + &dao.voting_module_addr, + &dao_voting_cw20_staked::msg::QueryMsg::TokenContract {}, + ) + .unwrap(); + assert_eq!(cw20_addr, dao.x.cw20_addr); + + let staking_addr: Addr = suite + .querier() + .query_wasm_smart( + &dao.voting_module_addr, + &dao_voting_cw20_staked::msg::QueryMsg::StakingContract {}, + ) + .unwrap(); + assert_eq!(staking_addr, dao.x.staking_addr); + + let total_weight: dao_interface::voting::TotalPowerAtHeightResponse = suite + .querier() + .query_wasm_smart( + &dao.core_addr, + &dao_interface::msg::QueryMsg::TotalPowerAtHeight { height: None }, + ) + .unwrap(); + assert_eq!( + total_weight.power, + suite + .initial_balances + .iter() + .fold(Uint128::zero(), |acc, m| acc + m.amount) + ); + } +} diff --git a/packages/dao-testing/src/suite/cw4_suite.rs b/packages/dao-testing/src/suite/cw4_suite.rs index 5809e1936..3bd955593 100644 --- a/packages/dao-testing/src/suite/cw4_suite.rs +++ b/packages/dao-testing/src/suite/cw4_suite.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::to_json_binary; +use cosmwasm_std::{to_json_binary, Addr}; use super::*; @@ -8,6 +8,12 @@ pub struct DaoTestingSuiteCw4<'a> { pub members: Vec, } +pub struct Cw4DaoExtra { + pub group_addr: Addr, +} + +pub type Cw4TestDao = TestDao; + impl<'a> DaoTestingSuiteCw4<'a> { pub fn new(base: &'a mut DaoTestingSuiteBase) -> Self { Self { @@ -43,7 +49,7 @@ impl<'a> DaoTestingSuiteCw4<'a> { } } -impl<'a> DaoTestingSuite for DaoTestingSuiteCw4<'a> { +impl<'a> DaoTestingSuite for DaoTestingSuiteCw4<'a> { fn base(&self) -> &DaoTestingSuiteBase { self.base } @@ -67,4 +73,75 @@ impl<'a> DaoTestingSuite for DaoTestingSuiteCw4<'a> { label: "voting module".to_string(), } } + + fn get_dao_extra(&self, dao: &TestDao) -> Cw4DaoExtra { + let group_addr: Addr = self + .querier() + .query_wasm_smart( + &dao.voting_module_addr, + &dao_voting_cw4::msg::QueryMsg::GroupContract {}, + ) + .unwrap(); + + Cw4DaoExtra { group_addr } + } +} + +#[cfg(test)] +mod tests { + use cosmwasm_std::Uint128; + + use super::*; + + #[test] + fn dao_testing_suite_cw4() { + let mut suite = DaoTestingSuiteBase::new(); + let mut suite = suite.cw4(); + let dao = suite.dao(); + + let voting_module: Addr = suite + .querier() + .query_wasm_smart( + &dao.core_addr, + &dao_interface::msg::QueryMsg::VotingModule {}, + ) + .unwrap(); + assert_eq!(voting_module, dao.voting_module_addr); + + let proposal_modules: Vec = suite + .querier() + .query_wasm_smart( + &dao.core_addr, + &dao_interface::msg::QueryMsg::ProposalModules { + start_after: None, + limit: None, + }, + ) + .unwrap(); + assert_eq!(proposal_modules.len(), 2); + + let group_addr: Addr = suite + .querier() + .query_wasm_smart( + &dao.voting_module_addr, + &dao_voting_cw4::msg::QueryMsg::GroupContract {}, + ) + .unwrap(); + assert_eq!(group_addr, dao.x.group_addr); + + let total_weight: dao_interface::voting::TotalPowerAtHeightResponse = suite + .querier() + .query_wasm_smart( + &dao.core_addr, + &dao_interface::msg::QueryMsg::TotalPowerAtHeight { height: None }, + ) + .unwrap(); + assert_eq!( + total_weight.power, + suite + .members + .iter() + .fold(Uint128::zero(), |acc, m| acc + Uint128::from(m.weight)) + ); + } } diff --git a/packages/dao-testing/src/suite/cw721_suite.rs b/packages/dao-testing/src/suite/cw721_suite.rs new file mode 100644 index 000000000..fa8ce286b --- /dev/null +++ b/packages/dao-testing/src/suite/cw721_suite.rs @@ -0,0 +1,245 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{to_json_binary, Addr, Binary, Empty}; +use cw_utils::Duration; + +use super::*; + +#[cw_serde] +pub struct InitialNft { + pub token_id: String, + pub owner: String, +} + +pub struct DaoTestingSuiteCw721<'a> { + pub base: &'a mut DaoTestingSuiteBase, + + pub initial_nfts: Vec, + pub unstaking_duration: Option, + pub active_threshold: Option, +} + +pub struct Cw721DaoExtra { + pub cw721_addr: Addr, +} + +pub type Cw721TestDao = TestDao; + +impl<'a> DaoTestingSuiteCw721<'a> { + pub fn new(base: &'a mut DaoTestingSuiteBase) -> Self { + Self { + base, + + initial_nfts: vec![ + InitialNft { + token_id: "1".to_string(), + owner: MEMBER1.to_string(), + }, + InitialNft { + token_id: "2".to_string(), + owner: MEMBER2.to_string(), + }, + InitialNft { + token_id: "3".to_string(), + owner: MEMBER3.to_string(), + }, + InitialNft { + token_id: "4".to_string(), + owner: MEMBER4.to_string(), + }, + InitialNft { + token_id: "5".to_string(), + owner: MEMBER5.to_string(), + }, + ], + unstaking_duration: Some(Duration::Height(10)), + active_threshold: None, + } + } + + pub fn with_initial_nfts(&mut self, initial_nfts: Vec) -> &mut Self { + self.initial_nfts = initial_nfts; + self + } + + pub fn with_unstaking_duration(&mut self, unstaking_duration: Option) -> &mut Self { + self.unstaking_duration = unstaking_duration; + self + } + + pub fn with_active_threshold( + &mut self, + active_threshold: Option, + ) -> &mut Self { + self.active_threshold = active_threshold; + self + } + + /// stake NFT + pub fn stake( + &mut self, + dao: &Cw721TestDao, + staker: impl Into, + token_id: impl Into, + ) { + self.base + .app + .execute_contract( + Addr::unchecked(staker), + dao.x.cw721_addr.clone(), + &cw721_base::msg::ExecuteMsg::::SendNft { + contract: dao.voting_module_addr.to_string(), + token_id: token_id.into(), + msg: Binary::default(), + }, + &[], + ) + .unwrap(); + } + + /// unstake NFT + pub fn unstake( + &mut self, + dao: &Cw721TestDao, + staker: impl Into, + token_id: impl Into, + ) { + self.base + .app + .execute_contract( + Addr::unchecked(staker), + dao.voting_module_addr.clone(), + &dao_voting_cw721_staked::msg::ExecuteMsg::Unstake { + token_ids: vec![token_id.into()], + }, + &[], + ) + .unwrap(); + } +} + +impl<'a> DaoTestingSuite for DaoTestingSuiteCw721<'a> { + fn base(&self) -> &DaoTestingSuiteBase { + self.base + } + + fn base_mut(&mut self) -> &mut DaoTestingSuiteBase { + self.base + } + + fn get_voting_module_info(&self) -> dao_interface::state::ModuleInstantiateInfo { + dao_interface::state::ModuleInstantiateInfo { + code_id: self.base.voting_cw721_staked_id, + msg: to_json_binary(&dao_voting_cw721_staked::msg::InstantiateMsg { + nft_contract: dao_voting_cw721_staked::msg::NftContract::New { + code_id: self.base.cw721_base_id, + label: "voting NFT".to_string(), + msg: to_json_binary(&cw721_base::msg::InstantiateMsg { + name: "Voting NFT".to_string(), + symbol: "VOTE".to_string(), + minter: CREATOR.to_string(), + }) + .unwrap(), + initial_nfts: self + .initial_nfts + .iter() + .map(|x| { + to_json_binary(&cw721_base::msg::ExecuteMsg::::Mint { + token_id: x.token_id.clone(), + owner: x.owner.clone(), + token_uri: None, + extension: Empty {}, + }) + .unwrap() + }) + .collect(), + }, + unstaking_duration: self.unstaking_duration, + active_threshold: self.active_threshold.clone(), + }) + .unwrap(), + admin: Some(dao_interface::state::Admin::CoreModule {}), + funds: vec![], + label: "voting module".to_string(), + } + } + + fn get_dao_extra(&self, dao: &TestDao) -> Cw721DaoExtra { + let dao_voting_cw721_staked::state::Config { nft_address, .. } = self + .querier() + .query_wasm_smart( + &dao.voting_module_addr, + &dao_voting_cw721_staked::msg::QueryMsg::Config {}, + ) + .unwrap(); + + Cw721DaoExtra { + cw721_addr: nft_address, + } + } + + /// stake all initial NFTs and progress one block + fn dao_setup(&mut self, dao: &mut Cw721TestDao) { + for nft in self.initial_nfts.clone() { + self.stake(&dao, nft.owner, nft.token_id); + } + + // staking takes effect at the next block + self.base.advance_block(); + } +} + +#[cfg(test)] +mod tests { + use cosmwasm_std::Uint128; + + use super::*; + + #[test] + fn dao_testing_suite_cw721() { + let mut suite = DaoTestingSuiteBase::new(); + let mut suite = suite.cw721(); + let dao = suite.dao(); + + let voting_module: Addr = suite + .querier() + .query_wasm_smart( + &dao.core_addr, + &dao_interface::msg::QueryMsg::VotingModule {}, + ) + .unwrap(); + assert_eq!(voting_module, dao.voting_module_addr); + + let proposal_modules: Vec = suite + .querier() + .query_wasm_smart( + &dao.core_addr, + &dao_interface::msg::QueryMsg::ProposalModules { + start_after: None, + limit: None, + }, + ) + .unwrap(); + assert_eq!(proposal_modules.len(), 2); + + let dao_voting_cw721_staked::state::Config { nft_address, .. } = suite + .querier() + .query_wasm_smart( + &dao.voting_module_addr, + &dao_voting_cw721_staked::msg::QueryMsg::Config {}, + ) + .unwrap(); + assert_eq!(nft_address, dao.x.cw721_addr); + + let total_weight: dao_interface::voting::TotalPowerAtHeightResponse = suite + .querier() + .query_wasm_smart( + &dao.core_addr, + &dao_interface::msg::QueryMsg::TotalPowerAtHeight { height: None }, + ) + .unwrap(); + assert_eq!( + total_weight.power, + Uint128::from(suite.initial_nfts.len() as u128) + ); + } +} diff --git a/packages/dao-testing/src/suite/mod.rs b/packages/dao-testing/src/suite/mod.rs index 14c6ad197..1f09c5d4b 100644 --- a/packages/dao-testing/src/suite/mod.rs +++ b/packages/dao-testing/src/suite/mod.rs @@ -1,5 +1,8 @@ mod base; +mod cw20_suite; mod cw4_suite; +mod cw721_suite; +mod token_suite; pub const CREATOR: &str = "creator"; @@ -10,7 +13,11 @@ pub const MEMBER4: &str = "member4"; pub const MEMBER5: &str = "member5"; pub const GOVTOKEN1: &str = "govtoken1"; -pub const GOVTOKEN2: &str = "govtoken2"; + +pub use cw_multi_test::Executor; pub use base::*; +pub use cw20_suite::*; pub use cw4_suite::*; +pub use cw721_suite::*; +pub use token_suite::*; diff --git a/packages/dao-testing/src/suite/token_suite.rs b/packages/dao-testing/src/suite/token_suite.rs new file mode 100644 index 000000000..e5f9441c4 --- /dev/null +++ b/packages/dao-testing/src/suite/token_suite.rs @@ -0,0 +1,227 @@ +use cosmwasm_std::{coins, to_json_binary, Addr, Uint128}; +use cw_multi_test::{BankSudo, SudoMsg}; +use cw_utils::Duration; +use dao_interface::token::InitialBalance; + +use super::*; + +pub struct DaoTestingSuiteToken<'a> { + pub base: &'a mut DaoTestingSuiteBase, + + pub initial_balances: Vec, + pub unstaking_duration: Option, + pub active_threshold: Option, +} + +pub struct TokenDaoExtra { + pub denom: String, +} + +pub type TokenTestDao = TestDao; + +impl<'a> DaoTestingSuiteToken<'a> { + pub fn new(base: &'a mut DaoTestingSuiteBase) -> Self { + Self { + base, + + initial_balances: vec![ + InitialBalance { + address: MEMBER1.to_string(), + amount: Uint128::new(100), + }, + InitialBalance { + address: MEMBER2.to_string(), + amount: Uint128::new(200), + }, + InitialBalance { + address: MEMBER3.to_string(), + amount: Uint128::new(300), + }, + InitialBalance { + address: MEMBER4.to_string(), + amount: Uint128::new(300), + }, + InitialBalance { + address: MEMBER5.to_string(), + amount: Uint128::new(100), + }, + ], + unstaking_duration: Some(Duration::Height(10)), + active_threshold: None, + } + } + + pub fn with_initial_balances(&mut self, initial_balances: Vec) -> &mut Self { + self.initial_balances = initial_balances; + self + } + + pub fn with_unstaking_duration(&mut self, unstaking_duration: Option) -> &mut Self { + self.unstaking_duration = unstaking_duration; + self + } + + pub fn with_active_threshold( + &mut self, + active_threshold: Option, + ) -> &mut Self { + self.active_threshold = active_threshold; + self + } + + /// mint tokens + pub fn mint( + &mut self, + dao: &TokenTestDao, + recipient: impl Into, + amount: impl Into, + ) { + self.base + .app + .sudo(SudoMsg::Bank({ + BankSudo::Mint { + to_address: recipient.into(), + amount: coins(amount.into(), &dao.x.denom), + } + })) + .unwrap(); + } + + /// stake tokens + pub fn stake( + &mut self, + dao: &TokenTestDao, + staker: impl Into, + amount: impl Into, + ) { + self.base + .app + .execute_contract( + Addr::unchecked(staker), + dao.voting_module_addr.clone(), + &dao_voting_token_staked::msg::ExecuteMsg::Stake {}, + &coins(amount.into(), &dao.x.denom), + ) + .unwrap(); + } + + /// unstake tokens + pub fn unstake( + &mut self, + dao: &TokenTestDao, + staker: impl Into, + amount: impl Into, + ) { + self.base + .app + .execute_contract( + Addr::unchecked(staker), + dao.voting_module_addr.clone(), + &dao_voting_token_staked::msg::ExecuteMsg::Unstake { + amount: amount.into(), + }, + &[], + ) + .unwrap(); + } +} + +impl<'a> DaoTestingSuite for DaoTestingSuiteToken<'a> { + fn base(&self) -> &DaoTestingSuiteBase { + self.base + } + + fn base_mut(&mut self) -> &mut DaoTestingSuiteBase { + self.base + } + + fn get_voting_module_info(&self) -> dao_interface::state::ModuleInstantiateInfo { + dao_interface::state::ModuleInstantiateInfo { + code_id: self.base.voting_token_staked_id, + msg: to_json_binary(&dao_voting_token_staked::msg::InstantiateMsg { + token_info: dao_voting_token_staked::msg::TokenInfo::Existing { + denom: GOVTOKEN1.to_string(), + }, + unstaking_duration: self.unstaking_duration, + active_threshold: self.active_threshold.clone(), + }) + .unwrap(), + admin: Some(dao_interface::state::Admin::CoreModule {}), + funds: vec![], + label: "voting module".to_string(), + } + } + + fn get_dao_extra(&self, dao: &TestDao) -> TokenDaoExtra { + let dao_interface::voting::DenomResponse { denom } = self + .querier() + .query_wasm_smart( + &dao.voting_module_addr, + &dao_voting_token_staked::msg::QueryMsg::Denom {}, + ) + .unwrap(); + + TokenDaoExtra { denom } + } + + /// mint and stake all initial balances and progress one block + fn dao_setup(&mut self, dao: &mut TokenTestDao) { + for member in self.initial_balances.clone() { + self.mint(&dao, member.address.clone(), member.amount); + self.stake(&dao, member.address, member.amount); + } + + // staking takes effect at the next block + self.base.advance_block(); + } +} + +#[cfg(test)] +mod tests { + use cosmwasm_std::Uint128; + + use super::*; + + #[test] + fn dao_testing_suite_token() { + let mut suite = DaoTestingSuiteBase::new(); + let mut suite = suite.token(); + let dao = suite.dao(); + + let voting_module: Addr = suite + .querier() + .query_wasm_smart( + &dao.core_addr, + &dao_interface::msg::QueryMsg::VotingModule {}, + ) + .unwrap(); + assert_eq!(voting_module, dao.voting_module_addr); + + let proposal_modules: Vec = suite + .querier() + .query_wasm_smart( + &dao.core_addr, + &dao_interface::msg::QueryMsg::ProposalModules { + start_after: None, + limit: None, + }, + ) + .unwrap(); + assert_eq!(proposal_modules.len(), 2); + + let total_weight: dao_interface::voting::TotalPowerAtHeightResponse = suite + .querier() + .query_wasm_smart( + &dao.core_addr, + &dao_interface::msg::QueryMsg::TotalPowerAtHeight { height: None }, + ) + .unwrap(); + assert_eq!( + total_weight.power, + suite + .initial_balances + .iter() + .fold(Uint128::zero(), |acc, m| acc + m.amount) + ); + } +}