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

fix(group)!: Fix group min execution period #13876

Merged
merged 12 commits into from
Nov 16, 2022
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
160 changes: 136 additions & 24 deletions x/group/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import (
minttypes "github.com/cosmos/cosmos-sdk/x/mint/types"
)

const minExecutionPeriod = 5 * time.Second

type TestSuite struct {
suite.Suite

Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -1531,36 +1533,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,
Expand Down Expand Up @@ -1679,13 +1693,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",
Expand All @@ -1696,24 +1710,24 @@ 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))
},
},
"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
Expand Down Expand Up @@ -2379,6 +2393,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,
Expand All @@ -2392,6 +2407,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,
Expand All @@ -2403,6 +2419,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,
},
Expand All @@ -2419,34 +2436,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,
Expand All @@ -2461,6 +2503,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,
},
Expand All @@ -2469,6 +2512,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)
Expand All @@ -2478,6 +2526,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,
},
Expand Down Expand Up @@ -2669,6 +2718,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)

Expand All @@ -2690,6 +2744,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 {
Expand Down Expand Up @@ -3153,3 +3209,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) })
}
6 changes: 3 additions & 3 deletions x/group/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 the result was final (i.e. enough votes to pass) or if the voting
// period ended, then we consider the proposal as final.
isFinal := result.Final || ctx.BlockTime().After(p.VotingPeriodEnd)
Expand Down Expand Up @@ -754,7 +753,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)
Expand Down
8 changes: 7 additions & 1 deletion x/group/keeper/proposal_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading