diff --git a/CHANGELOG.md b/CHANGELOG.md index c61d11a6d03a..f0cbd06da5cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -114,6 +114,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ * (x/gov) [#12771](https://github.com/cosmos/cosmos-sdk/pull/12771) Initial deposit requirement for proposals at submission time. * (x/staking) [#12967](https://github.com/cosmos/cosmos-sdk/pull/12967) `unbond` now creates only one unbonding delegation entry when multiple unbondings exist at a single height (e.g. through multiple messages in a transaction). * (x/auth/vesting) [#13502](https://github.com/cosmos/cosmos-sdk/pull/13502) Add Amino Msg registration for `MsgCreatePeriodicVestingAccount`. +* (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 @@ -168,6 +169,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ * [#13794](https://github.com/cosmos/cosmos-sdk/pull/13794) Most methods on `types/module.AppModule` have been moved to extension interfaces. `module.Manager.Modules` is now of type `map[string]interface{}` to support in parallel the new `cosmossdk.io/core/appmodule.AppModule` API. +* (x/group) [#13876](https://github.com/cosmos/cosmos-sdk/pull/13876) Add `GetMinExecutionPeriod` method on DecisionPolicy interface. ### CLI Breaking Changes diff --git a/x/group/README.md b/x/group/README.md index 802f438872c7..edbbc3486775 100644 --- a/x/group/README.md +++ b/x/group/README.md @@ -109,6 +109,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 @@ -117,6 +127,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/keeper/keeper_test.go b/x/group/keeper/keeper_test.go index 017c1981346a..33a0b969db37 100644 --- a/x/group/keeper/keeper_test.go +++ b/x/group/keeper/keeper_test.go @@ -33,6 +33,8 @@ import ( minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" ) +const minExecutionPeriod = 5 * time.Second + type TestSuite struct { suite.Suite @@ -98,7 +100,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(), @@ -1551,36 +1553,48 @@ 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( - "100", + noMinExecPeriodPolicy := group.NewThresholdDecisionPolicy( + "2", time.Second, - 0, + 0, // no MinExecutionPeriod to test TRY_EXEC ) - err := policyReq.SetDecisionPolicy(policy) + err := policyReq.SetDecisionPolicy(noMinExecPeriodPolicy) s.Require().NoError(err) + s.setNextAccount() + res, err := s.groupKeeper.CreateGroupPolicy(s.ctx, policyReq) + s.Require().NoError(err) + noMinExecPeriodPolicyAddr := sdk.MustAccAddressFromBech32(res.Address) + // Create a new group policy with super high threshold + bigThresholdPolicy := group.NewThresholdDecisionPolicy( + "100", + time.Second, + minExecutionPeriod, + ) s.setNextAccount() + err = policyReq.SetDecisionPolicy(bigThresholdPolicy) + s.Require().NoError(err) bigThresholdRes, err := s.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, @@ -1699,13 +1713,13 @@ func (s *TestSuite) TestSubmitProposal() { } }, 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", @@ -1716,10 +1730,10 @@ func (s *TestSuite) TestSubmitProposal() { ExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS, }, postRun: func(sdkCtx sdk.Context) { - s.bankKeeper.EXPECT().GetAllBalances(sdkCtx, accountAddr).Return(sdk.NewCoins(sdk.NewInt64Coin("test", 9900))) + s.bankKeeper.EXPECT().GetAllBalances(sdkCtx, noMinExecPeriodPolicyAddr).Return(sdk.NewCoins(sdk.NewInt64Coin("test", 9900))) s.bankKeeper.EXPECT().GetAllBalances(sdkCtx, addr2).Return(sdk.NewCoins(sdk.NewInt64Coin("test", 100))) - fromBalances := s.bankKeeper.GetAllBalances(sdkCtx, accountAddr) + fromBalances := s.bankKeeper.GetAllBalances(sdkCtx, noMinExecPeriodPolicyAddr) s.Require().Contains(fromBalances, sdk.NewInt64Coin("test", 9900)) toBalances := s.bankKeeper.GetAllBalances(sdkCtx, addr2) s.Require().Contains(toBalances, sdk.NewInt64Coin("test", 100)) @@ -1727,13 +1741,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 @@ -2399,6 +2413,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, @@ -2412,6 +2427,7 @@ func (s *TestSuite) TestExecProposal() { 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, @@ -2423,6 +2439,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, }, @@ -2439,34 +2456,59 @@ 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} + s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend1).Return(nil, nil) + 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) s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend1).Return(nil, nil) + // 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.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, @@ -2481,6 +2523,7 @@ func (s *TestSuite) TestExecProposal() { 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, }, @@ -2489,6 +2532,11 @@ func (s *TestSuite) TestExecProposal() { msgs := []sdk.Msg{msgSend2} myProposalID := submitProposalAndVote(ctx, s, msgs, proposers, group.VOTE_OPTION_YES) + // 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) + s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend2).Return(nil, fmt.Errorf("error")) _, err := s.groupKeeper.Exec(ctx, &group.MsgExec{Executor: addr1.String(), ProposalId: myProposalID}) s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend2).Return(nil, nil) @@ -2498,6 +2546,7 @@ func (s *TestSuite) TestExecProposal() { return myProposalID }, + srcBlockTime: s.blockTime.Add(minExecutionPeriod), // After min execution period end expProposalStatus: group.PROPOSAL_STATUS_ACCEPTED, expExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_SUCCESS, }, @@ -2689,6 +2738,11 @@ func (s *TestSuite) TestExecPrunedProposalsAndVotes() { s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend2).Return(nil, fmt.Errorf("error")) + // Wait for min execution period end + sdkCtx := sdk.UnwrapSDKContext(ctx) + sdkCtx = sdkCtx.WithBlockTime(sdkCtx.BlockTime().Add(minExecutionPeriod)) + ctx = sdk.WrapSDKContext(sdkCtx) + _, err := s.groupKeeper.Exec(ctx, &group.MsgExec{Executor: addr1.String(), ProposalId: myProposalID}) s.bankKeeper.EXPECT().Send(gomock.Any(), msgSend2).Return(nil, nil) @@ -2710,6 +2764,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.groupKeeper.Exec(ctx, &group.MsgExec{Executor: addr1.String(), ProposalId: proposalID}) if spec.expErr { @@ -3173,3 +3229,59 @@ 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)) + + s.setNextAccount() + groupRes, err := s.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.groupKeeper.SubmitProposal(s.ctx, &group.MsgSubmitProposal{ + GroupPolicyAddress: accountAddr, + Proposers: []string{addr1.String()}, + Messages: nil, + }) + s.Require().NoError(err) + + _, err = s.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.groupKeeper.TallyResult(ctx, &group.QueryTallyResultRequest{ + ProposalId: proposalRes.ProposalId, + }) + s.Require().Equal("1", result.Tally.YesCount) + s.Require().NoError(err) + + s.Require().NoError(s.groupKeeper.TallyProposalsAtVPEnd(ctx)) + s.NotPanics(func() { module.EndBlocker(ctx, s.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 bc1317c629ac..4ae0b305179f 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, errors.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/types.go b/x/group/types.go index ce01ff7b267d..49798968af46 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 {