State

Proposals

Proposal objects are used to tally votes and generally track the proposal's state. They contain an array of arbitrary sdk.Msg's which the governance module will attempt to resolve and then execute if the proposal passes. Proposal's are identified by a unique id and contains a series of timestamps: submit_time, deposit_end_time, voting_start_time, voting_end_time which track the lifecycle of a proposal

// Proposal defines the core field members of a governance proposal.
message Proposal {
  uint64   id                           = 1;
  repeated google.protobuf.Any messages = 2;
  ProposalStatus               status   = 3;
  // final_tally_result is the final tally result of the proposal. When
  // querying a proposal via gRPC, this field is not populated until the
  // proposal's voting period has ended.
  TallyResult               final_tally_result        = 4;
  google.protobuf.Timestamp submit_time               = 5 [(gogoproto.stdtime) = true];
  google.protobuf.Timestamp deposit_end_time          = 6 [(gogoproto.stdtime) = true];
  repeated cosmos.base.v1beta1.Coin total_deposit     = 7 [(gogoproto.nullable) = false];
  google.protobuf.Timestamp         voting_start_time = 8 [(gogoproto.stdtime) = true];
  google.protobuf.Timestamp         voting_end_time   = 9 [(gogoproto.stdtime) = true];

  // metadata is any arbitrary metadata attached to the proposal.
  string metadata = 10;
}

A proposal will generally require more than just a set of messages to explain its purpose but need some greater justification and allow a means for interested participants to discuss and debate the proposal. In most cases, it is encouraged to have an off-chain system that supports the on-chain governance process.

To accommodate for this, a proposal contains a special metadata field, a string, which can be used to add context to the proposal. The metadata field allows custom use for networks, however, it is expected that the field contains a URL or some form of CID using a system such as IPFS. To support the case of interoperability across networks, the SDK recommends that the metadata represents the following JSON template:

{
  "title": "...",
  "description": "...",
  "forum": "...", // a link to the discussion platform (i.e. Discord)
  "other": "..." // any extra data that doesn't correspond to the other fields
}

This makes it far easier for clients to support multiple networks.

The metadata has a maximum length that is chosen by the app developer, and passed into the gov keeper as a config. The default maximum length in the SDK is 255 characters.

Writing a module that uses governance

There are many aspects of a chain, or of the individual modules that you may want to use governance to perform such as changing various parameters. This is very simple to do. First, write out your message types and MsgServer implementation.

Add an authority field to the keeper which will be populated in the constructor with the governance module account: govKeeper.GetGovernanceAccount().GetAddress(). Then for the methods in the msg_server.go, perform a check on the message that the signer matches authority. This will prevent any user from executing that message.

Parameters and base types

Parameters define the rules according to which votes are run. There can only be one active parameter set at any given time. If governance wants to change a parameter set, either to modify a value or add/remove a parameter field, a new parameter set has to be created and the previous one rendered inactive.

DepositParams

// DepositParams defines the params for deposits on governance proposals.
message DepositParams {
  //  Minimum deposit for a proposal to enter voting period.
  repeated cosmos.base.v1beta1.Coin min_deposit = 1
      [(gogoproto.nullable) = false, (gogoproto.jsontag) = "min_deposit,omitempty"];

  //  Maximum period for Atom holders to deposit on a proposal. Initial value: 2
  //  months.
  google.protobuf.Duration max_deposit_period = 2
      [(gogoproto.stdduration) = true, (gogoproto.jsontag) = "max_deposit_period,omitempty"];
}

VotingParams

// VotingParams defines the params for voting on governance proposals.
message VotingParams {
  //  Length of the voting period.
  google.protobuf.Duration voting_period = 1 [(gogoproto.stdduration) = true];
}

TallyParams

// TallyParams defines the params for tallying votes on governance proposals.
message TallyParams {
  //  Minimum percentage of total stake needed to vote for a result to be
  //  considered valid.
  string quorum = 1 [(cosmos_proto.scalar) = "cosmos.Dec", (gogoproto.jsontag) = "quorum,omitempty"];

  //  Minimum proportion of Yes votes for proposal to pass. Default value: 0.5.
  string threshold = 2 [(cosmos_proto.scalar) = "cosmos.Dec", (gogoproto.jsontag) = "threshold,omitempty"];

  //  Minimum value of Veto votes to Total votes ratio for proposal to be
  //  vetoed. Default value: 1/3.
  string veto_threshold = 3 [(cosmos_proto.scalar) = "cosmos.Dec", (gogoproto.jsontag) = "veto_threshold,omitempty"];
}

Parameters are stored in a global GlobalParams KVStore.

Additionally, we introduce some basic types:

type Vote byte

const (
    VoteYes         = 0x1
    VoteNo          = 0x2
    VoteNoWithVeto  = 0x3
    VoteAbstain     = 0x4
)

type ProposalType  string

const (
    ProposalTypePlainText       = "Text"
    ProposalTypeSoftwareUpgrade = "SoftwareUpgrade"
)

type ProposalStatus byte


const (
    StatusNil           ProposalStatus = 0x00
    StatusDepositPeriod ProposalStatus = 0x01  // Proposal is submitted. Participants can deposit on it but not vote
    StatusVotingPeriod  ProposalStatus = 0x02  // MinDeposit is reached, participants can vote
    StatusPassed        ProposalStatus = 0x03  // Proposal passed and successfully executed
    StatusRejected      ProposalStatus = 0x04  // Proposal has been rejected
    StatusFailed        ProposalStatus = 0x05  // Proposal passed but failed execution
)

Deposit

// Deposit defines an amount deposited by an account address to an active
// proposal.
message Deposit {
  uint64   proposal_id                     = 1;
  string   depositor                       = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"];
  repeated cosmos.base.v1beta1.Coin amount = 3 [(gogoproto.nullable) = false];
}

ValidatorGovInfo

This type is used in a temp map when tallying

type ValidatorGovInfo struct {
    Minus     sdk.Dec
    Vote      Vote
}

Stores

Note: Stores are KVStores in the multi-store. The key to find the store is the first parameter in the list

We will use one KVStore Governance to store two mappings:

  • A mapping from proposalID|'proposal' to Proposal.

  • A mapping from proposalID|'addresses'|address to Vote. This mapping allows us to query all addresses that voted on the proposal along with their vote by doing a range query on proposalID:addresses.

  • A mapping from ParamsKey|'Params' to Params. This map allows to query all x/gov params.

For pseudocode purposes, here are the two function we will use to read or write in stores:

  • load(StoreKey, Key): Retrieve item stored at key Key in store found at key StoreKey in the multistore

  • store(StoreKey, Key, value): Write value Value at key Key in store found at key StoreKey in the multistore

Proposal Processing Queue

Store:

  • ProposalProcessingQueue: A queue queue[proposalID] containing all the ProposalIDs of proposals that reached MinDeposit. During each EndBlock, all the proposals that have reached the end of their voting period are processed. To process a finished proposal, the application tallies the votes, computes the votes of each validator and checks if every validator in the validator set has voted. If the proposal is accepted, deposits are refunded. Finally, the proposal content Handler is executed.

And the pseudocode for the ProposalProcessingQueue:

  in EndBlock do

    for finishedProposalID in GetAllFinishedProposalIDs(block.Time)
      proposal = load(Governance, <proposalID|'proposal'>) // proposal is a const key

      validators = Keeper.getAllValidators()
      tmpValMap := map(sdk.AccAddress)ValidatorGovInfo

      // Initiate mapping at 0. This is the amount of shares of the validator's vote that will be overridden by their delegator's votes
      for each validator in validators
        tmpValMap(validator.OperatorAddr).Minus = 0

      // Tally
      voterIterator = rangeQuery(Governance, <proposalID|'addresses'>) //return all the addresses that voted on the proposal
      for each (voterAddress, vote) in voterIterator
        delegations = stakingKeeper.getDelegations(voterAddress) // get all delegations for current voter

        for each delegation in delegations
          // make sure delegation.Shares does NOT include shares being unbonded
          tmpValMap(delegation.ValidatorAddr).Minus += delegation.Shares
          proposal.updateTally(vote, delegation.Shares)

        _, isVal = stakingKeeper.getValidator(voterAddress)
        if (isVal)
          tmpValMap(voterAddress).Vote = vote

      tallyingParam = load(GlobalParams, 'TallyingParam')

      // Update tally if validator voted
      for each validator in validators
        if tmpValMap(validator).HasVoted
          proposal.updateTally(tmpValMap(validator).Vote, (validator.TotalShares - tmpValMap(validator).Minus))



      // Check if proposal is accepted or rejected
      totalNonAbstain := proposal.YesVotes + proposal.NoVotes + proposal.NoWithVetoVotes
      if (proposal.Votes.YesVotes/totalNonAbstain > tallyingParam.Threshold AND proposal.Votes.NoWithVetoVotes/totalNonAbstain  < tallyingParam.Veto)
        //  proposal was accepted at the end of the voting period
        //  refund deposits (non-voters already punished)
        for each (amount, depositor) in proposal.Deposits
          depositor.AtomBalance += amount

        stateWriter, err := proposal.Handler()
        if err != nil
            // proposal passed but failed during state execution
            proposal.CurrentStatus = ProposalStatusFailed
         else
            // proposal pass and state is persisted
            proposal.CurrentStatus = ProposalStatusAccepted
            stateWriter.save()
      else
        // proposal was rejected
        proposal.CurrentStatus = ProposalStatusRejected

      store(Governance, <proposalID|'proposal'>, proposal)

Legacy Proposal

A legacy proposal is the old implementation of governance proposal. Contrary to proposal that can contain any messages, a legacy proposal allows to submit a set of pre-defined proposals. These proposal are defined by their types.

While proposals should use the new implementation of the governance proposal, we need still to use legacy proposal in order to submit a software-upgrade and a cancel-software-upgrade proposal.

More information on how to submit proposals in the client section.

Last updated