diff --git a/Cargo.lock b/Cargo.lock index 6742e170e..539833257 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2072,6 +2072,7 @@ dependencies = [ "cw4-group 1.1.2", "dao-dao-macros", "dao-interface", + "dao-voting 2.4.2", "thiserror", ] diff --git a/contracts/pre-propose/dao-pre-propose-approval-single/src/tests.rs b/contracts/pre-propose/dao-pre-propose-approval-single/src/tests.rs index 99b8c0369..92240f9a1 100644 --- a/contracts/pre-propose/dao-pre-propose-approval-single/src/tests.rs +++ b/contracts/pre-propose/dao-pre-propose-approval-single/src/tests.rs @@ -135,6 +135,7 @@ fn setup_default_test( amount: Uint128::new(8), }, ]), + None, ); let proposal_modules: Vec = app .wrap() @@ -1385,6 +1386,7 @@ fn test_instantiate_with_zero_native_deposit() { amount: Uint128::new(8), }, ]), + None, ); } @@ -1450,6 +1452,7 @@ fn test_instantiate_with_zero_cw20_deposit() { amount: Uint128::new(8), }, ]), + None, ); } diff --git a/contracts/pre-propose/dao-pre-propose-approver/src/tests.rs b/contracts/pre-propose/dao-pre-propose-approver/src/tests.rs index fbef73e87..c47e4d550 100644 --- a/contracts/pre-propose/dao-pre-propose-approver/src/tests.rs +++ b/contracts/pre-propose/dao-pre-propose-approver/src/tests.rs @@ -201,6 +201,7 @@ fn setup_default_test( amount: Uint128::new(8), }, ]), + None, ); let proposal_modules: Vec = app .wrap() @@ -255,6 +256,7 @@ fn setup_default_test( amount: Uint128::new(8), }, ]), + None, ); let proposal_modules: Vec = app .wrap() diff --git a/contracts/pre-propose/dao-pre-propose-multiple/src/tests.rs b/contracts/pre-propose/dao-pre-propose-multiple/src/tests.rs index 56f310ee2..08d845179 100644 --- a/contracts/pre-propose/dao-pre-propose-multiple/src/tests.rs +++ b/contracts/pre-propose/dao-pre-propose-multiple/src/tests.rs @@ -134,6 +134,7 @@ fn setup_default_test( amount: Uint128::new(8), }, ]), + None, ); let proposal_modules: Vec = app .wrap() @@ -1088,6 +1089,7 @@ fn test_instantiate_with_zero_native_deposit() { amount: Uint128::new(8), }, ]), + None, ); } @@ -1151,6 +1153,7 @@ fn test_instantiate_with_zero_cw20_deposit() { amount: Uint128::new(8), }, ]), + None, ); } diff --git a/contracts/pre-propose/dao-pre-propose-single/src/tests.rs b/contracts/pre-propose/dao-pre-propose-single/src/tests.rs index 0475d13b6..827caba22 100644 --- a/contracts/pre-propose/dao-pre-propose-single/src/tests.rs +++ b/contracts/pre-propose/dao-pre-propose-single/src/tests.rs @@ -132,6 +132,7 @@ fn setup_default_test( amount: Uint128::new(8), }, ]), + None, ); let proposal_modules: Vec = app .wrap() @@ -1024,6 +1025,7 @@ fn test_instantiate_with_zero_native_deposit() { amount: Uint128::new(8), }, ]), + None, ); } @@ -1087,6 +1089,7 @@ fn test_instantiate_with_zero_cw20_deposit() { amount: Uint128::new(8), }, ]), + None, ); } diff --git a/contracts/proposal/dao-proposal-condorcet/src/testing/suite.rs b/contracts/proposal/dao-proposal-condorcet/src/testing/suite.rs index 79d18154f..58d55833a 100644 --- a/contracts/proposal/dao-proposal-condorcet/src/testing/suite.rs +++ b/contracts/proposal/dao-proposal-condorcet/src/testing/suite.rs @@ -8,7 +8,7 @@ use dao_interface::{ use dao_testing::contracts::{ cw4_group_contract, dao_dao_contract, dao_voting_cw4_contract, proposal_condorcet_contract, }; -use dao_voting::threshold::PercentageThreshold; +use dao_voting::threshold::{ActiveThreshold, PercentageThreshold}; use dao_voting_cw4::msg::GroupContract; use crate::{ @@ -30,6 +30,7 @@ pub(crate) struct SuiteBuilder { pub instantiate: InstantiateMsg, with_proposal: Option, with_voters: Vec<(String, u64)>, + active_threshold: Option, } impl Default for SuiteBuilder { @@ -43,6 +44,7 @@ impl Default for SuiteBuilder { }, with_proposal: None, with_voters: vec![("sender".to_string(), 10)], + active_threshold: None, } } } @@ -93,6 +95,7 @@ impl SuiteBuilder { cw4_group_code_id: cw4_id, initial_members, }, + active_threshold: self.active_threshold.clone(), }) .unwrap(), admin: Some(Admin::CoreModule {}), diff --git a/contracts/proposal/dao-proposal-multiple/src/testing/instantiate.rs b/contracts/proposal/dao-proposal-multiple/src/testing/instantiate.rs index 8fd7e0c95..f9c877cef 100644 --- a/contracts/proposal/dao-proposal-multiple/src/testing/instantiate.rs +++ b/contracts/proposal/dao-proposal-multiple/src/testing/instantiate.rs @@ -766,6 +766,7 @@ pub fn _instantiate_with_cw4_groups_governance( app: &mut App, proposal_module_instantiate: InstantiateMsg, initial_weights: Option>, + active_threshold: Option, ) -> Addr { let proposal_module_code_id = app.store_code(proposal_multiple_contract()); let cw4_id = app.store_code(cw4_group_contract()); @@ -813,6 +814,7 @@ pub fn _instantiate_with_cw4_groups_governance( cw4_group_code_id: cw4_id, initial_members: initial_weights, }, + active_threshold, }) .unwrap(), admin: Some(Admin::CoreModule {}), diff --git a/contracts/proposal/dao-proposal-single/src/testing/instantiate.rs b/contracts/proposal/dao-proposal-single/src/testing/instantiate.rs index 020154700..eb99cac8d 100644 --- a/contracts/proposal/dao-proposal-single/src/testing/instantiate.rs +++ b/contracts/proposal/dao-proposal-single/src/testing/instantiate.rs @@ -590,6 +590,7 @@ pub(crate) fn instantiate_with_cw4_groups_governance( cw4_group_code_id: cw4_id, initial_members: initial_weights, }, + active_threshold: None, }) .unwrap(), admin: Some(Admin::CoreModule {}), diff --git a/contracts/proposal/dao-proposal-single/src/testing/tests.rs b/contracts/proposal/dao-proposal-single/src/testing/tests.rs index 4246e5a09..20123565d 100644 --- a/contracts/proposal/dao-proposal-single/src/testing/tests.rs +++ b/contracts/proposal/dao-proposal-single/src/testing/tests.rs @@ -1055,6 +1055,7 @@ fn test_timelocked_proposal_veto_expired_timelock() -> anyhow::Result<()> { }, ]), ); + let proposal_module = query_single_proposal_module(&app, &core_addr); let gov_token = query_dao_token(&app, &core_addr); @@ -1141,6 +1142,7 @@ fn test_timelocked_proposal_execute_no_early_exec() -> anyhow::Result<()> { amount: Uint128::new(85), }]), ); + let proposal_module = query_single_proposal_module(&app, &core_addr); let gov_token = query_dao_token(&app, &core_addr); @@ -1225,6 +1227,7 @@ fn test_timelocked_proposal_execute_early() -> anyhow::Result<()> { amount: Uint128::new(85), }]), ); + let proposal_module = query_single_proposal_module(&app, &core_addr); let gov_token = query_dao_token(&app, &core_addr); @@ -1315,6 +1318,7 @@ fn test_timelocked_proposal_execute_active_timelock_unauthorized() -> anyhow::Re amount: Uint128::new(85), }]), ); + let proposal_module = query_single_proposal_module(&app, &core_addr); let gov_token = query_dao_token(&app, &core_addr); @@ -1406,6 +1410,7 @@ fn test_timelocked_proposal_execute_expired_timelock_not_vetoer() -> anyhow::Res amount: Uint128::new(85), }]), ); + let proposal_module = query_single_proposal_module(&app, &core_addr); let gov_token = query_dao_token(&app, &core_addr); @@ -1492,6 +1497,7 @@ fn test_proposal_message_timelock_veto() -> anyhow::Result<()> { amount: Uint128::new(85), }]), ); + let proposal_module = query_single_proposal_module(&app, &core_addr); let gov_token = query_dao_token(&app, &core_addr); @@ -1617,6 +1623,7 @@ fn test_proposal_message_timelock_early_execution() -> anyhow::Result<()> { }, ]), ); + let proposal_module = query_single_proposal_module(&app, &core_addr); let gov_token = query_dao_token(&app, &core_addr); @@ -1704,6 +1711,7 @@ fn test_proposal_message_timelock_veto_before_passed() { }, ]), ); + let proposal_module = query_single_proposal_module(&app, &core_addr); let gov_token = query_dao_token(&app, &core_addr); @@ -1777,6 +1785,7 @@ fn test_veto_only_members_execute_proposal() -> anyhow::Result<()> { amount: Uint128::new(85), }]), ); + let proposal_module = query_single_proposal_module(&app, &core_addr); let gov_token = query_dao_token(&app, &core_addr); @@ -1902,6 +1911,7 @@ fn test_proposal_cant_close_after_expiry_is_passed() { }, ]), ); + let proposal_module = query_single_proposal_module(&app, &core_addr); let gov_token = query_dao_token(&app, &core_addr); @@ -2629,6 +2639,7 @@ fn test_min_duration_same_as_proposal_duration() { }, ]), ); + let gov_token = query_dao_token(&app, &core_addr); let proposal_module = query_single_proposal_module(&app, &core_addr); diff --git a/contracts/voting/dao-voting-cw4/Cargo.toml b/contracts/voting/dao-voting-cw4/Cargo.toml index 55c671560..52d81213a 100644 --- a/contracts/voting/dao-voting-cw4/Cargo.toml +++ b/contracts/voting/dao-voting-cw4/Cargo.toml @@ -27,6 +27,7 @@ dao-dao-macros = { workspace = true } dao-interface = { workspace = true } cw4 = { workspace = true } cw4-group = { workspace = true } +dao-voting = { workspace = true } [dev-dependencies] cw-multi-test = { workspace = true } diff --git a/contracts/voting/dao-voting-cw4/src/contract.rs b/contracts/voting/dao-voting-cw4/src/contract.rs index 48307676f..a9e1da3d9 100644 --- a/contracts/voting/dao-voting-cw4/src/contract.rs +++ b/contracts/voting/dao-voting-cw4/src/contract.rs @@ -8,9 +8,12 @@ use cw2::{get_contract_version, set_contract_version, ContractVersion}; use cw4::{MemberListResponse, MemberResponse, TotalWeightResponse}; use cw_utils::parse_reply_instantiate_data; +use dao_interface::voting::IsActiveResponse; +use dao_voting::threshold::ActiveThreshold; + use crate::error::ContractError; use crate::msg::{ExecuteMsg, GroupContract, InstantiateMsg, MigrateMsg, QueryMsg}; -use crate::state::{DAO, GROUP_CONTRACT}; +use crate::state::{ACTIVE_THRESHOLD, DAO, GROUP_CONTRACT}; pub(crate) const CONTRACT_NAME: &str = "crates.io:dao-voting-cw4"; pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -26,6 +29,19 @@ pub fn instantiate( ) -> Result { set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + // Validate and save the active threshold if provided + if let Some(threshold) = msg.active_threshold { + if let ActiveThreshold::AbsoluteCount { count } = threshold { + if count > Uint128::zero() { + ACTIVE_THRESHOLD.save(deps.storage, &threshold)?; + } else { + return Err(ContractError::InvalidThreshold {}); + } + } else { + return Err(ContractError::InvalidThreshold {}); + } + } + DAO.save(deps.storage, &info.sender)?; match msg.group_contract { @@ -108,12 +124,69 @@ pub fn instantiate( #[cfg_attr(not(feature = "library"), entry_point)] pub fn execute( - _deps: DepsMut, + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::UpdateActiveThreshold { new_threshold } => { + let dao = DAO.load(deps.storage)?; + if info.sender != dao { + return Err(ContractError::Unauthorized {}); + } + if let Some(threshold) = new_threshold { + if let ActiveThreshold::AbsoluteCount { count } = threshold { + if count > Uint128::zero() { + ACTIVE_THRESHOLD.save(deps.storage, &threshold)?; + } else { + return Err(ContractError::InvalidThreshold {}); + } + } else { + return Err(ContractError::InvalidThreshold {}); + } + } else { + ACTIVE_THRESHOLD.remove(deps.storage); + } + + Ok(Response::new() + .add_attribute("method", "update_active_threshold") + .add_attribute("status", "success")) + } + } +} + +pub fn execute_update_active_threshold( + deps: DepsMut, _env: Env, - _info: MessageInfo, - _msg: ExecuteMsg, + info: MessageInfo, + new_active_threshold: Option, ) -> Result { - Err(ContractError::NoExecute {}) + let dao = DAO.load(deps.storage)?; + if info.sender != dao { + return Err(ContractError::Unauthorized {}); + } + + if let Some(active_threshold) = new_active_threshold { + match active_threshold { + ActiveThreshold::AbsoluteCount { count } => { + if count.is_zero() { + return Err(ContractError::InvalidThreshold {}); + } + ACTIVE_THRESHOLD.save(deps.storage, &active_threshold)?; + } + // Reject percentage-based thresholds + ActiveThreshold::Percentage { .. } => { + return Err(ContractError::InvalidThreshold {}); + } + } + } else { + ACTIVE_THRESHOLD.remove(deps.storage); + } + + Ok(Response::new() + .add_attribute("method", "update_active_threshold") + .add_attribute("status", "success")) } #[cfg_attr(not(feature = "library"), entry_point)] @@ -126,6 +199,7 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { QueryMsg::Info {} => query_info(deps), QueryMsg::GroupContract {} => to_json_binary(&GROUP_CONTRACT.load(deps.storage)?), QueryMsg::Dao {} => to_json_binary(&DAO.load(deps.storage)?), + QueryMsg::IsActive {} => query_is_active(deps), } } @@ -164,10 +238,28 @@ pub fn query_total_power_at_height(deps: Deps, env: Env, height: Option) -> } pub fn query_info(deps: Deps) -> StdResult { - let info = cw2::get_contract_version(deps.storage)?; + let info = get_contract_version(deps.storage)?; to_json_binary(&dao_interface::voting::InfoResponse { info }) } +pub fn query_is_active(deps: Deps) -> StdResult { + let active_threshold = ACTIVE_THRESHOLD.load(deps.storage)?; + let group_contract = GROUP_CONTRACT.load(deps.storage)?; + let total_weight: TotalWeightResponse = deps.querier.query_wasm_smart( + group_contract, + &cw4_group::msg::QueryMsg::TotalWeight { at_height: None }, + )?; + + let is_active = match active_threshold { + ActiveThreshold::AbsoluteCount { count } => { + Uint128::new(total_weight.weight as u128) >= count + } + _ => false, // Should never happen as percentage is not supported + }; + + to_json_binary(&IsActiveResponse { active: is_active }) +} + #[cfg_attr(not(feature = "library"), entry_point)] pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { let storage_version: ContractVersion = get_contract_version(deps.storage)?; diff --git a/contracts/voting/dao-voting-cw4/src/error.rs b/contracts/voting/dao-voting-cw4/src/error.rs index a0ce03e9c..d7846fe98 100644 --- a/contracts/voting/dao-voting-cw4/src/error.rs +++ b/contracts/voting/dao-voting-cw4/src/error.rs @@ -29,4 +29,7 @@ pub enum ContractError { #[error("Total weight of the CW4 contract cannot be zero")] ZeroTotalWeight {}, + + #[error("The provided threshold is invalid: thresholds must be positive and within designated limits (e.g., percentages between 0 and 100)")] + InvalidThreshold {}, } diff --git a/contracts/voting/dao-voting-cw4/src/msg.rs b/contracts/voting/dao-voting-cw4/src/msg.rs index 24bd0eebc..271f9eb03 100644 --- a/contracts/voting/dao-voting-cw4/src/msg.rs +++ b/contracts/voting/dao-voting-cw4/src/msg.rs @@ -1,5 +1,6 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use dao_dao_macros::voting_module_query; +use dao_dao_macros::{active_query, voting_module_query}; +use dao_voting::threshold::ActiveThreshold; #[cw_serde] pub enum GroupContract { @@ -15,11 +16,20 @@ pub enum GroupContract { #[cw_serde] pub struct InstantiateMsg { pub group_contract: GroupContract, + pub active_threshold: Option, } #[cw_serde] -pub enum ExecuteMsg {} +pub enum ExecuteMsg { + /// Sets the active threshold to a new value. Only the + /// instantiator of this contract (a DAO most likely) may call this + /// method. + UpdateActiveThreshold { + new_threshold: Option, + }, +} +#[active_query] #[voting_module_query] #[cw_serde] #[derive(QueryResponses)] diff --git a/contracts/voting/dao-voting-cw4/src/state.rs b/contracts/voting/dao-voting-cw4/src/state.rs index bdc0d8004..7e74fdb26 100644 --- a/contracts/voting/dao-voting-cw4/src/state.rs +++ b/contracts/voting/dao-voting-cw4/src/state.rs @@ -1,5 +1,9 @@ use cosmwasm_std::Addr; use cw_storage_plus::Item; +use dao_voting::threshold::ActiveThreshold; pub const GROUP_CONTRACT: Item = Item::new("group_contract"); pub const DAO: Item = Item::new("dao_address"); + +/// The minimum amount of users for the DAO to be active +pub const ACTIVE_THRESHOLD: Item = Item::new("active_threshold"); diff --git a/contracts/voting/dao-voting-cw4/src/tests.rs b/contracts/voting/dao-voting-cw4/src/tests.rs index 769c53c4c..14c830b6f 100644 --- a/contracts/voting/dao-voting-cw4/src/tests.rs +++ b/contracts/voting/dao-voting-cw4/src/tests.rs @@ -1,13 +1,15 @@ use cosmwasm_std::{ testing::{mock_dependencies, mock_env}, - to_json_binary, Addr, CosmosMsg, Empty, Uint128, WasmMsg, + to_json_binary, Addr, CosmosMsg, Decimal, Empty, Uint128, WasmMsg, }; use cw2::ContractVersion; use cw_multi_test::{next_block, App, Contract, ContractWrapper, Executor}; use dao_interface::voting::{ - InfoResponse, TotalPowerAtHeightResponse, VotingPowerAtHeightResponse, + InfoResponse, IsActiveResponse, TotalPowerAtHeightResponse, VotingPowerAtHeightResponse, }; +use dao_voting::threshold::ActiveThreshold; +use crate::msg::ExecuteMsg::UpdateActiveThreshold; use crate::{ contract::{migrate, CONTRACT_NAME, CONTRACT_VERSION}, msg::{GroupContract, InstantiateMsg, MigrateMsg, QueryMsg}, @@ -82,6 +84,7 @@ fn setup_test_case(app: &mut App) -> Addr { cw4_group_code_id: cw4_id, initial_members: members, }, + active_threshold: None, }, ) } @@ -100,6 +103,7 @@ fn test_instantiate() { cw4_group_code_id: cw4_id, initial_members: [].into(), }, + active_threshold: None, }; let _err = app .instantiate_contract( @@ -131,6 +135,7 @@ fn test_instantiate() { }, ], }, + active_threshold: None, }; let _err = app .instantiate_contract( @@ -174,6 +179,7 @@ pub fn test_instantiate_existing_contract() { group_contract: GroupContract::Existing { address: cw4_addr.to_string(), }, + active_threshold: None, }, &[], "voting module", @@ -206,6 +212,7 @@ pub fn test_instantiate_existing_contract() { group_contract: GroupContract::Existing { address: cw4_addr.to_string(), }, + active_threshold: None, }; let _err = app .instantiate_contract( @@ -580,6 +587,7 @@ fn test_migrate() { cw4_group_code_id: cw4_id, initial_members, }, + active_threshold: None, }; let voting_addr = app .instantiate_contract( @@ -657,6 +665,7 @@ fn test_duplicate_member() { }, ], }, + active_threshold: None, }; // Previous versions voting power was 100, due to no dedup. // Now we error @@ -741,3 +750,365 @@ pub fn test_migrate_update_version() { assert_eq!(version.version, CONTRACT_VERSION); assert_eq!(version.contract, CONTRACT_NAME); } + +/// First test checks if the contract correctly initializes with the specified +/// active threshold. +#[test] +fn test_initialization_with_active_threshold() { + let mut app = App::default(); + let voting_id = app.store_code(voting_contract()); + + // Define an active threshold for initialization. + let active_threshold = Some(dao_voting::threshold::ActiveThreshold::AbsoluteCount { + count: Uint128::new(5), + }); + + // Instantiate the contract with the defined active threshold. + let instantiate_msg = InstantiateMsg { + group_contract: GroupContract::New { + cw4_group_code_id: app.store_code(cw4_contract()), + initial_members: vec![ + cw4::Member { + addr: ADDR1.to_string(), + weight: 1, + }, + cw4::Member { + addr: ADDR2.to_string(), + weight: 2, + }, + cw4::Member { + addr: ADDR3.to_string(), + weight: 3, + }, + ], + }, + active_threshold: active_threshold.clone(), + }; + let voting_addr = app + .instantiate_contract( + voting_id, + Addr::unchecked(DAO_ADDR), + &instantiate_msg, + &[], + "voting", + None, + ) + .unwrap(); + + // Query the contract to see if the active threshold was set correctly. + let active_response: dao_interface::voting::IsActiveResponse = app + .wrap() + .query_wasm_smart(voting_addr, &QueryMsg::IsActive {}) + .unwrap(); + + // Making sure the response matches the expected active status based on the active threshold set. + let expected_active_status = active_response.active; + // Now assuming that the total weight is greater than or equal to the threshold count for the DAO to be active. + assert!(expected_active_status); +} + +/// Second test checks if the contract rejects updates to the active +/// threshold from unauthorized accounts. +#[test] +fn test_reject_active_threshold_update_unauthorized() { + let mut app = App::default(); + let voting_id = app.store_code(voting_contract()); + let cw4_id = app.store_code(cw4_contract()); + + // Instantiate the contract. + let instantiate_msg = InstantiateMsg { + group_contract: GroupContract::New { + cw4_group_code_id: cw4_id, + initial_members: vec![ + cw4::Member { + addr: ADDR1.to_string(), + weight: 1, + }, + cw4::Member { + addr: ADDR2.to_string(), + weight: 1, + }, + ], + }, + active_threshold: None, + }; + let voting_addr = app + .instantiate_contract( + voting_id, + Addr::unchecked(DAO_ADDR), + &instantiate_msg, + &[], + "voting", + None, + ) + .unwrap(); + + // Attempt to update the active threshold from a non-DAO account. + let unauthorized_addr = "unauthorized"; + let update_msg = UpdateActiveThreshold { + new_threshold: Some(dao_voting::threshold::ActiveThreshold::AbsoluteCount { + count: Uint128::new(10), + }), + }; + let err = app + .execute_contract( + Addr::unchecked(unauthorized_addr), + voting_addr, + &update_msg, + &[], + ) + .unwrap_err(); + + // Check that the error returned matches the expected unauthorized error. + assert_eq!( + err.root_cause().to_string(), + ContractError::Unauthorized {}.to_string() + ); +} + +/// Third test verifies if the contract accepts updates to the active +/// threshold from the DAO account. This is important to ensure only +/// the authorized DAO address can update the active threshold. +#[test] +fn test_accept_active_threshold_update_authorized() { + let mut app = App::default(); + let voting_id = app.store_code(voting_contract()); + let cw4_id = app.store_code(cw4_contract()); + + // Instantiate the contract. + let instantiate_msg = InstantiateMsg { + group_contract: GroupContract::New { + cw4_group_code_id: cw4_id, + initial_members: vec![ + cw4::Member { + addr: ADDR1.to_string(), + weight: 1, + }, + cw4::Member { + addr: ADDR2.to_string(), + weight: 1, + }, + ], + }, + active_threshold: None, + }; + let voting_addr = app + .instantiate_contract( + voting_id, + Addr::unchecked(DAO_ADDR), + &instantiate_msg, + &[], + "voting", + None, + ) + .unwrap(); + + // Update the active threshold from the DAO account. + let update_msg = UpdateActiveThreshold { + new_threshold: Some(dao_voting::threshold::ActiveThreshold::AbsoluteCount { + count: Uint128::new(10), + }), + }; + let response = app + .execute_contract( + Addr::unchecked(DAO_ADDR), + voting_addr.clone(), + &update_msg, + &[], + ) + .unwrap(); + + // Check the response for a successful update using events. + assert!(response.events.iter().any(|event| event.ty == "wasm" + && event + .attributes + .iter() + .any(|attr| attr.key == "method" && attr.value == "update_active_threshold") + && event + .attributes + .iter() + .any(|attr| attr.key == "status" && attr.value == "success"))); + + // Query if IsActive is true when it should not be (since the count is set to 10, but we don't have that many members). + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(voting_addr, &QueryMsg::IsActive {}) + .unwrap(); + + // The DAO should not be active as the total weight does not meet the new threshold. + assert!( + !is_active.active, + "DAO should be inactive as the threshold is not met by the members' total weight." + ); +} + +/// Fourth test checks if the IsActive query responds correctly based on the +/// set active threshold. +#[test] +fn test_is_active_query_response() { + let mut app = App::default(); + let voting_id = app.store_code(voting_contract()); + let cw4_id = app.store_code(cw4_contract()); + + // Define members and set a specific active threshold. + let members = vec![ + cw4::Member { + addr: ADDR1.to_string(), + weight: 3, + }, + cw4::Member { + addr: ADDR2.to_string(), + weight: 2, + }, + cw4::Member { + addr: ADDR3.to_string(), + weight: 1, + }, + ]; + let active_threshold = dao_voting::threshold::ActiveThreshold::AbsoluteCount { + count: Uint128::new(5), // Threshold set such that it's just met by the sum of members' weights + }; + + // Instantiate the contract with these settings. + let instantiate_msg = InstantiateMsg { + group_contract: GroupContract::New { + cw4_group_code_id: cw4_id, + initial_members: members, + }, + active_threshold: Some(active_threshold), + }; + let voting_addr = app + .instantiate_contract( + voting_id, + Addr::unchecked(DAO_ADDR), + &instantiate_msg, + &[], + "voting", + None, + ) + .unwrap(); + + // Query the IsActive endpoint to check the active status. + let is_active_response: IsActiveResponse = app + .wrap() + .query_wasm_smart(voting_addr.clone(), &QueryMsg::IsActive {}) + .unwrap(); + + // Check if IsActive correctly reflects the status based on the active threshold. + // Since the total weight (6) meets the active threshold (5), the DAO should be active. + assert!( + is_active_response.active, + "DAO should be active as the threshold is met by the members' total weight." + ); + + // Change the threshold to a value higher than the total weight to test the negative case. + let update_threshold_msg = UpdateActiveThreshold { + new_threshold: Some(dao_voting::threshold::ActiveThreshold::AbsoluteCount { + count: Uint128::new(10), // This threshold is not met. + }), + }; + app.execute_contract( + Addr::unchecked(DAO_ADDR), + voting_addr.clone(), + &update_threshold_msg, + &[], + ) + .unwrap(); + + // Query again after updating the threshold. + let is_active_response_after_update: IsActiveResponse = app + .wrap() + .query_wasm_smart(voting_addr, &QueryMsg::IsActive {}) + .unwrap(); + + // Now, the DAO should not be active as the total weight does not meet the new threshold. + assert!( + !is_active_response_after_update.active, + "DAO should not be active as the new threshold is not met." + ); +} + +/// Fifth test checks if the contract rejects active threshold updates. +/// I included a trial to update the threshold to a percentage, which should fail +/// because the contract only supports absolute count. +#[test] +fn test_reject_invalid_active_threshold_updates() { + let mut app = App::default(); + let voting_id = app.store_code(voting_contract()); + let cw4_id = app.store_code(cw4_contract()); + + // Instantiate the contract with a valid initial threshold. + let members = vec![ + cw4::Member { + addr: ADDR1.to_string(), + weight: 1, + }, + cw4::Member { + addr: ADDR2.to_string(), + weight: 2, + }, + ]; + let instantiate_msg = InstantiateMsg { + group_contract: GroupContract::New { + cw4_group_code_id: cw4_id, + initial_members: members, + }, + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(3), + }), + }; + let voting_addr = app + .instantiate_contract( + voting_id, + Addr::unchecked(DAO_ADDR), + &instantiate_msg, + &[], + "voting", + None, + ) + .unwrap(); + + // Attempt to set an invalid percentage threshold. + let update_msg_percentage = UpdateActiveThreshold { + new_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(50), + }), + }; + let err_percentage = app + .execute_contract( + Addr::unchecked(DAO_ADDR), + voting_addr.clone(), + &update_msg_percentage, + &[], + ) + .unwrap_err(); + + // Expect the error to be about invalid threshold type. + assert_eq!( + err_percentage.root_cause().to_string(), + ContractError::InvalidThreshold {}.to_string(), + "Expected to fail with InvalidThreshold error for percentage update" + ); + + // Attempt to set an absolute count to zero. + let update_msg_zero = UpdateActiveThreshold { + new_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::zero(), + }), + }; + let err_zero = app + .execute_contract( + Addr::unchecked(DAO_ADDR), + voting_addr, + &update_msg_zero, + &[], + ) + .unwrap_err(); + + // Expect the error to be about invalid zero threshold. + assert_eq!( + err_zero.root_cause().to_string(), + ContractError::InvalidThreshold {}.to_string(), + "Expected to fail with InvalidThreshold error for zero absolute count" + ); +} diff --git a/contracts/voting/dao-voting-cw721-staked/src/testing/mod.rs b/contracts/voting/dao-voting-cw721-staked/src/testing/mod.rs index de0824f52..1ffb20058 100644 --- a/contracts/voting/dao-voting-cw721-staked/src/testing/mod.rs +++ b/contracts/voting/dao-voting-cw721-staked/src/testing/mod.rs @@ -5,7 +5,7 @@ mod instantiate; mod queries; mod tests; -// Integrationg tests using an actual chain binary, requires +// Integrating tests using an actual chain binary, requires // the "test-tube" feature to be enabled // cargo test --features test-tube #[cfg(test)] diff --git a/packages/dao-testing/src/helpers.rs b/packages/dao-testing/src/helpers.rs index b629e7ba0..c03eda2e3 100644 --- a/packages/dao-testing/src/helpers.rs +++ b/packages/dao-testing/src/helpers.rs @@ -305,6 +305,7 @@ pub fn instantiate_with_cw4_groups_governance( core_code_id: u64, proposal_module_instantiate: Binary, initial_weights: Option>, + active_threshold: Option, ) -> Addr { let cw4_id = app.store_code(cw4_group_contract()); let core_id = app.store_code(dao_dao_contract()); @@ -347,6 +348,7 @@ pub fn instantiate_with_cw4_groups_governance( cw4_group_code_id: cw4_id, initial_members: initial_weights, }, + active_threshold, }) .unwrap(), admin: Some(Admin::CoreModule {}),