Repository: DA0-DA0/dao-contracts
Pull request #257 (2022-04-22 11:14:29)

Active query
Closes https://github.com/DA0-DA0/dao-contracts/issues/246
Allows voting modules to support an IsActive {} query route which can return true or false if the DAO is allowed to create proposals or not.

Feedback appreciated.


Patch:
diff --git a/contracts/cw20-staked-balance-voting/src/contract.rs b/contracts/cw20-staked-balance-voting/src/contract.rs
index 6afd9119..0facc355 100644
--- a/contracts/cw20-staked-balance-voting/src/contract.rs
+++ b/contracts/cw20-staked-balance-voting/src/contract.rs
@@ -1,17 +1,23 @@
 #[cfg(not(feature = "library"))]
 use cosmwasm_std::entry_point;
 use cosmwasm_std::{
-    to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, StdResult, SubMsg,
-    Uint128, WasmMsg,
+    to_binary, Binary, Decimal, Deps, DepsMut, Env, MessageInfo, Reply, Response, StdResult,
+    SubMsg, Uint128, Uint256, WasmMsg,
 };
 use cw2::set_contract_version;
-use cw20::Cw20Coin;
+use cw20::{Cw20Coin, TokenInfoResponse};
+use cw_core_interface::voting::IsActiveResponse;
 use cw_utils::parse_reply_instantiate_data;
+use std::convert::TryInto;
 
 use crate::error::ContractError;
-use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg, StakingInfo, TokenInfo};
+use crate::msg::{
+    ActiveThreshold, ActiveThresholdResponse, ExecuteMsg, InstantiateMsg, QueryMsg, StakingInfo,
+    TokenInfo,
+};
 use crate::state::{
-    DAO, STAKING_CONTRACT, STAKING_CONTRACT_CODE_ID, STAKING_CONTRACT_UNSTAKING_DURATION, TOKEN,
+    ACTIVE_THRESHOLD, DAO, STAKING_CONTRACT, STAKING_CONTRACT_CODE_ID,
+    STAKING_CONTRACT_UNSTAKING_DURATION, TOKEN,
 };
 
 const CONTRACT_NAME: &str = "crates.io:cw20-staked-balance-voting";
@@ -20,6 +26,10 @@ const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
 const INSTANTIATE_TOKEN_REPLY_ID: u64 = 0;
 const INSTANTIATE_STAKING_REPLY_ID: u64 = 1;
 
+// We multiply by this when calculating needed power for being active
+// when using active threshold with percent
+const PRECISION_FACTOR: u128 = 10u128.pow(9);
+
 #[cfg_attr(not(feature = "library"), entry_point)]
 pub fn instantiate(
     deps: DepsMut,
@@ -29,6 +39,14 @@ pub fn instantiate(
 ) -> Result<Response, ContractError> {
     set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
     DAO.save(deps.storage, &info.sender)?;
+    if let Some(active_threshold) = msg.active_threshold {
+        if let ActiveThreshold::Percentage { percent } = active_threshold {
+            if percent > Decimal::percent(100) {
+                return Err(ContractError::InvalidActivePercentage {});
+            }
+        }
+        ACTIVE_THRESHOLD.save(deps.storage, &active_threshold)?;
+    }
 
     match msg.token_info {
         TokenInfo::Existing {
@@ -147,14 +165,42 @@ pub fn instantiate(
 
 #[cfg_attr(not(feature = "library"), entry_point)]
 pub fn execute(
-    _deps: DepsMut,
-    _env: Env,
-    _info: MessageInfo,
+    deps: DepsMut,
+    env: Env,
+    info: MessageInfo,
     msg: ExecuteMsg,
 ) -> Result<Response, ContractError> {
-    match msg {}
+    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,
+    new_active_threshold: Option<ActiveThreshold>,
+) -> Result<Response, ContractError> {
+    let dao = DAO.load(deps.storage)?;
+    if info.sender != dao {
+        return Err(ContractError::Unauthorized {});
+    }
+
+    if let Some(active_threshold) = new_active_threshold {
+        if let ActiveThreshold::Percentage { percent } = active_threshold {
+            if percent > Decimal::percent(100) {
+                return Err(ContractError::InvalidActivePercentage {});
+            }
+        }
+        ACTIVE_THRESHOLD.save(deps.storage, &active_threshold)?;
+    } else {
+        ACTIVE_THRESHOLD.remove(deps.storage);
+    }
+
+    Ok(Response::new().add_attribute("action", "update_active_threshold"))
+}
 #[cfg_attr(not(feature = "library"), entry_point)]
 pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
     match msg {
@@ -166,6 +212,8 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
         QueryMsg::TotalPowerAtHeight { height } => query_total_power_at_height(deps, env, height),
         QueryMsg::Info {} => query_info(deps),
         QueryMsg::Dao {} => query_dao(deps),
+        QueryMsg::IsActive {} => query_is_active(deps),
+        QueryMsg::ActiveThreshold {} => query_active_threshold(deps),
     }
 }
 
@@ -226,6 +274,50 @@ pub fn query_dao(deps: Deps) -> StdResult<Binary> {
     to_binary(&dao)
 }
 
+pub fn query_is_active(deps: Deps) -> StdResult<Binary> {
+    let threshold = ACTIVE_THRESHOLD.may_load(deps.storage)?;
+    if let Some(threshold) = threshold {
+        let token_contract = TOKEN.load(deps.storage)?;
+        let staking_contract = STAKING_CONTRACT.load(deps.storage)?;
+        let actual_power: stake_cw20::msg::TotalStakedAtHeightResponse =
+            deps.querier.query_wasm_smart(
+                staking_contract,
+                &stake_cw20::msg::QueryMsg::TotalStakedAtHeight { height: None },
+            )?;
+        match threshold {
+            ActiveThreshold::AbsoluteCount { count } => to_binary(&IsActiveResponse {
+                active: actual_power.total >= count,
+            }),
+            ActiveThreshold::Percentage { percent } => {
+                let total_potential_power: TokenInfoResponse = deps
+                    .querier
+                    .query_wasm_smart(token_contract, &cw20_base::msg::QueryMsg::TokenInfo {})?;
+                let total_power = total_potential_power
+                    .total_supply
+                    .full_mul(PRECISION_FACTOR);
+                let applied = total_power.multiply_ratio(
+                    percent.atomics(),
+                    Uint256::from(10u64).pow(percent.decimal_places()),
+                );
+                let rounded = (applied + Uint256::from(PRECISION_FACTOR) - Uint256::from(1u128))
+                    / Uint256::from(PRECISION_FACTOR);
+                let count: Uint128 = rounded.try_into().unwrap();
+                to_binary(&IsActiveResponse {
+                    active: actual_power.total >= count,
+                })
+            }
+        }
+    } else {
+        to_binary(&IsActiveResponse { active: true })
+    }
+}
+
+pub fn query_active_threshold(deps: Deps) -> StdResult<Binary> {
+    to_binary(&ActiveThresholdResponse {
+        active_threshold: ACTIVE_THRESHOLD.may_load(deps.storage)?,
+    })
+}
+
 #[cfg_attr(not(feature = "library"), entry_point)]
 pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result<Response, ContractError> {
     match msg.id {
diff --git a/contracts/cw20-staked-balance-voting/src/error.rs b/contracts/cw20-staked-balance-voting/src/error.rs
index 27c5ac5d..4ca98fe1 100644
--- a/contracts/cw20-staked-balance-voting/src/error.rs
+++ b/contracts/cw20-staked-balance-voting/src/error.rs
@@ -26,4 +26,7 @@ pub enum ContractError {
 
     #[error("Can not change the contract's staking contract after it has been set")]
     DuplicateStakingContract {},
+
+    #[error("Active threshold percentage must be greater than 0 and less than 1")]
+    InvalidActivePercentage {},
 }
diff --git a/contracts/cw20-staked-balance-voting/src/msg.rs b/contracts/cw20-staked-balance-voting/src/msg.rs
index ea626ec8..e5275408 100644
--- a/contracts/cw20-staked-balance-voting/src/msg.rs
+++ b/contracts/cw20-staked-balance-voting/src/msg.rs
@@ -1,7 +1,7 @@
-use cosmwasm_std::Uint128;
+use cosmwasm_std::{Decimal, Uint128};
 use cw20::Cw20Coin;
 use cw20_base::msg::InstantiateMarketingInfo;
-use cw_core_macros::{token_query, voting_query};
+use cw_core_macros::{active_query, token_query, voting_query};
 use cw_utils::Duration;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
@@ -41,20 +41,40 @@ pub enum TokenInfo {
     },
 }
 
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum ActiveThreshold {
+    AbsoluteCount { count: Uint128 },
+    Percentage { percent: Decimal },
+}
+
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
 pub struct InstantiateMsg {
     pub token_info: TokenInfo,
+    pub active_threshold: Option<ActiveThreshold>,
 }
 
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
 #[serde(rename_all = "snake_case")]
-pub enum ExecuteMsg {}
+pub enum ExecuteMsg {
+    UpdateActiveThreshold {
+        new_threshold: Option<ActiveThreshold>,
+    },
+}
 
 #[voting_query]
 #[token_query]
+#[active_query]
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
 #[serde(rename_all = "snake_case")]
 pub enum QueryMsg {
     StakingContract {},
     Dao {},
+    ActiveThreshold {},
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub struct ActiveThresholdResponse {
+    pub active_threshold: Option<ActiveThreshold>,
 }
diff --git a/contracts/cw20-staked-balance-voting/src/state.rs b/contracts/cw20-staked-balance-voting/src/state.rs
index d3d043cd..05ea2723 100644
--- a/contracts/cw20-staked-balance-voting/src/state.rs
+++ b/contracts/cw20-staked-balance-voting/src/state.rs
@@ -1,7 +1,9 @@
+use crate::msg::ActiveThreshold;
 use cosmwasm_std::Addr;
 use cw_storage_plus::Item;
 use cw_utils::Duration;
 
+pub const ACTIVE_THRESHOLD: Item<ActiveThreshold> = Item::new("active_threshold");
 pub const TOKEN: Item<Addr> = Item::new("token");
 pub const DAO: Item<Addr> = Item::new("dao");
 pub const STAKING_CONTRACT: Item<Addr> = Item::new("staking_contract");
diff --git a/packages/cw-core-interface/src/voting.rs b/packages/cw-core-interface/src/voting.rs
index 06dfa9e8..672415c2 100644
--- a/packages/cw-core-interface/src/voting.rs
+++ b/packages/cw-core-interface/src/voting.rs
@@ -26,3 +26,8 @@ pub struct TotalPowerAtHeightResponse {
 pub struct InfoResponse {
     pub info: ContractVersion,
 }
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+pub struct IsActiveResponse {
+    pub active: bool,
+}
diff --git a/packages/cw-core-macros/src/lib.rs b/packages/cw-core-macros/src/lib.rs
index 1595ee0a..fa99f7cc 100644
--- a/packages/cw-core-macros/src/lib.rs
+++ b/packages/cw-core-macros/src/lib.rs
@@ -162,6 +162,77 @@ pub fn token_query(metadata: TokenStream, input: TokenStream) -> TokenStream {
     .into()
 }
 
+/// Adds the nesecary fields to an enum such that it implements the
+/// interface needed to be a cw-governance voting module that has an
+/// active check threshold.
+///
+/// For example:
+///
+/// ```
+/// use cw_core_macros::active_query;
+///
+/// #[active_query]
+/// enum QueryMsg {}
+/// ```
+///
+/// Will transform the enum to:
+///
+/// ```
+/// enum QueryMsg {
+///     IsActive {},
+/// }
+/// ```
+///
+/// Note that other derive macro invocations must occur after this
+/// procedural macro as they may depend on the new fields. For
+/// example, the following will fail becase the `Clone` derivation
+/// occurs before the addition of the field.
+///
+/// ```compile_fail
+/// use cw_core_macros::active_query;
+///
+/// #[derive(Clone)]
+/// #[active_query]
+/// #[allow(dead_code)]
+/// enum Test {
+///     Foo,
+///     Bar(u64),
+///     Baz { foo: u64 },
+/// }
+/// ```
+#[proc_macro_attribute]
+pub fn active_query(metadata: TokenStream, input: TokenStream) -> TokenStream {
+    // Make sure that no arguments were passed in.
+    let args = parse_macro_input!(metadata as AttributeArgs);
+    if let Some(first_arg) = args.first() {
+        return syn::Error::new_spanned(first_arg, "token query macro takes no arguments")
+            .to_compile_error()
+            .into();
+    }
+
+    let mut ast: DeriveInput = parse_macro_input!(input);
+    match &mut ast.data {
+        syn::Data::Enum(DataEnum { variants, .. }) => {
+            let info: Variant = syn::parse2(quote! { IsActive {} }).unwrap();
+
+            variants.push(info);
+        }
+        _ => {
+            return syn::Error::new(
+                ast.ident.span(),
+                "token query types can not be only be derived for enums",
+            )
+            .to_compile_error()
+            .into()
+        }
+    };
+
+    quote! {
+    #ast
+    }
+    .into()
+}
+
 /// Adds the nesecary fields to an enum such that it implements the
 /// interface needed to be a cw-governance governance module.
 ///


Test patch:
diff --git a/contracts/cw20-staked-balance-voting/src/tests.rs b/contracts/cw20-staked-balance-voting/src/tests.rs
index f2a733ff..248b8d62 100644
--- a/contracts/cw20-staked-balance-voting/src/tests.rs
+++ b/contracts/cw20-staked-balance-voting/src/tests.rs
@@ -1,10 +1,12 @@
-use cosmwasm_std::{to_binary, Addr, Empty, Uint128};
+use cosmwasm_std::{to_binary, Addr, Decimal, Empty, Uint128};
 use cw2::ContractVersion;
 use cw20::{BalanceResponse, Cw20Coin, MinterResponse, TokenInfoResponse};
-use cw_core_interface::voting::{InfoResponse, VotingPowerAtHeightResponse};
+use cw_core_interface::voting::{InfoResponse, IsActiveResponse, VotingPowerAtHeightResponse};
 use cw_multi_test::{next_block, App, Contract, ContractWrapper, Executor};
 
-use crate::msg::{InstantiateMsg, QueryMsg, StakingInfo};
+use crate::msg::{
+    ActiveThreshold, ActiveThresholdResponse, ExecuteMsg, InstantiateMsg, QueryMsg, StakingInfo,
+};
 
 const DAO_ADDR: &str = "dao";
 const CREATOR_ADDR: &str = "creator";
@@ -85,6 +87,7 @@ fn test_instantiate_zero_supply() {
                 staking_code_id: staking_contract_id,
                 initial_dao_balance: Some(Uint128::zero()),
             },
+            active_threshold: None,
         },
     );
 }
@@ -112,6 +115,7 @@ fn test_instantiate_no_balances() {
                 staking_code_id: staking_contract_id,
                 initial_dao_balance: Some(Uint128::zero()),
             },
+            active_threshold: None,
         },
     );
 }
@@ -142,6 +146,7 @@ fn test_contract_info() {
                 staking_code_id: staking_contract_id,
                 initial_dao_balance: Some(Uint128::zero()),
             },
+            active_threshold: None,
         },
     );
 
@@ -192,6 +197,7 @@ fn test_new_cw20() {
                 staking_code_id: staking_contract_id,
                 initial_dao_balance: Some(Uint128::from(10u64)),
             },
+            active_threshold: None,
         },
     );
 
@@ -365,6 +371,7 @@ fn test_existing_cw20_new_staking() {
                     unstaking_duration: None,
                 },
             },
+            active_threshold: None,
         },
     );
 
@@ -515,6 +522,7 @@ fn test_existing_cw20_existing_staking() {
                     unstaking_duration: None,
                 },
             },
+            active_threshold: None,
         },
     );
 
@@ -552,6 +560,7 @@ fn test_existing_cw20_existing_staking() {
                     staking_contract_address: staking_addr.to_string(),
                 },
             },
+            active_threshold: None,
         },
     );
 
@@ -666,6 +675,7 @@ fn test_existing_cw20_existing_staking() {
                     staking_contract_address: staking_addr.to_string(),
                 },
             },
+            active_threshold: None,
         },
         &[],
         "voting module",
@@ -713,6 +723,7 @@ fn test_different_heights() {
                     unstaking_duration: None,
                 },
             },
+            active_threshold: None,
         },
     );
 
@@ -872,3 +883,262 @@ fn test_different_heights() {
         }
     );
 }
+
+#[test]
+fn test_active_threshold_absolute_count() {
+    let mut app = App::default();
+    let cw20_id = app.store_code(cw20_contract());
+    let voting_id = app.store_code(staked_balance_voting_contract());
+    let staking_contract_id = app.store_code(staking_contract());
+
+    let voting_addr = instantiate_voting(
+        &mut app,
+        voting_id,
+        InstantiateMsg {
+            token_info: crate::msg::TokenInfo::New {
+                code_id: cw20_id,
+                label: "DAO DAO voting".to_string(),
+                name: "DAO DAO".to_string(),
+                symbol: "DAO".to_string(),
+                decimals: 6,
+                initial_balances: vec![Cw20Coin {
+                    address: CREATOR_ADDR.to_string(),
+                    amount: Uint128::from(200u64),
+                }],
+                marketing: None,
+                unstaking_duration: None,
+                staking_code_id: staking_contract_id,
+                initial_dao_balance: Some(Uint128::from(100u64)),
+            },
+            active_threshold: Some(ActiveThreshold::AbsoluteCount {
+                count: Uint128::new(100),
+            }),
+        },
+    );
+
+    let token_addr: Addr = app
+        .wrap()
+        .query_wasm_smart(voting_addr.clone(), &QueryMsg::TokenContract {})
+        .unwrap();
+    let staking_addr: Addr = app
+        .wrap()
+        .query_wasm_smart(voting_addr.clone(), &QueryMsg::StakingContract {})
+        .unwrap();
+
+    // Not active as none staked
+    let is_active: IsActiveResponse = app
+        .wrap()
+        .query_wasm_smart(voting_addr.clone(), &QueryMsg::IsActive {})
+        .unwrap();
+    assert!(!is_active.active);
+
+    // Stake 100 token as creator
+    stake_tokens(&mut app, staking_addr, token_addr, CREATOR_ADDR, 100);
+    app.update_block(next_block);
+
+    // Active as enough staked
+    let is_active: IsActiveResponse = app
+        .wrap()
+        .query_wasm_smart(voting_addr, &QueryMsg::IsActive {})
+        .unwrap();
+    assert!(is_active.active);
+}
+
+#[test]
+fn test_active_threshold_percent() {
+    let mut app = App::default();
+    let cw20_id = app.store_code(cw20_contract());
+    let voting_id = app.store_code(staked_balance_voting_contract());
+    let staking_contract_id = app.store_code(staking_contract());
+
+    let voting_addr = instantiate_voting(
+        &mut app,
+        voting_id,
+        InstantiateMsg {
+            token_info: crate::msg::TokenInfo::New {
+                code_id: cw20_id,
+                label: "DAO DAO voting".to_string(),
+                name: "DAO DAO".to_string(),
+                symbol: "DAO".to_string(),
+                decimals: 6,
+                initial_balances: vec![Cw20Coin {
+                    address: CREATOR_ADDR.to_string(),
+                    amount: Uint128::from(200u64),
+                }],
+                marketing: None,
+                unstaking_duration: None,
+                staking_code_id: staking_contract_id,
+                initial_dao_balance: Some(Uint128::from(100u64)),
+            },
+            active_threshold: Some(ActiveThreshold::Percentage {
+                percent: Decimal::percent(20),
+            }),
+        },
+    );
+
+    let token_addr: Addr = app
+        .wrap()
+        .query_wasm_smart(voting_addr.clone(), &QueryMsg::TokenContract {})
+        .unwrap();
+    let staking_addr: Addr = app
+        .wrap()
+        .query_wasm_smart(voting_addr.clone(), &QueryMsg::StakingContract {})
+        .unwrap();
+
+    // Not active as none staked
+    let is_active: IsActiveResponse = app
+        .wrap()
+        .query_wasm_smart(voting_addr.clone(), &QueryMsg::IsActive {})
+        .unwrap();
+    assert!(!is_active.active);
+
+    // Stake 60 token as creator, now active
+    stake_tokens(&mut app, staking_addr, token_addr, CREATOR_ADDR, 60);
+    app.update_block(next_block);
+
+    // Active as enough staked
+    let is_active: IsActiveResponse = app
+        .wrap()
+        .query_wasm_smart(voting_addr, &QueryMsg::IsActive {})
+        .unwrap();
+    assert!(is_active.active);
+}
+
+#[test]
+fn test_active_threshold_none() {
+    let mut app = App::default();
+    let cw20_id = app.store_code(cw20_contract());
+    let voting_id = app.store_code(staked_balance_voting_contract());
+    let staking_contract_id = app.store_code(staking_contract());
+
+    let voting_addr = instantiate_voting(
+        &mut app,
+        voting_id,
+        InstantiateMsg {
+            token_info: crate::msg::TokenInfo::New {
+                code_id: cw20_id,
+                label: "DAO DAO voting".to_string(),
+                name: "DAO DAO".to_string(),
+                symbol: "DAO".to_string(),
+                decimals: 6,
+                initial_balances: vec![Cw20Coin {
+                    address: CREATOR_ADDR.to_string(),
+                    amount: Uint128::from(200u64),
+                }],
+                marketing: None,
+                unstaking_duration: None,
+                staking_code_id: staking_contract_id,
+                initial_dao_balance: Some(Uint128::from(100u64)),
+            },
+            active_threshold: None,
+        },
+    );
+
+    // Active as no threshold
+    let is_active: IsActiveResponse = app
+        .wrap()
+        .query_wasm_smart(voting_addr, &QueryMsg::IsActive {})
+        .unwrap();
+    assert!(is_active.active);
+}
+
+#[test]
+fn test_update_active_threshold() {
+    let mut app = App::default();
+    let cw20_id = app.store_code(cw20_contract());
+    let voting_id = app.store_code(staked_balance_voting_contract());
+    let staking_contract_id = app.store_code(staking_contract());
+
+    let voting_addr = instantiate_voting(
+        &mut app,
+        voting_id,
+        InstantiateMsg {
+            token_info: crate::msg::TokenInfo::New {
+                code_id: cw20_id,
+                label: "DAO DAO voting".to_string(),
+                name: "DAO DAO".to_string(),
+                symbol: "DAO".to_string(),
+                decimals: 6,
+                initial_balances: vec![Cw20Coin {
+                    address: CREATOR_ADDR.to_string(),
+                    amount: Uint128::from(200u64),
+                }],
+                marketing: None,
+                unstaking_duration: None,
+                staking_code_id: staking_contract_id,
+                initial_dao_balance: Some(Uint128::from(100u64)),
+            },
+            active_threshold: None,
+        },
+    );
+
+    let resp: ActiveThresholdResponse = app
+        .wrap()
+        .query_wasm_smart(voting_addr.clone(), &QueryMsg::ActiveThreshold {})
+        .unwrap();
+    assert_eq!(resp.active_threshold, None);
+
+    let msg = ExecuteMsg::UpdateActiveThreshold {
+        new_threshold: Some(ActiveThreshold::AbsoluteCount {
+            count: Uint128::new(100),
+        }),
+    };
+
+    // Expect failure as sender is not the DAO
+    app.execute_contract(
+        Addr::unchecked(CREATOR_ADDR),
+        voting_addr.clone(),
+        &msg,
+        &[],
+    )
+    .unwrap_err();
+
+    // Expect success as sender is the DAO
+    app.execute_contract(Addr::unchecked(DAO_ADDR), voting_addr.clone(), &msg, &[])
+        .unwrap();
+
+    let resp: ActiveThresholdResponse = app
+        .wrap()
+        .query_wasm_smart(voting_addr, &QueryMsg::ActiveThreshold {})
+        .unwrap();
+    assert_eq!(
+        resp.active_threshold,
+        Some(ActiveThreshold::AbsoluteCount {
+            count: Uint128::new(100)
+        })
+    );
+}
+
+#[test]
+#[should_panic(expected = "Active threshold percentage must be greater than 0 and less than 1")]
+fn test_active_threshold_invalid() {
+    let mut app = App::default();
+    let cw20_id = app.store_code(cw20_contract());
+    let voting_id = app.store_code(staked_balance_voting_contract());
+    let staking_contract_id = app.store_code(staking_contract());
+
+    instantiate_voting(
+        &mut app,
+        voting_id,
+        InstantiateMsg {
+            token_info: crate::msg::TokenInfo::New {
+                code_id: cw20_id,
+                label: "DAO DAO voting".to_string(),
+                name: "DAO DAO".to_string(),
+                symbol: "DAO".to_string(),
+                decimals: 6,
+                initial_balances: vec![Cw20Coin {
+                    address: CREATOR_ADDR.to_string(),
+                    amount: Uint128::from(200u64),
+                }],
+                marketing: None,
+                unstaking_duration: None,
+                staking_code_id: staking_contract_id,
+                initial_dao_balance: Some(Uint128::from(100u64)),
+            },
+            active_threshold: Some(ActiveThreshold::Percentage {
+                percent: Decimal::percent(120),
+            }),
+        },
+    );
+}