From c02d80f3cf114729ef07e3bea9239268078c2e61 Mon Sep 17 00:00:00 2001 From: Noah Saso Date: Mon, 24 Jun 2024 17:37:03 -0400 Subject: [PATCH] added custom query for total staked tokens --- .../dao-voting-cosmos-staked/Cargo.toml | 2 +- .../voting/dao-voting-cosmos-staked/README.md | 27 +---- .../dao-voting-cosmos-staked/src/contract.rs | 98 ++++++++----------- .../dao-voting-cosmos-staked/src/error.rs | 4 +- .../dao-voting-cosmos-staked/src/msg.rs | 14 +-- .../dao-voting-cosmos-staked/src/state.rs | 12 +-- .../src/tests/multitest/tests.rs | 45 +-------- .../src/tests/test_tube/authz.rs | 41 -------- .../src/tests/test_tube/integration_tests.rs | 93 +----------------- .../src/tests/test_tube/mod.rs | 1 - 10 files changed, 57 insertions(+), 280 deletions(-) delete mode 100644 contracts/voting/dao-voting-cosmos-staked/src/tests/test_tube/authz.rs diff --git a/contracts/voting/dao-voting-cosmos-staked/Cargo.toml b/contracts/voting/dao-voting-cosmos-staked/Cargo.toml index 6d57073d0..a7de8d34f 100644 --- a/contracts/voting/dao-voting-cosmos-staked/Cargo.toml +++ b/contracts/voting/dao-voting-cosmos-staked/Cargo.toml @@ -36,6 +36,7 @@ thiserror = { workspace = true } dao-dao-macros = { workspace = true } dao-interface = { workspace = true } dao-voting = { workspace = true } +osmosis-std = { workspace = true } [dev-dependencies] anyhow = { workspace = true } @@ -45,6 +46,5 @@ cw-utils = { workspace = true } dao-proposal-single = { workspace = true } dao-test-custom-factory = { workspace = true } dao-testing = { workspace = true } -osmosis-std = { workspace = true } osmosis-test-tube = { workspace = true } serde = { workspace = true } diff --git a/contracts/voting/dao-voting-cosmos-staked/README.md b/contracts/voting/dao-voting-cosmos-staked/README.md index e4377d30e..ed87023ab 100644 --- a/contracts/voting/dao-voting-cosmos-staked/README.md +++ b/contracts/voting/dao-voting-cosmos-staked/README.md @@ -11,9 +11,8 @@ power in chain governance props). ## Limitations -Unfortunately, CosmWasm does not currently allow querying historically staked -amounts, nor does it allow querying the total amount staked with the staking -module. Thus, this module suffers from two primary limitations. +Unfortunately, the Cosmos SDK does not currently store historical staked +amounts, so this module suffers from some limitations. ### Voter's staked amount @@ -35,25 +34,3 @@ Cosmos SDK governance operates the same way—allowing for voting power to chang throughout a proposal's voting duration—though it at least re-tallies votes when the proposal closes so that all voters have equal opportunity to acquire more voting power. - -### Total staked amount - -The contract cannot determine the total amount staked on its own and thus relies -on the DAO to set and keep this value up-to-date. Essentially, it relies on -governance to source this value, which introduces the potential for human error. - -If the total staked amount is ever set to _less_ than any voter's staked amount -or the sum of all voter's staked amounts, proposal outcomes may erroneously pass -or fail too early as this interferes with the passing threshold calculation. - -## Solutions - -There is no solution to the problem of freezing voter's staked amount at the -time of a vote. This mechanic must be accepted by the DAO if it wishes to use -this contract. - -For the total staked amount, the easiest solution is to set up a bot with a -wallet that is entrusted with the task of updating the total staked amount on -behalf of the DAO. The DAO needs to authz-grant the bot's wallet the ability to -update the total staked amount, and the bot needs to periodically submit update -transactions via the wallet it controls. diff --git a/contracts/voting/dao-voting-cosmos-staked/src/contract.rs b/contracts/voting/dao-voting-cosmos-staked/src/contract.rs index e38c8f733..1714d2dda 100644 --- a/contracts/voting/dao-voting-cosmos-staked/src/contract.rs +++ b/contracts/voting/dao-voting-cosmos-staked/src/contract.rs @@ -1,3 +1,5 @@ +use std::str::FromStr; + #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; @@ -9,7 +11,7 @@ use dao_interface::voting::{TotalPowerAtHeightResponse, VotingPowerAtHeightRespo use crate::error::ContractError; use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; -use crate::state::{DAO, STAKED_TOTAL}; +use crate::state::DAO; pub(crate) const CONTRACT_NAME: &str = "crates.io:dao-voting-cosmos-staked"; pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -17,60 +19,35 @@ pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( deps: DepsMut, - env: Env, + _env: Env, info: MessageInfo, - msg: InstantiateMsg, + _msg: InstantiateMsg, ) -> Result { set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; DAO.save(deps.storage, &info.sender)?; - STAKED_TOTAL.save(deps.storage, &msg.total_staked, env.block.height)?; Ok(Response::default()) } #[cfg_attr(not(feature = "library"), entry_point)] pub fn execute( - deps: DepsMut, - env: Env, - info: MessageInfo, - msg: ExecuteMsg, -) -> Result { - let dao = DAO.load(deps.storage)?; - if info.sender != dao { - return Err(ContractError::Unauthorized {}); - } - - match msg { - ExecuteMsg::UpdateTotalStaked { amount, height } => { - execute_update_total_staked(deps, env, amount, height) - } - } -} - -pub fn execute_update_total_staked( - deps: DepsMut, - env: Env, - amount: Uint128, - height: Option, + _deps: DepsMut, + _env: Env, + _info: MessageInfo, + _msg: ExecuteMsg, ) -> Result { - let height = height.unwrap_or(env.block.height); - STAKED_TOTAL.save(deps.storage, &amount, height)?; - - Ok(Response::new() - .add_attribute("action", "update_total_staked") - .add_attribute("amount", amount) - .add_attribute("height", height.to_string())) + Err(ContractError::NoExecute {}) } #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { match msg { - QueryMsg::VotingPowerAtHeight { address, height } => { - to_json_binary(&query_voting_power_at_height(deps, env, address, height)?) + QueryMsg::VotingPowerAtHeight { address, .. } => { + to_json_binary(&query_voting_power_at_height(deps, env, address)?) } - QueryMsg::TotalPowerAtHeight { height } => { - to_json_binary(&query_total_power_at_height(deps, env, height)?) + QueryMsg::TotalPowerAtHeight { .. } => { + to_json_binary(&query_total_power_at_height(deps, env)?) } QueryMsg::Info {} => query_info(deps), QueryMsg::Dao {} => query_dao(deps), @@ -81,28 +58,26 @@ pub fn query_voting_power_at_height( deps: Deps, env: Env, address: String, - height: Option, ) -> StdResult { - // Lie about height since we can't access historical data. - let height = height.unwrap_or(env.block.height); - let power = get_total_delegations(deps, address)?; - - Ok(VotingPowerAtHeightResponse { power, height }) + let power = get_delegator_total(deps, address)?; + + Ok(VotingPowerAtHeightResponse { + power, + // always return the latest block height since we can't access + // historical data + height: env.block.height, + }) } -pub fn query_total_power_at_height( - deps: Deps, - env: Env, - height: Option, -) -> StdResult { - let height = height.unwrap_or(env.block.height); - // Total staked amount is initialized to a value during contract - // instantiation. Any block before that block returns 0. - let power = STAKED_TOTAL - .may_load_at_height(deps.storage, height)? - .unwrap_or_default(); - - Ok(TotalPowerAtHeightResponse { power, height }) +pub fn query_total_power_at_height(deps: Deps, env: Env) -> StdResult { + let power = get_total_delegated(deps)?; + + Ok(TotalPowerAtHeightResponse { + power, + // always return the latest block height since we can't access + // historical data + height: env.block.height, + }) } pub fn query_info(deps: Deps) -> StdResult { @@ -115,7 +90,7 @@ pub fn query_dao(deps: Deps) -> StdResult { to_json_binary(&dao) } -fn get_total_delegations(deps: Deps, delegator: String) -> StdResult { +fn get_delegator_total(deps: Deps, delegator: String) -> StdResult { let delegations = deps.querier.query_all_delegations(delegator)?; let mut amount_staked = Uint128::zero(); @@ -127,3 +102,12 @@ fn get_total_delegations(deps: Deps, delegator: String) -> StdResult { Ok(amount_staked) } + +fn get_total_delegated(deps: Deps) -> StdResult { + let pool = osmosis_std::types::cosmos::staking::v1beta1::QueryPoolRequest {} + .query(&deps.querier)? + .pool + .unwrap(); + + Ok(Uint128::from_str(pool.bonded_tokens.as_ref()).unwrap()) +} diff --git a/contracts/voting/dao-voting-cosmos-staked/src/error.rs b/contracts/voting/dao-voting-cosmos-staked/src/error.rs index 383cfab3c..0b55019df 100644 --- a/contracts/voting/dao-voting-cosmos-staked/src/error.rs +++ b/contracts/voting/dao-voting-cosmos-staked/src/error.rs @@ -6,6 +6,6 @@ pub enum ContractError { #[error(transparent)] Std(#[from] StdError), - #[error("Unauthorized: only the DAO can execute this contract")] - Unauthorized {}, + #[error("Contract does not support executing")] + NoExecute {}, } diff --git a/contracts/voting/dao-voting-cosmos-staked/src/msg.rs b/contracts/voting/dao-voting-cosmos-staked/src/msg.rs index d5f599287..3e60d9377 100644 --- a/contracts/voting/dao-voting-cosmos-staked/src/msg.rs +++ b/contracts/voting/dao-voting-cosmos-staked/src/msg.rs @@ -1,21 +1,11 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::Uint128; use dao_dao_macros::voting_module_query; #[cw_serde] -pub struct InstantiateMsg { - /// Total staked balance to start with. - pub total_staked: Uint128, -} +pub struct InstantiateMsg {} #[cw_serde] -pub enum ExecuteMsg { - /// Set the total staked balance at a given height or the current height. - UpdateTotalStaked { - amount: Uint128, - height: Option, - }, -} +pub enum ExecuteMsg {} #[voting_module_query] #[cw_serde] diff --git a/contracts/voting/dao-voting-cosmos-staked/src/state.rs b/contracts/voting/dao-voting-cosmos-staked/src/state.rs index 84691053f..efc10b3a1 100644 --- a/contracts/voting/dao-voting-cosmos-staked/src/state.rs +++ b/contracts/voting/dao-voting-cosmos-staked/src/state.rs @@ -1,13 +1,5 @@ -use cosmwasm_std::{Addr, Uint128}; -use cw_storage_plus::{Item, SnapshotItem, Strategy}; +use cosmwasm_std::Addr; +use cw_storage_plus::Item; /// The address of the DAO this voting contract is connected to. pub const DAO: Item = Item::new("dao"); - -/// Keeps track of staked total over time. -pub const STAKED_TOTAL: SnapshotItem = SnapshotItem::new( - "total_staked", - "total_staked__checkpoints", - "total_staked__changelog", - Strategy::EveryBlock, -); diff --git a/contracts/voting/dao-voting-cosmos-staked/src/tests/multitest/tests.rs b/contracts/voting/dao-voting-cosmos-staked/src/tests/multitest/tests.rs index d622cbd5c..567a40ad0 100644 --- a/contracts/voting/dao-voting-cosmos-staked/src/tests/multitest/tests.rs +++ b/contracts/voting/dao-voting-cosmos-staked/src/tests/multitest/tests.rs @@ -6,10 +6,7 @@ use dao_interface::voting::{ InfoResponse, TotalPowerAtHeightResponse, VotingPowerAtHeightResponse, }; -use crate::{ - msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, - ContractError, -}; +use crate::msg::{InstantiateMsg, QueryMsg}; fn cosmos_staked_contract() -> Box> { let contract = ContractWrapper::new( @@ -73,9 +70,7 @@ fn happy_path() { .instantiate_contract( cosmos_staking_code_id, Addr::unchecked(DAO), - &InstantiateMsg { - total_staked: Uint128::zero(), - }, + &InstantiateMsg {}, &[], "cosmos_voting_power_contract", None, @@ -92,34 +87,6 @@ fn happy_path() { ) .unwrap(); - // Error if non-DAO attempts to update total staked. - let error: ContractError = app - .execute_contract( - Addr::unchecked(DELEGATOR), - vp_contract.clone(), - &ExecuteMsg::UpdateTotalStaked { - amount: Uint128::new(100000), - height: None, - }, - &[], - ) - .unwrap_err() - .downcast() - .unwrap(); - assert_eq!(error, ContractError::Unauthorized {}); - - // Update total staked manually (responsibility of DAO). - app.execute_contract( - Addr::unchecked(DAO), - vp_contract.clone(), - &ExecuteMsg::UpdateTotalStaked { - amount: Uint128::new(100000), - height: None, - }, - &[], - ) - .unwrap(); - // Update block height app.update_block(|block| block.height += 1); @@ -163,9 +130,7 @@ fn test_query_dao() { .instantiate_contract( cosmos_staking_code_id, Addr::unchecked(DAO), - &InstantiateMsg { - total_staked: Uint128::zero(), - }, + &InstantiateMsg {}, &[], "cosmos_voting_power_contract", None, @@ -189,9 +154,7 @@ fn test_query_info() { .instantiate_contract( cosmos_staking_code_id, Addr::unchecked(DAO), - &InstantiateMsg { - total_staked: Uint128::zero(), - }, + &InstantiateMsg {}, &[], "cosmos_voting_power_contract", None, diff --git a/contracts/voting/dao-voting-cosmos-staked/src/tests/test_tube/authz.rs b/contracts/voting/dao-voting-cosmos-staked/src/tests/test_tube/authz.rs deleted file mode 100644 index 810e517eb..000000000 --- a/contracts/voting/dao-voting-cosmos-staked/src/tests/test_tube/authz.rs +++ /dev/null @@ -1,41 +0,0 @@ -use osmosis_std::types::cosmos::authz::v1beta1::{ - MsgExec, MsgExecResponse, MsgGrant, MsgGrantResponse, QueryGranteeGrantsRequest, - QueryGranteeGrantsResponse, QueryGranterGrantsRequest, QueryGranterGrantsResponse, - QueryGrantsRequest, QueryGrantsResponse, -}; -use osmosis_test_tube::{fn_execute, fn_query, Module, Runner}; - -pub struct Authz<'a, R: Runner<'a>> { - runner: &'a R, -} - -impl<'a, R: Runner<'a>> Module<'a, R> for Authz<'a, R> { - fn new(runner: &'a R) -> Self { - Self { runner } - } -} - -impl<'a, R> Authz<'a, R> -where - R: Runner<'a>, -{ - fn_execute! { - pub exec: MsgExec["/cosmos.authz.v1beta1.MsgExec"] => MsgExecResponse - } - - fn_execute! { - pub grant: MsgGrant["/cosmos.authz.v1beta1.MsgGrant"] => MsgGrantResponse - } - - fn_query! { - pub query_grantee_grants ["/cosmos.authz.v1beta1.Query/GranteeGrants"]: QueryGranteeGrantsRequest => QueryGranteeGrantsResponse - } - - fn_query! { - pub query_granter_grants ["/cosmos.authz.v1beta1.Query/GranterGrants"]: QueryGranterGrantsRequest => QueryGranterGrantsResponse - } - - fn_query! { - pub query_grants ["/cosmos.authz.v1beta1.Query/Grants"]: QueryGrantsRequest => QueryGrantsResponse - } -} diff --git a/contracts/voting/dao-voting-cosmos-staked/src/tests/test_tube/integration_tests.rs b/contracts/voting/dao-voting-cosmos-staked/src/tests/test_tube/integration_tests.rs index 1ea6f3006..b626bbd34 100644 --- a/contracts/voting/dao-voting-cosmos-staked/src/tests/test_tube/integration_tests.rs +++ b/contracts/voting/dao-voting-cosmos-staked/src/tests/test_tube/integration_tests.rs @@ -5,10 +5,7 @@ use crate::{ use cosmwasm_std::{to_json_binary, Addr, Coin, CosmosMsg, Uint128}; use dao_voting::voting::{SingleChoiceAutoVote, Vote}; use osmosis_std::types::{ - cosmos::{ - authz::v1beta1::{Grant, MsgExec, MsgGrant}, - staking::v1beta1::MsgDelegate, - }, + cosmos::staking::v1beta1::MsgDelegate, cosmwasm::wasm::v1::{ AcceptedMessageKeysFilter, ContractExecutionAuthorization, ContractGrant, MaxCallsLimit, MsgExecuteContract, @@ -34,7 +31,7 @@ fn test_full_integration_correct_setup() { } #[test] -fn test_staked_voting_power_and_update() { +fn test_staked_voting_power() { let app = OsmosisTestApp::new(); let env = TestEnvBuilder::new(); let TestEnv { @@ -51,7 +48,6 @@ fn test_staked_voting_power_and_update() { let staker = &accounts[0]; let bot = &accounts[1]; - let authz = Authz::new(&app); let staking = Staking::new(&app); staking @@ -65,93 +61,10 @@ fn test_staked_voting_power_and_update() { ) .unwrap(); - // Query voting power + // Query address voting power let voting_power = vp_contract.query_vp(&accounts[0].address(), None).unwrap(); assert_eq!(voting_power.power, Uint128::new(100)); - let expiration = app.get_block_timestamp().plus_days(1); - - // Authz grant bot to execute by creating a proposal, voting on it, and - // executing it. - proposal_single - .execute( - &dao_proposal_single::msg::ExecuteMsg::Propose( - dao_voting::proposal::SingleChoiceProposeMsg { - title: "authz".to_string(), - description: "authz".to_string(), - msgs: vec![CosmosMsg::Stargate { - type_url: "/cosmos.authz.v1beta1.MsgGrant".to_string(), - value: MsgGrant { - granter: dao.contract_addr.clone(), - grantee: bot.address(), - grant: Some(Grant { - authorization: Some( - ContractExecutionAuthorization { - grants: vec![ContractGrant { - contract: vp_contract.contract_addr.clone(), - limit: Some(MaxCallsLimit { remaining: 10 }.to_any()), - filter: Some( - AcceptedMessageKeysFilter { - keys: vec!["update_total_staked".to_string()], - } - .to_any(), - ), - }], - } - .to_any(), - ), - expiration: Some(osmosis_std::shim::Timestamp { - seconds: expiration.seconds() as i64, - nanos: expiration.subsec_nanos() as i32, - }), - }), - } - .into(), - }], - proposer: None, - vote: Some(SingleChoiceAutoVote { - vote: Vote::Yes, - rationale: None, - }), - }, - ), - &[], - staker, - ) - .unwrap(); - proposal_single - .execute( - &dao_proposal_single::msg::ExecuteMsg::Execute { proposal_id: 1 }, - &[], - staker, - ) - .unwrap(); - - // Update total power from bot via authz exec on behalf of DAO - authz - .exec( - MsgExec { - grantee: bot.address(), - msgs: vec![MsgExecuteContract { - sender: dao.contract_addr, - contract: vp_contract.contract_addr.clone(), - msg: to_json_binary(&ExecuteMsg::UpdateTotalStaked { - amount: Uint128::new(100), - height: None, - }) - .unwrap() - .into(), - funds: vec![], - } - .to_any()], - }, - bot, - ) - .unwrap(); - - // Move chain forward so we can update total staked on a new block - app.increase_time(100); - // Query total power let total_power = vp_contract.query_tp(None).unwrap(); assert_eq!(total_power.power, Uint128::new(100)); diff --git a/contracts/voting/dao-voting-cosmos-staked/src/tests/test_tube/mod.rs b/contracts/voting/dao-voting-cosmos-staked/src/tests/test_tube/mod.rs index 8de367a47..26856cb9a 100644 --- a/contracts/voting/dao-voting-cosmos-staked/src/tests/test_tube/mod.rs +++ b/contracts/voting/dao-voting-cosmos-staked/src/tests/test_tube/mod.rs @@ -2,7 +2,6 @@ // and also, tarpaulin will not be able read coverage out of wasm binary anyway #![cfg(not(tarpaulin))] -mod authz; mod staking; mod integration_tests;