From 11ddaffa995549eb85c3cc69c3ed45cddd6a369b Mon Sep 17 00:00:00 2001 From: Gabriel Lopez Date: Fri, 8 Sep 2023 13:28:27 -0500 Subject: [PATCH 1/4] Voting power limits for dao-voting-native-staked Added 2 new macros for limitable voting modules Added an error for LimitExceeded when attempting to stake more than allowed Added test case for limits Solution is optimistic hence the map instead of snapshotmap for limits, so reducing limit after a user has already staked will not be affected #684 Also fixed some clippy errors --- .../dao-voting-native-staked/src/contract.rs | 44 ++++++- .../dao-voting-native-staked/src/error.rs | 5 +- .../dao-voting-native-staked/src/msg.rs | 6 +- .../dao-voting-native-staked/src/state.rs | 5 +- .../dao-voting-native-staked/src/tests.rs | 121 ++++++++++++------ packages/dao-dao-macros/src/lib.rs | 39 +++++- 6 files changed, 178 insertions(+), 42 deletions(-) diff --git a/contracts/voting/dao-voting-native-staked/src/contract.rs b/contracts/voting/dao-voting-native-staked/src/contract.rs index 80a413a9f..c3e6d2e23 100644 --- a/contracts/voting/dao-voting-native-staked/src/contract.rs +++ b/contracts/voting/dao-voting-native-staked/src/contract.rs @@ -18,7 +18,8 @@ use crate::msg::{ QueryMsg, StakerBalanceResponse, }; use crate::state::{ - Config, ACTIVE_THRESHOLD, CLAIMS, CONFIG, DAO, HOOKS, MAX_CLAIMS, STAKED_BALANCES, STAKED_TOTAL, + Config, ACTIVE_THRESHOLD, CLAIMS, CONFIG, DAO, HOOKS, LIMITS, MAX_CLAIMS, STAKED_BALANCES, + STAKED_TOTAL, }; pub(crate) const CONTRACT_NAME: &str = "crates.io:dao-voting-native-staked"; @@ -115,6 +116,7 @@ pub fn execute( } ExecuteMsg::AddHook { addr } => execute_add_hook(deps, env, info, addr), ExecuteMsg::RemoveHook { addr } => execute_remove_hook(deps, env, info, addr), + ExecuteMsg::UpdateLimit { addr, limit } => execute_update_limit(deps, info, addr, limit), } } @@ -125,6 +127,13 @@ pub fn execute_stake( ) -> Result { let config = CONFIG.load(deps.storage)?; let amount = must_pay(&info, &config.denom)?; + let limit = LIMITS.may_load(deps.storage, &info.sender)?; + + if let Some(limit) = limit { + if amount > limit { + return Err(ContractError::LimitExceeded { limit }); + } + } STAKED_BALANCES.update( deps.storage, @@ -322,6 +331,36 @@ pub fn execute_remove_hook( .add_attribute("hook", addr)) } +pub fn execute_update_limit( + deps: DepsMut, + info: MessageInfo, + addr: String, + limit: Option, +) -> Result { + let dao = DAO.load(deps.storage)?; + if info.sender != dao { + return Err(ContractError::Unauthorized {}); + } + + let addr = deps.api.addr_validate(&addr)?; + + if let Some(limit) = limit { + LIMITS.save(deps.storage, &addr, &limit)?; + } else { + LIMITS.remove(deps.storage, &addr); + } + + Ok(Response::new() + .add_attribute("action", "update_limit") + .add_attribute("addr", addr.to_string()) + .add_attribute( + "limit", + limit + .as_ref() + .map_or("None".to_owned(), ToString::to_string), + )) +} + #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { match msg { @@ -342,6 +381,9 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { QueryMsg::IsActive {} => query_is_active(deps), QueryMsg::ActiveThreshold {} => query_active_threshold(deps), QueryMsg::GetHooks {} => to_binary(&query_hooks(deps)?), + QueryMsg::Limit { addr } => { + to_binary(&LIMITS.may_load(deps.storage, &deps.api.addr_validate(&addr)?)?) + } } } diff --git a/contracts/voting/dao-voting-native-staked/src/error.rs b/contracts/voting/dao-voting-native-staked/src/error.rs index b87342fb4..b4590cb41 100644 --- a/contracts/voting/dao-voting-native-staked/src/error.rs +++ b/contracts/voting/dao-voting-native-staked/src/error.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::StdError; +use cosmwasm_std::{StdError, Uint128}; use cw_utils::PaymentError; use thiserror::Error; @@ -39,4 +39,7 @@ pub enum ContractError { #[error("Amount being unstaked must be non-zero")] ZeroUnstake {}, + + #[error("Limit cannot be exceeded")] + LimitExceeded { limit: Uint128 }, } diff --git a/contracts/voting/dao-voting-native-staked/src/msg.rs b/contracts/voting/dao-voting-native-staked/src/msg.rs index a8b562524..0d0509278 100644 --- a/contracts/voting/dao-voting-native-staked/src/msg.rs +++ b/contracts/voting/dao-voting-native-staked/src/msg.rs @@ -1,7 +1,9 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::Uint128; use cw_utils::Duration; -use dao_dao_macros::{active_query, voting_module_query}; +use dao_dao_macros::{ + active_query, limitable_voting_module, limitable_voting_module_query, voting_module_query, +}; use dao_voting::threshold::{ActiveThreshold, ActiveThresholdResponse}; #[cw_serde] @@ -15,6 +17,7 @@ pub struct InstantiateMsg { pub active_threshold: Option, } +#[limitable_voting_module] #[cw_serde] pub enum ExecuteMsg { /// Stakes tokens with the contract to get voting power in the DAO @@ -37,6 +40,7 @@ pub enum ExecuteMsg { RemoveHook { addr: String }, } +#[limitable_voting_module_query] #[voting_module_query] #[active_query] #[cw_serde] diff --git a/contracts/voting/dao-voting-native-staked/src/state.rs b/contracts/voting/dao-voting-native-staked/src/state.rs index 3e46d0281..c0435eb50 100644 --- a/contracts/voting/dao-voting-native-staked/src/state.rs +++ b/contracts/voting/dao-voting-native-staked/src/state.rs @@ -2,7 +2,7 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Uint128}; use cw_controllers::Claims; use cw_hooks::Hooks; -use cw_storage_plus::{Item, SnapshotItem, SnapshotMap, Strategy}; +use cw_storage_plus::{Item, Map, SnapshotItem, SnapshotMap, Strategy}; use cw_utils::Duration; use dao_voting::threshold::ActiveThreshold; @@ -26,6 +26,9 @@ pub const STAKED_BALANCES: SnapshotMap<&Addr, Uint128> = SnapshotMap::new( Strategy::EveryBlock, ); +/// Keeps track of limits by address +pub const LIMITS: Map<&Addr, Uint128> = Map::new("limits"); + /// Keeps track of staked total over time pub const STAKED_TOTAL: SnapshotItem = SnapshotItem::new( "total_staked", diff --git a/contracts/voting/dao-voting-native-staked/src/tests.rs b/contracts/voting/dao-voting-native-staked/src/tests.rs index 75a3b5672..2346e3757 100644 --- a/contracts/voting/dao-voting-native-staked/src/tests.rs +++ b/contracts/voting/dao-voting-native-staked/src/tests.rs @@ -157,7 +157,7 @@ fn update_config( } fn get_voting_power_at_height( - app: &mut App, + app: &App, staking_addr: Addr, address: String, height: Option, @@ -171,7 +171,7 @@ fn get_voting_power_at_height( } fn get_total_power_at_height( - app: &mut App, + app: &App, staking_addr: Addr, height: Option, ) -> TotalPowerAtHeightResponse { @@ -180,19 +180,19 @@ fn get_total_power_at_height( .unwrap() } -fn get_config(app: &mut App, staking_addr: Addr) -> Config { +fn get_config(app: &App, staking_addr: Addr) -> Config { app.wrap() .query_wasm_smart(staking_addr, &QueryMsg::GetConfig {}) .unwrap() } -fn get_claims(app: &mut App, staking_addr: Addr, address: String) -> ClaimsResponse { +fn get_claims(app: &App, staking_addr: Addr, address: String) -> ClaimsResponse { app.wrap() .query_wasm_smart(staking_addr, &QueryMsg::Claims { address }) .unwrap() } -fn get_balance(app: &mut App, address: &str, denom: &str) -> Uint128 { +fn get_balance(app: &App, address: &str, denom: &str) -> Uint128 { app.wrap().query_balance(address, denom).unwrap().amount } @@ -382,7 +382,7 @@ fn test_unstake() { unstake_tokens(&mut app, addr.clone(), ADDR1, 75).unwrap(); // Query claims - let claims = get_claims(&mut app, addr.clone(), ADDR1.to_string()); + let claims = get_claims(&app, addr.clone(), ADDR1.to_string()); assert_eq!(claims.claims.len(), 1); app.update_block(next_block); @@ -390,7 +390,7 @@ fn test_unstake() { unstake_tokens(&mut app, addr.clone(), ADDR1, 25).unwrap(); // Query claims - let claims = get_claims(&mut app, addr, ADDR1.to_string()); + let claims = get_claims(&app, addr, ADDR1.to_string()); assert_eq!(claims.claims.len(), 2); } @@ -417,14 +417,14 @@ fn test_unstake_no_unstaking_duration() { app.update_block(next_block); - let balance = get_balance(&mut app, ADDR1, DENOM); + let balance = get_balance(&app, ADDR1, DENOM); // 10000 (initial bal) - 100 (staked) + 75 (unstaked) = 9975 assert_eq!(balance, Uint128::new(9975)); // Unstake the rest unstake_tokens(&mut app, addr, ADDR1, 25).unwrap(); - let balance = get_balance(&mut app, ADDR1, DENOM); + let balance = get_balance(&app, ADDR1, DENOM); // 10000 (initial bal) - 100 (staked) + 75 (unstaked 1) + 25 (unstaked 2) = 10000 assert_eq!(balance, Uint128::new(10000)) } @@ -503,7 +503,7 @@ fn test_claim() { claim(&mut app, addr.clone(), ADDR1).unwrap(); // Query balance - let balance = get_balance(&mut app, ADDR1, DENOM); + let balance = get_balance(&app, ADDR1, DENOM); // 10000 (initial bal) - 100 (staked) + 75 (unstaked) = 9975 assert_eq!(balance, Uint128::new(9975)); @@ -518,7 +518,7 @@ fn test_claim() { claim(&mut app, addr, ADDR1).unwrap(); // Query balance - let balance = get_balance(&mut app, ADDR1, DENOM); + let balance = get_balance(&app, ADDR1, DENOM); // 10000 (initial bal) - 100 (staked) + 75 (unstaked 1) + 25 (unstaked 2) = 10000 assert_eq!(balance, Uint128::new(10000)); } @@ -559,7 +559,7 @@ fn test_update_config_as_dao() { // Swap owner and manager, change duration update_config(&mut app, addr.clone(), DAO_ADDR, Some(Duration::Height(10))).unwrap(); - let config = get_config(&mut app, addr); + let config = get_config(&app, addr); assert_eq!( Config { denom: DENOM.to_string(), @@ -659,7 +659,7 @@ fn test_query_claims() { }, ); - let claims = get_claims(&mut app, addr.clone(), ADDR1.to_string()); + let claims = get_claims(&app, addr.clone(), ADDR1.to_string()); assert_eq!(claims.claims.len(), 0); // Stake some tokens @@ -670,13 +670,13 @@ fn test_query_claims() { unstake_tokens(&mut app, addr.clone(), ADDR1, 25).unwrap(); app.update_block(next_block); - let claims = get_claims(&mut app, addr.clone(), ADDR1.to_string()); + let claims = get_claims(&app, addr.clone(), ADDR1.to_string()); assert_eq!(claims.claims.len(), 1); unstake_tokens(&mut app, addr.clone(), ADDR1, 25).unwrap(); app.update_block(next_block); - let claims = get_claims(&mut app, addr, ADDR1.to_string()); + let claims = get_claims(&app, addr, ADDR1.to_string()); assert_eq!(claims.claims.len(), 2); } @@ -694,7 +694,7 @@ fn test_query_get_config() { }, ); - let config = get_config(&mut app, addr); + let config = get_config(&app, addr); assert_eq!( config, Config { @@ -719,11 +719,11 @@ fn test_voting_power_queries() { ); // Total power is 0 - let resp = get_total_power_at_height(&mut app, addr.clone(), None); + let resp = get_total_power_at_height(&app, addr.clone(), None); assert!(resp.power.is_zero()); // ADDR1 has no power, none staked - let resp = get_voting_power_at_height(&mut app, addr.clone(), ADDR1.to_string(), None); + let resp = get_voting_power_at_height(&app, addr.clone(), ADDR1.to_string(), None); assert!(resp.power.is_zero()); // ADDR1 stakes @@ -731,15 +731,15 @@ fn test_voting_power_queries() { app.update_block(next_block); // Total power is 100 - let resp = get_total_power_at_height(&mut app, addr.clone(), None); + let resp = get_total_power_at_height(&app, addr.clone(), None); assert_eq!(resp.power, Uint128::new(100)); // ADDR1 has 100 power - let resp = get_voting_power_at_height(&mut app, addr.clone(), ADDR1.to_string(), None); + let resp = get_voting_power_at_height(&app, addr.clone(), ADDR1.to_string(), None); assert_eq!(resp.power, Uint128::new(100)); // ADDR2 still has 0 power - let resp = get_voting_power_at_height(&mut app, addr.clone(), ADDR2.to_string(), None); + let resp = get_voting_power_at_height(&app, addr.clone(), ADDR2.to_string(), None); assert!(resp.power.is_zero()); // ADDR2 stakes @@ -749,30 +749,28 @@ fn test_voting_power_queries() { // Query the previous height, total 100, ADDR1 100, ADDR2 0 // Total power is 100 - let resp = get_total_power_at_height(&mut app, addr.clone(), Some(prev_height)); + let resp = get_total_power_at_height(&app, addr.clone(), Some(prev_height)); assert_eq!(resp.power, Uint128::new(100)); // ADDR1 has 100 power - let resp = - get_voting_power_at_height(&mut app, addr.clone(), ADDR1.to_string(), Some(prev_height)); + let resp = get_voting_power_at_height(&app, addr.clone(), ADDR1.to_string(), Some(prev_height)); assert_eq!(resp.power, Uint128::new(100)); // ADDR2 still has 0 power - let resp = - get_voting_power_at_height(&mut app, addr.clone(), ADDR2.to_string(), Some(prev_height)); + let resp = get_voting_power_at_height(&app, addr.clone(), ADDR2.to_string(), Some(prev_height)); assert!(resp.power.is_zero()); // For current height, total 150, ADDR1 100, ADDR2 50 // Total power is 150 - let resp = get_total_power_at_height(&mut app, addr.clone(), None); + let resp = get_total_power_at_height(&app, addr.clone(), None); assert_eq!(resp.power, Uint128::new(150)); // ADDR1 has 100 power - let resp = get_voting_power_at_height(&mut app, addr.clone(), ADDR1.to_string(), None); + let resp = get_voting_power_at_height(&app, addr.clone(), ADDR1.to_string(), None); assert_eq!(resp.power, Uint128::new(100)); // ADDR2 now has 50 power - let resp = get_voting_power_at_height(&mut app, addr.clone(), ADDR2.to_string(), None); + let resp = get_voting_power_at_height(&app, addr.clone(), ADDR2.to_string(), None); assert_eq!(resp.power, Uint128::new(50)); // ADDR1 unstakes half @@ -782,30 +780,28 @@ fn test_voting_power_queries() { // Query the previous height, total 150, ADDR1 100, ADDR2 50 // Total power is 100 - let resp = get_total_power_at_height(&mut app, addr.clone(), Some(prev_height)); + let resp = get_total_power_at_height(&app, addr.clone(), Some(prev_height)); assert_eq!(resp.power, Uint128::new(150)); // ADDR1 has 100 power - let resp = - get_voting_power_at_height(&mut app, addr.clone(), ADDR1.to_string(), Some(prev_height)); + let resp = get_voting_power_at_height(&app, addr.clone(), ADDR1.to_string(), Some(prev_height)); assert_eq!(resp.power, Uint128::new(100)); // ADDR2 still has 0 power - let resp = - get_voting_power_at_height(&mut app, addr.clone(), ADDR2.to_string(), Some(prev_height)); + let resp = get_voting_power_at_height(&app, addr.clone(), ADDR2.to_string(), Some(prev_height)); assert_eq!(resp.power, Uint128::new(50)); // For current height, total 100, ADDR1 50, ADDR2 50 // Total power is 100 - let resp = get_total_power_at_height(&mut app, addr.clone(), None); + let resp = get_total_power_at_height(&app, addr.clone(), None); assert_eq!(resp.power, Uint128::new(100)); // ADDR1 has 50 power - let resp = get_voting_power_at_height(&mut app, addr.clone(), ADDR1.to_string(), None); + let resp = get_voting_power_at_height(&app, addr.clone(), ADDR1.to_string(), None); assert_eq!(resp.power, Uint128::new(50)); // ADDR2 now has 50 power - let resp = get_voting_power_at_height(&mut app, addr, ADDR2.to_string(), None); + let resp = get_voting_power_at_height(&app, addr, ADDR2.to_string(), None); assert_eq!(resp.power, Uint128::new(50)); } @@ -1269,3 +1265,54 @@ pub fn test_migrate_update_version() { assert_eq!(version.version, CONTRACT_VERSION); assert_eq!(version.contract, CONTRACT_NAME); } + +#[test] +pub fn test_limit() { + let mut app = mock_app(); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + denom: DENOM.to_string(), + unstaking_duration: Some(Duration::Height(5)), + active_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(20), + }), + }, + ); + + let limit = Uint128::from(100u128); + + // Non-owner cannot update limits + let res = app.execute_contract( + Addr::unchecked("random"), + addr.clone(), + &ExecuteMsg::UpdateLimit { + addr: ADDR1.to_string(), + limit: Some(Uint128::from(100u128)), + }, + &[], + ); + assert!(res.is_err()); + + // Owner can update limits + app.execute_contract( + Addr::unchecked(DAO_ADDR), + addr.clone(), + &ExecuteMsg::UpdateLimit { + addr: ADDR1.to_string(), + limit: Some(limit.clone()), + }, + &[], + ) + .unwrap(); + + // Stake 6000 tokens - should fail from limit + let res = stake_tokens(&mut app, addr.clone(), ADDR1, 6000, DENOM); + assert!(res.is_err()); + + // Stake limit of tokens - success + stake_tokens(&mut app, addr.clone(), ADDR1, limit.u128(), DENOM).unwrap(); + app.update_block(next_block); +} diff --git a/packages/dao-dao-macros/src/lib.rs b/packages/dao-dao-macros/src/lib.rs index 4c11c8b71..6672bfa6f 100644 --- a/packages/dao-dao-macros/src/lib.rs +++ b/packages/dao-dao-macros/src/lib.rs @@ -26,7 +26,7 @@ fn merge_variants(metadata: TokenStream, left: TokenStream, right: TokenStream) }), ) = (&mut left.data, right.data) { - variants.extend(to_add.into_iter()); + variants.extend(to_add); quote! { #left }.into() } else { @@ -397,3 +397,40 @@ pub fn limit_variant_count(metadata: TokenStream, input: TokenStream) -> TokenSt } .into() } + +/// Limits the voting power for an address +#[proc_macro_attribute] +pub fn limitable_voting_module(metadata: TokenStream, input: TokenStream) -> TokenStream { + merge_variants( + metadata, + input, + quote! { + enum Right { + UpdateLimit { + addr: ::std::string::String, + limit: ::std::option::Option<::cosmwasm_std::Uint128> + } + } + } + .into(), + ) +} + +/// Allows querying voting module limits +#[proc_macro_attribute] +pub fn limitable_voting_module_query(metadata: TokenStream, input: TokenStream) -> TokenStream { + merge_variants( + metadata, + input, + quote! { + enum Right { + /// Returns the voting power limit for an address at a given height. + #[returns(::std::option::Option<::cosmwasm_std::Uint128>)] + Limit { + addr: ::std::string::String + } + } + } + .into(), + ) +} From db9e4454903dd9dcfe7fe1f48cefad5f677e1ab3 Mon Sep 17 00:00:00 2001 From: Gabriel Lopez Date: Fri, 8 Sep 2023 14:03:59 -0500 Subject: [PATCH 2/4] Fix comment in macro --- packages/dao-dao-macros/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dao-dao-macros/src/lib.rs b/packages/dao-dao-macros/src/lib.rs index 6672bfa6f..be11e1f48 100644 --- a/packages/dao-dao-macros/src/lib.rs +++ b/packages/dao-dao-macros/src/lib.rs @@ -424,7 +424,7 @@ pub fn limitable_voting_module_query(metadata: TokenStream, input: TokenStream) input, quote! { enum Right { - /// Returns the voting power limit for an address at a given height. + /// Returns the voting power limit for an address #[returns(::std::option::Option<::cosmwasm_std::Uint128>)] Limit { addr: ::std::string::String From 5661dab6134e1ce640ef8b81a0109490a6c1cec3 Mon Sep 17 00:00:00 2001 From: Gabriel Lopez Date: Wed, 13 Sep 2023 16:09:19 -0500 Subject: [PATCH 3/4] Enforce limits in VotingPowerAtHeight and TotalPowerAtHeight queries --- .../dao-voting-native-staked/src/contract.rs | 62 +++++++++---- .../dao-voting-native-staked/src/state.rs | 9 +- .../dao-voting-native-staked/src/tests.rs | 90 +++++++++++++++++-- packages/dao-dao-macros/src/lib.rs | 9 +- packages/dao-interface/src/voting.rs | 6 ++ 5 files changed, 150 insertions(+), 26 deletions(-) diff --git a/contracts/voting/dao-voting-native-staked/src/contract.rs b/contracts/voting/dao-voting-native-staked/src/contract.rs index c3e6d2e23..dd9f08995 100644 --- a/contracts/voting/dao-voting-native-staked/src/contract.rs +++ b/contracts/voting/dao-voting-native-staked/src/contract.rs @@ -8,7 +8,8 @@ use cw2::set_contract_version; use cw_controllers::ClaimsResponse; use cw_utils::{must_pay, Duration}; use dao_interface::voting::{ - IsActiveResponse, TotalPowerAtHeightResponse, VotingPowerAtHeightResponse, + IsActiveResponse, LimitAtHeightResponse, TotalPowerAtHeightResponse, + VotingPowerAtHeightResponse, }; use dao_voting::threshold::{ActiveThreshold, ActiveThresholdResponse}; @@ -116,7 +117,9 @@ pub fn execute( } ExecuteMsg::AddHook { addr } => execute_add_hook(deps, env, info, addr), ExecuteMsg::RemoveHook { addr } => execute_remove_hook(deps, env, info, addr), - ExecuteMsg::UpdateLimit { addr, limit } => execute_update_limit(deps, info, addr, limit), + ExecuteMsg::UpdateLimit { addr, limit } => { + execute_update_limit(deps, env, info, addr, limit) + } } } @@ -127,13 +130,6 @@ pub fn execute_stake( ) -> Result { let config = CONFIG.load(deps.storage)?; let amount = must_pay(&info, &config.denom)?; - let limit = LIMITS.may_load(deps.storage, &info.sender)?; - - if let Some(limit) = limit { - if amount > limit { - return Err(ContractError::LimitExceeded { limit }); - } - } STAKED_BALANCES.update( deps.storage, @@ -333,6 +329,7 @@ pub fn execute_remove_hook( pub fn execute_update_limit( deps: DepsMut, + env: Env, info: MessageInfo, addr: String, limit: Option, @@ -345,9 +342,9 @@ pub fn execute_update_limit( let addr = deps.api.addr_validate(&addr)?; if let Some(limit) = limit { - LIMITS.save(deps.storage, &addr, &limit)?; + LIMITS.save(deps.storage, &addr, &limit, env.block.height)?; } else { - LIMITS.remove(deps.storage, &addr); + LIMITS.remove(deps.storage, &addr, env.block.height)?; } Ok(Response::new() @@ -381,12 +378,25 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { QueryMsg::IsActive {} => query_is_active(deps), QueryMsg::ActiveThreshold {} => query_active_threshold(deps), QueryMsg::GetHooks {} => to_binary(&query_hooks(deps)?), - QueryMsg::Limit { addr } => { - to_binary(&LIMITS.may_load(deps.storage, &deps.api.addr_validate(&addr)?)?) + QueryMsg::LimitAtHeight { address, height } => { + to_binary(&query_limit_at_height(deps, env, address, height)?) } } } +pub fn query_limit_at_height( + deps: Deps, + env: Env, + address: String, + height: Option, +) -> StdResult { + let height = height.unwrap_or(env.block.height); + let address = deps.api.addr_validate(&address)?; + let limit = LIMITS.may_load_at_height(deps.storage, &address, height)?; + + Ok(LimitAtHeightResponse { limit, height }) +} + pub fn query_voting_power_at_height( deps: Deps, env: Env, @@ -395,9 +405,16 @@ pub fn query_voting_power_at_height( ) -> StdResult { let height = height.unwrap_or(env.block.height); let address = deps.api.addr_validate(&address)?; - let power = STAKED_BALANCES + let mut power = STAKED_BALANCES .may_load_at_height(deps.storage, &address, height)? .unwrap_or_default(); + + // Apply limit + let limit = LIMITS.may_load_at_height(deps.storage, &address, height)?; + if let Some(limit) = limit { + power = Uint128::min(power, limit); + } + Ok(VotingPowerAtHeightResponse { power, height }) } @@ -407,9 +424,24 @@ pub fn query_total_power_at_height( height: Option, ) -> StdResult { let height = height.unwrap_or(env.block.height); - let power = STAKED_TOTAL + let mut power = STAKED_TOTAL .may_load_at_height(deps.storage, height)? .unwrap_or_default(); + + // Adjust power according to limits + for entry in LIMITS.range(deps.storage, None, None, cosmwasm_std::Order::Ascending) { + let user_limit = entry?; + + let user_power = STAKED_BALANCES + .may_load_at_height(deps.storage, &user_limit.0, height)? + .unwrap_or_default(); + + if user_power > user_limit.1 { + let reduced_power = user_power.checked_sub(user_limit.1)?; + power = power.checked_sub(reduced_power)?; + } + } + Ok(TotalPowerAtHeightResponse { power, height }) } diff --git a/contracts/voting/dao-voting-native-staked/src/state.rs b/contracts/voting/dao-voting-native-staked/src/state.rs index c0435eb50..4a93d23c0 100644 --- a/contracts/voting/dao-voting-native-staked/src/state.rs +++ b/contracts/voting/dao-voting-native-staked/src/state.rs @@ -2,7 +2,7 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Uint128}; use cw_controllers::Claims; use cw_hooks::Hooks; -use cw_storage_plus::{Item, Map, SnapshotItem, SnapshotMap, Strategy}; +use cw_storage_plus::{Item, SnapshotItem, SnapshotMap, Strategy}; use cw_utils::Duration; use dao_voting::threshold::ActiveThreshold; @@ -27,7 +27,12 @@ pub const STAKED_BALANCES: SnapshotMap<&Addr, Uint128> = SnapshotMap::new( ); /// Keeps track of limits by address -pub const LIMITS: Map<&Addr, Uint128> = Map::new("limits"); +pub const LIMITS: SnapshotMap<&Addr, Uint128> = SnapshotMap::new( + "limits", + "limits__checkpoints", + "limits__changelog", + Strategy::EveryBlock, +); /// Keeps track of staked total over time pub const STAKED_TOTAL: SnapshotItem = SnapshotItem::new( diff --git a/contracts/voting/dao-voting-native-staked/src/tests.rs b/contracts/voting/dao-voting-native-staked/src/tests.rs index 2346e3757..5972af797 100644 --- a/contracts/voting/dao-voting-native-staked/src/tests.rs +++ b/contracts/voting/dao-voting-native-staked/src/tests.rs @@ -13,7 +13,8 @@ use cw_multi_test::{ }; use cw_utils::Duration; use dao_interface::voting::{ - InfoResponse, IsActiveResponse, TotalPowerAtHeightResponse, VotingPowerAtHeightResponse, + InfoResponse, IsActiveResponse, LimitAtHeightResponse, TotalPowerAtHeightResponse, + VotingPowerAtHeightResponse, }; use dao_voting::threshold::{ActiveThreshold, ActiveThresholdResponse}; @@ -1282,6 +1283,7 @@ pub fn test_limit() { }, ); + let staked = Uint128::from(5000u128); let limit = Uint128::from(100u128); // Non-owner cannot update limits @@ -1307,12 +1309,88 @@ pub fn test_limit() { &[], ) .unwrap(); + app.update_block(next_block); - // Stake 6000 tokens - should fail from limit - let res = stake_tokens(&mut app, addr.clone(), ADDR1, 6000, DENOM); - assert!(res.is_err()); + // Assert that the limit was set + let limit_response: LimitAtHeightResponse = app + .wrap() + .query_wasm_smart( + addr.clone(), + &QueryMsg::LimitAtHeight { + address: ADDR1.to_string(), + height: None, + }, + ) + .unwrap(); + assert_eq!(limit_response.limit, Some(limit.clone())); + + // Stake 5000 tokens which is over the limit of 100 + stake_tokens(&mut app, addr.clone(), ADDR1, staked.u128(), DENOM).unwrap(); + app.update_block(next_block); - // Stake limit of tokens - success - stake_tokens(&mut app, addr.clone(), ADDR1, limit.u128(), DENOM).unwrap(); + // Query voting power + let voting_power_response: VotingPowerAtHeightResponse = app + .wrap() + .query_wasm_smart( + addr.clone(), + &QueryMsg::VotingPowerAtHeight { + address: ADDR1.to_string(), + height: None, + }, + ) + .unwrap(); + assert_eq!(voting_power_response.power, limit.clone()); + + // Query total power + let total_power_response: TotalPowerAtHeightResponse = app + .wrap() + .query_wasm_smart(addr.clone(), &QueryMsg::TotalPowerAtHeight { height: None }) + .unwrap(); + assert_eq!(total_power_response.power, limit.clone()); + + // Owner can remove limit + app.execute_contract( + Addr::unchecked(DAO_ADDR), + addr.clone(), + &ExecuteMsg::UpdateLimit { + addr: ADDR1.to_string(), + limit: None, + }, + &[], + ) + .unwrap(); app.update_block(next_block); + + // Assert that the limit was removed + let limit_response: LimitAtHeightResponse = app + .wrap() + .query_wasm_smart( + addr.clone(), + &QueryMsg::LimitAtHeight { + address: ADDR1.to_string(), + height: None, + }, + ) + .unwrap(); + assert_eq!(limit_response.limit, None); + + // Query voting power without limit + let voting_power_response: VotingPowerAtHeightResponse = app + .wrap() + .query_wasm_smart( + addr.clone(), + &QueryMsg::VotingPowerAtHeight { + address: ADDR1.to_string(), + height: None, + }, + ) + .unwrap(); + assert_eq!(voting_power_response.power, staked.clone()); + + // Query total power without limit + let total_power_response: TotalPowerAtHeightResponse = app + .wrap() + .query_wasm_smart(addr.clone(), &QueryMsg::TotalPowerAtHeight { height: None }) + .unwrap(); + assert_eq!(total_power_response.power, staked.clone()); } diff --git a/packages/dao-dao-macros/src/lib.rs b/packages/dao-dao-macros/src/lib.rs index be11e1f48..7fa5e1033 100644 --- a/packages/dao-dao-macros/src/lib.rs +++ b/packages/dao-dao-macros/src/lib.rs @@ -419,15 +419,18 @@ pub fn limitable_voting_module(metadata: TokenStream, input: TokenStream) -> Tok /// Allows querying voting module limits #[proc_macro_attribute] pub fn limitable_voting_module_query(metadata: TokenStream, input: TokenStream) -> TokenStream { + let l = dao_interface_path("voting::LimitAtHeightResponse"); + merge_variants( metadata, input, quote! { enum Right { /// Returns the voting power limit for an address - #[returns(::std::option::Option<::cosmwasm_std::Uint128>)] - Limit { - addr: ::std::string::String + #[returns(#l)] + LimitAtHeight { + address: ::std::string::String, + height: ::std::option::Option<::std::primitive::u64> } } } diff --git a/packages/dao-interface/src/voting.rs b/packages/dao-interface/src/voting.rs index f2df11a44..694d196e8 100644 --- a/packages/dao-interface/src/voting.rs +++ b/packages/dao-interface/src/voting.rs @@ -42,6 +42,12 @@ pub struct TotalPowerAtHeightResponse { pub height: u64, } +#[cw_serde] +pub struct LimitAtHeightResponse { + pub limit: Option, + pub height: u64, +} + #[cw_serde] pub struct InfoResponse { pub info: ContractVersion, From 4f836cc8835167a195843ccc837d48740c0b1585 Mon Sep 17 00:00:00 2001 From: Gabriel Lopez Date: Thu, 14 Sep 2023 21:50:03 -0500 Subject: [PATCH 4/4] Fix a couple merge errors --- contracts/voting/dao-voting-token-staked/src/state.rs | 8 ++++++++ .../dao-voting-token-staked/src/tests/multitest/tests.rs | 4 +++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/contracts/voting/dao-voting-token-staked/src/state.rs b/contracts/voting/dao-voting-token-staked/src/state.rs index 6fb8bc4a0..3ba2bb41e 100644 --- a/contracts/voting/dao-voting-token-staked/src/state.rs +++ b/contracts/voting/dao-voting-token-staked/src/state.rs @@ -38,6 +38,14 @@ pub const STAKED_TOTAL: SnapshotItem = SnapshotItem::new( Strategy::EveryBlock, ); +/// Keeps track of voting power limits by address +pub const LIMITS: SnapshotMap<&Addr, Uint128> = SnapshotMap::new( + "limits", + "limits__checkpoints", + "limits__changelog", + Strategy::EveryBlock, +); + /// The maximum number of claims that may be outstanding. pub const MAX_CLAIMS: u64 = 100; diff --git a/contracts/voting/dao-voting-token-staked/src/tests/multitest/tests.rs b/contracts/voting/dao-voting-token-staked/src/tests/multitest/tests.rs index 47cc7f826..f8e36f5db 100644 --- a/contracts/voting/dao-voting-token-staked/src/tests/multitest/tests.rs +++ b/contracts/voting/dao-voting-token-staked/src/tests/multitest/tests.rs @@ -1371,7 +1371,9 @@ pub fn test_limit() { &mut app, staking_id, InstantiateMsg { - denom: DENOM.to_string(), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, unstaking_duration: Some(Duration::Height(5)), active_threshold: Some(ActiveThreshold::Percentage { percent: Decimal::percent(20),