Skip to content

Commit

Permalink
added delegated VP cap
Browse files Browse the repository at this point in the history
  • Loading branch information
NoahSaso committed Oct 14, 2024
1 parent 73c857b commit 24313b6
Show file tree
Hide file tree
Showing 7 changed files with 196 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -647,7 +647,7 @@
"additionalProperties": false
},
{
"description": "Returns the VP delegated to a delegate that has not yet been used in votes cast by delegators in a specific proposal.",
"description": "Returns the VP delegated to a delegate that has not yet been used in votes cast by delegators in a specific proposal. This updates immediately via vote hooks (instead of being delayed 1 block like other historical queries), making it safe to vote multiple times in the same block. Proposal modules are responsible for maintaining the effective VP cap when a delegator overrides a delegate's vote.",
"type": "object",
"required": [
"unvoted_delegated_voting_power"
Expand Down Expand Up @@ -919,9 +919,37 @@
},
"unvoted_delegated_voting_power": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Uint128",
"description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
"type": "string"
"title": "UnvotedDelegatedVotingPowerResponse",
"type": "object",
"required": [
"effective",
"total"
],
"properties": {
"effective": {
"description": "The unvoted delegated voting power in effect, with configured constraints applied, such as the VP cap.",
"allOf": [
{
"$ref": "#/definitions/Uint128"
}
]
},
"total": {
"description": "The total unvoted delegated voting power.",
"allOf": [
{
"$ref": "#/definitions/Uint128"
}
]
}
},
"additionalProperties": false,
"definitions": {
"Uint128": {
"description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
"type": "string"
}
}
},
"voting_power_hook_callers": {
"$schema": "http://json-schema.org/draft-07/schema#",
Expand Down
27 changes: 23 additions & 4 deletions contracts/delegation/dao-vote-delegation/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ use cw_utils::{maybe_addr, nonpayable};
use dao_interface::helpers::OptionalUpdate;
use dao_interface::state::{ProposalModule, ProposalModuleStatus};
use dao_interface::voting::InfoResponse;
use dao_voting::delegation::calculate_delegated_vp;
use dao_voting::delegation::{calculate_delegated_vp, UnvotedDelegatedVotingPowerResponse};
use dao_voting::voting;
use semver::Version;

use crate::helpers::{
Expand Down Expand Up @@ -540,17 +541,35 @@ fn query_unvoted_delegated_vp(
proposal_module: String,
proposal_id: u64,
height: u64,
) -> StdResult<Uint128> {
) -> StdResult<UnvotedDelegatedVotingPowerResponse> {
let delegate = deps.api.addr_validate(&delegate)?;

Check warning on line 545 in contracts/delegation/dao-vote-delegation/src/contract.rs

View check run for this annotation

Codecov / codecov/patch

contracts/delegation/dao-vote-delegation/src/contract.rs#L538-L545

Added lines #L538 - L545 were not covered by tests

// if delegate not registered, they have no unvoted delegated VP.
if !is_delegate_registered(deps, &delegate, Some(height))? {
return Ok(Uint128::zero());
return Ok(UnvotedDelegatedVotingPowerResponse {
total: Uint128::zero(),
effective: Uint128::zero(),
});
}

Check warning on line 553 in contracts/delegation/dao-vote-delegation/src/contract.rs

View check run for this annotation

Codecov / codecov/patch

contracts/delegation/dao-vote-delegation/src/contract.rs#L548-L553

Added lines #L548 - L553 were not covered by tests

let proposal_module = deps.api.addr_validate(&proposal_module)?;

Check warning on line 555 in contracts/delegation/dao-vote-delegation/src/contract.rs

View check run for this annotation

Codecov / codecov/patch

contracts/delegation/dao-vote-delegation/src/contract.rs#L555

Added line #L555 was not covered by tests

get_udvp(deps, &delegate, &proposal_module, proposal_id, height)
let total = get_udvp(deps, &delegate, &proposal_module, proposal_id, height)?;
let mut effective = total;

Check warning on line 558 in contracts/delegation/dao-vote-delegation/src/contract.rs

View check run for this annotation

Codecov / codecov/patch

contracts/delegation/dao-vote-delegation/src/contract.rs#L557-L558

Added lines #L557 - L558 were not covered by tests

// if a VP cap is set, apply it to the total VP to get the effective VP.
let config = CONFIG.load(deps.storage)?;
if let Some(vp_cap_percent) = config.vp_cap_percent {
if vp_cap_percent < Decimal::one() {
let dao = DAO.load(deps.storage)?;
let total_power = voting::get_total_power(deps, &dao, Some(height))?;
let cap = calculate_delegated_vp(total_power, vp_cap_percent);

effective = total.min(cap);
}
}

Check warning on line 570 in contracts/delegation/dao-vote-delegation/src/contract.rs

View check run for this annotation

Codecov / codecov/patch

contracts/delegation/dao-vote-delegation/src/contract.rs#L561-L570

Added lines #L561 - L570 were not covered by tests

Ok(UnvotedDelegatedVotingPowerResponse { total, effective })
}

Check warning on line 573 in contracts/delegation/dao-vote-delegation/src/contract.rs

View check run for this annotation

Codecov / codecov/patch

contracts/delegation/dao-vote-delegation/src/contract.rs#L572-L573

Added lines #L572 - L573 were not covered by tests

fn query_proposal_modules(
Expand Down
64 changes: 57 additions & 7 deletions contracts/proposal/dao-proposal-multiple/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ use dao_hooks::proposal::{
};
use dao_hooks::vote::new_vote_hooks;
use dao_interface::voting::IsActiveResponse;
use dao_voting::delegation::{self, calculate_delegated_vp, Delegation};
use dao_voting::delegation::{
self, calculate_delegated_vp, Delegation, UnvotedDelegatedVotingPowerResponse,
};
use dao_voting::voting::get_voting_power_with_delegation;
use dao_voting::{
multiple_choice::{MultipleChoiceVote, MultipleChoiceVotes, VotingStrategy},
Expand Down Expand Up @@ -483,12 +485,60 @@ pub fn execute_vote(
if let Some(mut delegate_ballot) =
BALLOTS.may_load(deps.storage, (proposal_id, &delegate))?

Check warning on line 486 in contracts/proposal/dao-proposal-multiple/src/contract.rs

View check run for this annotation

Codecov / codecov/patch

contracts/proposal/dao-proposal-multiple/src/contract.rs#L485-L486

Added lines #L485 - L486 were not covered by tests
{
let delegated_vp = calculate_delegated_vp(vote_power, percent);

prop.votes.remove_vote(delegate_ballot.vote, delegated_vp)?;

delegate_ballot.power = delegate_ballot.power.checked_sub(delegated_vp)?;
BALLOTS.save(deps.storage, (proposal_id, &delegate), &delegate_ballot)?;
// get the delegate's current unvoted delegated VP. since we
// are currently overriding this delegate's vote, this UDVP
// response will not yet take into account the loss of this
// current voter's delegated VP, so we have to do math below
// to remove this voter's VP from the delegate's effective
// VP. the vote hook at the end of this fn will update this
// UDVP in the delegation module for future votes.
//
// NOTE: this UDVP query reflects updates immediately,
// instead of waiting 1 block to take effect like other
// historical queries, so this will reflect the updated UDVP
// from the vote hooks within the same block, making it safe
// to vote twice in the same block.
let prev_udvp: UnvotedDelegatedVotingPowerResponse =
deps.querier.query_wasm_smart(
delegation_module,
&delegation::QueryMsg::UnvotedDelegatedVotingPower {
delegate: delegate.to_string(),
proposal_module: env.contract.address.to_string(),
proposal_id,
height: prop.start_height,
},
)?;

Check warning on line 510 in contracts/proposal/dao-proposal-multiple/src/contract.rs

View check run for this annotation

Codecov / codecov/patch

contracts/proposal/dao-proposal-multiple/src/contract.rs#L501-L510

Added lines #L501 - L510 were not covered by tests

let voter_delegated_vp = calculate_delegated_vp(vote_power, percent);

Check warning on line 512 in contracts/proposal/dao-proposal-multiple/src/contract.rs

View check run for this annotation

Codecov / codecov/patch

contracts/proposal/dao-proposal-multiple/src/contract.rs#L512

Added line #L512 was not covered by tests

// subtract this voter's delegated VP from the delegate's
// total VP, and cap the result at the delegate's effective
// VP. if the delegate has been delegated in total more than
// this voter's delegated VP above the cap, they will not
// lose any VP. they will lose part or all of this voter's
// delegated VP based on how their total VP ranks relative
// to the cap.
let new_effective_delegated = prev_udvp
.total
.checked_sub(voter_delegated_vp)?
.min(prev_udvp.effective);

// if the new effective VP is less than the previous
// effective VP, update the delegate's ballot and tally.
if new_effective_delegated < prev_udvp.effective {

Check warning on line 528 in contracts/proposal/dao-proposal-multiple/src/contract.rs

View check run for this annotation

Codecov / codecov/patch

contracts/proposal/dao-proposal-multiple/src/contract.rs#L521-L528

Added lines #L521 - L528 were not covered by tests
// how much VP the delegate is losing based on this
// voter's VP and the cap.
let diff = prev_udvp.effective.checked_sub(new_effective_delegated)?;

Check warning on line 531 in contracts/proposal/dao-proposal-multiple/src/contract.rs

View check run for this annotation

Codecov / codecov/patch

contracts/proposal/dao-proposal-multiple/src/contract.rs#L531

Added line #L531 was not covered by tests

// update ballot total and vote tally by removing the
// lost delegated VP only. this makes sure to fully
// preserve the delegate's personal VP even if they lose
// all delegated VP due to delegators overriding votes.
delegate_ballot.power -= diff;
prop.votes.remove_vote(delegate_ballot.vote, diff)?;

Check warning on line 538 in contracts/proposal/dao-proposal-multiple/src/contract.rs

View check run for this annotation

Codecov / codecov/patch

contracts/proposal/dao-proposal-multiple/src/contract.rs#L537-L538

Added lines #L537 - L538 were not covered by tests

BALLOTS.save(deps.storage, (proposal_id, &delegate), &delegate_ballot)?;
}
}

Check warning on line 542 in contracts/proposal/dao-proposal-multiple/src/contract.rs

View check run for this annotation

Codecov / codecov/patch

contracts/proposal/dao-proposal-multiple/src/contract.rs#L540-L542

Added lines #L540 - L542 were not covered by tests
}
}
Expand Down
64 changes: 57 additions & 7 deletions contracts/proposal/dao-proposal-single/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ use dao_hooks::proposal::{
};
use dao_hooks::vote::new_vote_hooks;
use dao_interface::voting::IsActiveResponse;
use dao_voting::delegation::{self, calculate_delegated_vp, Delegation};
use dao_voting::delegation::{
self, calculate_delegated_vp, Delegation, UnvotedDelegatedVotingPowerResponse,
};
use dao_voting::pre_propose::{PreProposeInfo, ProposalCreationPolicy};
use dao_voting::proposal::{
SingleChoiceProposeMsg as ProposeMsg, DEFAULT_LIMIT, MAX_PROPOSAL_SIZE,
Expand Down Expand Up @@ -588,12 +590,60 @@ pub fn execute_vote(
if let Some(mut delegate_ballot) =
BALLOTS.may_load(deps.storage, (proposal_id, &delegate))?

Check warning on line 591 in contracts/proposal/dao-proposal-single/src/contract.rs

View check run for this annotation

Codecov / codecov/patch

contracts/proposal/dao-proposal-single/src/contract.rs#L590-L591

Added lines #L590 - L591 were not covered by tests
{
let delegated_vp = calculate_delegated_vp(vote_power, percent);

prop.votes.remove_vote(delegate_ballot.vote, delegated_vp);

delegate_ballot.power = delegate_ballot.power.checked_sub(delegated_vp)?;
BALLOTS.save(deps.storage, (proposal_id, &delegate), &delegate_ballot)?;
// get the delegate's current unvoted delegated VP. since we
// are currently overriding this delegate's vote, this UDVP
// response will not yet take into account the loss of this
// current voter's delegated VP, so we have to do math below
// to remove this voter's VP from the delegate's effective
// VP. the vote hook at the end of this fn will update this
// UDVP in the delegation module for future votes.
//
// NOTE: this UDVP query reflects updates immediately,
// instead of waiting 1 block to take effect like other
// historical queries, so this will reflect the updated UDVP
// from the vote hooks within the same block, making it safe
// to vote twice in the same block.
let prev_udvp: UnvotedDelegatedVotingPowerResponse =
deps.querier.query_wasm_smart(
delegation_module,
&delegation::QueryMsg::UnvotedDelegatedVotingPower {
delegate: delegate.to_string(),
proposal_module: env.contract.address.to_string(),
proposal_id,
height: prop.start_height,
},
)?;

Check warning on line 615 in contracts/proposal/dao-proposal-single/src/contract.rs

View check run for this annotation

Codecov / codecov/patch

contracts/proposal/dao-proposal-single/src/contract.rs#L606-L615

Added lines #L606 - L615 were not covered by tests

let voter_delegated_vp = calculate_delegated_vp(vote_power, percent);

Check warning on line 617 in contracts/proposal/dao-proposal-single/src/contract.rs

View check run for this annotation

Codecov / codecov/patch

contracts/proposal/dao-proposal-single/src/contract.rs#L617

Added line #L617 was not covered by tests

// subtract this voter's delegated VP from the delegate's
// total VP, and cap the result at the delegate's effective
// VP. if the delegate has been delegated in total more than
// this voter's delegated VP above the cap, they will not
// lose any VP. they will lose part or all of this voter's
// delegated VP based on how their total VP ranks relative
// to the cap.
let new_effective_delegated = prev_udvp
.total
.checked_sub(voter_delegated_vp)?
.min(prev_udvp.effective);

// if the new effective VP is less than the previous
// effective VP, update the delegate's ballot and tally.
if new_effective_delegated < prev_udvp.effective {

Check warning on line 633 in contracts/proposal/dao-proposal-single/src/contract.rs

View check run for this annotation

Codecov / codecov/patch

contracts/proposal/dao-proposal-single/src/contract.rs#L626-L633

Added lines #L626 - L633 were not covered by tests
// how much VP the delegate is losing based on this
// voter's VP and the cap.
let diff = prev_udvp.effective.checked_sub(new_effective_delegated)?;

Check warning on line 636 in contracts/proposal/dao-proposal-single/src/contract.rs

View check run for this annotation

Codecov / codecov/patch

contracts/proposal/dao-proposal-single/src/contract.rs#L636

Added line #L636 was not covered by tests

// update ballot total and vote tally by removing the
// lost delegated VP only. this makes sure to fully
// preserve the delegate's personal VP even if they lose
// all delegated VP due to delegators overriding votes.
delegate_ballot.power -= diff;
prop.votes.remove_vote(delegate_ballot.vote, diff);

BALLOTS.save(deps.storage, (proposal_id, &delegate), &delegate_ballot)?;
}
}

Check warning on line 647 in contracts/proposal/dao-proposal-single/src/contract.rs

View check run for this annotation

Codecov / codecov/patch

contracts/proposal/dao-proposal-single/src/contract.rs#L642-L647

Added lines #L642 - L647 were not covered by tests
}
}
Expand Down
3 changes: 2 additions & 1 deletion contracts/proposal/dao-proposal-single/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ use crate::proposal::SingleChoiceProposal;
/// A vote cast for a proposal.
#[cw_serde]
pub struct Ballot {
/// The amount of voting power behind the vote.
/// The amount of voting power behind the vote, including any delegated VP.
/// This is the amount tallied in the proposal for this ballot.
pub power: Uint128,
/// The position.
pub vote: Vote,
Expand Down
18 changes: 16 additions & 2 deletions packages/dao-voting/src/delegation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,12 @@ pub enum QueryMsg {
limit: Option<u64>,
},
/// Returns the VP delegated to a delegate that has not yet been used in
/// votes cast by delegators in a specific proposal.
#[returns(Uint128)]
/// votes cast by delegators in a specific proposal. This updates
/// immediately via vote hooks (instead of being delayed 1 block like other
/// historical queries), making it safe to vote multiple times in the same
/// block. Proposal modules are responsible for maintaining the effective VP
/// cap when a delegator overrides a delegate's vote.
#[returns(UnvotedDelegatedVotingPowerResponse)]
UnvotedDelegatedVotingPower {
delegate: String,
proposal_module: String,
Expand Down Expand Up @@ -69,6 +73,16 @@ pub struct DelegationsResponse {
pub height: u64,
}

#[cw_serde]

Check warning on line 76 in packages/dao-voting/src/delegation.rs

View check run for this annotation

Codecov / codecov/patch

packages/dao-voting/src/delegation.rs#L76

Added line #L76 was not covered by tests
#[derive(Default)]
pub struct UnvotedDelegatedVotingPowerResponse {
/// The total unvoted delegated voting power.
pub total: Uint128,
/// The unvoted delegated voting power in effect, with configured
/// constraints applied, such as the VP cap.
pub effective: Uint128,
}

#[cw_serde]

Check warning on line 86 in packages/dao-voting/src/delegation.rs

View check run for this annotation

Codecov / codecov/patch

packages/dao-voting/src/delegation.rs#L86

Added line #L86 was not covered by tests
pub struct Delegate {}

Expand Down
14 changes: 9 additions & 5 deletions packages/dao-voting/src/voting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ use cosmwasm_std::{Addr, Decimal, Deps, StdResult, Uint128, Uint256};
use cw_utils::Duration;
use dao_interface::voting;

use crate::{delegation, threshold::PercentageThreshold};
use crate::{
delegation::{self, UnvotedDelegatedVotingPowerResponse},
threshold::PercentageThreshold,
};

// We multiply by this when calculating needed_votes in order to round
// up properly.
Expand Down Expand Up @@ -240,13 +243,13 @@ pub fn get_voting_power_with_delegation(
},
)?;

// get voting power delegated to this address from other members of the DAO
// get effective VP delegated to this address from other members of the DAO
// that has not yet been used to vote on the given proposal. if this query
// fails, fail gracefully and assume 0 delegated VP to ensure votes can
// still be cast.
let udvp = delegation_module
.as_ref()
.map(|dm| -> StdResult<Uint128> {
.map(|dm| -> StdResult<UnvotedDelegatedVotingPowerResponse> {
deps.querier.query_wasm_smart(
dm,
&delegation::QueryMsg::UnvotedDelegatedVotingPower {
Expand All @@ -257,9 +260,10 @@ pub fn get_voting_power_with_delegation(
},
)

Check warning on line 261 in packages/dao-voting/src/voting.rs

View check run for this annotation

Codecov / codecov/patch

packages/dao-voting/src/voting.rs#L253-L261

Added lines #L253 - L261 were not covered by tests
})
.unwrap_or_else(|| Ok(Uint128::zero()))
.unwrap_or_else(|| Ok(UnvotedDelegatedVotingPowerResponse::default()))
// fail gracefully if the query fails
.unwrap_or_default();
.unwrap_or_default()
.effective;

// sum both to get total voting power for this address on this proposal
Ok(power.checked_add(udvp)?)
Expand Down

0 comments on commit 24313b6

Please sign in to comment.