diff --git a/.changelog/unreleased/SDK/2128-governance-abstain-vote.md b/.changelog/unreleased/SDK/2128-governance-abstain-vote.md new file mode 100644 index 00000000000..64d93375e7d --- /dev/null +++ b/.changelog/unreleased/SDK/2128-governance-abstain-vote.md @@ -0,0 +1,2 @@ +- Updated the `LedgerProposalVote` display method to account for the new + `Abstain` vote variant. ([\#2128](https://github.com/anoma/namada/pull/2128)) \ No newline at end of file diff --git a/.changelog/unreleased/features/2128-governance-abstain-vote.md b/.changelog/unreleased/features/2128-governance-abstain-vote.md new file mode 100644 index 00000000000..58f21aa36dd --- /dev/null +++ b/.changelog/unreleased/features/2128-governance-abstain-vote.md @@ -0,0 +1,2 @@ +- Added the option to abstain from voting a governance proposal. + ([\#2128](https://github.com/anoma/namada/pull/2128)) \ No newline at end of file diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index 0231e3731dd..34bac93d4cf 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -1226,7 +1226,7 @@ pub async fn query_proposal_result<'a>( let proposal_result = compute_proposal_result( proposal_votes, total_voting_power, - TallyType::TwoThird, + TallyType::TwoThirds, ); display_line!( diff --git a/apps/src/lib/node/ledger/shell/governance.rs b/apps/src/lib/node/ledger/shell/governance.rs index 8f2d3a9b4c7..9cad57168ea 100644 --- a/apps/src/lib/node/ledger/shell/governance.rs +++ b/apps/src/lib/node/ledger/shell/governance.rs @@ -147,8 +147,7 @@ where } TallyResult::Rejected => { if let ProposalType::PGFPayment(_) = proposal_type { - let two_third_nay = proposal_result.two_third_nay(); - if two_third_nay { + if proposal_result.two_thirds_nay_over_two_thirds_total() { pgf::remove_steward( &mut shell.wl_storage, &proposal_author, @@ -156,7 +155,9 @@ where tracing::info!( "Governance proposal {} was rejected with 2/3 of \ - nay votes. Removing {} from stewards set.", + nay votes over 2/3 of the total voting power. If \ + {} is a steward, it's being removed from the \ + stewards set.", id, proposal_author ); diff --git a/core/src/ledger/governance/cli/offline.rs b/core/src/ledger/governance/cli/offline.rs index 64b13e29db5..ccf06a0b984 100644 --- a/core/src/ledger/governance/cli/offline.rs +++ b/core/src/ledger/governance/cli/offline.rs @@ -241,6 +241,21 @@ impl OfflineVote { self.vote.is_yay() } + /// Check if the vote is nay + pub fn is_nay(&self) -> bool { + self.vote.is_nay() + } + + /// Check if the vote is abstain + pub fn is_abstain(&self) -> bool { + self.vote.is_abstain() + } + + /// Check if two votes are equal + pub fn is_same_side(&self, other: &Self) -> bool { + self.vote.is_same_side(&other.vote) + } + /// compute the hash of a proposal pub fn compute_hash(&self) -> Hash { let proposal_hash_data = self.proposal_hash.serialize_to_vec(); diff --git a/core/src/ledger/governance/cli/onchain.rs b/core/src/ledger/governance/cli/onchain.rs index 53d3377de20..b47985af3f3 100644 --- a/core/src/ledger/governance/cli/onchain.rs +++ b/core/src/ledger/governance/cli/onchain.rs @@ -313,7 +313,7 @@ pub struct PgfFundingTarget { pub address: Address, } -/// Rappresent an proposal vote +/// Represent an proposal vote #[derive( Debug, Clone, @@ -324,12 +324,12 @@ pub struct PgfFundingTarget { PartialEq, )] pub enum ProposalVote { - /// Rappresent an yay proposal vote + /// Represent an yay proposal vote Yay, - /// Rappresent an nay proposal vote + /// Represent an nay proposal vote Nay, - /// Rappresent an invalid proposal vote - Invalid, + /// Represent an abstain proposal vote + Abstain, } impl TryFrom for ProposalVote { @@ -339,14 +339,30 @@ impl TryFrom for ProposalVote { match value.trim().to_lowercase().as_str() { "yay" => Ok(ProposalVote::Yay), "nay" => Ok(ProposalVote::Nay), + "abstain" => Ok(ProposalVote::Abstain), _ => Err("invalid vote".to_string()), } } } impl ProposalVote { - /// Check if the proposal type is yay + /// Check if the vote type is yay pub fn is_yay(&self) -> bool { matches!(self, ProposalVote::Yay) } + + /// Check if the vote type is nay + pub fn is_nay(&self) -> bool { + matches!(self, ProposalVote::Nay) + } + + /// Check if the vote type is abstain + pub fn is_abstain(&self) -> bool { + matches!(self, ProposalVote::Abstain) + } + + /// Check if two votes are equal + pub fn is_same_side(&self, other: &Self) -> bool { + std::mem::discriminant(self) == std::mem::discriminant(other) + } } diff --git a/core/src/ledger/governance/storage/vote.rs b/core/src/ledger/governance/storage/vote.rs index 3ba8ec2ae25..780d4fdf2ec 100644 --- a/core/src/ledger/governance/storage/vote.rs +++ b/core/src/ledger/governance/storage/vote.rs @@ -42,6 +42,8 @@ pub enum StorageProposalVote { Yay(VoteType), /// No Nay, + /// Abstain + Abstain, } impl StorageProposalVote { @@ -50,6 +52,21 @@ impl StorageProposalVote { matches!(self, StorageProposalVote::Yay(_)) } + /// Check if a vote is nay + pub fn is_nay(&self) -> bool { + matches!(self, StorageProposalVote::Nay) + } + + /// Check if a vote is abstain + pub fn is_abstain(&self) -> bool { + matches!(self, StorageProposalVote::Abstain) + } + + /// Check if two votes are equal + pub fn is_same_side(&self, other: &Self) -> bool { + std::mem::discriminant(self) == std::mem::discriminant(other) + } + /// Check if vote is of type default pub fn is_default_vote(&self) -> bool { matches!( @@ -64,6 +81,7 @@ impl StorageProposalVote { match self { StorageProposalVote::Yay(vote_type) => proposal_type.eq(vote_type), StorageProposalVote::Nay => true, + StorageProposalVote::Abstain => true, } } @@ -106,6 +124,7 @@ impl Display for StorageProposalVote { }, StorageProposalVote::Nay => write!(f, "nay"), + StorageProposalVote::Abstain => write!(f, "abstain"), } } } diff --git a/core/src/ledger/governance/utils.rs b/core/src/ledger/governance/utils.rs index 33f032def1a..6b3129b588d 100644 --- a/core/src/ledger/governance/utils.rs +++ b/core/src/ledger/governance/utils.rs @@ -51,25 +51,31 @@ impl Vote { } } -/// Rappresent a tally type +/// Represent a tally type pub enum TallyType { - /// Rappresent a tally type for proposal requiring 2/3 of the votes - TwoThird, - /// Rappresent a tally type for proposal requiring 1/3 of the votes - OneThird, - /// Rappresent a tally type for proposal requiring less than 1/3 of the - /// votes to be nay - LessOneThirdNay, + /// Represent a tally type for proposal requiring 2/3 of the total voting + /// power to be yay + TwoThirds, + /// Represent a tally type for proposal requiring 1/2 of yay votes over at + /// least 1/3 of the voting power + OneHalfOverOneThird, + /// Represent a tally type for proposal requiring less than 1/2 of nay + /// votes over at least 1/3 of the voting power + LessOneHalfOverOneThirdNay, } impl TallyType { /// Compute the type of tally for a proposal pub fn from(proposal_type: ProposalType, is_steward: bool) -> Self { match (proposal_type, is_steward) { - (ProposalType::Default(_), _) => TallyType::TwoThird, - (ProposalType::PGFSteward(_), _) => TallyType::TwoThird, - (ProposalType::PGFPayment(_), true) => TallyType::LessOneThirdNay, - (ProposalType::PGFPayment(_), false) => TallyType::OneThird, + (ProposalType::Default(_), _) => TallyType::TwoThirds, + (ProposalType::PGFSteward(_), _) => TallyType::OneHalfOverOneThird, + (ProposalType::PGFPayment(_), true) => { + TallyType::LessOneHalfOverOneThirdNay + } + (ProposalType::PGFPayment(_), false) => { + TallyType::OneHalfOverOneThird + } } } } @@ -98,27 +104,32 @@ impl TallyResult { tally_type: &TallyType, yay_voting_power: VotePower, nay_voting_power: VotePower, + abstain_voting_power: VotePower, total_voting_power: VotePower, ) -> Self { let passed = match tally_type { - TallyType::TwoThird => { - let at_least_two_third_voted = yay_voting_power - + nay_voting_power - >= total_voting_power / 3 * 2; - let at_last_half_voted_yay = - yay_voting_power > nay_voting_power; - at_least_two_third_voted && at_last_half_voted_yay + TallyType::TwoThirds => { + yay_voting_power >= total_voting_power * 2 / 3 } - TallyType::OneThird => { - let at_least_two_third_voted = yay_voting_power - + nay_voting_power - >= total_voting_power / 3; + TallyType::OneHalfOverOneThird => { + let at_least_one_third_voted = + yay_voting_power + nay_voting_power + abstain_voting_power + >= total_voting_power / 3; + + // At least half of non-abstained votes are yay let at_last_half_voted_yay = - yay_voting_power > nay_voting_power; - at_least_two_third_voted && at_last_half_voted_yay + yay_voting_power >= nay_voting_power; + at_least_one_third_voted && at_last_half_voted_yay } - TallyType::LessOneThirdNay => { - nay_voting_power <= total_voting_power / 3 + TallyType::LessOneHalfOverOneThirdNay => { + let less_one_third_voted = + yay_voting_power + nay_voting_power + abstain_voting_power + < total_voting_power / 3; + + // More than half of non-abstained votes are yay + let more_than_half_voted_yay = + yay_voting_power > nay_voting_power; + less_one_third_voted || more_than_half_voted_yay } }; @@ -135,8 +146,10 @@ pub struct ProposalResult { pub total_voting_power: VotePower, /// The total voting power from yay votes pub total_yay_power: VotePower, - /// The total voting power from nay votes (unused at the moment) + /// The total voting power from nay votes pub total_nay_power: VotePower, + /// The total voting power from abstained votes + pub total_abstain_power: VotePower, } impl Display for ProposalResult { @@ -161,9 +174,18 @@ impl Display for ProposalResult { } impl ProposalResult { - /// Return true if two third of total voting power voted nay - pub fn two_third_nay(&self) -> bool { - self.total_nay_power >= (self.total_voting_power / 3) * 2 + /// Return true if at least 1/3 of the total voting power voted and at least + /// two third of the non-abstained voting power voted nay + pub fn two_thirds_nay_over_two_thirds_total(&self) -> bool { + let at_least_two_thirds_voted = self.total_yay_power + + self.total_nay_power + + self.total_abstain_power + >= self.total_voting_power * 2 / 3; + + let at_least_two_thirds_nay = self.total_nay_power + >= (self.total_nay_power + self.total_yay_power) * 2 / 3; + + at_least_two_thirds_voted && at_least_two_thirds_nay } } @@ -196,12 +218,37 @@ impl TallyVote { } } - /// Check if two votes are equal - pub fn is_same_side(&self, other: &TallyVote) -> bool { - let both_yay = self.is_yay() && other.is_yay(); - let both_nay = !self.is_yay() && !other.is_yay(); + /// Check if a vote is nay + pub fn is_nay(&self) -> bool { + match self { + TallyVote::OnChain(vote) => vote.is_nay(), + TallyVote::Offline(vote) => vote.is_nay(), + } + } + + /// Check if a vote is abstain + pub fn is_abstain(&self) -> bool { + match self { + TallyVote::OnChain(vote) => vote.is_abstain(), + TallyVote::Offline(vote) => vote.is_abstain(), + } + } - both_yay || !both_nay + /// Check if two votes are equal, returns an error if the variants of the + /// two instances are different + pub fn is_same_side( + &self, + other: &TallyVote, + ) -> Result { + match (self, other) { + (TallyVote::OnChain(vote), TallyVote::OnChain(other_vote)) => { + Ok(vote.is_same_side(other_vote)) + } + (TallyVote::Offline(vote), TallyVote::Offline(other_vote)) => { + Ok(vote.is_same_side(other_vote)) + } + _ => Err("Cannot compare different variants of governance votes"), + } } } @@ -226,39 +273,80 @@ pub fn compute_proposal_result( ) -> ProposalResult { let mut yay_voting_power = VotePower::default(); let mut nay_voting_power = VotePower::default(); + let mut abstain_voting_power = VotePower::default(); for (address, vote_power) in votes.validator_voting_power { let vote_type = votes.validators_vote.get(&address); if let Some(vote) = vote_type { if vote.is_yay() { yay_voting_power += vote_power; - } else { + } else if vote.is_nay() { nay_voting_power += vote_power; + } else if vote.is_abstain() { + abstain_voting_power += vote_power; } } } - for (delegator, degalations) in votes.delegator_voting_power { + for (delegator, delegations) in votes.delegator_voting_power { let delegator_vote = match votes.delegators_vote.get(&delegator) { Some(vote) => vote, None => continue, }; - for (validator, voting_power) in degalations { + for (validator, voting_power) in delegations { let validator_vote = votes.validators_vote.get(&validator); if let Some(validator_vote) = validator_vote { - if !validator_vote.is_same_side(delegator_vote) { + let validator_vote_is_same_side = + match validator_vote.is_same_side(delegator_vote) { + Ok(result) => result, + Err(_) => { + // Unexpected path, all the votes should be + // validated by the VP and only online votes should + // be allowed in storage + tracing::warn!( + "Found unexpected offline vote type: forcing \ + the proposal to fail." + ); + // Force failure of the proposal + return ProposalResult { + result: TallyResult::Rejected, + total_voting_power: VotePower::default(), + total_yay_power: VotePower::default(), + total_nay_power: VotePower::default(), + total_abstain_power: VotePower::default(), + }; + } + }; + if !validator_vote_is_same_side { if delegator_vote.is_yay() { yay_voting_power += voting_power; - nay_voting_power -= voting_power; - } else { + if validator_vote.is_nay() { + nay_voting_power -= voting_power; + } else if validator_vote.is_abstain() { + abstain_voting_power -= voting_power; + } + } else if delegator_vote.is_nay() { nay_voting_power += voting_power; - yay_voting_power -= voting_power; + if validator_vote.is_yay() { + yay_voting_power -= voting_power; + } else if validator_vote.is_abstain() { + abstain_voting_power -= voting_power; + } + } else if delegator_vote.is_abstain() { + abstain_voting_power += voting_power; + if validator_vote.is_yay() { + yay_voting_power -= voting_power; + } else if validator_vote.is_nay() { + nay_voting_power -= voting_power; + } } } } else if delegator_vote.is_yay() { yay_voting_power += voting_power; - } else { + } else if delegator_vote.is_nay() { nay_voting_power += voting_power; + } else if delegator_vote.is_abstain() { + abstain_voting_power += voting_power; } } } @@ -267,6 +355,7 @@ pub fn compute_proposal_result( &tally_at, yay_voting_power, nay_voting_power, + abstain_voting_power, total_voting_power, ); @@ -275,6 +364,7 @@ pub fn compute_proposal_result( total_voting_power, total_yay_power: yay_voting_power, total_nay_power: nay_voting_power, + total_abstain_power: abstain_voting_power, } } diff --git a/sdk/src/signing.rs b/sdk/src/signing.rs index 381df346347..988eee71e96 100644 --- a/sdk/src/signing.rs +++ b/sdk/src/signing.rs @@ -861,6 +861,7 @@ impl<'a> Display for LedgerProposalVote<'a> { }, StorageProposalVote::Nay => write!(f, "nay"), + StorageProposalVote::Abstain => write!(f, "abstain"), } } } diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 809feb334d4..a631bb88ca4 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -2095,7 +2095,7 @@ fn proposal_submission() -> Result<()> { let mut client = run!(test, Bin::Client, query_proposal, Some(15))?; client.exp_string("Proposal Id: 0")?; client.exp_string( - "passed with 120900.000000 yay votes and 0.000000 nay votes (0.%)", + "passed with 120000.000000 yay votes and 900.000000 nay votes (0.%)", )?; client.assert_success();