Skip to content

Commit

Permalink
Partial governance voting support for CLI (#9293)
Browse files Browse the repository at this point in the history
Co-authored-by: Greg Nazario <[email protected]>
  • Loading branch information
xindingw and gregnazario authored Jul 28, 2023
1 parent ffdeefe commit 21b8566
Show file tree
Hide file tree
Showing 6 changed files with 608 additions and 114 deletions.
12 changes: 11 additions & 1 deletion crates/aptos/src/common/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ use aptos_telemetry::service::telemetry_is_disabled;
use aptos_types::{
account_address::create_multisig_account_address,
chain_id::ChainId,
on_chain_config::{FeatureFlag, Features},
transaction::{authenticator::AuthenticationKey, TransactionPayload},
};
use itertools::Itertools;
use move_core_types::account_address::AccountAddress;
use move_core_types::{account_address::AccountAddress, language_storage::CORE_CODE_ADDRESS};
use reqwest::Url;
use serde::{Deserialize, Serialize};
#[cfg(unix)]
Expand Down Expand Up @@ -281,6 +282,15 @@ pub async fn get_auth_key(
Ok(get_account(client, address).await?.authentication_key)
}

/// Retrieves the value of the specified feature flag from the rest client
pub async fn get_feature_flag(client: &Client, flag: FeatureFlag) -> CliTypedResult<bool> {
let features = client
.get_account_resource_bcs::<Features>(CORE_CODE_ADDRESS, "0x1::features::Features")
.await?
.into_inner();
Ok(features.is_enabled(flag))
}

/// Retrieves the chain id from the rest client
pub async fn chain_id(rest_client: &Client) -> CliTypedResult<ChainId> {
let state = rest_client
Expand Down
269 changes: 269 additions & 0 deletions crates/aptos/src/governance/delegation_pool.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
// Copyright © Aptos Foundation
// SPDX-License-Identifier: Apache-2.0

use crate::governance::{utils::*, *};
use clap::Subcommand;

/// Tool for on-chain governance from delegation pools
///
/// This tool allows voters that have stake in a delegation pool to submit proposals or vote on
/// a proposal.
#[derive(Subcommand)]
pub enum DelegationPoolTool {
Propose(SubmitProposal),
Vote(SubmitVote),
}

impl DelegationPoolTool {
pub async fn execute(self) -> CliResult {
use DelegationPoolTool::*;
match self {
Propose(tool) => tool.execute_serialized().await,
Vote(tool) => tool.execute_serialized().await,
}
}
}

/// Submit a governance proposal
///
/// You can only submit a proposal when the remaining lockup period of this delegation pool is
/// longer than a proposal duration and you have enough voting power to meet the minimum proposing
/// threshold. If you are voting with a delegation pool which hasn't enabled partial governance
/// voting yet, this command will enable it for you.
#[derive(Parser)]
pub struct SubmitProposal {
/// The address of the delegation pool to propose.
#[clap(long)]
delegation_pool_address: AccountAddress,
#[clap(flatten)]
pub(crate) args: SubmitProposalArgs,
}

#[derive(Debug, Deserialize, Serialize)]
pub struct ProposalSubmissionSummary {
proposal_id: Option<u64>,
txn_summaries: Vec<TransactionSummary>,
}

#[async_trait]
impl CliCommand<ProposalSubmissionSummary> for SubmitProposal {
fn command_name(&self) -> &'static str {
"SubmitDelegationPoolProposal"
}

async fn execute(mut self) -> CliTypedResult<ProposalSubmissionSummary> {
let mut summaries = vec![];
if let Some(txn_summary) = delegation_pool_governance_precheck(
&self.args.txn_options,
self.delegation_pool_address,
)
.await?
{
summaries.push(txn_summary);
};
// Validate the proposal metadata
let (script_hash, metadata_hash) = self.args.compile_proposals().await?;
prompt_yes_with_override(
"Do you want to submit this proposal?",
self.args.txn_options.prompt_options,
)?;

let txn: Transaction = self
.args
.txn_options
.submit_transaction(aptos_stdlib::delegation_pool_create_proposal(
self.delegation_pool_address,
script_hash.to_vec(),
self.args.metadata_url.to_string().as_bytes().to_vec(),
metadata_hash.to_hex().as_bytes().to_vec(),
self.args.is_multi_step,
))
.await?;
let proposal_id = extract_proposal_id(&txn)?;
summaries.push(TransactionSummary::from(&txn));
Ok(ProposalSubmissionSummary {
proposal_id,
txn_summaries: summaries,
})
}
}

/// Submit a vote on a proposal
///
/// Votes can only be given on proposals that are currently open for voting. You can vote
/// with `--yes` for a yes vote, and `--no` for a no vote. If you are voting with a delegation pool
/// which hasn't enabled partial governance voting yet, this command will enable it for you.
#[derive(Parser)]
pub struct SubmitVote {
/// The address of the delegation pool to vote.
#[clap(long)]
delegation_pool_address: AccountAddress,

#[clap(flatten)]
pub(crate) args: SubmitVoteArgs,
}

#[async_trait]
impl CliCommand<Vec<TransactionSummary>> for SubmitVote {
fn command_name(&self) -> &'static str {
"SubmitDelegationPoolVote"
}

async fn execute(mut self) -> CliTypedResult<Vec<TransactionSummary>> {
// The vote option is a group, so only one of yes and no must be true.
let vote = self.args.yes;
let mut summaries: Vec<TransactionSummary> = vec![];
if let Some(txn_summary) = delegation_pool_governance_precheck(
&self.args.txn_options,
self.delegation_pool_address,
)
.await?
{
summaries.push(txn_summary);
};

let client = &self
.args
.txn_options
.rest_options
.client(&self.args.txn_options.profile_options)?;
let voter_address = self.args.txn_options.profile_options.account_address()?;
let remaining_voting_power = get_remaining_voting_power(
client,
self.delegation_pool_address,
voter_address,
self.args.proposal_id,
)
.await?;
if remaining_voting_power == 0 {
return Err(CliError::CommandArgumentError(
"Voter has no voting power left on this proposal".to_string(),
));
};
let voting_power =
check_remaining_voting_power(remaining_voting_power, self.args.voting_power);
prompt_yes_with_override(
&format!(
"Vote {} with voting power = {} from stake pool {} on proposal {}?",
vote_to_string(vote),
voting_power,
self.delegation_pool_address,
self.args.proposal_id,
),
self.args.txn_options.prompt_options,
)?;
summaries.push(
self.args
.txn_options
.submit_transaction(aptos_stdlib::delegation_pool_vote(
self.delegation_pool_address,
self.args.proposal_id,
voting_power,
vote,
))
.await
.map(TransactionSummary::from)?,
);

Ok(summaries)
}
}

/// Precheck before any delegation pool governance operations. Check if feature flags are enabled.
/// Also check if partial governance voting is enabled for delegation pool. If not, send a
/// transaction to enable it.
async fn delegation_pool_governance_precheck(
txn_options: &TransactionOptions,
pool_address: AccountAddress,
) -> CliTypedResult<Option<TransactionSummary>> {
let client = &txn_options
.rest_options
.client(&txn_options.profile_options)?;
if !is_partial_governance_voting_enabled(client).await? {
return Err(CliError::CommandArgumentError(
"Partial governance voting feature flag is not enabled".to_string(),
));
};
if !is_delegation_pool_partial_governance_voting_enabled(client).await? {
return Err(CliError::CommandArgumentError(
"Delegation pool partial governance voting feature flag is not enabled".to_string(),
));
};
if is_partial_governance_voting_enabled_for_delegation_pool(client, pool_address).await? {
Ok(None)
} else {
println!("Partial governance voting for delegation pool {} hasn't been enabled yet. Enabling it now...",
pool_address);
let txn_summary = txn_options
.submit_transaction(
aptos_stdlib::delegation_pool_enable_partial_governance_voting(pool_address),
)
.await
.map(TransactionSummary::from)?;
Ok(Some(txn_summary))
}
}

async fn is_partial_governance_voting_enabled_for_delegation_pool(
client: &Client,
pool_address: AccountAddress,
) -> CliTypedResult<bool> {
let response = client
.view(
&ViewRequest {
function: "0x1::delegation_pool::partial_governance_voting_enabled"
.parse()
.unwrap(),
type_arguments: vec![],
arguments: vec![serde_json::Value::String(pool_address.to_string())],
},
None,
)
.await?;
response.inner()[0]
.as_bool()
.ok_or(CliError::UnexpectedError(
"Unexpected response from node when checking if partial governance_voting is \
enabled for delegation pool"
.to_string(),
))
}

async fn get_remaining_voting_power(
client: &Client,
pool_address: AccountAddress,
voter_address: AccountAddress,
proposal_id: u64,
) -> CliTypedResult<u64> {
let response = client
.view(
&ViewRequest {
function: "0x1::delegation_pool::calculate_and_update_remaining_voting_power"
.parse()
.unwrap(),
type_arguments: vec![],
arguments: vec![
serde_json::Value::String(pool_address.to_string()),
serde_json::Value::String(voter_address.to_string()),
serde_json::Value::String(proposal_id.to_string()),
],
},
None,
)
.await?;
let remaining_voting_power_str =
response.inner()[0]
.as_str()
.ok_or(CliError::UnexpectedError(format!(
"Unexpected response from node when getting remaining voting power of {}\
in delegation pool {}",
pool_address, voter_address
)))?;
remaining_voting_power_str.parse().map_err(|err| {
CliError::UnexpectedError(format!(
"Unexpected response from node when getting remaining voting power of {}\
in delegation pool {}: {}",
pool_address, voter_address, err
))
})
}
Loading

0 comments on commit 21b8566

Please sign in to comment.