Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(x/gov): optimistic proposals #18620

Merged
merged 45 commits into from
Dec 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
62ef693
feat(gov): add proposal types and spam votes
julienrbrt Nov 21, 2023
2a536e8
updates
julienrbrt Nov 21, 2023
e8da7ac
wip
julienrbrt Nov 21, 2023
fb281bf
fix tests
julienrbrt Nov 22, 2023
a568dc9
updates
julienrbrt Nov 22, 2023
50f1015
updates
julienrbrt Nov 22, 2023
c9dc8c8
validate previous options
julienrbrt Nov 22, 2023
9013310
Merge branch 'main' into julien/gov-spam-proposal-type
julienrbrt Nov 22, 2023
7eed169
updates
julienrbrt Nov 22, 2023
411378e
proto lint
julienrbrt Nov 22, 2023
f0464f5
nits
julienrbrt Nov 24, 2023
2e62db1
remove duplicate tests
julienrbrt Nov 24, 2023
2bdf2bc
nits
julienrbrt Nov 24, 2023
74c7f77
Merge branch 'main' into julien/gov-spam-proposal-type
julienrbrt Nov 25, 2023
aaf6d2f
Merge branch 'main' into julien/gov-spam-proposal-type
julienrbrt Nov 30, 2023
84a2e4b
Merge branch 'main' into julien/gov-spam-proposal-type
julienrbrt Dec 1, 2023
b000ac0
feedback
julienrbrt Dec 1, 2023
4aa5df7
Merge branch 'main' into julien/gov-spam-proposal-type
julienrbrt Dec 4, 2023
d1ff59b
feat(gov): optimisic proposals
julienrbrt Dec 4, 2023
b5ae484
updates
julienrbrt Dec 4, 2023
f172a45
updates
julienrbrt Dec 4, 2023
96cb2d9
Merge branch 'main' into julien/gov-spam-proposal-type
julienrbrt Dec 6, 2023
2d105ee
updates
julienrbrt Dec 6, 2023
715d5f1
Merge branch 'main' into julien/gov-spam-proposal-type
julienrbrt Dec 6, 2023
87514ed
Merge branch 'julien/gov-spam-proposal-type' into julien/optimistic-p…
julienrbrt Dec 6, 2023
bf109d8
Merge branch 'main' into julien/optimistic-proposals
julienrbrt Dec 11, 2023
eee0945
updates
julienrbrt Dec 11, 2023
d565d5d
tally
julienrbrt Dec 12, 2023
3d9db0e
Merge branch 'main' into julien/optimistic-proposals
julienrbrt Dec 12, 2023
80012dd
changelog
julienrbrt Dec 12, 2023
ca75bee
typos
julienrbrt Dec 13, 2023
05b29c7
lint
julienrbrt Dec 13, 2023
8d3e75b
updates
julienrbrt Dec 14, 2023
d3cf8c1
updates
julienrbrt Dec 14, 2023
8dde530
Merge branch 'main' into julien/optimistic-proposals
julienrbrt Dec 14, 2023
4363e30
lint
julienrbrt Dec 14, 2023
bc30897
Update README.md
julienrbrt Dec 15, 2023
ddc9b2e
feedback
julienrbrt Dec 15, 2023
c0a41a3
updates
julienrbrt Dec 15, 2023
4540b87
feedback
julienrbrt Dec 15, 2023
bb30865
checks done in msg server
julienrbrt Dec 15, 2023
51049d5
revert increase
julienrbrt Dec 15, 2023
b473c11
Merge branch 'main' into julien/optimistic-proposals
julienrbrt Dec 15, 2023
228789f
fix integration
julienrbrt Dec 15, 2023
862ace6
Merge branch 'main' into julien/optimistic-proposals
julienrbrt Dec 19, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"];
julienrbrt marked this conversation as resolved.
Show resolved Hide resolved

// 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() {
julienrbrt marked this conversation as resolved.
Show resolved Hide resolved
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} |
julienrbrt marked this conversation as resolved.
Show resolved Hide resolved
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
Loading