Skip to content

Commit

Permalink
fix(group)!: Fix group min execution period (#13876)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
amaury1093 committed Nov 16, 2022
1 parent 3423442 commit 7661f62
Show file tree
Hide file tree
Showing 7 changed files with 178 additions and 84 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
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
13 changes: 13 additions & 0 deletions x/group/README.md
Expand Up @@ -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
Expand All @@ -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.
Expand Down
160 changes: 136 additions & 24 deletions x/group/keeper/keeper_test.go
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 @@ -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,
Expand Down Expand Up @@ -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",
Expand All @@ -1716,24 +1730,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 @@ -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,
Expand All @@ -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,
Expand All @@ -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,
},
Expand All @@ -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,
Expand All @@ -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,
},
Expand All @@ -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)
Expand All @@ -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,
},
Expand Down Expand Up @@ -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)

Expand All @@ -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 {
Expand Down Expand Up @@ -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) })
}
6 changes: 3 additions & 3 deletions x/group/keeper/msg_server.go
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 err != nil {
return sdkerrors.Wrap(err, "policy allow")
}
Expand Down Expand Up @@ -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)
Expand Down
8 changes: 7 additions & 1 deletion x/group/keeper/proposal_executor.go
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

0 comments on commit 7661f62

Please sign in to comment.