Skip to content

Commit

Permalink
added custom query for total staked tokens
Browse files Browse the repository at this point in the history
  • Loading branch information
NoahSaso committed Jun 24, 2024
1 parent 88e2965 commit c02d80f
Show file tree
Hide file tree
Showing 10 changed files with 57 additions and 280 deletions.
2 changes: 1 addition & 1 deletion contracts/voting/dao-voting-cosmos-staked/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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 }
27 changes: 2 additions & 25 deletions contracts/voting/dao-voting-cosmos-staked/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.
98 changes: 41 additions & 57 deletions contracts/voting/dao-voting-cosmos-staked/src/contract.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::str::FromStr;

#[cfg(not(feature = "library"))]
use cosmwasm_std::entry_point;

Expand All @@ -9,68 +11,43 @@ 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");

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
deps: DepsMut,
env: Env,
_env: Env,
info: MessageInfo,
msg: InstantiateMsg,
_msg: InstantiateMsg,
) -> Result<Response, ContractError> {
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<Response, ContractError> {
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<u64>,
_deps: DepsMut,
_env: Env,
_info: MessageInfo,
_msg: ExecuteMsg,
) -> Result<Response, ContractError> {
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<Binary> {
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),
Expand All @@ -81,28 +58,26 @@ pub fn query_voting_power_at_height(
deps: Deps,
env: Env,
address: String,
height: Option<u64>,
) -> StdResult<VotingPowerAtHeightResponse> {
// 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<u64>,
) -> StdResult<TotalPowerAtHeightResponse> {
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<TotalPowerAtHeightResponse> {
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<Binary> {
Expand All @@ -115,7 +90,7 @@ pub fn query_dao(deps: Deps) -> StdResult<Binary> {
to_json_binary(&dao)
}

fn get_total_delegations(deps: Deps, delegator: String) -> StdResult<Uint128> {
fn get_delegator_total(deps: Deps, delegator: String) -> StdResult<Uint128> {
let delegations = deps.querier.query_all_delegations(delegator)?;

let mut amount_staked = Uint128::zero();
Expand All @@ -127,3 +102,12 @@ fn get_total_delegations(deps: Deps, delegator: String) -> StdResult<Uint128> {

Ok(amount_staked)
}

fn get_total_delegated(deps: Deps) -> StdResult<Uint128> {
let pool = osmosis_std::types::cosmos::staking::v1beta1::QueryPoolRequest {}
.query(&deps.querier)?
.pool
.unwrap();

Ok(Uint128::from_str(pool.bonded_tokens.as_ref()).unwrap())
}
4 changes: 2 additions & 2 deletions contracts/voting/dao-voting-cosmos-staked/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {},
}
14 changes: 2 additions & 12 deletions contracts/voting/dao-voting-cosmos-staked/src/msg.rs
Original file line number Diff line number Diff line change
@@ -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<u64>,
},
}
pub enum ExecuteMsg {}

#[voting_module_query]
#[cw_serde]
Expand Down
12 changes: 2 additions & 10 deletions contracts/voting/dao-voting-cosmos-staked/src/state.rs
Original file line number Diff line number Diff line change
@@ -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<Addr> = Item::new("dao");

/// Keeps track of staked total over time.
pub const STAKED_TOTAL: SnapshotItem<Uint128> = SnapshotItem::new(
"total_staked",
"total_staked__checkpoints",
"total_staked__changelog",
Strategy::EveryBlock,
);
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn Contract<Empty>> {
let contract = ContractWrapper::new(
Expand Down Expand Up @@ -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,
Expand All @@ -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);

Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down

This file was deleted.

Loading

0 comments on commit c02d80f

Please sign in to comment.