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/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..28f716b10 100644 --- a/contracts/voting/dao-voting-cw4/src/contract.rs +++ b/contracts/voting/dao-voting-cw4/src/contract.rs @@ -1,16 +1,24 @@ -#[cfg(not(feature = "library"))] -use cosmwasm_std::entry_point; +use std::ops::{Div, Mul}; + use cosmwasm_std::{ - to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, StdResult, SubMsg, - Uint128, WasmMsg, + Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, StdResult, SubMsg, + to_json_binary, Uint128, Uint256, WasmMsg, }; -use cw2::{get_contract_version, set_contract_version, ContractVersion}; +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cw2::{ContractVersion, get_contract_version, set_contract_version}; use cw4::{MemberListResponse, MemberResponse, TotalWeightResponse}; use cw_utils::parse_reply_instantiate_data; +use dao_interface::voting::IsActiveResponse; +use dao_voting::threshold::{ + ActiveThreshold, assert_valid_percentage_threshold + , +}; + use crate::error::ContractError; use crate::msg::{ExecuteMsg, GroupContract, InstantiateMsg, MigrateMsg, QueryMsg}; -use crate::state::{DAO, GROUP_CONTRACT}; +use crate::state::{ACTIVE_THRESHOLD, Config, CONFIG, 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 +34,20 @@ pub fn instantiate( ) -> Result { set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + let config: Config = if let Some(active_threshold) = msg.active_threshold { + Config { + active_threshold_enabled: true, + active_threshold: Some(active_threshold), + } + } else { + Config { + active_threshold_enabled: false, + active_threshold: None, + } + }; + + CONFIG.save(deps.storage, &config)?; + DAO.save(deps.storage, &info.sender)?; match msg.group_contract { @@ -108,12 +130,70 @@ 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 } => { + execute_update_active_threshold(deps, env, info, new_threshold) + } + } +} + +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 { + // Pre-validation before state changes + match active_threshold { + ActiveThreshold::Percentage { percent } => { + assert_valid_percentage_threshold(*percent)?; + } + ActiveThreshold::AbsoluteCount { count } => { + if *count.is_zero() { + return Err(ContractError::InvalidThreshold {}); + } + } + } + } + + // Safe to modify state after validation + match new_active_threshold { + Some(threshold) => ACTIVE_THRESHOLD.save(deps.storage, &threshold)?, + None => ACTIVE_THRESHOLD.remove(deps.storage), + } + + // As opposed to doing it this way: + // if let Some(active_threshold) = new_active_threshold { + // // Pre-validation before state changes. + // match active_threshold { + // ActiveThreshold::Percentage { percent } => { + // assert_valid_percentage_threshold(percent)?; + // } + // ActiveThreshold::AbsoluteCount { count } => { + // if count.is_zero() { + // return Err(ContractError::InvalidThreshold {}); + // } + // } + // } + // ACTIVE_THRESHOLD.save(deps.storage, &active_threshold)?; + // } 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 +206,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,12 +245,48 @@ 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.may_load(deps.storage)?; + + match active_threshold { + Some(ActiveThreshold::AbsoluteCount { count }) => { + 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 }, + )?; + to_json_binary(&IsActiveResponse { + active: total_weight.weight >= count.into(), + }) + } + Some(ActiveThreshold::Percentage { percent, .. }) => { + 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 required_weight: Uint256 = Uint256::from(total_weight.weight).mul(Uint256::from(percent)).div(Uint256::from(100)); + + to_json_binary(&IsActiveResponse { + active: total_weight.weight >= required_weight.into(), + }) + } + None => { + to_json_binary(&IsActiveResponse { active: true }) + } + } +} + #[cfg_attr(not(feature = "library"), entry_point)] pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { + let config: Config = CONFIG.load(deps.storage)?; + // Update config as necessary + CONFIG.save(deps.storage, &config)?; + let storage_version: ContractVersion = get_contract_version(deps.storage)?; // Only migrate if newer diff --git a/contracts/voting/dao-voting-cw4/src/error.rs b/contracts/voting/dao-voting-cw4/src/error.rs index a0ce03e9c..03951acca 100644 --- a/contracts/voting/dao-voting-cw4/src/error.rs +++ b/contracts/voting/dao-voting-cw4/src/error.rs @@ -29,4 +29,8 @@ 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..59a61c8c6 100644 --- a/contracts/voting/dao-voting-cw4/src/state.rs +++ b/contracts/voting/dao-voting-cw4/src/state.rs @@ -1,5 +1,19 @@ +use cosmwasm_schema::cw_serde; use cosmwasm_std::Addr; use cw_storage_plus::Item; +use cw_utils::Duration; +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"); + +#[cw_serde] +pub struct Config { + pub active_threshold_enabled: bool, + pub active_threshold: Option, +} + +pub const CONFIG: Item = Item::new("config"); \ No newline at end of file