Skip to content

Commit

Permalink
feat(x/gov): optimistic proposals (#18620)
Browse files Browse the repository at this point in the history
Co-authored-by: Facundo Medica <[email protected]>
  • Loading branch information
julienrbrt and facundomedica authored Dec 20, 2023
1 parent 4753d16 commit eb3ea8d
Show file tree
Hide file tree
Showing 25 changed files with 1,550 additions and 545 deletions.
365 changes: 296 additions & 69 deletions api/cosmos/gov/v1/gov.pulsar.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/architecture/adr-069-gov-improvements.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

## Status

PROPOSED
ACCEPTED

## Abstract

Expand Down
12 changes: 12 additions & 0 deletions proto/cosmos/gov/v1/gov.proto
Original file line number Diff line number Diff line change
Expand Up @@ -303,4 +303,16 @@ message Params {
//
// Since: cosmos-sdk 0.50
string min_deposit_ratio = 16 [(cosmos_proto.scalar) = "cosmos.Dec"];

// optimistic_authorized_addresses is an optional governance parameter that limits the authorized accounts than can
// submit optimistic proposals
//
// Since: x/gov v1.0.0
repeated string optimistic_authorized_addresses = 17 [(cosmos_proto.scalar) = "cosmos.AddressString"];

// optimistic rejected threshold defines at which percentage of NO votes, the optimistic proposal should fail and be
// converted to a standard proposal. The threshold is expressed as a percentage of the total bonded tokens.
//
// Since: x/gov v1.0.0
string optimistic_rejected_threshold = 18 [(cosmos_proto.scalar) = "cosmos.Dec"];
}
27 changes: 5 additions & 22 deletions tests/integration/gov/keeper/grpc_query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,8 @@ func TestLegacyGRPCQueryTally(t *testing.T) {
addrs, _ := createValidators(t, f, []int64{5, 5, 5})

var (
req *v1beta1.QueryTallyResultRequest
expRes *v1beta1.QueryTallyResultResponse
proposal v1.Proposal
req *v1beta1.QueryTallyResultRequest
expRes *v1beta1.QueryTallyResultResponse
)

testCases := []struct {
Expand All @@ -33,29 +32,13 @@ func TestLegacyGRPCQueryTally(t *testing.T) {
expPass bool
expErrMsg string
}{
{
"create a proposal and get tally",
func() {
var err error
proposal, err = f.govKeeper.SubmitProposal(ctx, TestProposal, "", "test", "description", addrs[0], v1.ProposalType_PROPOSAL_TYPE_STANDARD)
assert.NilError(t, err)
assert.Assert(t, proposal.String() != "")

req = &v1beta1.QueryTallyResultRequest{ProposalId: proposal.Id}

tallyResult := v1beta1.EmptyTallyResult()
expRes = &v1beta1.QueryTallyResultResponse{
Tally: tallyResult,
}
},
true,
"",
},
{
"request tally after few votes",
func() {
proposal, err := f.govKeeper.SubmitProposal(ctx, TestProposal, "", "test", "description", addrs[0], v1.ProposalType_PROPOSAL_TYPE_STANDARD)
assert.NilError(t, err)
proposal.Status = v1.StatusVotingPeriod
err := f.govKeeper.SetProposal(ctx, proposal)
err = f.govKeeper.SetProposal(ctx, proposal)
assert.NilError(t, err)
assert.NilError(t, f.govKeeper.AddVote(ctx, proposal.Id, addrs[0], v1.NewNonSplitVoteOption(v1.OptionYes), ""))
assert.NilError(t, f.govKeeper.AddVote(ctx, proposal.Id, addrs[1], v1.NewNonSplitVoteOption(v1.OptionYes), ""))
Expand Down
8 changes: 3 additions & 5 deletions x/gov/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,15 @@ Ref: https://keepachangelog.com/en/1.0.0/

## [Unreleased]

## Improvements

* [#18445](https://github.com/cosmos/cosmos-sdk/pull/18445) Extend gov config

### Features

* [#18532](https://github.com/cosmos/cosmos-sdk/pull/18532) Add SPAM vote proposals.
* [#18532](https://github.com/cosmos/cosmos-sdk/pull/18532) Add SPAM vote to proposals.
* [#18532](https://github.com/cosmos/cosmos-sdk/pull/18532) Add proposal types to proposals.
* [#18620](https://github.com/cosmos/cosmos-sdk/pull/18620) Add optimistic proposals.

### Improvements

* [#18445](https://github.com/cosmos/cosmos-sdk/pull/18445) Extend gov config
* [#18532](https://github.com/cosmos/cosmos-sdk/pull/18532) Repurpose `govcliutils.NormalizeProposalType` to work for gov v1 proposal types.

### API Breaking Changes
Expand Down
125 changes: 42 additions & 83 deletions x/gov/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ staking token of the chain.
* [Proposal submission](#proposal-submission)
* [Deposit](#deposit)
* [Vote](#vote)
* [Software Upgrade](#software-upgrade)
* [State](#state)
* [Proposals](#proposals)
* [Parameters and base types](#parameters-and-base-types)
Expand Down Expand Up @@ -187,10 +186,29 @@ For a weighted vote to be valid, the `options` field must not contain duplicate
Quorum is defined as the minimum percentage of voting power that needs to be
cast on a proposal for the result to be valid.

### Expedited Proposals
### Proposal Types

Proposal types have been introduced in ADR-069.

#### Standard proposal

A standard proposal is a proposal that can contain any messages. The proposal follows the standard governance flow and governance parameters.

#### Expedited Proposal

A proposal can be expedited, making the proposal use shorter voting duration and a higher tally threshold by its default. If an expedited proposal fails to meet the threshold within the scope of shorter voting duration, the expedited proposal is then converted to a regular proposal and restarts voting under regular voting conditions.

#### Optimistic Proposal

An optimistic proposal is a proposal that passes unless a threshold a NO votes is reached.
Voter can only vote NO on the proposal. If the NO threshold is reached, the optimistic proposal is converted to a standard proposal.

#### Multiple Choice Proposals

A multiple choice proposal is a proposal where the voting options can be defined by the proposer.
The number of voting options is limited to a maximum of 4.
Multiple choice proposals, contrary to any other proposal type, cannot have messages to execute. They are only text proposals.

#### Threshold

Threshold is defined as the minimum proportion of `Yes` votes (excluding
Expand Down Expand Up @@ -427,67 +445,6 @@ For pseudocode purposes, here are the two function we will use to read or write
voted. If the proposal is accepted, deposits are refunded. Finally, the proposal
content `Handler` is executed.

And the pseudocode for the `ProposalProcessingQueue`:

```go
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

:::warning
Expand Down Expand Up @@ -575,7 +532,7 @@ The governance module emits the following events:
### EndBlocker

| Type | Attribute Key | Attribute Value |
|-------------------|-----------------|------------------|
| ----------------- | --------------- | ---------------- |
| inactive_proposal | proposal_id | {proposalID} |
| inactive_proposal | proposal_result | {proposalResult} |
| active_proposal | proposal_id | {proposalID} |
Expand All @@ -586,7 +543,7 @@ The governance module emits the following events:
#### MsgSubmitProposal

| Type | Attribute Key | Attribute Value |
|---------------------|---------------------|-----------------|
| ------------------- | ------------------- | --------------- |
| submit_proposal | proposal_id | {proposalID} |
| submit_proposal [0] | voting_period_start | {proposalID} |
| proposal_deposit | amount | {depositAmount} |
Expand All @@ -600,7 +557,7 @@ The governance module emits the following events:
#### MsgVote

| Type | Attribute Key | Attribute Value |
|---------------|---------------|-----------------|
| ------------- | ------------- | --------------- |
| proposal_vote | option | {voteOption} |
| proposal_vote | proposal_id | {proposalID} |
| message | module | governance |
Expand All @@ -610,7 +567,7 @@ The governance module emits the following events:
#### MsgVoteWeighted

| Type | Attribute Key | Attribute Value |
|---------------|---------------|-----------------------|
| ------------- | ------------- | --------------------- |
| proposal_vote | option | {weightedVoteOptions} |
| proposal_vote | proposal_id | {proposalID} |
| message | module | governance |
Expand All @@ -620,7 +577,7 @@ The governance module emits the following events:
#### MsgDeposit

| Type | Attribute Key | Attribute Value |
|----------------------|---------------------|-----------------|
| -------------------- | ------------------- | --------------- |
| proposal_deposit | amount | {depositAmount} |
| proposal_deposit | proposal_id | {proposalID} |
| proposal_deposit [0] | voting_period_start | {proposalID} |
Expand All @@ -634,21 +591,23 @@ The governance module emits the following events:

The governance module contains the following parameters:

| Key | Type | Example |
|-------------------------------|------------------|-----------------------------------------|
| min_deposit | array (coins) | [{"denom":"uatom","amount":"10000000"}] |
| max_deposit_period | string (time ns) | "172800000000000" (17280s) |
| voting_period | string (time ns) | "172800000000000" (17280s) |
| quorum | string (dec) | "0.334000000000000000" |
| threshold | string (dec) | "0.500000000000000000" |
| veto | string (dec) | "0.334000000000000000" |
| expedited_threshold | string (time ns) | "0.667000000000000000" |
| expedited_voting_period | string (time ns) | "86400000000000" (8600s) |
| expedited_min_deposit | array (coins) | [{"denom":"uatom","amount":"50000000"}] |
| burn_proposal_deposit_prevote | bool | false |
| burn_vote_quorum | bool | false |
| burn_vote_veto | bool | true |
| min_initial_deposit_ratio | string | "0.1" |
| Key | Type | Example |
| ------------------------------- | ---------------------- | --------------------------------------- |
| min_deposit | array (coins) | [{"denom":"uatom","amount":"10000000"}] |
| max_deposit_period | string (time ns) | "172800000000000" (17280s) |
| voting_period | string (time ns) | "172800000000000" (17280s) |
| quorum | string (dec) | "0.334000000000000000" |
| threshold | string (dec) | "0.500000000000000000" |
| veto | string (dec) | "0.334000000000000000" |
| expedited_threshold | string (time ns) | "0.667000000000000000" |
| expedited_voting_period | string (time ns) | "86400000000000" (8600s) |
| expedited_min_deposit | array (coins) | [{"denom":"uatom","amount":"50000000"}] |
| burn_proposal_deposit_prevote | bool | false |
| burn_vote_quorum | bool | false |
| burn_vote_veto | bool | true |
| min_initial_deposit_ratio | string | "0.1" |
| optimistic_rejected_threshold | string (dec) | "0.1" |
| optimistic_authorized_addresses | bytes array (addreses) | [][] |

**NOTE**: The governance module contains parameters that are objects unlike other
modules. If only a subset of parameters are desired to be changed, only they need
Expand Down
40 changes: 22 additions & 18 deletions x/gov/abci.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,19 +127,17 @@ func EndBlocker(ctx sdk.Context, keeper *keeper.Keeper) error {
return false, err
}

// If an expedited proposal fails, we do not want to update
// the deposit at this point since the proposal is converted to regular.
// As a result, the deposits are either deleted or refunded in all cases
// EXCEPT when an expedited proposal fails.
if passes || !(proposal.ProposalType == v1.ProposalType_PROPOSAL_TYPE_EXPEDITED) {
if burnDeposits {
err = keeper.DeleteAndBurnDeposits(ctx, proposal.Id)
} else {
err = keeper.RefundAndDeleteDeposits(ctx, proposal.Id)
}
if err != nil {
return false, err
}
// Deposits are always burned if tally said so, regardless of the proposal type.
// If a proposal passes, deposits are always refunded, regardless of the proposal type.
// If a proposal fails, and isn't spammy, deposits are refunded, unless the proposal is expedited or optimistic.
// An expedited or optimistic proposal that fails and isn't spammy is converted to a regular proposal.
if burnDeposits {
err = keeper.DeleteAndBurnDeposits(ctx, proposal.Id)
} else if passes || !(proposal.ProposalType == v1.ProposalType_PROPOSAL_TYPE_EXPEDITED || proposal.ProposalType == v1.ProposalType_PROPOSAL_TYPE_OPTIMISTIC) {
err = keeper.RefundAndDeleteDeposits(ctx, proposal.Id)
}
if err != nil {
return false, err
}

if err = keeper.ActiveProposalsQueue.Remove(ctx, collections.Join(*proposal.VotingEndTime, proposal.Id)); err != nil {
Expand Down Expand Up @@ -199,8 +197,9 @@ func EndBlocker(ctx sdk.Context, keeper *keeper.Keeper) error {
tagValue = types.AttributeValueProposalFailed
logMsg = fmt.Sprintf("passed, but msg %d (%s) failed on execution: %s", idx, sdk.MsgTypeURL(msg), err)
}
case proposal.ProposalType == v1.ProposalType_PROPOSAL_TYPE_EXPEDITED:
// When expedited proposal fails, it is converted
case !burnDeposits && (proposal.ProposalType == v1.ProposalType_PROPOSAL_TYPE_EXPEDITED ||
proposal.ProposalType == v1.ProposalType_PROPOSAL_TYPE_OPTIMISTIC):
// When a non spammy expedited/optimistic proposal fails, it is converted
// to a regular proposal. As a result, the voting period is extended, and,
// once the regular voting period expires again, the tally is repeated
// according to the regular proposal rules.
Expand All @@ -218,8 +217,13 @@ func EndBlocker(ctx sdk.Context, keeper *keeper.Keeper) error {
return false, err
}

tagValue = types.AttributeValueExpeditedProposalRejected
logMsg = "expedited proposal converted to regular"
if proposal.ProposalType == v1.ProposalType_PROPOSAL_TYPE_EXPEDITED {
tagValue = types.AttributeValueExpeditedProposalRejected
logMsg = "expedited proposal converted to regular"
} else {
tagValue = types.AttributeValueOptimisticProposalRejected
logMsg = "optimistic proposal converted to regular"
}
default:
proposal.Status = v1.StatusRejected
proposal.FailedReason = "proposal did not get enough votes to pass"
Expand All @@ -246,8 +250,8 @@ func EndBlocker(ctx sdk.Context, keeper *keeper.Keeper) error {
logger.Info(
"proposal tallied",
"proposal", proposal.Id,
"status", proposal.Status.String(),
"proposal_type", proposal.ProposalType,
"status", proposal.Status.String(),
"title", proposal.Title,
"results", logMsg,
)
Expand Down
9 changes: 9 additions & 0 deletions x/gov/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package keeper

import (
"context"
"errors"
"fmt"
"time"

Expand Down Expand Up @@ -208,6 +209,10 @@ func (k Keeper) validateProposalLengths(metadata, title, summary string) error {
// assertTitleLength returns an error if given title length
// is greater than a pre-defined MaxTitleLen.
func (k Keeper) assertTitleLength(title string) error {
if len(title) == 0 {
return errors.New("proposal title cannot be empty")
}

if uint64(len(title)) > k.config.MaxTitleLen {
return types.ErrTitleTooLong.Wrapf("got title with length %d", len(title))
}
Expand All @@ -226,6 +231,10 @@ func (k Keeper) assertMetadataLength(metadata string) error {
// assertSummaryLength returns an error if given summary length
// is greater than a pre-defined MaxSummaryLen.
func (k Keeper) assertSummaryLength(summary string) error {
if len(summary) == 0 {
return errors.New("proposal summary cannot be empty")
}

if uint64(len(summary)) > k.config.MaxSummaryLen {
return types.ErrSummaryTooLong.Wrapf("got summary with length %d", len(summary))
}
Expand Down
2 changes: 1 addition & 1 deletion x/gov/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func (suite *KeeperTestSuite) reset() {
suite.msgSrvr = keeper.NewMsgServerImpl(suite.govKeeper)

suite.legacyMsgSrvr = keeper.NewLegacyMsgServerImpl(govAcct.String(), suite.msgSrvr)
suite.addrs = simtestutil.AddTestAddrsIncremental(bankKeeper, stakingKeeper, ctx, 3, sdkmath.NewInt(30000000))
suite.addrs = simtestutil.AddTestAddrsIncremental(bankKeeper, stakingKeeper, ctx, 3, sdkmath.NewInt(300000000))

suite.acctKeeper.EXPECT().AddressCodec().Return(address.NewBech32Codec("cosmos")).AnyTimes()
}
Expand Down
2 changes: 1 addition & 1 deletion x/gov/keeper/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,5 @@ func (m Migrator) Migrate4to5(ctx sdk.Context) error {

// Migrate4to5 migrates from version 5 to 6.
func (m Migrator) Migrate5to6(ctx sdk.Context) error {
return v6.MigrateStore(ctx, m.keeper.Proposals)
return v6.MigrateStore(ctx, m.keeper.Params, m.keeper.Proposals)
}
Loading

0 comments on commit eb3ea8d

Please sign in to comment.