/**
* AptosGovernance represents the on-chain governance of the Aptos network. Voting power is calculated based on the
* current epoch's voting power of the proposer or voter's backing stake pool. In addition, for it to count,
* the stake pool's lockup needs to be at least as long as the proposal's duration.
*
* It provides the following flow:
* 1. Proposers can create a proposal by calling AptosGovernance::create_proposal. The proposer's backing stake pool
* needs to have the minimum proposer stake required. Off-chain components can subscribe to CreateProposalEvent to
* track proposal creation and proposal ids.
* 2. Voters can vote on a proposal. Their voting power is derived from the backing stake pool. Each stake pool can
* only be used to vote on each proposal exactly once.
*
*/
module aptos_framework::aptos_governance {
use std::error;
use std::option;
use std::signer;
use std::string::utf8;
use aptos_std::event::{Self, EventHandle};
use aptos_std::simple_map::{Self, SimpleMap};
use aptos_std::table::{Self, Table};
use aptos_framework::account::{SignerCapability, create_signer_with_capability};
use aptos_framework::coin;
use aptos_framework::governance_proposal::{Self, GovernanceProposal};
use aptos_framework::reconfiguration;
use aptos_framework::stake;
use aptos_framework::system_addresses;
use aptos_framework::aptos_coin::AptosCoin;
use aptos_framework::timestamp;
use aptos_framework::voting;
/// Error codes.
const EINSUFFICIENT_PROPOSER_STAKE: u64 = 1;
const ENOT_DELEGATED_VOTER: u64 = 2;
const EINSUFFICIENT_STAKE_LOCKUP: u64 = 3;
const EALREADY_VOTED: u64 = 4;
const ENO_VOTING_POWER: u64 = 5;
/// Store the SignerCapabilities of accounts under the on-chain governance's control.
struct GovernanceResponsbility has key {
signer_caps: SimpleMap
,
}
/// Configurations of the AptosGovernance, set during Genesis and can be updated by the same process offered
/// by this AptosGovernance module.
struct GovernanceConfig has key {
min_voting_threshold: u128,
required_proposer_stake: u64,
voting_duration_secs: u64,
}
struct RecordKey has copy, drop, store {
stake_pool: address,
proposal_id: u64,
}
/// Records to track the proposals each stake pool has been used to vote on.
struct VotingRecords has key {
votes: Table
}
/// Events generated by interactions with the AptosGovernance module.
struct GovernanceEvents has key {
create_proposal_events: EventHandle,
update_config_events: EventHandle,
vote_events: EventHandle,
}
/// Event emitted when a proposal is created.
struct CreateProposalEvent has drop, store {
proposer: address,
stake_pool: address,
proposal_id: u64,
execution_hash: vector,
metadata_location: vector,
metadata_hash: vector,
}
/// Event emitted when there's a vote on a proposa;
struct VoteEvent has drop, store {
proposal_id: u64,
voter: address,
stake_pool: address,
num_votes: u64,
should_pass: bool,
}
/// Event emitted when the governance configs are updated.
struct UpdateConfigEvent has drop, store {
min_voting_threshold: u128,
required_proposer_stake: u64,
voting_duration_secs: u64,
}
/// Stores the signer capability for 0x1.
public fun store_signer_cap(
aptos_framework: &signer,
signer_address: address,
signer_cap: SignerCapability,
) acquires GovernanceResponsbility {
system_addresses::assert_aptos_framework(aptos_framework);
if (!exists(@aptos_framework)) {
move_to(aptos_framework, GovernanceResponsbility{ signer_caps: simple_map::create() });
};
let signer_caps = &mut borrow_global_mut(@aptos_framework).signer_caps;
simple_map::add(signer_caps, signer_address, signer_cap);
}
/// Initializes the state for Aptos Governance. Can only be called during Genesis with a signer
/// for the aptos_framework (0x1) account.
/// This function is private because it's called directly from the vm.
fun initialize(
aptos_framework: &signer,
min_voting_threshold: u128,
required_proposer_stake: u64,
voting_duration_secs: u64,
) {
system_addresses::assert_aptos_framework(aptos_framework);
voting::register(aptos_framework);
move_to(aptos_framework, GovernanceConfig {
voting_duration_secs,
min_voting_threshold,
required_proposer_stake,
});
move_to(aptos_framework, GovernanceEvents {
create_proposal_events: event::new_event_handle(aptos_framework),
update_config_events: event::new_event_handle(aptos_framework),
vote_events: event::new_event_handle(aptos_framework),
});
move_to(aptos_framework, VotingRecords {
votes: table::new(),
});
}
/// Update the governance configurations. This can only be called as part of resolving a proposal in this same
/// AptosGovernance.
public fun update_governance_config(
_proposal: GovernanceProposal,
min_voting_threshold: u128,
required_proposer_stake: u64,
voting_duration_secs: u64,
) acquires GovernanceConfig, GovernanceEvents {
let governance_config = borrow_global_mut(@aptos_framework);
governance_config.voting_duration_secs = voting_duration_secs;
governance_config.min_voting_threshold = min_voting_threshold;
governance_config.required_proposer_stake = required_proposer_stake;
let events = borrow_global_mut(@aptos_framework);
event::emit_event(
&mut events.update_config_events,
UpdateConfigEvent {
min_voting_threshold,
required_proposer_stake,
voting_duration_secs
},
);
}
/// Create a proposal with the backing `stake_pool`.
/// @param execution_hash Required. This is the hash of the resolution script. When the proposal is resolved,
/// only the exact script with matching hash can be successfully executed.
public entry fun create_proposal(
proposer: &signer,
stake_pool: address,
execution_hash: vector,
metadata_location: vector,
metadata_hash: vector,
) acquires GovernanceConfig, GovernanceEvents {
let proposer_address = signer::address_of(proposer);
assert!(stake::get_delegated_voter(stake_pool) == proposer_address, error::invalid_argument(ENOT_DELEGATED_VOTER));
// The proposer's stake needs to be at least the required bond amount.
let governance_config = borrow_global(@aptos_framework);
let stake_balance = stake::get_current_epoch_voting_power(stake_pool);
assert!(
stake_balance >= governance_config.required_proposer_stake,
error::invalid_argument(EINSUFFICIENT_PROPOSER_STAKE),
);
// The proposer's stake needs to be locked up at least as long as the proposal's voting period.
let current_time = timestamp::now_seconds();
let proposal_expiration = current_time + governance_config.voting_duration_secs;
assert!(
stake::get_lockup_secs(stake_pool) >= proposal_expiration,
error::invalid_argument(EINSUFFICIENT_STAKE_LOCKUP),
);
// We want to allow early resolution of proposals if more than 50% of the total supply of the network coins
// has voted. This doesn't take into subsequent inflation/deflation (rewards are issued every epoch and gas fees
// are burnt after every transaction), but inflation/delation is very unlikely to have a major impact on total
// supply during the voting period.
let total_voting_token_supply = coin::supply();
let early_resolution_vote_threshold = option::none();
if (option::is_some(&total_voting_token_supply)) {
let total_supply = *option::borrow(&total_voting_token_supply);
// 50% + 1 to avoid rounding errors.
early_resolution_vote_threshold = option::some(total_supply / 2 + 1);
};
let proposal_id = voting::create_proposal(
proposer_address,
@aptos_framework,
governance_proposal::create_proposal(
utf8(metadata_location),
utf8(metadata_hash),
),
execution_hash,
governance_config.min_voting_threshold,
proposal_expiration,
early_resolution_vote_threshold,
);
let events = borrow_global_mut(@aptos_framework);
event::emit_event(
&mut events.create_proposal_events,
CreateProposalEvent {
proposal_id,
proposer: proposer_address,
stake_pool,
execution_hash,
metadata_location,
metadata_hash,
},
);
}
/// Vote on proposal with `proposal_id` and voting power from `stake_pool`.
public entry fun vote(
voter: &signer,
stake_pool: address,
proposal_id: u64,
should_pass: bool,
) acquires GovernanceEvents, VotingRecords {
let voter_address = signer::address_of(voter);
assert!(stake::get_delegated_voter(stake_pool) == voter_address, error::invalid_argument(ENOT_DELEGATED_VOTER));
// Ensure the voter doesn't double vote with the same stake pool.
let voting_records = borrow_global_mut(@aptos_framework);
let record_key = RecordKey {
stake_pool,
proposal_id,
};
assert!(
!table::contains(&voting_records.votes, record_key),
error::invalid_argument(EALREADY_VOTED));
table::add(&mut voting_records.votes, record_key, true);
// Voting power does not include pending_active or pending_inactive balances.
// In general, the stake pool should not have pending_inactive balance if it still has lockup (required to vote)
// And if pending_active will be added to active in the next epoch.
let voting_power = stake::get_current_epoch_voting_power(stake_pool);
// Short-circuit if the voter has no voting power.
assert!(voting_power > 0, error::invalid_argument(ENO_VOTING_POWER));
// The voter's stake needs to be locked up at least as long as the proposal's expiration.
let proposal_expiration = voting::get_proposal_expiration_secs(@aptos_framework, proposal_id);
assert!(
stake::get_lockup_secs(stake_pool) >= proposal_expiration,
error::invalid_argument(EINSUFFICIENT_STAKE_LOCKUP),
);
voting::vote(
&governance_proposal::create_empty_proposal(),
@aptos_framework,
proposal_id,
voting_power,
should_pass,
);
let events = borrow_global_mut(@aptos_framework);
event::emit_event(
&mut events.vote_events,
VoteEvent {
proposal_id,
voter: voter_address,
stake_pool,
num_votes: voting_power,
should_pass,
},
);
}
/// Return a signer for making changes to 0x1 as part of on-chain governance proposal process.
public fun get_signer(_proposal: GovernanceProposal, signer_address: address): signer acquires GovernanceResponsbility {
let governance_responsibility = borrow_global(@aptos_framework);
let signer_cap = simple_map::borrow(&governance_responsibility.signer_caps, &signer_address);
create_signer_with_capability(signer_cap)
}
/// Force reconfigure. To be called at the end of a proposal that alters on-chain configs.
public fun reconfigure(_proposal: &GovernanceProposal) {
reconfiguration::reconfigure();
}
#[test(core_resources = @core_resources, aptos_framework = @aptos_framework, proposer = @0x123, yes_voter = @0x234, no_voter = @345)]
public entry fun test_voting(
core_resources: signer,
aptos_framework: signer,
proposer: signer,
yes_voter: signer,
no_voter: signer,
) acquires GovernanceConfig, GovernanceEvents, VotingRecords {
setup_voting(
&core_resources,
&aptos_framework,
&proposer,
&yes_voter,
&no_voter,
);
create_proposal(
&proposer,
signer::address_of(&proposer),
b"123",
b"",
b"",
);
vote(&yes_voter, signer::address_of(&yes_voter), 0, true);
vote(&no_voter, signer::address_of(&no_voter), 0, false);
// Once expiration time has passed, the proposal should be considered resolve now as there are more yes votes
// than no.
timestamp::update_global_time_for_test(100001000000);
let proposal_state = voting::get_proposal_state(signer::address_of(&aptos_framework), 0);
assert!(proposal_state == 1, proposal_state);
}
#[test(core_resources = @core_resources, aptos_framework = @aptos_framework, proposer = @0x123, voter_1 = @0x234, voter_2 = @345)]
#[expected_failure(abort_code = 0x10004)]
public entry fun test_cannot_double_vote(
core_resources: signer,
aptos_framework: signer,
proposer: signer,
voter_1: signer,
voter_2: signer,
) acquires GovernanceConfig, GovernanceEvents, VotingRecords {
setup_voting(
&core_resources,
&aptos_framework,
&proposer,
&voter_1,
&voter_2,
);
create_proposal(
&proposer,
signer::address_of(&proposer),
b"",
b"",
b"",
);
// Double voting should throw an error.
vote(&voter_1, signer::address_of(&voter_1), 0, true);
vote(&voter_1, signer::address_of(&voter_1), 0, true);
}
#[test(core_resources = @core_resources, aptos_framework = @aptos_framework, proposer = @0x123, voter_1 = @0x234, voter_2 = @345)]
#[expected_failure(abort_code = 0x10004)]
public entry fun test_cannot_double_vote_with_different_voter_addresses(
core_resources: signer,
aptos_framework: signer,
proposer: signer,
voter_1: signer,
voter_2: signer,
) acquires GovernanceConfig, GovernanceEvents, VotingRecords {
setup_voting(
&core_resources,
&aptos_framework,
&proposer,
&voter_1,
&voter_2,
);
create_proposal(
&proposer,
signer::address_of(&proposer),
b"",
b"",
b"",
);
// Double voting should throw an error for 2 different voters if they still use the same stake pool.
vote(&voter_1, signer::address_of(&voter_1), 0, true);
stake::set_delegated_voter(&voter_1, signer::address_of(&voter_2));
vote(&voter_2, signer::address_of(&voter_1), 0, true);
}
#[test_only]
fun setup_voting(
core_resources: &signer,
aptos_framework: &signer,
proposer: &signer,
yes_voter: &signer,
no_voter: &signer,
) {
use std::vector;
use aptos_framework::coin;
use aptos_framework::aptos_coin::{Self, AptosCoin};
timestamp::set_time_has_started_for_testing(aptos_framework);
// Initialize the governance.
initialize(aptos_framework, 10, 100, 1000);
// Initialize the stake pools for proposer and voters.
let active_validators = vector::empty();
vector::push_back(&mut active_validators, signer::address_of(proposer));
vector::push_back(&mut active_validators, signer::address_of(yes_voter));
vector::push_back(&mut active_validators, signer::address_of(no_voter));
stake::create_validator_set(aptos_framework, active_validators);
let (mint_cap, burn_cap) = aptos_coin::initialize(aptos_framework, core_resources);
// Spread stake among active and pending_inactive because both need to be accounted for when computing voting
// power.
stake::create_stake_pool(proposer, coin::mint(50, &mint_cap), coin::mint(50, &mint_cap), 10000);
stake::create_stake_pool(yes_voter, coin::mint(10, &mint_cap), coin::mint(10, &mint_cap), 10000);
stake::create_stake_pool(no_voter, coin::mint(5, &mint_cap), coin::mint(5, &mint_cap), 10000);
coin::destroy_mint_cap(mint_cap);
coin::destroy_burn_cap(burn_cap);
}
}