From 6b633ef0b7ac248e7334c3ef8b1b4d688e683fdf Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 16 Nov 2022 16:43:38 +0100 Subject: [PATCH] fix(group)!: Fix group min execution period (backport #13876) (#13885) * fix(group)!: Fix group min execution period (#13876) * fix: don't check MinExecutionPeriod in `Allow` * Check MinExecutionPeriod on doExecuteMsgs * Fix TestExec * Fix TestExec * test exec pruned * Fix submitproposal * Add changelog * typo * revert some changes * add minExecutionPeriod * Add docs and specs (cherry picked from commit 7661f627370f73379d9b04c02ad3828ee600dbec) # Conflicts: # CHANGELOG.md # x/group/README.md # x/group/keeper/keeper_test.go * fix docs * fix other conflicts * fix test Co-authored-by: Amaury <1293565+amaurym@users.noreply.github.com> Co-authored-by: Julien Robert --- CHANGELOG.md | 8 ++ RELEASE_NOTES.md | 9 +- x/group/keeper/keeper_test.go | 164 +++++++++++++++++++++++----- x/group/keeper/msg_server.go | 6 +- x/group/keeper/proposal_executor.go | 8 +- x/group/spec/01_concepts.md | 13 +++ x/group/types.go | 26 +++-- x/group/types_test.go | 47 +------- 8 files changed, 190 insertions(+), 91 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81c204f0b437..e8e9b024045f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,14 @@ Ref: https://keepachangelog.com/en/1.0.0/ * [#13826](https://github.com/cosmos/cosmos-sdk/pull/13826) Support custom `GasConfig` configuration for applications. * (deps) Bump Tendermint version to [v0.34.23](https://github.com/tendermint/tendermint/releases/tag/v0.34.23). +### State Machine Breaking + +* (x/group) [#13876](https://github.com/cosmos/cosmos-sdk/pull/13876) Fix group MinExecutionPeriod that is checked on execution now, instead of voting period end. + +### API Breaking Changes + +* (x/group) [#13876](https://github.com/cosmos/cosmos-sdk/pull/13876) Add `GetMinExecutionPeriod` method on DecisionPolicy interface. + ### Bug Fixes * (x/group) [#13869](https://github.com/cosmos/cosmos-sdk/pull/13869) Group members weight must be positive and a finite number. diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 0699aae74568..cac1c27efafd 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,14 +1,17 @@ # Cosmos SDK v0.46.5 Release Notes -This release introduces a number of bug fixes and improvements. -Notably, an upgrade to Tendermint [v0.34.23](https://github.com/tendermint/tendermint/releases/tag/v0.34.23). +This release introduces a number of bug fixes and improvements. Notably, an upgrade to Tendermint [v0.34.23](https://github.com/tendermint/tendermint/releases/tag/v0.34.23). -If your chain's state has coin metadata, an issue has been discovered in the coin metadata migration. This issue is fixed in `v0.46.5`. +If your chain's state has coin metadata, an issue has been discovered in the bank module coin metadata migration. This issue is fixed in `v0.46.5`. * If you are planning to migrate to v0.46, please use `v0.46.5`. All releases prior to `v0.46.5`, **must NOT be used**. All previous version of `v0.46` are retracted. * If your chain is already on v0.46 using `<= v0.46.4` and has coin metadata, a **coordinated upgrade** to `v0.46.5` is required. * If your chain is already on v0.46 using `<= v0.46.4` but has no coin metadata, this release is **non-breaking**. +Moreover, an issue have been found in the group module. This issue is fixed in `v0.46.5`. + +* If you use the group module, upgrade to `v0.46.5` **immediately**. A **coordinated upgrade** to `v0.46.5` is required. + Please see the [CHANGELOG](https://github.com/cosmos/cosmos-sdk/blob/release/v0.46.x/CHANGELOG.md) for an exhaustive list of changes. Full Commit History: https://github.com/cosmos/cosmos-sdk/compare/v0.46.4...v0.46.5 diff --git a/x/group/keeper/keeper_test.go b/x/group/keeper/keeper_test.go index 0db2166b6861..c7b140819f6a 100644 --- a/x/group/keeper/keeper_test.go +++ b/x/group/keeper/keeper_test.go @@ -23,6 +23,8 @@ import ( "github.com/cosmos/cosmos-sdk/x/group/module" ) +const minExecutionPeriod = 5 * time.Second + type TestSuite struct { suite.Suite @@ -64,7 +66,7 @@ func (s *TestSuite) SetupTest() { policy := group.NewThresholdDecisionPolicy( "2", time.Second, - 0, + minExecutionPeriod, // Must wait 5 seconds before executing proposal ) policyReq := &group.MsgCreateGroupPolicy{ Admin: s.addrs[0].String(), @@ -1465,34 +1467,47 @@ func (s *TestSuite) TestGroupPoliciesByAdminOrGroup() { func (s *TestSuite) TestSubmitProposal() { addrs := s.addrs addr1 := addrs[0] - addr2 := addrs[1] + addr2 := addrs[1] // Has weight 2 addr4 := addrs[3] - addr5 := addrs[4] + addr5 := addrs[4] // Has weight 1 myGroupID := s.groupID accountAddr := s.groupPolicyAddr - msgSend := &banktypes.MsgSend{ - FromAddress: s.groupPolicyAddr.String(), - ToAddress: addr2.String(), - Amount: sdk.Coins{sdk.NewInt64Coin("test", 100)}, - } - + // Create a new group policy to test TRY_EXEC policyReq := &group.MsgCreateGroupPolicy{ Admin: addr1.String(), GroupId: myGroupID, } - policy := group.NewThresholdDecisionPolicy( + noMinExecPeriodPolicy := group.NewThresholdDecisionPolicy( + "2", + time.Second, + 0, // no MinExecutionPeriod to test TRY_EXEC + ) + err := policyReq.SetDecisionPolicy(noMinExecPeriodPolicy) + s.Require().NoError(err) + res, err := s.app.GroupKeeper.CreateGroupPolicy(s.ctx, policyReq) + s.Require().NoError(err) + noMinExecPeriodPolicyAddr := sdk.MustAccAddressFromBech32(res.Address) + s.Require().NoError(testutil.FundAccount(s.app.BankKeeper, s.sdkCtx, noMinExecPeriodPolicyAddr, sdk.Coins{sdk.NewInt64Coin("test", 10000)})) + + // Create a new group policy with super high threshold + bigThresholdPolicy := group.NewThresholdDecisionPolicy( "100", time.Second, - 0, + minExecutionPeriod, ) - err := policyReq.SetDecisionPolicy(policy) + err = policyReq.SetDecisionPolicy(bigThresholdPolicy) s.Require().NoError(err) - bigThresholdRes, err := s.keeper.CreateGroupPolicy(s.ctx, policyReq) + bigThresholdRes, err := s.app.GroupKeeper.CreateGroupPolicy(s.ctx, policyReq) s.Require().NoError(err) bigThresholdAddr := bigThresholdRes.Address + msgSend := &banktypes.MsgSend{ + FromAddress: noMinExecPeriodPolicyAddr.String(), + ToAddress: addr2.String(), + Amount: sdk.Coins{sdk.NewInt64Coin("test", 100)}, + } defaultProposal := group.Proposal{ GroupPolicyAddress: accountAddr.String(), Status: group.PROPOSAL_STATUS_SUBMITTED, @@ -1605,13 +1620,13 @@ func (s *TestSuite) TestSubmitProposal() { }, "with try exec": { req: &group.MsgSubmitProposal{ - GroupPolicyAddress: accountAddr.String(), + GroupPolicyAddress: noMinExecPeriodPolicyAddr.String(), Proposers: []string{addr2.String()}, Exec: group.Exec_EXEC_TRY, }, msgs: []sdk.Msg{msgSend}, expProposal: group.Proposal{ - GroupPolicyAddress: accountAddr.String(), + GroupPolicyAddress: noMinExecPeriodPolicyAddr.String(), Status: group.PROPOSAL_STATUS_ACCEPTED, FinalTallyResult: group.TallyResult{ YesCount: "2", @@ -1622,7 +1637,7 @@ func (s *TestSuite) TestSubmitProposal() { ExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS, }, postRun: func(sdkCtx sdk.Context) { - fromBalances := s.app.BankKeeper.GetAllBalances(sdkCtx, accountAddr) + fromBalances := s.app.BankKeeper.GetAllBalances(sdkCtx, noMinExecPeriodPolicyAddr) s.Require().Contains(fromBalances, sdk.NewInt64Coin("test", 9900)) toBalances := s.app.BankKeeper.GetAllBalances(sdkCtx, addr2) s.Require().Contains(toBalances, sdk.NewInt64Coin("test", 100)) @@ -1630,13 +1645,13 @@ func (s *TestSuite) TestSubmitProposal() { }, "with try exec, not enough yes votes for proposal to pass": { req: &group.MsgSubmitProposal{ - GroupPolicyAddress: accountAddr.String(), + GroupPolicyAddress: noMinExecPeriodPolicyAddr.String(), Proposers: []string{addr5.String()}, Exec: group.Exec_EXEC_TRY, }, msgs: []sdk.Msg{msgSend}, expProposal: group.Proposal{ - GroupPolicyAddress: accountAddr.String(), + GroupPolicyAddress: noMinExecPeriodPolicyAddr.String(), Status: group.PROPOSAL_STATUS_SUBMITTED, FinalTallyResult: group.TallyResult{ YesCount: "0", // Since tally doesn't pass Allow(), we consider the proposal not final @@ -1689,6 +1704,7 @@ func (s *TestSuite) TestSubmitProposal() { } } + s.sdkCtx.WithBlockTime(s.blockTime) spec.postRun(s.sdkCtx) }) } @@ -2284,6 +2300,7 @@ func (s *TestSuite) TestExecProposal() { msgs := []sdk.Msg{msgSend1} return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES) }, + srcBlockTime: s.blockTime.Add(minExecutionPeriod), // After min execution period end expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED, expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS, expBalance: true, @@ -2295,6 +2312,7 @@ func (s *TestSuite) TestExecProposal() { msgs := []sdk.Msg{msgSend1, msgSend1} return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES) }, + srcBlockTime: s.blockTime.Add(minExecutionPeriod), // After min execution period end expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED, expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS, expBalance: true, @@ -2306,6 +2324,7 @@ func (s *TestSuite) TestExecProposal() { msgs := []sdk.Msg{msgSend1} return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_NO) }, + srcBlockTime: s.blockTime.Add(minExecutionPeriod), // After min execution period end expProposalStatus: group.PROPOSAL_STATUS_REJECTED, expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, }, @@ -2322,33 +2341,57 @@ func (s *TestSuite) TestExecProposal() { }, expErr: true, }, - "Decision policy also applied on timeout": { + "Decision policy also applied on exactly voting period end": { setupProposal: func(ctx context.Context) uint64 { msgs := []sdk.Msg{msgSend1} return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_NO) }, - srcBlockTime: s.blockTime.Add(time.Second), + srcBlockTime: s.blockTime.Add(time.Second), // Voting period is 1s expProposalStatus: group.PROPOSAL_STATUS_REJECTED, expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, }, - "Decision policy also applied after timeout": { + "Decision policy also applied after voting period end": { setupProposal: func(ctx context.Context) uint64 { msgs := []sdk.Msg{msgSend1} return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_NO) }, - srcBlockTime: s.blockTime.Add(time.Second).Add(time.Millisecond), + srcBlockTime: s.blockTime.Add(time.Second).Add(time.Millisecond), // Voting period is 1s expProposalStatus: group.PROPOSAL_STATUS_REJECTED, expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN, }, + "exec proposal before MinExecutionPeriod should fail": { + setupProposal: func(ctx context.Context) uint64 { + msgs := []sdk.Msg{msgSend1} + return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES) + }, + srcBlockTime: s.blockTime.Add(4 * time.Second), // min execution date is 5s later after s.blockTime + expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_FAILURE, // Because MinExecutionPeriod has not passed + }, + "exec proposal at exactly MinExecutionPeriod should pass": { + setupProposal: func(ctx context.Context) uint64 { + msgs := []sdk.Msg{msgSend1} + return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES) + }, + srcBlockTime: s.blockTime.Add(5 * time.Second), // min execution date is 5s later after s.blockTime + expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED, + expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS, + }, "prevent double execution when successful": { setupProposal: func(ctx context.Context) uint64 { myProposalID := submitProposalAndVote(ctx, s, []sdk.Msg{msgSend1}, proposers, group.VOTE_OPTION_YES) - _, err := s.keeper.Exec(ctx, &group.MsgExec{Executor: addr1.String(), ProposalId: myProposalID}) + // Wait after min execution period end before Exec + sdkCtx := sdk.UnwrapSDKContext(ctx) + sdkCtx = sdkCtx.WithBlockTime(sdkCtx.BlockTime().Add(minExecutionPeriod)) // MinExecutionPeriod is 5s + ctx = sdk.WrapSDKContext(sdkCtx) + + _, err := s.app.GroupKeeper.Exec(ctx, &group.MsgExec{Executor: addr1.String(), ProposalId: myProposalID}) s.Require().NoError(err) return myProposalID }, - expErr: true, // since proposal is pruned after a successful MsgExec + srcBlockTime: s.blockTime.Add(minExecutionPeriod), // After min execution period end + expErr: true, // since proposal is pruned after a successful MsgExec expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED, expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS, expBalance: true, @@ -2360,6 +2403,7 @@ func (s *TestSuite) TestExecProposal() { msgs := []sdk.Msg{msgSend1, msgSend2} return submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES) }, + srcBlockTime: s.blockTime.Add(minExecutionPeriod), // After min execution period end expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED, expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_FAILURE, }, @@ -2368,13 +2412,14 @@ func (s *TestSuite) TestExecProposal() { msgs := []sdk.Msg{msgSend2} myProposalID := submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES) - _, err := s.keeper.Exec(ctx, &group.MsgExec{Executor: addr1.String(), ProposalId: myProposalID}) - s.Require().NoError(err) + // Wait after min execution period end before Exec sdkCtx := sdk.UnwrapSDKContext(ctx) + sdkCtx = sdkCtx.WithBlockTime(sdkCtx.BlockTime().Add(minExecutionPeriod)) // MinExecutionPeriod is 5s s.Require().NoError(testutil.FundAccount(s.app.BankKeeper, sdkCtx, s.groupPolicyAddr, sdk.Coins{sdk.NewInt64Coin("test", 10002)})) return myProposalID }, + srcBlockTime: s.blockTime.Add(minExecutionPeriod), // After min execution period end expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED, expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS, }, @@ -2516,9 +2561,15 @@ func (s *TestSuite) TestExecPrunedProposalsAndVotes() { msgs := []sdk.Msg{msgSend2} myProposalID := submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES) - _, err := s.keeper.Exec(ctx, &group.MsgExec{Executor: addr1.String(), ProposalId: myProposalID}) - s.Require().NoError(err) + // Wait for min execution period end sdkCtx := sdk.UnwrapSDKContext(ctx) + sdkCtx = sdkCtx.WithBlockTime(sdkCtx.BlockTime().Add(minExecutionPeriod)) + ctx = sdk.WrapSDKContext(sdkCtx) + + _, err := s.app.GroupKeeper.Exec(ctx, &group.MsgExec{Executor: addr1.String(), ProposalId: myProposalID}) + s.Require().NoError(err) + + sdkCtx = sdk.UnwrapSDKContext(ctx) s.Require().NoError(testutil.FundAccount(s.app.BankKeeper, sdkCtx, s.groupPolicyAddr, sdk.Coins{sdk.NewInt64Coin("test", 10002)})) return myProposalID @@ -2538,6 +2589,8 @@ func (s *TestSuite) TestExecPrunedProposalsAndVotes() { sdkCtx = sdkCtx.WithBlockTime(spec.srcBlockTime) } + // Wait for min execution period end + sdkCtx = sdkCtx.WithBlockTime(sdkCtx.BlockTime().Add(minExecutionPeriod)) ctx = sdk.WrapSDKContext(sdkCtx) _, err := s.keeper.Exec(ctx, &group.MsgExec{Executor: addr1.String(), ProposalId: proposalID}) if spec.expErr { @@ -2990,3 +3043,58 @@ func (s *TestSuite) createGroupAndGroupPolicy( return policyAddr, groupID } + +func (s *TestSuite) TestTallyProposalsAtVPEnd() { + addrs := s.addrs + addr1 := addrs[0] + addr2 := addrs[1] + votingPeriod := time.Duration(4 * time.Minute) + minExecutionPeriod := votingPeriod + group.DefaultConfig().MaxExecutionPeriod + + groupMsg := &group.MsgCreateGroupWithPolicy{ + Admin: addr1.String(), + Members: []group.MemberRequest{ + {Address: addr1.String(), Weight: "1"}, + {Address: addr2.String(), Weight: "1"}, + }, + } + policy := group.NewThresholdDecisionPolicy( + "1", + votingPeriod, + minExecutionPeriod, + ) + s.Require().NoError(groupMsg.SetDecisionPolicy(policy)) + + groupRes, err := s.app.GroupKeeper.CreateGroupWithPolicy(s.ctx, groupMsg) + s.Require().NoError(err) + accountAddr := groupRes.GetGroupPolicyAddress() + groupPolicy, err := sdk.AccAddressFromBech32(accountAddr) + s.Require().NoError(err) + s.Require().NotNil(groupPolicy) + + proposalRes, err := s.app.GroupKeeper.SubmitProposal(s.ctx, &group.MsgSubmitProposal{ + GroupPolicyAddress: accountAddr, + Proposers: []string{addr1.String()}, + Messages: nil, + }) + s.Require().NoError(err) + + _, err = s.app.GroupKeeper.Vote(s.ctx, &group.MsgVote{ + ProposalId: proposalRes.ProposalId, + Voter: addr1.String(), + Option: group.VOTE_OPTION_YES, + }) + s.Require().NoError(err) + + // move forward in time + ctx := s.sdkCtx.WithBlockTime(s.sdkCtx.BlockTime().Add(votingPeriod + 1)) + + result, err := s.app.GroupKeeper.TallyResult(ctx, &group.QueryTallyResultRequest{ + ProposalId: proposalRes.ProposalId, + }) + s.Require().Equal("1", result.Tally.YesCount) + s.Require().NoError(err) + + s.Require().NoError(s.app.GroupKeeper.TallyProposalsAtVPEnd(ctx)) + s.NotPanics(func() { module.EndBlocker(ctx, s.app.GroupKeeper) }) +} diff --git a/x/group/keeper/msg_server.go b/x/group/keeper/msg_server.go index 2320b417ca4c..0023c57a90b4 100644 --- a/x/group/keeper/msg_server.go +++ b/x/group/keeper/msg_server.go @@ -685,8 +685,7 @@ func (k Keeper) doTallyAndUpdate(ctx sdk.Context, p *group.Proposal, electorate return err } - sinceSubmission := ctx.BlockTime().Sub(p.SubmitTime) // duration passed since proposal submission. - result, err := policy.Allow(tallyResult, electorate.TotalWeight, sinceSubmission) + result, err := policy.Allow(tallyResult, electorate.TotalWeight) if err != nil { return sdkerrors.Wrap(err, "policy allow") } @@ -752,7 +751,8 @@ func (k Keeper) Exec(goCtx context.Context, req *group.MsgExec) (*group.MsgExecR return nil, err } - if results, err := k.doExecuteMsgs(cacheCtx, k.router, proposal, addr); err != nil { + decisionPolicy := policyInfo.DecisionPolicy.GetCachedValue().(group.DecisionPolicy) + if results, err := k.doExecuteMsgs(cacheCtx, k.router, proposal, addr, decisionPolicy); err != nil { proposal.ExecutorResult = group.PROPOSAL_EXECUTOR_RESULT_FAILURE logs = fmt.Sprintf("proposal execution failed on proposal %d, because of error %s", id, err.Error()) k.Logger(ctx).Info("proposal execution failed", "cause", err, "proposalID", id) diff --git a/x/group/keeper/proposal_executor.go b/x/group/keeper/proposal_executor.go index 29d9d1e8bed9..d0c612f664b7 100644 --- a/x/group/keeper/proposal_executor.go +++ b/x/group/keeper/proposal_executor.go @@ -12,7 +12,13 @@ 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 middlewares are bypassed -func (s Keeper) doExecuteMsgs(ctx sdk.Context, router *baseapp.MsgServiceRouter, proposal group.Proposal, groupPolicyAcc sdk.AccAddress) ([]sdk.Result, error) { +func (s Keeper) doExecuteMsgs(ctx sdk.Context, router *baseapp.MsgServiceRouter, proposal group.Proposal, groupPolicyAcc sdk.AccAddress, decisionPolicy group.DecisionPolicy) ([]sdk.Result, error) { + // Ensure it's not too early to execute the messages. + minExecutionDate := proposal.SubmitTime.Add(decisionPolicy.GetMinExecutionPeriod()) + if ctx.BlockTime().Before(minExecutionDate) { + return nil, grouperrors.ErrInvalid.Wrapf("must wait until %s to execute proposal %d", minExecutionDate, proposal.Id) + } + // 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 diff --git a/x/group/spec/01_concepts.md b/x/group/spec/01_concepts.md index 07d42b10c784..fb581930c2f1 100644 --- a/x/group/spec/01_concepts.md +++ b/x/group/spec/01_concepts.md @@ -54,6 +54,16 @@ A threshold decision policy defines a threshold of yes votes (based on a tally of voter weights) that must be achieved in order for a proposal to pass. For this decision policy, abstain and veto are simply treated as no's. +This decision policy also has a VotingPeriod window and a MinExecutionPeriod +window. The former defines the duration after proposal submission where members +are allowed to vote, after which tallying is performed. The latter specifies +the minimum duration after proposal submission where the proposal can be +executed. If set to 0, then the proposal is allowed to be executed immediately +on submission (using the `TRY_EXEC` option). Obviously, MinExecutionPeriod +cannot be greater than VotingPeriod+MaxExecutionPeriod (where MaxExecution is +the app-defined duration that specifies the window after voting ended where a +proposal can be executed). + ### Percentage decision policy A percentage decision policy is similar to a threshold decision policy, except @@ -62,6 +72,9 @@ It's more suited for groups where the group members' weights can be updated, as the percentage threshold stays the same, and doesn't depend on how those member weights get updated. +Same as the Threshold decision policy, the percentage decision policy has the +two VotingPeriod and MinExecutionPeriod parameters. + ## Proposal Any member(s) of a group can submit a proposal for a group policy account to decide upon. diff --git a/x/group/types.go b/x/group/types.go index f6b757f4d70d..a48a851f73fd 100644 --- a/x/group/types.go +++ b/x/group/types.go @@ -31,10 +31,14 @@ type DecisionPolicy interface { // GetVotingPeriod returns the duration after proposal submission where // votes are accepted. GetVotingPeriod() time.Duration + // GetMinExecutionPeriod returns the minimum duration after submission + // where we can execution a proposal. It can be set to 0 or to a value + // lesser than VotingPeriod to allow TRY_EXEC. + GetMinExecutionPeriod() time.Duration // Allow defines policy-specific logic to allow a proposal to pass or not, // based on its tally result, the group's total power and the time since // the proposal was submitted. - Allow(tallyResult TallyResult, totalPower string, sinceSubmission time.Duration) (DecisionPolicyResult, error) + Allow(tallyResult TallyResult, totalPower string) (DecisionPolicyResult, error) ValidateBasic() error Validate(g GroupInfo, config Config) error @@ -52,6 +56,10 @@ func (p ThresholdDecisionPolicy) GetVotingPeriod() time.Duration { return p.Windows.VotingPeriod } +func (p ThresholdDecisionPolicy) GetMinExecutionPeriod() time.Duration { + return p.Windows.MinExecutionPeriod +} + func (p ThresholdDecisionPolicy) ValidateBasic() error { if _, err := math.NewPositiveDecFromString(p.Threshold); err != nil { return sdkerrors.Wrap(err, "threshold") @@ -65,11 +73,7 @@ func (p ThresholdDecisionPolicy) ValidateBasic() error { } // Allow allows a proposal to pass when the tally of yes votes equals or exceeds the threshold before the timeout. -func (p ThresholdDecisionPolicy) Allow(tallyResult TallyResult, totalPower string, sinceSubmission time.Duration) (DecisionPolicyResult, error) { - if sinceSubmission < p.Windows.MinExecutionPeriod { - return DecisionPolicyResult{}, errors.ErrUnauthorized.Wrapf("must wait %s after submission before execution, currently at %s", p.Windows.MinExecutionPeriod, sinceSubmission) - } - +func (p ThresholdDecisionPolicy) Allow(tallyResult TallyResult, totalPower string) (DecisionPolicyResult, error) { threshold, err := math.NewPositiveDecFromString(p.Threshold) if err != nil { return DecisionPolicyResult{}, sdkerrors.Wrap(err, "threshold") @@ -154,6 +158,10 @@ func (p PercentageDecisionPolicy) GetVotingPeriod() time.Duration { return p.Windows.VotingPeriod } +func (p PercentageDecisionPolicy) GetMinExecutionPeriod() time.Duration { + return p.Windows.MinExecutionPeriod +} + func (p PercentageDecisionPolicy) ValidateBasic() error { percentage, err := math.NewPositiveDecFromString(p.Percentage) if err != nil { @@ -178,11 +186,7 @@ func (p *PercentageDecisionPolicy) Validate(g GroupInfo, config Config) error { } // Allow allows a proposal to pass when the tally of yes votes equals or exceeds the percentage threshold before the timeout. -func (p PercentageDecisionPolicy) Allow(tally TallyResult, totalPower string, sinceSubmission time.Duration) (DecisionPolicyResult, error) { - if sinceSubmission < p.Windows.MinExecutionPeriod { - return DecisionPolicyResult{}, errors.ErrUnauthorized.Wrapf("must wait %s after submission before execution, currently at %s", p.Windows.MinExecutionPeriod, sinceSubmission) - } - +func (p PercentageDecisionPolicy) Allow(tally TallyResult, totalPower string) (DecisionPolicyResult, error) { percentage, err := math.NewPositiveDecFromString(p.Percentage) if err != nil { return DecisionPolicyResult{}, sdkerrors.Wrap(err, "percentage") diff --git a/x/group/types_test.go b/x/group/types_test.go index db40b238433f..0bf8f2e70f06 100644 --- a/x/group/types_test.go +++ b/x/group/types_test.go @@ -239,30 +239,10 @@ func TestPercentageDecisionPolicyAllow(t *testing.T) { }, false, }, - { - "time since submission < min execution period", - &group.PercentageDecisionPolicy{ - Percentage: "0.5", - Windows: &group.DecisionPolicyWindows{ - VotingPeriod: time.Second * 10, - MinExecutionPeriod: time.Minute, - }, - }, - &group.TallyResult{ - YesCount: "2", - NoCount: "0", - AbstainCount: "0", - NoWithVetoCount: "0", - }, - "3", - time.Duration(time.Second * 50), - group.DecisionPolicyResult{}, - true, - }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - policyResult, err := tc.policy.Allow(*tc.tally, tc.totalPower, tc.votingDuration) + policyResult, err := tc.policy.Allow(*tc.tally, tc.totalPower) if tc.expErr { require.Error(t, err) } else { @@ -393,33 +373,10 @@ func TestThresholdDecisionPolicyAllow(t *testing.T) { }, false, }, - { - "time since submission < min execution period", - &group.ThresholdDecisionPolicy{ - Threshold: "3", - Windows: &group.DecisionPolicyWindows{ - VotingPeriod: time.Second * 10, - MinExecutionPeriod: time.Minute, - }, - }, - &group.TallyResult{ - YesCount: "3", - NoCount: "0", - AbstainCount: "0", - NoWithVetoCount: "0", - }, - "3", - time.Duration(time.Second * 50), - group.DecisionPolicyResult{ - Allow: false, - Final: true, - }, - true, - }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - policyResult, err := tc.policy.Allow(*tc.tally, tc.totalPower, tc.votingDuration) + policyResult, err := tc.policy.Allow(*tc.tally, tc.totalPower) if tc.expErr { require.Error(t, err) } else {