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): Update TresholdDecisionPolicy to handle threshold>totalWeight #11325

Merged
merged 6 commits into from
Mar 8, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 19 additions & 9 deletions x/group/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1422,8 +1422,9 @@ func (s *TestSuite) TestSubmitProposal() {
bigThresholdAddr := bigThresholdRes.Address

defaultProposal := group.Proposal{
Status: group.PROPOSAL_STATUS_SUBMITTED,
Result: group.PROPOSAL_RESULT_UNFINALIZED,
Address: accountAddr.String(),
Status: group.PROPOSAL_STATUS_SUBMITTED,
Result: group.PROPOSAL_RESULT_UNFINALIZED,
FinalTallyResult: group.TallyResult{
YesCount: "0",
NoCount: "0",
Expand Down Expand Up @@ -1484,12 +1485,19 @@ func (s *TestSuite) TestSubmitProposal() {
expErr: true,
postRun: func(sdkCtx sdk.Context) {},
},
"impossible case: decision policy threshold > total group weight": {
"decision policy threshold > total group weight": {
req: &group.MsgSubmitProposal{
Address: bigThresholdAddr,
Proposers: []string{addr2.String()},
},
expErr: true,
expErr: false,
expProposal: group.Proposal{
Address: bigThresholdAddr,
Status: group.PROPOSAL_STATUS_SUBMITTED,
Result: group.PROPOSAL_RESULT_UNFINALIZED,
FinalTallyResult: group.DefaultTallyResult(),
ExecutorResult: group.PROPOSAL_EXECUTOR_RESULT_NOT_RUN,
},
postRun: func(sdkCtx sdk.Context) {},
},
"only group members can create a proposal": {
Expand Down Expand Up @@ -1533,8 +1541,9 @@ func (s *TestSuite) TestSubmitProposal() {
},
msgs: []sdk.Msg{msgSend},
expProposal: group.Proposal{
Status: group.PROPOSAL_STATUS_CLOSED,
Result: group.PROPOSAL_RESULT_ACCEPTED,
Address: accountAddr.String(),
Status: group.PROPOSAL_STATUS_CLOSED,
Result: group.PROPOSAL_RESULT_ACCEPTED,
FinalTallyResult: group.TallyResult{
YesCount: "2",
NoCount: "0",
Expand All @@ -1558,8 +1567,9 @@ func (s *TestSuite) TestSubmitProposal() {
},
msgs: []sdk.Msg{msgSend},
expProposal: group.Proposal{
Status: group.PROPOSAL_STATUS_SUBMITTED,
Result: group.PROPOSAL_RESULT_UNFINALIZED,
Address: accountAddr.String(),
Status: group.PROPOSAL_STATUS_SUBMITTED,
Result: group.PROPOSAL_RESULT_UNFINALIZED,
FinalTallyResult: group.TallyResult{
YesCount: "0", // Since tally doesn't pass Allow(), we consider the proposal not final
NoCount: "0",
Expand Down Expand Up @@ -1590,7 +1600,7 @@ func (s *TestSuite) TestSubmitProposal() {
s.Require().NoError(err)
proposal := proposalRes.Proposal

s.Assert().Equal(accountAddr.String(), proposal.Address)
s.Assert().Equal(spec.expProposal.Address, proposal.Address)
s.Assert().Equal(spec.req.Metadata, proposal.Metadata)
s.Assert().Equal(spec.req.Proposers, proposal.Proposers)
s.Assert().Equal(s.blockTime, proposal.SubmitTime)
Expand Down
54 changes: 53 additions & 1 deletion x/group/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,11 @@ func (k Keeper) UpdateGroupMembers(goCtx context.Context, req *group.MsgUpdateGr
// Update group in the groupTable.
g.TotalWeight = totalWeight.String()
g.Version++

if err := k.validateDecisionPolicies(ctx, *g); err != nil {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm adding DecisionPolicy.Validate() calls in a few places in the msg server:

  • MsgCreateGroupPolicy and MsgCreateGroupWithPolicy
  • MsgUpdateDecisionPolicy
  • MsgLeaveGroup
  • MsgUpdateGroupMembers

anywhere else you think we need to add validation?

return err
}

return k.groupTable.Update(ctx.KVStore(k.key), g.Id, g)
}

Expand Down Expand Up @@ -314,6 +319,11 @@ func (k Keeper) CreateGroupPolicy(goCtx context.Context, req *group.MsgCreateGro
return nil, sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "not group admin")
}

err = policy.Validate(g, k.config)
if err != nil {
return nil, err
}

// Generate account address of group policy.
var accountAddr sdk.AccAddress
// loop here in the rare case of a collision
Expand Down Expand Up @@ -386,7 +396,17 @@ func (k Keeper) UpdateGroupPolicyDecisionPolicy(goCtx context.Context, req *grou
policy := req.GetDecisionPolicy()

action := func(groupPolicy *group.GroupPolicyInfo) error {
err := groupPolicy.SetDecisionPolicy(policy)
g, err := k.getGroupInfo(ctx, groupPolicy.GroupId)
if err != nil {
return err
}

err = policy.Validate(g, k.config)
if err != nil {
return err
}

err = groupPolicy.SetDecisionPolicy(policy)
if err != nil {
return err
}
Expand Down Expand Up @@ -840,6 +860,10 @@ func (k Keeper) LeaveGroup(goCtx context.Context, req *group.MsgLeaveGroup) (*gr
return nil, err
}

if err := k.validateDecisionPolicies(ctx, groupInfo); err != nil {
return nil, err
}

ctx.EventManager().EmitTypedEvent(&group.EventLeaveGroup{
GroupId: req.GroupId,
Address: req.Address,
Expand Down Expand Up @@ -949,3 +973,31 @@ func (k Keeper) assertMetadataLength(metadata string, description string) error
}
return nil
}

// validateDecisionPolicies loops through all decision policies from the group,
// and calls each of their Validate() method.
func (k Keeper) validateDecisionPolicies(ctx sdk.Context, g group.GroupInfo) error {
it, err := k.groupPolicyByGroupIndex.Get(ctx.KVStore(k.key), g.Id)
if err != nil {
return err
}
defer it.Close()

var groupPolicy group.GroupPolicyInfo
for {
_, err = it.LoadNext(&groupPolicy)
if errors.ErrORMIteratorDone.Is(err) {
break
}
if err != nil {
return err
}

err = groupPolicy.DecisionPolicy.GetCachedValue().(group.DecisionPolicy).Validate(g, k.config)
if err != nil {
return err
}
}

return nil
}
41 changes: 30 additions & 11 deletions x/group/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,23 @@ func (p ThresholdDecisionPolicy) Allow(tallyResult TallyResult, totalPower strin
if err != nil {
return DecisionPolicyResult{}, err
}
if yesCount.Cmp(threshold) >= 0 {
return DecisionPolicyResult{Allow: true, Final: true}, nil
}

totalPowerDec, err := math.NewNonNegativeDecFromString(totalPower)
if err != nil {
return DecisionPolicyResult{}, err
}

// the real threshold of the policy is `min(threshold,total_weight)`. If
// the group member weights changes (member leaving, member weight update)
// and the threshold doesn't, we can end up with threshold > total_weight.
// In this case, as long as everyone votes yes (in which case
// `yesCount`==`realThreshold`), then the proposal still passes.
realThreshold := min(threshold, totalPowerDec)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AmauryM here can we just check if everyone voted yes instead of checking against realThreshold?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. There are 2 scenarios:

  • if threshold>totalPower, then we should check that everyone votes yes
  • if threshold<totalPower, we don't need everyone to vote yes

In the 2nd cases, we don't check everyone voted yes. The realThreshold is used to share logic to avoid having an if then else case, but I can introduce that if it's clearer.

Copy link
Collaborator

@anilcse anilcse Mar 8, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes. I don't have a strong preference now. Let's go with this unless anyone are strongly in favor of checking all-yes-votes for the special case. @blushi @robert-zaremba wdyt?


if yesCount.Cmp(realThreshold) >= 0 {
return DecisionPolicyResult{Allow: true, Final: true}, nil
}

totalCounts, err := tallyResult.TotalCounts()
if err != nil {
return DecisionPolicyResult{}, err
Expand All @@ -90,29 +99,39 @@ func (p ThresholdDecisionPolicy) Allow(tallyResult TallyResult, totalPower strin
if err != nil {
return DecisionPolicyResult{}, err
}
sum, err := yesCount.Add(undecided)
// maxYesCount is the max number of yes count, i.e the current yes count
amaury1093 marked this conversation as resolved.
Show resolved Hide resolved
// plus all undecided count (supposing they all vote yes).
maxYesCount, err := yesCount.Add(undecided)
if err != nil {
return DecisionPolicyResult{}, err
}
if sum.Cmp(threshold) < 0 {

if maxYesCount.Cmp(realThreshold) < 0 {
return DecisionPolicyResult{Allow: false, Final: true}, nil
}
return DecisionPolicyResult{Allow: false, Final: false}, nil
}

// Validate returns an error if policy threshold is greater than the total group weight
func min(a, b math.Dec) math.Dec {
if a.Cmp(b) < 0 {
return a
}
return b
}

// Validate validates the policy against the group. Note that the threshold
// can actually be greater than the group's total weight: in the Allow method
// we check the tally weight against `min(threshold,total_weight)`.
func (p *ThresholdDecisionPolicy) Validate(g GroupInfo, config Config) error {
threshold, err := math.NewPositiveDecFromString(p.Threshold)
_, err := math.NewPositiveDecFromString(p.Threshold)
if err != nil {
return sdkerrors.Wrap(err, "threshold")
}
totalWeight, err := math.NewNonNegativeDecFromString(g.TotalWeight)
_, err = math.NewNonNegativeDecFromString(g.TotalWeight)
if err != nil {
return sdkerrors.Wrap(err, "group total weight")
}
if threshold.Cmp(totalWeight) > 0 {
return sdkerrors.Wrapf(errors.ErrInvalid, "policy threshold %s should not be greater than the total group weight %s", p.Threshold, g.TotalWeight)
}

if p.Windows.MinExecutionPeriod > p.Windows.VotingPeriod+config.MaxExecutionPeriod {
return sdkerrors.Wrap(errors.ErrInvalid, "min_execution_period should be smaller than voting_period + max_execution_period")
}
Expand Down
55 changes: 33 additions & 22 deletions x/group/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,6 @@ func TestThresholdDecisionPolicyValidate(t *testing.T) {
policy group.ThresholdDecisionPolicy
expErr bool
}{

{
"threshold bigger than total weight",
group.ThresholdDecisionPolicy{
Threshold: "12",
Windows: &group.DecisionPolicyWindows{
VotingPeriod: time.Second,
},
},
true,
},
{
"min exec period too big",
group.ThresholdDecisionPolicy{
Expand Down Expand Up @@ -297,13 +286,13 @@ func TestThresholdDecisionPolicyAllow(t *testing.T) {
{
"YesCount >= threshold decision policy",
&group.ThresholdDecisionPolicy{
Threshold: "3",
Threshold: "2",
Windows: &group.DecisionPolicyWindows{
VotingPeriod: time.Second * 100,
},
},
&group.TallyResult{
YesCount: "3",
YesCount: "2",
NoCount: "0",
AbstainCount: "0",
NoWithVetoCount: "0",
Expand All @@ -319,7 +308,7 @@ func TestThresholdDecisionPolicyAllow(t *testing.T) {
{
"YesCount < threshold decision policy",
&group.ThresholdDecisionPolicy{
Threshold: "3",
Threshold: "2",
Windows: &group.DecisionPolicyWindows{
VotingPeriod: time.Second * 100,
},
Expand All @@ -339,20 +328,42 @@ func TestThresholdDecisionPolicyAllow(t *testing.T) {
false,
},
{
"sum votes < threshold decision policy",
"YesCount == group total weight < threshold",
&group.ThresholdDecisionPolicy{
Threshold: "3",
Threshold: "20",
Windows: &group.DecisionPolicyWindows{
VotingPeriod: time.Second * 100,
},
},
&group.TallyResult{
YesCount: "1",
YesCount: "3",
NoCount: "0",
AbstainCount: "0",
NoWithVetoCount: "0",
},
"2",
"3",
time.Duration(time.Second * 50),
group.DecisionPolicyResult{
Allow: true,
Final: true,
},
false,
},
{
"maxYesCount < threshold decision policy",
&group.ThresholdDecisionPolicy{
Threshold: "2",
Windows: &group.DecisionPolicyWindows{
VotingPeriod: time.Second * 100,
},
},
&group.TallyResult{
YesCount: "0",
NoCount: "1",
AbstainCount: "1",
NoWithVetoCount: "0",
},
"3",
time.Duration(time.Second * 50),
group.DecisionPolicyResult{
Allow: false,
Expand All @@ -361,16 +372,16 @@ func TestThresholdDecisionPolicyAllow(t *testing.T) {
false,
},
{
"sum votes >= threshold decision policy",
"maxYesCount >= threshold decision policy",
&group.ThresholdDecisionPolicy{
Threshold: "3",
Threshold: "2",
Windows: &group.DecisionPolicyWindows{
VotingPeriod: time.Second * 100,
},
},
&group.TallyResult{
YesCount: "1",
NoCount: "0",
YesCount: "0",
NoCount: "1",
AbstainCount: "0",
NoWithVetoCount: "0",
},
Expand Down