refactor(group): Distinguish Voting period and Execution period for group policies (#11198)
## Description Closes: #11092 ## TODOs I'm thinking to do the 2 todos in a separate PR, or else this PR is too big. WDYT? - [ ] #11246 This involves adding a new index ProposalsByVotingPeriodEnd, so might be better to do in another PR - [ ] #11245 Also should be done in a separate PR (as it needs the above index) ### Main change 1: Group policy proto defs have `voting_period` and `min_execution_period` For group policies: ```diff - // Within this times votes and exec messages can be submitted. - // timeout is the duration from submission of a proposal to the end of voting period - google.protobuf.Duration timeout = 2 [(gogoproto.stdduration) = true, (gogoproto.nullable) = false]; + // voting_period is the duration from submission of a proposal to the end of voting period + // Within this times votes can be submitted with MsgVote. + google.protobuf.Duration voting_period = 2 [(gogoproto.stdduration) = true, (gogoproto.nullable) = false]; + // min_execution_period is the minimum duration after the proposal submission + // where members can start sending MsgExec. This means that the window for + // sending a MsgExec transaction is: + // `[ submission + min_execution_period ; submission + voting_period + max_execution_period]` + // where max_execution_period is a app-specific config, defined in the keeper. + // If not set, min_execution_period will default to 0. + google.protobuf.Duration min_execution_period = 3 [(gogoproto.stdduration) = true, (gogoproto.nullable) = false]; ``` ### Main Change 2: We don't update proposal's FinalTallyResult result on MsgVote/MsgSubmitProposal Unless the msg has TryExec set to true, in which case the FinalTallyResult is updated ONLY if the tally is final. ### Main Change 3: Add a keeper-level `MaxExecutionPeriod` MsgExecs will be rejected if they are sent after `voting_period_end + MaxExecutionPeriod` --- ### Author Checklist *All items are required. Please add a note to the item if the item is not applicable and please add links to any relevant follow up issues.* I have... - [ ] included the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title - [ ] added `!` to the type prefix if API or client breaking change - [ ] targeted the correct branch (see [PR Targeting](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#pr-targeting)) - [ ] provided a link to the relevant issue or specification - [ ] followed the guidelines for [building modules](https://github.com/cosmos/cosmos-sdk/blob/master/docs/building-modules) - [ ] included the necessary unit and integration [tests](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#testing) - [ ] added a changelog entry to `CHANGELOG.md` - [ ] included comments for [documenting Go code](https://blog.golang.org/godoc) - [ ] updated the relevant documentation or specification - [ ] reviewed "Files changed" and left comments if necessary - [ ] confirmed all CI checks have passed ### Reviewers Checklist *All items are required. Please add a note if the item is not applicable and please add your handle next to the items reviewed if you only reviewed selected items.* I have... - [ ] confirmed the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title - [ ] confirmed `!` in the type prefix if API or client breaking change - [ ] confirmed all author checklist items have been addressed - [ ] reviewed state machine logic - [ ] reviewed API design and naming - [ ] reviewed documentation is accurate - [ ] reviewed tests and test coverage - [ ] manually tested (if applicable)
This commit is contained in:
@@ -1,14 +0,0 @@
|
||||
package keeper
|
||||
|
||||
// Config is a config struct used for intialising the group module to avoid using globals.
|
||||
type Config struct {
|
||||
// MaxMetadataLen defines the max length of the metadata bytes field for various entities within the group module. Defaults to 255 if not explicitly set.
|
||||
MaxMetadataLen uint64
|
||||
}
|
||||
|
||||
// DefaultConfig returns the default config for group.
|
||||
func DefaultConfig() Config {
|
||||
return Config{
|
||||
MaxMetadataLen: 255,
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,9 @@ func (s *GenesisTestSuite) TestInitExportGenesis() {
|
||||
}
|
||||
err := groupPolicy.SetDecisionPolicy(&group.ThresholdDecisionPolicy{
|
||||
Threshold: "1",
|
||||
Timeout: time.Second,
|
||||
Windows: &group.DecisionPolicyWindows{
|
||||
VotingPeriod: time.Second,
|
||||
},
|
||||
})
|
||||
s.Require().NoError(err)
|
||||
|
||||
@@ -95,8 +97,8 @@ func (s *GenesisTestSuite) TestInitExportGenesis() {
|
||||
AbstainCount: "0",
|
||||
NoWithVetoCount: "0",
|
||||
},
|
||||
Timeout: timeout,
|
||||
ExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS,
|
||||
VotingPeriodEnd: timeout,
|
||||
ExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS,
|
||||
}
|
||||
err = proposal.SetMsgs([]sdk.Msg{&banktypes.MsgSend{
|
||||
FromAddress: accAddr.String(),
|
||||
@@ -216,7 +218,7 @@ func (s *GenesisTestSuite) assertProposalsEqual(g *group.Proposal, other *group.
|
||||
require.Equal(g.Status, other.Status)
|
||||
require.Equal(g.Result, other.Result)
|
||||
require.Equal(g.FinalTallyResult, other.FinalTallyResult)
|
||||
require.Equal(g.Timeout, other.Timeout)
|
||||
require.Equal(g.VotingPeriodEnd, other.VotingPeriodEnd)
|
||||
require.Equal(g.ExecutorResult, other.ExecutorResult)
|
||||
require.Equal(g.GetMsgs(), other.GetMsgs())
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
"github.com/cosmos/cosmos-sdk/types/query"
|
||||
"github.com/cosmos/cosmos-sdk/x/group"
|
||||
"github.com/cosmos/cosmos-sdk/x/group/errors"
|
||||
"github.com/cosmos/cosmos-sdk/x/group/internal/orm"
|
||||
)
|
||||
|
||||
@@ -303,3 +304,55 @@ func (q Keeper) getVotesByProposal(ctx sdk.Context, proposalID uint64, pageReque
|
||||
func (q Keeper) getVotesByVoter(ctx sdk.Context, voter sdk.AccAddress, pageRequest *query.PageRequest) (orm.Iterator, error) {
|
||||
return q.voteByVoterIndex.GetPaginated(ctx.KVStore(q.key), voter.Bytes(), pageRequest)
|
||||
}
|
||||
|
||||
// Tally is a function that tallies a proposal by iterating through its votes,
|
||||
// and returns the tally result without modifying the proposal or any state.
|
||||
// TODO Merge with https://github.com/cosmos/cosmos-sdk/issues/11151
|
||||
func (q Keeper) Tally(ctx sdk.Context, p group.Proposal, groupId uint64) (group.TallyResult, error) {
|
||||
// If proposal has already been tallied and updated, then its status is
|
||||
// closed, in which case we just return the previously stored result.
|
||||
if p.Status == group.PROPOSAL_STATUS_CLOSED {
|
||||
return p.FinalTallyResult, nil
|
||||
}
|
||||
|
||||
it, err := q.voteByProposalIndex.Get(ctx.KVStore(q.key), p.Id)
|
||||
if err != nil {
|
||||
return group.TallyResult{}, err
|
||||
}
|
||||
defer it.Close()
|
||||
|
||||
tallyResult := group.DefaultTallyResult()
|
||||
|
||||
var vote group.Vote
|
||||
for {
|
||||
_, err = it.LoadNext(&vote)
|
||||
if errors.ErrORMIteratorDone.Is(err) {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return group.TallyResult{}, err
|
||||
}
|
||||
|
||||
var member group.GroupMember
|
||||
err := q.groupMemberTable.GetOne(ctx.KVStore(q.key), orm.PrimaryKey(&group.GroupMember{
|
||||
GroupId: groupId,
|
||||
Member: &group.Member{Address: vote.Voter},
|
||||
}), &member)
|
||||
|
||||
switch {
|
||||
case sdkerrors.ErrNotFound.Is(err):
|
||||
// If the member left the group after voting, then we simply skip the
|
||||
// vote.
|
||||
continue
|
||||
case err != nil:
|
||||
// For any other errors, we stop and return the error.
|
||||
return group.TallyResult{}, err
|
||||
}
|
||||
|
||||
if err := tallyResult.Add(vote, member.Member.Weight); err != nil {
|
||||
return group.TallyResult{}, sdkerrors.Wrap(err, "add new vote")
|
||||
}
|
||||
}
|
||||
|
||||
return tallyResult, nil
|
||||
}
|
||||
|
||||
@@ -25,10 +25,9 @@ import (
|
||||
type invariantTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
ctx sdk.Context
|
||||
cdc *codec.ProtoCodec
|
||||
key *storetypes.KVStoreKey
|
||||
blockTime time.Time
|
||||
ctx sdk.Context
|
||||
cdc *codec.ProtoCodec
|
||||
key *storetypes.KVStoreKey
|
||||
}
|
||||
|
||||
func TestInvariantTestSuite(t *testing.T) {
|
||||
@@ -81,7 +80,7 @@ func (s *invariantTestSuite) TestTallyVotesInvariant() {
|
||||
Status: group.PROPOSAL_STATUS_SUBMITTED,
|
||||
Result: group.PROPOSAL_RESULT_UNFINALIZED,
|
||||
FinalTallyResult: group.TallyResult{YesCount: "1", NoCount: "0", AbstainCount: "0", NoWithVetoCount: "0"},
|
||||
Timeout: prevCtx.BlockTime().Add(time.Second * 600),
|
||||
VotingPeriodEnd: prevCtx.BlockTime().Add(time.Second * 600),
|
||||
ExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
|
||||
},
|
||||
|
||||
@@ -95,7 +94,7 @@ func (s *invariantTestSuite) TestTallyVotesInvariant() {
|
||||
Status: group.PROPOSAL_STATUS_SUBMITTED,
|
||||
Result: group.PROPOSAL_RESULT_UNFINALIZED,
|
||||
FinalTallyResult: group.TallyResult{YesCount: "2", NoCount: "0", AbstainCount: "0", NoWithVetoCount: "0"},
|
||||
Timeout: curCtx.BlockTime().Add(time.Second * 600),
|
||||
VotingPeriodEnd: curCtx.BlockTime().Add(time.Second * 600),
|
||||
ExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
|
||||
},
|
||||
},
|
||||
@@ -110,7 +109,7 @@ func (s *invariantTestSuite) TestTallyVotesInvariant() {
|
||||
Status: group.PROPOSAL_STATUS_SUBMITTED,
|
||||
Result: group.PROPOSAL_RESULT_UNFINALIZED,
|
||||
FinalTallyResult: group.TallyResult{YesCount: "2", NoCount: "0", AbstainCount: "0", NoWithVetoCount: "0"},
|
||||
Timeout: prevCtx.BlockTime().Add(time.Second * 600),
|
||||
VotingPeriodEnd: prevCtx.BlockTime().Add(time.Second * 600),
|
||||
ExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
|
||||
},
|
||||
curProposal: &group.Proposal{
|
||||
@@ -123,7 +122,7 @@ func (s *invariantTestSuite) TestTallyVotesInvariant() {
|
||||
Status: group.PROPOSAL_STATUS_SUBMITTED,
|
||||
Result: group.PROPOSAL_RESULT_UNFINALIZED,
|
||||
FinalTallyResult: group.TallyResult{YesCount: "1", NoCount: "0", AbstainCount: "0", NoWithVetoCount: "0"},
|
||||
Timeout: curCtx.BlockTime().Add(time.Second * 600),
|
||||
VotingPeriodEnd: curCtx.BlockTime().Add(time.Second * 600),
|
||||
ExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
|
||||
},
|
||||
expBroken: true,
|
||||
@@ -139,7 +138,7 @@ func (s *invariantTestSuite) TestTallyVotesInvariant() {
|
||||
Status: group.PROPOSAL_STATUS_SUBMITTED,
|
||||
Result: group.PROPOSAL_RESULT_UNFINALIZED,
|
||||
FinalTallyResult: group.TallyResult{YesCount: "0", NoCount: "2", AbstainCount: "0", NoWithVetoCount: "0"},
|
||||
Timeout: prevCtx.BlockTime().Add(time.Second * 600),
|
||||
VotingPeriodEnd: prevCtx.BlockTime().Add(time.Second * 600),
|
||||
ExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
|
||||
},
|
||||
curProposal: &group.Proposal{
|
||||
@@ -152,7 +151,7 @@ func (s *invariantTestSuite) TestTallyVotesInvariant() {
|
||||
Status: group.PROPOSAL_STATUS_SUBMITTED,
|
||||
Result: group.PROPOSAL_RESULT_UNFINALIZED,
|
||||
FinalTallyResult: group.TallyResult{YesCount: "0", NoCount: "1", AbstainCount: "0", NoWithVetoCount: "0"},
|
||||
Timeout: curCtx.BlockTime().Add(time.Second * 600),
|
||||
VotingPeriodEnd: curCtx.BlockTime().Add(time.Second * 600),
|
||||
ExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
|
||||
},
|
||||
expBroken: true,
|
||||
@@ -168,7 +167,7 @@ func (s *invariantTestSuite) TestTallyVotesInvariant() {
|
||||
Status: group.PROPOSAL_STATUS_SUBMITTED,
|
||||
Result: group.PROPOSAL_RESULT_UNFINALIZED,
|
||||
FinalTallyResult: group.TallyResult{YesCount: "0", NoCount: "0", AbstainCount: "2", NoWithVetoCount: "0"},
|
||||
Timeout: prevCtx.BlockTime().Add(time.Second * 600),
|
||||
VotingPeriodEnd: prevCtx.BlockTime().Add(time.Second * 600),
|
||||
ExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
|
||||
},
|
||||
curProposal: &group.Proposal{
|
||||
@@ -181,7 +180,7 @@ func (s *invariantTestSuite) TestTallyVotesInvariant() {
|
||||
Status: group.PROPOSAL_STATUS_SUBMITTED,
|
||||
Result: group.PROPOSAL_RESULT_UNFINALIZED,
|
||||
FinalTallyResult: group.TallyResult{YesCount: "0", NoCount: "0", AbstainCount: "1", NoWithVetoCount: "0"},
|
||||
Timeout: curCtx.BlockTime().Add(time.Second * 600),
|
||||
VotingPeriodEnd: curCtx.BlockTime().Add(time.Second * 600),
|
||||
ExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
|
||||
},
|
||||
expBroken: true,
|
||||
@@ -197,7 +196,7 @@ func (s *invariantTestSuite) TestTallyVotesInvariant() {
|
||||
Status: group.PROPOSAL_STATUS_SUBMITTED,
|
||||
Result: group.PROPOSAL_RESULT_UNFINALIZED,
|
||||
FinalTallyResult: group.TallyResult{YesCount: "0", NoCount: "0", AbstainCount: "0", NoWithVetoCount: "2"},
|
||||
Timeout: prevCtx.BlockTime().Add(time.Second * 600),
|
||||
VotingPeriodEnd: prevCtx.BlockTime().Add(time.Second * 600),
|
||||
ExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
|
||||
},
|
||||
curProposal: &group.Proposal{
|
||||
@@ -210,7 +209,7 @@ func (s *invariantTestSuite) TestTallyVotesInvariant() {
|
||||
Status: group.PROPOSAL_STATUS_SUBMITTED,
|
||||
Result: group.PROPOSAL_RESULT_UNFINALIZED,
|
||||
FinalTallyResult: group.TallyResult{YesCount: "0", NoCount: "0", AbstainCount: "0", NoWithVetoCount: "1"},
|
||||
Timeout: curCtx.BlockTime().Add(time.Second * 600),
|
||||
VotingPeriodEnd: curCtx.BlockTime().Add(time.Second * 600),
|
||||
ExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
|
||||
},
|
||||
expBroken: true,
|
||||
@@ -413,7 +412,7 @@ func (s *invariantTestSuite) TestTallyVotesSumInvariant() {
|
||||
Status: group.PROPOSAL_STATUS_SUBMITTED,
|
||||
Result: group.PROPOSAL_RESULT_UNFINALIZED,
|
||||
FinalTallyResult: group.TallyResult{YesCount: "4", NoCount: "3", AbstainCount: "0", NoWithVetoCount: "0"},
|
||||
Timeout: curCtx.BlockTime().Add(time.Second * 600),
|
||||
VotingPeriodEnd: curCtx.BlockTime().Add(time.Second * 600),
|
||||
ExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
|
||||
},
|
||||
votes: []*group.Vote{
|
||||
@@ -471,7 +470,7 @@ func (s *invariantTestSuite) TestTallyVotesSumInvariant() {
|
||||
Status: group.PROPOSAL_STATUS_SUBMITTED,
|
||||
Result: group.PROPOSAL_RESULT_UNFINALIZED,
|
||||
FinalTallyResult: group.TallyResult{YesCount: "6", NoCount: "0", AbstainCount: "0", NoWithVetoCount: "0"},
|
||||
Timeout: curCtx.BlockTime().Add(time.Second * 600),
|
||||
VotingPeriodEnd: curCtx.BlockTime().Add(time.Second * 600),
|
||||
ExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
|
||||
},
|
||||
votes: []*group.Vote{
|
||||
@@ -529,7 +528,7 @@ func (s *invariantTestSuite) TestTallyVotesSumInvariant() {
|
||||
Status: group.PROPOSAL_STATUS_SUBMITTED,
|
||||
Result: group.PROPOSAL_RESULT_UNFINALIZED,
|
||||
FinalTallyResult: group.TallyResult{YesCount: "4", NoCount: "3", AbstainCount: "0", NoWithVetoCount: "0"},
|
||||
Timeout: curCtx.BlockTime().Add(time.Second * 600),
|
||||
VotingPeriodEnd: curCtx.BlockTime().Add(time.Second * 600),
|
||||
ExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
|
||||
},
|
||||
votes: []*group.Vote{
|
||||
@@ -561,7 +560,7 @@ func (s *invariantTestSuite) TestTallyVotesSumInvariant() {
|
||||
_, err := groupTable.Create(cacheCurCtx.KVStore(key), groupsInfo)
|
||||
s.Require().NoError(err)
|
||||
|
||||
err = groupPolicy.SetDecisionPolicy(group.NewThresholdDecisionPolicy("1", time.Second))
|
||||
err = groupPolicy.SetDecisionPolicy(group.NewThresholdDecisionPolicy("1", time.Second, 0))
|
||||
s.Require().NoError(err)
|
||||
err = groupPolicyTable.Create(cacheCurCtx.KVStore(key), groupPolicy)
|
||||
s.Require().NoError(err)
|
||||
|
||||
@@ -74,10 +74,10 @@ type Keeper struct {
|
||||
|
||||
router *authmiddleware.MsgServiceRouter
|
||||
|
||||
config Config
|
||||
config group.Config
|
||||
}
|
||||
|
||||
func NewKeeper(storeKey storetypes.StoreKey, cdc codec.Codec, router *authmiddleware.MsgServiceRouter, accKeeper group.AccountKeeper, config Config) Keeper {
|
||||
func NewKeeper(storeKey storetypes.StoreKey, cdc codec.Codec, router *authmiddleware.MsgServiceRouter, accKeeper group.AccountKeeper, config group.Config) Keeper {
|
||||
k := Keeper{
|
||||
key: storeKey,
|
||||
router: router,
|
||||
@@ -207,7 +207,10 @@ func NewKeeper(storeKey storetypes.StoreKey, cdc codec.Codec, router *authmiddle
|
||||
k.voteTable = *voteTable
|
||||
|
||||
if config.MaxMetadataLen == 0 {
|
||||
config.MaxMetadataLen = DefaultConfig().MaxMetadataLen
|
||||
config.MaxMetadataLen = group.DefaultConfig().MaxMetadataLen
|
||||
}
|
||||
if config.MaxExecutionPeriod == 0 {
|
||||
config.MaxExecutionPeriod = group.DefaultConfig().MaxExecutionPeriod
|
||||
}
|
||||
k.config = config
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ func (s *TestSuite) SetupTest() {
|
||||
policy := group.NewThresholdDecisionPolicy(
|
||||
"2",
|
||||
time.Second,
|
||||
0,
|
||||
)
|
||||
policyReq := &group.MsgCreateGroupPolicy{
|
||||
Admin: s.addrs[0].String(),
|
||||
@@ -693,6 +694,7 @@ func (s *TestSuite) TestCreateGroupWithPolicy() {
|
||||
policy: group.NewThresholdDecisionPolicy(
|
||||
"1",
|
||||
time.Second,
|
||||
0,
|
||||
),
|
||||
},
|
||||
"group policy as admin is true": {
|
||||
@@ -704,6 +706,7 @@ func (s *TestSuite) TestCreateGroupWithPolicy() {
|
||||
policy: group.NewThresholdDecisionPolicy(
|
||||
"1",
|
||||
time.Second,
|
||||
0,
|
||||
),
|
||||
},
|
||||
"group metadata too long": {
|
||||
@@ -716,6 +719,7 @@ func (s *TestSuite) TestCreateGroupWithPolicy() {
|
||||
policy: group.NewThresholdDecisionPolicy(
|
||||
"1",
|
||||
time.Second,
|
||||
0,
|
||||
),
|
||||
expErr: true,
|
||||
expErrMsg: "limit exceeded",
|
||||
@@ -730,6 +734,7 @@ func (s *TestSuite) TestCreateGroupWithPolicy() {
|
||||
policy: group.NewThresholdDecisionPolicy(
|
||||
"1",
|
||||
time.Second,
|
||||
0,
|
||||
),
|
||||
expErr: true,
|
||||
expErrMsg: "limit exceeded",
|
||||
@@ -747,6 +752,7 @@ func (s *TestSuite) TestCreateGroupWithPolicy() {
|
||||
policy: group.NewThresholdDecisionPolicy(
|
||||
"1",
|
||||
time.Second,
|
||||
0,
|
||||
),
|
||||
expErr: true,
|
||||
expErrMsg: "limit exceeded",
|
||||
@@ -763,6 +769,7 @@ func (s *TestSuite) TestCreateGroupWithPolicy() {
|
||||
policy: group.NewThresholdDecisionPolicy(
|
||||
"1",
|
||||
time.Second,
|
||||
0,
|
||||
),
|
||||
expErr: true,
|
||||
expErrMsg: "expected a positive decimal",
|
||||
@@ -776,6 +783,7 @@ func (s *TestSuite) TestCreateGroupWithPolicy() {
|
||||
policy: group.NewThresholdDecisionPolicy(
|
||||
"10",
|
||||
time.Second,
|
||||
0,
|
||||
),
|
||||
expErr: false,
|
||||
},
|
||||
@@ -876,6 +884,7 @@ func (s *TestSuite) TestCreateGroupPolicy() {
|
||||
policy: group.NewThresholdDecisionPolicy(
|
||||
"1",
|
||||
time.Second,
|
||||
0,
|
||||
),
|
||||
},
|
||||
"all good with percentage decision policy": {
|
||||
@@ -886,6 +895,7 @@ func (s *TestSuite) TestCreateGroupPolicy() {
|
||||
policy: group.NewPercentageDecisionPolicy(
|
||||
"0.5",
|
||||
time.Second,
|
||||
0,
|
||||
),
|
||||
},
|
||||
"decision policy threshold > total group weight": {
|
||||
@@ -896,6 +906,7 @@ func (s *TestSuite) TestCreateGroupPolicy() {
|
||||
policy: group.NewThresholdDecisionPolicy(
|
||||
"10",
|
||||
time.Second,
|
||||
0,
|
||||
),
|
||||
},
|
||||
"group id does not exists": {
|
||||
@@ -906,6 +917,7 @@ func (s *TestSuite) TestCreateGroupPolicy() {
|
||||
policy: group.NewThresholdDecisionPolicy(
|
||||
"1",
|
||||
time.Second,
|
||||
0,
|
||||
),
|
||||
expErr: true,
|
||||
expErrMsg: "not found",
|
||||
@@ -918,6 +930,7 @@ func (s *TestSuite) TestCreateGroupPolicy() {
|
||||
policy: group.NewThresholdDecisionPolicy(
|
||||
"1",
|
||||
time.Second,
|
||||
0,
|
||||
),
|
||||
expErr: true,
|
||||
expErrMsg: "not group admin",
|
||||
@@ -931,6 +944,7 @@ func (s *TestSuite) TestCreateGroupPolicy() {
|
||||
policy: group.NewThresholdDecisionPolicy(
|
||||
"1",
|
||||
time.Second,
|
||||
0,
|
||||
),
|
||||
expErr: true,
|
||||
expErrMsg: "limit exceeded",
|
||||
@@ -943,6 +957,7 @@ func (s *TestSuite) TestCreateGroupPolicy() {
|
||||
policy: group.NewPercentageDecisionPolicy(
|
||||
"-0.5",
|
||||
time.Second,
|
||||
0,
|
||||
),
|
||||
expErr: true,
|
||||
expErrMsg: "expected a positive decimal",
|
||||
@@ -955,6 +970,7 @@ func (s *TestSuite) TestCreateGroupPolicy() {
|
||||
policy: group.NewPercentageDecisionPolicy(
|
||||
"2",
|
||||
time.Second,
|
||||
0,
|
||||
),
|
||||
expErr: true,
|
||||
expErrMsg: "percentage must be > 0 and <= 1",
|
||||
@@ -1194,6 +1210,7 @@ func (s *TestSuite) TestUpdateGroupPolicyDecisionPolicy() {
|
||||
policy: group.NewThresholdDecisionPolicy(
|
||||
"2",
|
||||
time.Duration(2)*time.Second,
|
||||
0,
|
||||
),
|
||||
expGroupPolicy: &group.GroupPolicyInfo{
|
||||
Admin: admin.String(),
|
||||
@@ -1216,6 +1233,7 @@ func (s *TestSuite) TestUpdateGroupPolicyDecisionPolicy() {
|
||||
policy: group.NewPercentageDecisionPolicy(
|
||||
"0.5",
|
||||
time.Duration(2)*time.Second,
|
||||
0,
|
||||
),
|
||||
expGroupPolicy: &group.GroupPolicyInfo{
|
||||
Admin: admin.String(),
|
||||
@@ -1278,14 +1296,17 @@ func (s *TestSuite) TestGroupPoliciesByAdminOrGroup() {
|
||||
group.NewThresholdDecisionPolicy(
|
||||
"1",
|
||||
time.Second,
|
||||
0,
|
||||
),
|
||||
group.NewThresholdDecisionPolicy(
|
||||
"10",
|
||||
time.Second,
|
||||
0,
|
||||
),
|
||||
group.NewPercentageDecisionPolicy(
|
||||
"0.5",
|
||||
time.Second,
|
||||
0,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -1376,6 +1397,7 @@ func (s *TestSuite) TestSubmitProposal() {
|
||||
policy := group.NewThresholdDecisionPolicy(
|
||||
"100",
|
||||
time.Second,
|
||||
0,
|
||||
)
|
||||
err := policyReq.SetDecisionPolicy(policy)
|
||||
s.Require().NoError(err)
|
||||
@@ -1523,7 +1545,7 @@ func (s *TestSuite) TestSubmitProposal() {
|
||||
Status: group.PROPOSAL_STATUS_SUBMITTED,
|
||||
Result: group.PROPOSAL_RESULT_UNFINALIZED,
|
||||
FinalTallyResult: group.TallyResult{
|
||||
YesCount: "1",
|
||||
YesCount: "0", // Since tally doesn't pass Allow(), we consider the proposal not final
|
||||
NoCount: "0",
|
||||
AbstainCount: "0",
|
||||
NoWithVetoCount: "0",
|
||||
@@ -1562,7 +1584,7 @@ func (s *TestSuite) TestSubmitProposal() {
|
||||
s.Assert().Equal(spec.expProposal.Result, proposal.Result)
|
||||
s.Assert().Equal(spec.expProposal.FinalTallyResult, proposal.FinalTallyResult)
|
||||
s.Assert().Equal(spec.expProposal.ExecutorResult, proposal.ExecutorResult)
|
||||
s.Assert().Equal(s.blockTime.Add(time.Second), proposal.Timeout)
|
||||
s.Assert().Equal(s.blockTime.Add(time.Second), proposal.VotingPeriodEnd)
|
||||
|
||||
if spec.msgs == nil { // then empty list is ok
|
||||
s.Assert().Len(proposal.GetMsgs(), 0)
|
||||
@@ -1684,6 +1706,7 @@ func (s *TestSuite) TestVote() {
|
||||
policy := group.NewThresholdDecisionPolicy(
|
||||
"2",
|
||||
time.Duration(2),
|
||||
0,
|
||||
)
|
||||
policyReq := &group.MsgCreateGroupPolicy{
|
||||
Admin: addr1.String(),
|
||||
@@ -1732,23 +1755,19 @@ func (s *TestSuite) TestVote() {
|
||||
s.Assert().Equal(uint64(1), proposals[0].GroupPolicyVersion)
|
||||
s.Assert().Equal(group.PROPOSAL_STATUS_SUBMITTED, proposals[0].Status)
|
||||
s.Assert().Equal(group.PROPOSAL_RESULT_UNFINALIZED, proposals[0].Result)
|
||||
s.Assert().Equal(group.TallyResult{
|
||||
YesCount: "0",
|
||||
NoCount: "0",
|
||||
AbstainCount: "0",
|
||||
NoWithVetoCount: "0",
|
||||
}, proposals[0].FinalTallyResult)
|
||||
s.Assert().Equal(group.DefaultTallyResult(), proposals[0].FinalTallyResult)
|
||||
|
||||
specs := map[string]struct {
|
||||
srcCtx sdk.Context
|
||||
expFinalTallyResult group.TallyResult
|
||||
req *group.MsgVote
|
||||
doBefore func(ctx context.Context)
|
||||
postRun func(sdkCtx sdk.Context)
|
||||
expProposalStatus group.ProposalStatus
|
||||
expResult group.ProposalResult
|
||||
expExecutorResult group.ProposalExecutorResult
|
||||
expErr bool
|
||||
srcCtx sdk.Context
|
||||
expTallyResult group.TallyResult // expected after tallying
|
||||
isFinal bool // is the tally result final?
|
||||
req *group.MsgVote
|
||||
doBefore func(ctx context.Context)
|
||||
postRun func(sdkCtx sdk.Context)
|
||||
expProposalStatus group.ProposalStatus // expected after tallying
|
||||
expResult group.ProposalResult // expected after tallying
|
||||
expExecutorResult group.ProposalExecutorResult // expected after tallying
|
||||
expErr bool
|
||||
}{
|
||||
"vote yes": {
|
||||
req: &group.MsgVote{
|
||||
@@ -1756,7 +1775,7 @@ func (s *TestSuite) TestVote() {
|
||||
Voter: addr4.String(),
|
||||
Option: group.VOTE_OPTION_YES,
|
||||
},
|
||||
expFinalTallyResult: group.TallyResult{
|
||||
expTallyResult: group.TallyResult{
|
||||
YesCount: "1",
|
||||
NoCount: "0",
|
||||
AbstainCount: "0",
|
||||
@@ -1774,12 +1793,13 @@ func (s *TestSuite) TestVote() {
|
||||
Option: group.VOTE_OPTION_YES,
|
||||
Exec: group.Exec_EXEC_TRY,
|
||||
},
|
||||
expFinalTallyResult: group.TallyResult{
|
||||
expTallyResult: group.TallyResult{
|
||||
YesCount: "2",
|
||||
NoCount: "0",
|
||||
AbstainCount: "0",
|
||||
NoWithVetoCount: "0",
|
||||
},
|
||||
isFinal: true,
|
||||
expProposalStatus: group.PROPOSAL_STATUS_CLOSED,
|
||||
expResult: group.PROPOSAL_RESULT_ACCEPTED,
|
||||
expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS,
|
||||
@@ -1797,7 +1817,7 @@ func (s *TestSuite) TestVote() {
|
||||
Option: group.VOTE_OPTION_YES,
|
||||
Exec: group.Exec_EXEC_TRY,
|
||||
},
|
||||
expFinalTallyResult: group.TallyResult{
|
||||
expTallyResult: group.TallyResult{
|
||||
YesCount: "1",
|
||||
NoCount: "0",
|
||||
AbstainCount: "0",
|
||||
@@ -1814,7 +1834,7 @@ func (s *TestSuite) TestVote() {
|
||||
Voter: addr4.String(),
|
||||
Option: group.VOTE_OPTION_NO,
|
||||
},
|
||||
expFinalTallyResult: group.TallyResult{
|
||||
expTallyResult: group.TallyResult{
|
||||
YesCount: "0",
|
||||
NoCount: "1",
|
||||
AbstainCount: "0",
|
||||
@@ -1831,7 +1851,7 @@ func (s *TestSuite) TestVote() {
|
||||
Voter: addr4.String(),
|
||||
Option: group.VOTE_OPTION_ABSTAIN,
|
||||
},
|
||||
expFinalTallyResult: group.TallyResult{
|
||||
expTallyResult: group.TallyResult{
|
||||
YesCount: "0",
|
||||
NoCount: "0",
|
||||
AbstainCount: "1",
|
||||
@@ -1848,7 +1868,7 @@ func (s *TestSuite) TestVote() {
|
||||
Voter: addr4.String(),
|
||||
Option: group.VOTE_OPTION_NO_WITH_VETO,
|
||||
},
|
||||
expFinalTallyResult: group.TallyResult{
|
||||
expTallyResult: group.TallyResult{
|
||||
YesCount: "0",
|
||||
NoCount: "0",
|
||||
AbstainCount: "0",
|
||||
@@ -1865,7 +1885,7 @@ func (s *TestSuite) TestVote() {
|
||||
Voter: addr3.String(),
|
||||
Option: group.VOTE_OPTION_YES,
|
||||
},
|
||||
expFinalTallyResult: group.TallyResult{
|
||||
expTallyResult: group.TallyResult{
|
||||
YesCount: "2",
|
||||
NoCount: "0",
|
||||
AbstainCount: "0",
|
||||
@@ -1887,6 +1907,7 @@ func (s *TestSuite) TestVote() {
|
||||
ProposalId: myProposalID,
|
||||
Voter: addr3.String(),
|
||||
Option: group.VOTE_OPTION_NO_WITH_VETO,
|
||||
Exec: 1, // Execute the proposal so that its status is final
|
||||
})
|
||||
s.Require().NoError(err)
|
||||
},
|
||||
@@ -1947,7 +1968,7 @@ func (s *TestSuite) TestVote() {
|
||||
expErr: true,
|
||||
postRun: func(sdkCtx sdk.Context) {},
|
||||
},
|
||||
"on timeout": {
|
||||
"on voting period end": {
|
||||
req: &group.MsgVote{
|
||||
ProposalId: myProposalID,
|
||||
Voter: addr4.String(),
|
||||
@@ -1968,6 +1989,7 @@ func (s *TestSuite) TestVote() {
|
||||
ProposalId: myProposalID,
|
||||
Voter: addr3.String(),
|
||||
Option: group.VOTE_OPTION_YES,
|
||||
Exec: 1, // Execute to close the proposal.
|
||||
})
|
||||
s.Require().NoError(err)
|
||||
},
|
||||
@@ -2019,7 +2041,9 @@ func (s *TestSuite) TestVote() {
|
||||
groupPolicy,
|
||||
&group.ThresholdDecisionPolicy{
|
||||
Threshold: "1",
|
||||
Timeout: time.Second,
|
||||
Windows: &group.DecisionPolicyWindows{
|
||||
VotingPeriod: time.Second,
|
||||
},
|
||||
},
|
||||
)
|
||||
s.Require().NoError(err)
|
||||
@@ -2093,16 +2117,26 @@ func (s *TestSuite) TestVote() {
|
||||
s.Assert().Equal(spec.req.Metadata, votesByVoter[0].Metadata)
|
||||
s.Assert().Equal(s.blockTime, votesByVoter[0].SubmitTime)
|
||||
|
||||
// and proposal is updated
|
||||
proposalRes, err := s.keeper.Proposal(ctx, &group.QueryProposalRequest{
|
||||
ProposalId: spec.req.ProposalId,
|
||||
})
|
||||
s.Require().NoError(err)
|
||||
|
||||
proposal := proposalRes.Proposal
|
||||
s.Assert().Equal(spec.expFinalTallyResult, proposal.FinalTallyResult)
|
||||
s.Assert().Equal(spec.expResult, proposal.Result)
|
||||
s.Assert().Equal(spec.expProposalStatus, proposal.Status)
|
||||
s.Assert().Equal(spec.expExecutorResult, proposal.ExecutorResult)
|
||||
if spec.isFinal {
|
||||
s.Assert().Equal(spec.expTallyResult, proposal.FinalTallyResult)
|
||||
s.Assert().Equal(spec.expResult, proposal.Result)
|
||||
s.Assert().Equal(spec.expProposalStatus, proposal.Status)
|
||||
s.Assert().Equal(spec.expExecutorResult, proposal.ExecutorResult)
|
||||
} else {
|
||||
s.Assert().Equal(group.DefaultTallyResult(), proposal.FinalTallyResult) // Make sure proposal isn't mutated.
|
||||
|
||||
// do a round of tallying
|
||||
tallyResult, err := s.keeper.Tally(sdkCtx, *proposal, myGroupID)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Assert().Equal(spec.expTallyResult, tallyResult)
|
||||
}
|
||||
|
||||
spec.postRun(sdkCtx)
|
||||
})
|
||||
@@ -2370,6 +2404,7 @@ func createGroupAndGroupPolicy(
|
||||
policy := group.NewThresholdDecisionPolicy(
|
||||
"1",
|
||||
time.Second,
|
||||
0,
|
||||
)
|
||||
err = groupPolicy.SetDecisionPolicy(policy)
|
||||
s.Require().NoError(err)
|
||||
|
||||
@@ -467,16 +467,11 @@ func (k Keeper) SubmitProposal(goCtx context.Context, req *group.MsgSubmitPropos
|
||||
}
|
||||
|
||||
// Prevent proposal that can not succeed.
|
||||
err = policy.Validate(g)
|
||||
err = policy.Validate(g, k.config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Define proposal timout.
|
||||
// The voting window begins as soon as the proposal is submitted.
|
||||
timeout := policy.GetTimeout()
|
||||
window := timeout
|
||||
|
||||
m := &group.Proposal{
|
||||
Id: k.proposalTable.Sequence().PeekNextVal(ctx.KVStore(k.key)),
|
||||
Address: req.Address,
|
||||
@@ -488,14 +483,10 @@ func (k Keeper) SubmitProposal(goCtx context.Context, req *group.MsgSubmitPropos
|
||||
Result: group.PROPOSAL_RESULT_UNFINALIZED,
|
||||
Status: group.PROPOSAL_STATUS_SUBMITTED,
|
||||
ExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
|
||||
Timeout: ctx.BlockTime().Add(window),
|
||||
FinalTallyResult: group.TallyResult{
|
||||
YesCount: "0",
|
||||
NoCount: "0",
|
||||
AbstainCount: "0",
|
||||
NoWithVetoCount: "0",
|
||||
},
|
||||
VotingPeriodEnd: ctx.BlockTime().Add(policy.GetVotingPeriod()), // The voting window begins as soon as the proposal is submitted.
|
||||
FinalTallyResult: group.DefaultTallyResult(),
|
||||
}
|
||||
|
||||
if err := m.SetMsgs(msgs); err != nil {
|
||||
return nil, sdkerrors.Wrap(err, "create proposal")
|
||||
}
|
||||
@@ -524,6 +515,7 @@ func (k Keeper) SubmitProposal(goCtx context.Context, req *group.MsgSubmitPropos
|
||||
return &group.MsgSubmitProposalResponse{ProposalId: id}, sdkerrors.Wrap(err, "The proposal was created but failed on vote")
|
||||
}
|
||||
}
|
||||
|
||||
// Then try to execute the proposal
|
||||
_, err = k.Exec(sdk.WrapSDKContext(ctx), &group.MsgExec{
|
||||
ProposalId: id,
|
||||
@@ -619,15 +611,7 @@ func (k Keeper) Vote(goCtx context.Context, req *group.MsgVote) (*group.MsgVoteR
|
||||
if proposal.Status != group.PROPOSAL_STATUS_SUBMITTED {
|
||||
return nil, sdkerrors.Wrap(errors.ErrInvalid, "proposal not open for voting")
|
||||
}
|
||||
proposalTimeout, err := gogotypes.TimestampProto(proposal.Timeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
votingPeriodEnd, err := gogotypes.TimestampFromProto(proposalTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if votingPeriodEnd.Before(ctx.BlockTime()) || votingPeriodEnd.Equal(ctx.BlockTime()) {
|
||||
if ctx.BlockTime().After(proposal.VotingPeriodEnd) {
|
||||
return nil, sdkerrors.Wrap(errors.ErrExpired, "voting period has ended already")
|
||||
}
|
||||
|
||||
@@ -663,9 +647,6 @@ func (k Keeper) Vote(goCtx context.Context, req *group.MsgVote) (*group.MsgVoteR
|
||||
Metadata: metadata,
|
||||
SubmitTime: ctx.BlockTime(),
|
||||
}
|
||||
if err := proposal.FinalTallyResult.Add(newVote, voter.Member.Weight); err != nil {
|
||||
return nil, sdkerrors.Wrap(err, "add new vote")
|
||||
}
|
||||
|
||||
// The ORM will return an error if the vote already exists,
|
||||
// making sure than a voter hasn't already voted.
|
||||
@@ -673,15 +654,6 @@ func (k Keeper) Vote(goCtx context.Context, req *group.MsgVote) (*group.MsgVoteR
|
||||
return nil, sdkerrors.Wrap(err, "store vote")
|
||||
}
|
||||
|
||||
// Run tally with new votes to close early.
|
||||
if err := doTally(ctx, &proposal, electorate, policyInfo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = k.proposalTable.Update(ctx.KVStore(k.key), id, &proposal); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = ctx.EventManager().EmitTypedEvent(&group.EventVote{ProposalId: id})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -701,8 +673,9 @@ func (k Keeper) Vote(goCtx context.Context, req *group.MsgVote) (*group.MsgVoteR
|
||||
return &group.MsgVoteResponse{}, nil
|
||||
}
|
||||
|
||||
// doTally updates the proposal status and tally if necessary based on the group policy's decision policy.
|
||||
func doTally(ctx sdk.Context, p *group.Proposal, electorate group.GroupInfo, policyInfo group.GroupPolicyInfo) error {
|
||||
// doTallyAndUpdate performs a tally, and updates the proposal's
|
||||
// `FinalTallyResult` field only if the tally is final.
|
||||
func (k Keeper) doTallyAndUpdate(ctx sdk.Context, p *group.Proposal, electorate group.GroupInfo, policyInfo group.GroupPolicyInfo) error {
|
||||
policy := policyInfo.GetDecisionPolicy()
|
||||
pSubmittedAt, err := gogotypes.TimestampProto(p.SubmitTime)
|
||||
if err != nil {
|
||||
@@ -712,16 +685,25 @@ func doTally(ctx sdk.Context, p *group.Proposal, electorate group.GroupInfo, pol
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch result, err := policy.Allow(p.FinalTallyResult, electorate.TotalWeight, ctx.BlockTime().Sub(submittedAt)); {
|
||||
|
||||
tallyResult, err := k.Tally(ctx, *p, policyInfo.GroupId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch result, err := policy.Allow(tallyResult, electorate.TotalWeight, ctx.BlockTime().Sub(submittedAt)); {
|
||||
case err != nil:
|
||||
return sdkerrors.Wrap(err, "policy execution")
|
||||
case result.Allow && result.Final:
|
||||
p.FinalTallyResult = tallyResult
|
||||
p.Result = group.PROPOSAL_RESULT_ACCEPTED
|
||||
p.Status = group.PROPOSAL_STATUS_CLOSED
|
||||
case !result.Allow && result.Final:
|
||||
p.FinalTallyResult = tallyResult
|
||||
p.Result = group.PROPOSAL_RESULT_REJECTED
|
||||
p.Status = group.PROPOSAL_STATUS_CLOSED
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -770,7 +752,8 @@ func (k Keeper) Exec(goCtx context.Context, req *group.MsgExec) (*group.MsgExecR
|
||||
proposal.Status = group.PROPOSAL_STATUS_ABORTED
|
||||
return storeUpdates()
|
||||
}
|
||||
if err := doTally(ctx, &proposal, electorate, policyInfo); err != nil {
|
||||
|
||||
if err := k.doTallyAndUpdate(ctx, &proposal, electorate, policyInfo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,16 @@ import (
|
||||
// doExecuteMsgs routes the messages to the registered handlers. Messages are limited to those that require no authZ or
|
||||
// by the account of group policy only. Otherwise this gives access to other peoples accounts as the sdk ant handler is bypassed
|
||||
func (s Keeper) doExecuteMsgs(ctx sdk.Context, router *authmiddleware.MsgServiceRouter, proposal group.Proposal, groupPolicyAcc sdk.AccAddress) ([]sdk.Result, error) {
|
||||
// Ensure it's not too late to execute the messages.
|
||||
// After https://github.com/cosmos/cosmos-sdk/issues/11245, proposals should
|
||||
// be pruned automatically, so this function should not even be called, as
|
||||
// the proposal doesn't exist in state. For sanity check, we can still keep
|
||||
// this simple and cheap check.
|
||||
expiryDate := proposal.VotingPeriodEnd.Add(s.config.MaxExecutionPeriod)
|
||||
if expiryDate.Before(ctx.BlockTime()) {
|
||||
return nil, grouperrors.ErrExpired.Wrapf("proposal expired on %s", expiryDate)
|
||||
}
|
||||
|
||||
msgs := proposal.GetMsgs()
|
||||
|
||||
results := make([]sdk.Result, len(msgs))
|
||||
|
||||
Reference in New Issue
Block a user