On chain governance

On-chain governance

Governance Address

All on-chain governance mechanisms are handled under a single address, referred to as the GovernanceAddress. The GovernanceAddress is created during genesis, and handles the verification of submitted proposals, the tallying of votes, and the execution of proposals. This address also stores all previous proposals under its address space.

Proposals are submitted through the client, and are verified by the GovernanceAddress before being added to the pending proposals list.

The structure of proposals is outlined here.

The correct logic to handle these different types is hardcoded in protocol. We'll also rely on type checking to strictly enforce the correctness of a proposal given its type. These two approaches combined will prevent a user from deviating from the intended logic for a certain proposal type (e.g. providing a wasm code when it's not needed or allowing only validators to vote when also delegators should, etc...). More details on the specific types supported can be found in the relative section of the proposals page.

GovernanceAddress VP

The GovernanceAddress validity predicate (VP) task is to check the integrity and correctness of new proposals.

Submission validation

A proposal must satisfy the following mandatory storage writes:

  • counter - The number of proposals submitted so far
  • author - The address of the author of the proposal
  • type - The proposal type
  • funds - The amount of funds locked for this proposal
  • voting_start - The epoch specifying when the voting period starts
  • voting_end- The epoch specifying when the voting period ends
  • grace_epoch - The epoch specifying when the proposal becomes active, (and attached WASM code is executed if any), given that the proposal has a positive outcome.

Further, it must check that the proposal satisfies the following constraints:

  • The attached funds is >= min_proposal_fund, a protocol parameter
  • The id of the proposal is unique
  • The attached ProposalType is supported by the protocol
  • The difference between StartEpoch and EndEpoch is >= min_proposal_period
  • There is an attached description of the proposal with character length < max_proposal_content_size
  • The difference between the voting_end and voting_start epoch must be divisible by 3, i.e (EndEpoch - StartEpoch) % 3 == 0.
  • The difference between grace_epoch and voting_end is of at least min_proposal_grace_epochs, a protocol parameter. The reason for this constraint is explained below.

Voting validation

Once a proposal has been accepted by the protocol as valid, it will be added to the pending proposals list, and delegators and delegates will be able to vote on it. The VP must also check that voting adheres to the following constraints:

  • The voter is a delegator or a delegate (further constraints can be applied depending on the proposal type)
  • Given that non-validating accounts can vote, validators may only vote during the initial 23\frac{2}{3} of the whole proposal duration (voting_end - voting_start)

Once a proposal has been created, nobody can modify any of its fields.

Execution of WASM code

The VP is also responsible for handling the execution of WASM code attached to a DefaultProposal proposal type.

Examples of such code execution could be:

  • storage writes to change some protocol parameter
  • storage writes to restore a slash

This means that corresponding VPs will also be invoked.

Proposal transaction

The on-chain proposal transaction will have the following structure, where author address will be address of the account submitting the proposal, and hence the address which receives any deposited funds.

struct Proposal {
    id: u64,
    content: Vec<u8>,
    author: Address,
    r#type: ProposalType, // This is an enum, in which Wasm code is embedded for DefaultProposal
    votingStartEpoch: Epoch,
    votingEndEpoch: Epoch,
    graceEpoch: Epoch,
}

The optional proposal wasm code will be embedded inside the ProposalType enum variants to better perform validation through type checking.

Vote transaction

Vote transactions have the following structure:

struct OnChainVote {
    id: u64,
    voter: Address,
    yay: ProposalVote,
}

where ProposalVote is an enum representing a Yay, Nay, or Abstain vote: the yay variant also contains the specific memo (if any) required for that proposal.

Storage writes for a vote transaction are described here.

If non-validating accounts are allowed to vote, delegates will be able to vote only for 23\frac{2}{3} of the total voting period, while delegators can vote until the end of the voting period. If only validators are allowed to vote for the ProposalType in question, they are allowed to vote for the entire voting window.

If a delegator votes differently to its delegate, the delegate's vote will be overridden (e.g. if a delegator has a voting power of 200 and votes opposite to the delegate, then 200 will be subtracted from the voting power of the involved delegate).

Tallying votes

Proposals are tallied at the start of their grace_epoch during the finalize_block function. The tallying is based off of the votes collected at the end of the voting_end epoch. If the threshold specified by the ProposalType is reached, the proposal will be considered successful.

There are two types of thresholds:

  • Fraction of total staked NAM that voted - This checks whether enough staked NAM voted for the proposal at all.
  • Fraction of votes in favor of the proposal - This checks whether enough votes (weighted by their staked NAM) voted in favor of the proposal, out of the staked NAM that did vote.

E.g if the thresholds, respectively, are 12\frac{1}{2} and 13\frac{1}{3}, then at least 50% of the total staked NAM must have voted for the proposal AND out of this NAM, to be accepted.

Tallying is computed with the following rules:

  1. Sum all the voting power of delegates that voted Yay, call this sum SumYay
  2. Sum all the voting power of delegates that voted Nay, call this sum SumNay
  3. Sum all the voting power of delegates that voted Abstain, call this sum SumAbstain
  4. Check if the sum SumYay + SumNay + SumAbstain meets the first threshold, if not, the proposal outcome is negative
  5. For any delegate that voted Yay, subtract the voting power of any delegator that voted other than Yay from SumYay
  6. For any delegate that voted Nay, subtract the voting power of any delegator that voted other than Nay from SumNay
  7. For any delegate that voted Abstain, subtract the voting power of any delegator that voted other than Abstain from SumAbstain
  8. Add voting power for any delegation that voted Yay (whose corresponding delegate didn't vote Yay) to SumYay
  9. Add voting power for any delegation that voted Nay (whose corresponding delegate didn't vote Nay) to SumNay
  10. Add voting power for any delegation that voted Abstain (whose corresponding delegate didn't vote Abstain) to SumAbstain
  11. Set SumYeaOrNay to SumYay + SumNay
  12. Decide whether or not the proposal succeeds based on the proposal-type-specific tally instructions (see Proposal)

All the computation is done at the start of grace_epoch on data collected at the epoch specified in the voting-end field of the proposal.

Refund and Proposal Execution mechanism

In parallel to tallying, the protocol manages the execution of accepted proposals and refunding in the finalize_block function. If the proposals grace_epoch matches with the current epoch, AND the proposal had a positive outcome, the protocol refunds the locked funds from GovernanceAddress to the proposal author address (specified in the proposal author field).
Moreover, if the proposal had a positive outcome and had attached WASM code, the code is executed immediately.

On the other hand, should the proposal be rejected (negative outcome), any locked funds is burnt (removed from total supply).

The result is then signaled by creating and inserting a CometBFT Event (opens in a new tab).