github.com/cosmos/cosmos-sdk@v0.50.10/x/group/types.go (about) 1 package group 2 3 import ( 4 "fmt" 5 "time" 6 7 proto "github.com/cosmos/gogoproto/proto" 8 9 errorsmod "cosmossdk.io/errors" 10 11 codectypes "github.com/cosmos/cosmos-sdk/codec/types" 12 sdk "github.com/cosmos/cosmos-sdk/types" 13 sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" 14 "github.com/cosmos/cosmos-sdk/x/group/errors" 15 "github.com/cosmos/cosmos-sdk/x/group/internal/math" 16 "github.com/cosmos/cosmos-sdk/x/group/internal/orm" 17 ) 18 19 // DecisionPolicyResult is the result of whether a proposal passes or not a 20 // decision policy. 21 type DecisionPolicyResult struct { 22 // Allow determines if the proposal is allowed to pass. 23 Allow bool 24 // Final determines if the tally result is final or not. If final, then 25 // votes are pruned, and the tally result is saved in the proposal's 26 // `FinalTallyResult` field. 27 Final bool 28 } 29 30 // DecisionPolicy is the persistent set of rules to determine the result of election on a proposal. 31 type DecisionPolicy interface { 32 proto.Message 33 34 // GetVotingPeriod returns the duration after proposal submission where 35 // votes are accepted. 36 GetVotingPeriod() time.Duration 37 // GetMinExecutionPeriod returns the minimum duration after submission 38 // where we can execution a proposal. It can be set to 0 or to a value 39 // lesser than VotingPeriod to allow TRY_EXEC. 40 GetMinExecutionPeriod() time.Duration 41 // Allow defines policy-specific logic to allow a proposal to pass or not, 42 // based on its tally result, the group's total power and the time since 43 // the proposal was submitted. 44 Allow(tallyResult TallyResult, totalPower string) (DecisionPolicyResult, error) 45 46 ValidateBasic() error 47 Validate(g GroupInfo, config Config) error 48 } 49 50 // Implements DecisionPolicy Interface 51 var _ DecisionPolicy = &ThresholdDecisionPolicy{} 52 53 // NewThresholdDecisionPolicy creates a threshold DecisionPolicy 54 func NewThresholdDecisionPolicy(threshold string, votingPeriod, minExecutionPeriod time.Duration) DecisionPolicy { 55 return &ThresholdDecisionPolicy{threshold, &DecisionPolicyWindows{votingPeriod, minExecutionPeriod}} 56 } 57 58 // GetVotingPeriod returns the voitng period of ThresholdDecisionPolicy 59 func (p ThresholdDecisionPolicy) GetVotingPeriod() time.Duration { 60 return p.Windows.VotingPeriod 61 } 62 63 // GetMinExecutionPeriod returns the minimum execution period of ThresholdDecisionPolicy 64 func (p ThresholdDecisionPolicy) GetMinExecutionPeriod() time.Duration { 65 return p.Windows.MinExecutionPeriod 66 } 67 68 // ValidateBasic does basic validation on ThresholdDecisionPolicy 69 func (p ThresholdDecisionPolicy) ValidateBasic() error { 70 if _, err := math.NewPositiveDecFromString(p.Threshold); err != nil { 71 return errorsmod.Wrap(err, "threshold") 72 } 73 74 if p.Windows == nil || p.Windows.VotingPeriod == 0 { 75 return errorsmod.Wrap(errors.ErrInvalid, "voting period cannot be zero") 76 } 77 78 return nil 79 } 80 81 // Allow allows a proposal to pass when the tally of yes votes equals or exceeds the threshold before the timeout. 82 func (p ThresholdDecisionPolicy) Allow(tallyResult TallyResult, totalPower string) (DecisionPolicyResult, error) { 83 threshold, err := math.NewPositiveDecFromString(p.Threshold) 84 if err != nil { 85 return DecisionPolicyResult{}, errorsmod.Wrap(err, "threshold") 86 } 87 yesCount, err := math.NewNonNegativeDecFromString(tallyResult.YesCount) 88 if err != nil { 89 return DecisionPolicyResult{}, errorsmod.Wrap(err, "yes count") 90 } 91 92 totalPowerDec, err := math.NewNonNegativeDecFromString(totalPower) 93 if err != nil { 94 return DecisionPolicyResult{}, errorsmod.Wrap(err, "total power") 95 } 96 97 // the real threshold of the policy is `min(threshold,total_weight)`. If 98 // the group member weights changes (member leaving, member weight update) 99 // and the threshold doesn't, we can end up with threshold > total_weight. 100 // In this case, as long as everyone votes yes (in which case 101 // `yesCount`==`realThreshold`), then the proposal still passes. 102 realThreshold := min(threshold, totalPowerDec) 103 104 if yesCount.Cmp(realThreshold) >= 0 { 105 return DecisionPolicyResult{Allow: true, Final: true}, nil 106 } 107 108 totalCounts, err := tallyResult.TotalCounts() 109 if err != nil { 110 return DecisionPolicyResult{}, err 111 } 112 undecided, err := math.SubNonNegative(totalPowerDec, totalCounts) 113 if err != nil { 114 return DecisionPolicyResult{}, err 115 } 116 // maxYesCount is the max potential number of yes count, i.e the current yes count 117 // plus all undecided count (supposing they all vote yes). 118 maxYesCount, err := yesCount.Add(undecided) 119 if err != nil { 120 return DecisionPolicyResult{}, err 121 } 122 123 if maxYesCount.Cmp(realThreshold) < 0 { 124 return DecisionPolicyResult{Allow: false, Final: true}, nil 125 } 126 return DecisionPolicyResult{Allow: false, Final: false}, nil 127 } 128 129 func min(a, b math.Dec) math.Dec { 130 if a.Cmp(b) < 0 { 131 return a 132 } 133 return b 134 } 135 136 // Validate validates the policy against the group. Note that the threshold 137 // can actually be greater than the group's total weight: in the Allow method 138 // we check the tally weight against `min(threshold,total_weight)`. 139 func (p *ThresholdDecisionPolicy) Validate(g GroupInfo, config Config) error { 140 _, err := math.NewPositiveDecFromString(p.Threshold) 141 if err != nil { 142 return errorsmod.Wrap(err, "threshold") 143 } 144 _, err = math.NewNonNegativeDecFromString(g.TotalWeight) 145 if err != nil { 146 return errorsmod.Wrap(err, "group total weight") 147 } 148 149 if p.Windows.MinExecutionPeriod > p.Windows.VotingPeriod+config.MaxExecutionPeriod { 150 return errorsmod.Wrap(errors.ErrInvalid, "min_execution_period should be smaller than voting_period + max_execution_period") 151 } 152 return nil 153 } 154 155 // Implements DecisionPolicy Interface 156 var _ DecisionPolicy = &PercentageDecisionPolicy{} 157 158 // NewPercentageDecisionPolicy creates a new percentage DecisionPolicy 159 func NewPercentageDecisionPolicy(percentage string, votingPeriod, executionPeriod time.Duration) DecisionPolicy { 160 return &PercentageDecisionPolicy{percentage, &DecisionPolicyWindows{votingPeriod, executionPeriod}} 161 } 162 163 // GetVotingPeriod returns the voitng period of PercentageDecisionPolicy 164 func (p PercentageDecisionPolicy) GetVotingPeriod() time.Duration { 165 return p.Windows.VotingPeriod 166 } 167 168 // GetMinExecutionPeriod returns the minimum execution period of PercentageDecisionPolicy 169 func (p PercentageDecisionPolicy) GetMinExecutionPeriod() time.Duration { 170 return p.Windows.MinExecutionPeriod 171 } 172 173 // ValidateBasic does basic validation on PercentageDecisionPolicy 174 func (p PercentageDecisionPolicy) ValidateBasic() error { 175 percentage, err := math.NewPositiveDecFromString(p.Percentage) 176 if err != nil { 177 return errorsmod.Wrap(err, "percentage threshold") 178 } 179 if percentage.Cmp(math.NewDecFromInt64(1)) == 1 { 180 return errorsmod.Wrap(errors.ErrInvalid, "percentage must be > 0 and <= 1") 181 } 182 183 if p.Windows == nil || p.Windows.VotingPeriod == 0 { 184 return errorsmod.Wrap(errors.ErrInvalid, "voting period cannot be 0") 185 } 186 187 return nil 188 } 189 190 // Validate validates the policy against the group. 191 func (p *PercentageDecisionPolicy) Validate(g GroupInfo, config Config) error { 192 if p.Windows.MinExecutionPeriod > p.Windows.VotingPeriod+config.MaxExecutionPeriod { 193 return errorsmod.Wrap(errors.ErrInvalid, "min_execution_period should be smaller than voting_period + max_execution_period") 194 } 195 return nil 196 } 197 198 // Allow allows a proposal to pass when the tally of yes votes equals or exceeds the percentage threshold before the timeout. 199 func (p PercentageDecisionPolicy) Allow(tally TallyResult, totalPower string) (DecisionPolicyResult, error) { 200 percentage, err := math.NewPositiveDecFromString(p.Percentage) 201 if err != nil { 202 return DecisionPolicyResult{}, errorsmod.Wrap(err, "percentage") 203 } 204 yesCount, err := math.NewNonNegativeDecFromString(tally.YesCount) 205 if err != nil { 206 return DecisionPolicyResult{}, errorsmod.Wrap(err, "yes count") 207 } 208 totalPowerDec, err := math.NewNonNegativeDecFromString(totalPower) 209 if err != nil { 210 return DecisionPolicyResult{}, errorsmod.Wrap(err, "total power") 211 } 212 213 yesPercentage, err := yesCount.Quo(totalPowerDec) 214 if err != nil { 215 return DecisionPolicyResult{}, err 216 } 217 218 if yesPercentage.Cmp(percentage) >= 0 { 219 return DecisionPolicyResult{Allow: true, Final: true}, nil 220 } 221 222 totalCounts, err := tally.TotalCounts() 223 if err != nil { 224 return DecisionPolicyResult{}, err 225 } 226 undecided, err := math.SubNonNegative(totalPowerDec, totalCounts) 227 if err != nil { 228 return DecisionPolicyResult{}, err 229 } 230 sum, err := yesCount.Add(undecided) 231 if err != nil { 232 return DecisionPolicyResult{}, err 233 } 234 sumPercentage, err := sum.Quo(totalPowerDec) 235 if err != nil { 236 return DecisionPolicyResult{}, err 237 } 238 if sumPercentage.Cmp(percentage) < 0 { 239 return DecisionPolicyResult{Allow: false, Final: true}, nil 240 } 241 return DecisionPolicyResult{Allow: false, Final: false}, nil 242 } 243 244 var _ orm.Validateable = GroupPolicyInfo{} 245 246 // NewGroupPolicyInfo creates a new GroupPolicyInfo instance 247 func NewGroupPolicyInfo(address sdk.AccAddress, group uint64, admin sdk.AccAddress, metadata string, 248 version uint64, decisionPolicy DecisionPolicy, createdAt time.Time, 249 ) (GroupPolicyInfo, error) { 250 p := GroupPolicyInfo{ 251 Address: address.String(), 252 GroupId: group, 253 Admin: admin.String(), 254 Metadata: metadata, 255 Version: version, 256 CreatedAt: createdAt, 257 } 258 259 err := p.SetDecisionPolicy(decisionPolicy) 260 if err != nil { 261 return GroupPolicyInfo{}, err 262 } 263 264 return p, nil 265 } 266 267 // SetDecisionPolicy sets the decision policy for GroupPolicyInfo. 268 func (g *GroupPolicyInfo) SetDecisionPolicy(decisionPolicy DecisionPolicy) error { 269 any, err := codectypes.NewAnyWithValue(decisionPolicy) 270 if err != nil { 271 return err 272 } 273 g.DecisionPolicy = any 274 return nil 275 } 276 277 // GetDecisionPolicy gets the decision policy of GroupPolicyInfo 278 func (g GroupPolicyInfo) GetDecisionPolicy() (DecisionPolicy, error) { 279 decisionPolicy, ok := g.DecisionPolicy.GetCachedValue().(DecisionPolicy) 280 if !ok { 281 return nil, sdkerrors.ErrInvalidType.Wrapf("expected %T, got %T", (DecisionPolicy)(nil), g.DecisionPolicy.GetCachedValue()) 282 } 283 284 return decisionPolicy, nil 285 } 286 287 // UnpackInterfaces implements UnpackInterfacesMessage.UnpackInterfaces 288 func (g GroupPolicyInfo) UnpackInterfaces(unpacker codectypes.AnyUnpacker) error { 289 var decisionPolicy DecisionPolicy 290 return unpacker.UnpackAny(g.DecisionPolicy, &decisionPolicy) 291 } 292 293 func (g GroupInfo) PrimaryKeyFields() []interface{} { 294 return []interface{}{g.Id} 295 } 296 297 // ValidateBasic does basic validation on group info. 298 func (g GroupInfo) ValidateBasic() error { 299 if g.Id == 0 { 300 return errorsmod.Wrap(errors.ErrEmpty, "group's GroupId") 301 } 302 303 _, err := sdk.AccAddressFromBech32(g.Admin) 304 if err != nil { 305 return errorsmod.Wrap(err, "admin") 306 } 307 308 if _, err := math.NewNonNegativeDecFromString(g.TotalWeight); err != nil { 309 return errorsmod.Wrap(err, "total weight") 310 } 311 if g.Version == 0 { 312 return errorsmod.Wrap(errors.ErrEmpty, "version") 313 } 314 return nil 315 } 316 317 func (g GroupPolicyInfo) PrimaryKeyFields() []interface{} { 318 addr := sdk.MustAccAddressFromBech32(g.Address) 319 320 return []interface{}{addr.Bytes()} 321 } 322 323 func (g Proposal) PrimaryKeyFields() []interface{} { 324 return []interface{}{g.Id} 325 } 326 327 // ValidateBasic does basic validation on group policy info. 328 func (g GroupPolicyInfo) ValidateBasic() error { 329 _, err := sdk.AccAddressFromBech32(g.Admin) 330 if err != nil { 331 return errorsmod.Wrap(err, "group policy admin") 332 } 333 _, err = sdk.AccAddressFromBech32(g.Address) 334 if err != nil { 335 return errorsmod.Wrap(err, "group policy account address") 336 } 337 338 if g.GroupId == 0 { 339 return errorsmod.Wrap(errors.ErrEmpty, "group policy's group id") 340 } 341 if g.Version == 0 { 342 return errorsmod.Wrap(errors.ErrEmpty, "group policy version") 343 } 344 policy, err := g.GetDecisionPolicy() 345 if err != nil { 346 return errorsmod.Wrap(err, "group policy decision policy") 347 } 348 349 if err := policy.ValidateBasic(); err != nil { 350 return errorsmod.Wrap(err, "group policy's decision policy") 351 } 352 return nil 353 } 354 355 func (g GroupMember) PrimaryKeyFields() []interface{} { 356 addr := sdk.MustAccAddressFromBech32(g.Member.Address) 357 358 return []interface{}{g.GroupId, addr.Bytes()} 359 } 360 361 // ValidateBasic does basic validation on group member. 362 func (g GroupMember) ValidateBasic() error { 363 if g.GroupId == 0 { 364 return errorsmod.Wrap(errors.ErrEmpty, "group member's group id") 365 } 366 367 if _, err := sdk.AccAddressFromBech32(g.Member.Address); err != nil { 368 return errorsmod.Wrap(err, "group member's address") 369 } 370 371 if _, err := math.NewNonNegativeDecFromString(g.Member.Weight); err != nil { 372 return errorsmod.Wrap(err, "weight must be non negative") 373 } 374 375 return nil 376 } 377 378 // MemberToMemberRequest converts a `Member` (used for storage) 379 // to a `MemberRequest` (used in requests). The only difference 380 // between the two is that `MemberRequest` doesn't have any `AddedAt` field 381 // since it cannot be set as part of requests. 382 func MemberToMemberRequest(m *Member) MemberRequest { 383 return MemberRequest{ 384 Address: m.Address, 385 Weight: m.Weight, 386 Metadata: m.Metadata, 387 } 388 } 389 390 // ValidateBasic does basic validation on proposal. 391 func (g Proposal) ValidateBasic() error { 392 if g.Id == 0 { 393 return errorsmod.Wrap(errors.ErrEmpty, "proposal id") 394 } 395 _, err := sdk.AccAddressFromBech32(g.GroupPolicyAddress) 396 if err != nil { 397 return errorsmod.Wrap(err, "proposal group policy address") 398 } 399 if g.GroupVersion == 0 { 400 return errorsmod.Wrap(errors.ErrEmpty, "proposal group version") 401 } 402 if g.GroupPolicyVersion == 0 { 403 return errorsmod.Wrap(errors.ErrEmpty, "proposal group policy version") 404 } 405 _, err = g.FinalTallyResult.GetYesCount() 406 if err != nil { 407 return errorsmod.Wrap(err, "proposal FinalTallyResult yes count") 408 } 409 _, err = g.FinalTallyResult.GetNoCount() 410 if err != nil { 411 return errorsmod.Wrap(err, "proposal FinalTallyResult no count") 412 } 413 _, err = g.FinalTallyResult.GetAbstainCount() 414 if err != nil { 415 return errorsmod.Wrap(err, "proposal FinalTallyResult abstain count") 416 } 417 _, err = g.FinalTallyResult.GetNoWithVetoCount() 418 if err != nil { 419 return errorsmod.Wrap(err, "proposal FinalTallyResult veto count") 420 } 421 return nil 422 } 423 424 func (v Vote) PrimaryKeyFields() []interface{} { 425 addr := sdk.MustAccAddressFromBech32(v.Voter) 426 427 return []interface{}{v.ProposalId, addr.Bytes()} 428 } 429 430 var _ orm.Validateable = Vote{} 431 432 // ValidateBasic does basic validation on vote. 433 func (v Vote) ValidateBasic() error { 434 _, err := sdk.AccAddressFromBech32(v.Voter) 435 if err != nil { 436 return errorsmod.Wrap(err, "voter") 437 } 438 if v.ProposalId == 0 { 439 return errorsmod.Wrap(errors.ErrEmpty, "voter ProposalId") 440 } 441 if v.Option == VOTE_OPTION_UNSPECIFIED { 442 return errorsmod.Wrap(errors.ErrEmpty, "voter vote option") 443 } 444 if _, ok := VoteOption_name[int32(v.Option)]; !ok { 445 return errorsmod.Wrap(errors.ErrInvalid, "vote option") 446 } 447 return nil 448 } 449 450 // UnpackInterfaces implements UnpackInterfacesMessage.UnpackInterfaces 451 func (q QueryGroupPoliciesByGroupResponse) UnpackInterfaces(unpacker codectypes.AnyUnpacker) error { 452 return unpackGroupPolicies(unpacker, q.GroupPolicies) 453 } 454 455 // UnpackInterfaces implements UnpackInterfacesMessage.UnpackInterfaces 456 func (q QueryGroupPoliciesByAdminResponse) UnpackInterfaces(unpacker codectypes.AnyUnpacker) error { 457 return unpackGroupPolicies(unpacker, q.GroupPolicies) 458 } 459 460 func unpackGroupPolicies(unpacker codectypes.AnyUnpacker, accs []*GroupPolicyInfo) error { 461 for _, g := range accs { 462 err := g.UnpackInterfaces(unpacker) 463 if err != nil { 464 return err 465 } 466 } 467 468 return nil 469 } 470 471 type operation func(x, y math.Dec) (math.Dec, error) 472 473 func (t *TallyResult) operation(vote Vote, weight string, op operation) error { 474 weightDec, err := math.NewPositiveDecFromString(weight) 475 if err != nil { 476 return err 477 } 478 479 yesCount, err := t.GetYesCount() 480 if err != nil { 481 return errorsmod.Wrap(err, "yes count") 482 } 483 noCount, err := t.GetNoCount() 484 if err != nil { 485 return errorsmod.Wrap(err, "no count") 486 } 487 abstainCount, err := t.GetAbstainCount() 488 if err != nil { 489 return errorsmod.Wrap(err, "abstain count") 490 } 491 vetoCount, err := t.GetNoWithVetoCount() 492 if err != nil { 493 return errorsmod.Wrap(err, "veto count") 494 } 495 496 switch vote.Option { 497 case VOTE_OPTION_YES: 498 yesCount, err := op(yesCount, weightDec) 499 if err != nil { 500 return errorsmod.Wrap(err, "yes count") 501 } 502 t.YesCount = yesCount.String() 503 case VOTE_OPTION_NO: 504 noCount, err := op(noCount, weightDec) 505 if err != nil { 506 return errorsmod.Wrap(err, "no count") 507 } 508 t.NoCount = noCount.String() 509 case VOTE_OPTION_ABSTAIN: 510 abstainCount, err := op(abstainCount, weightDec) 511 if err != nil { 512 return errorsmod.Wrap(err, "abstain count") 513 } 514 t.AbstainCount = abstainCount.String() 515 case VOTE_OPTION_NO_WITH_VETO: 516 vetoCount, err := op(vetoCount, weightDec) 517 if err != nil { 518 return errorsmod.Wrap(err, "veto count") 519 } 520 t.NoWithVetoCount = vetoCount.String() 521 default: 522 return errorsmod.Wrapf(errors.ErrInvalid, "unknown vote option %s", vote.Option.String()) 523 } 524 return nil 525 } 526 527 // GetYesCount returns the number of yes counts from tally result. 528 func (t TallyResult) GetYesCount() (math.Dec, error) { 529 yesCount, err := math.NewNonNegativeDecFromString(t.YesCount) 530 if err != nil { 531 return math.Dec{}, err 532 } 533 return yesCount, nil 534 } 535 536 // GetNoCount returns the number of no counts from tally result. 537 func (t TallyResult) GetNoCount() (math.Dec, error) { 538 noCount, err := math.NewNonNegativeDecFromString(t.NoCount) 539 if err != nil { 540 return math.Dec{}, err 541 } 542 return noCount, nil 543 } 544 545 // GetAbstainCount returns the number of abstain counts from tally result. 546 func (t TallyResult) GetAbstainCount() (math.Dec, error) { 547 abstainCount, err := math.NewNonNegativeDecFromString(t.AbstainCount) 548 if err != nil { 549 return math.Dec{}, err 550 } 551 return abstainCount, nil 552 } 553 554 // GetNoWithVetoCount returns the number of no with veto counts from tally result. 555 func (t TallyResult) GetNoWithVetoCount() (math.Dec, error) { 556 vetoCount, err := math.NewNonNegativeDecFromString(t.NoWithVetoCount) 557 if err != nil { 558 return math.Dec{}, err 559 } 560 return vetoCount, nil 561 } 562 563 func (t *TallyResult) Add(vote Vote, weight string) error { 564 if err := t.operation(vote, weight, math.Add); err != nil { 565 return err 566 } 567 return nil 568 } 569 570 // TotalCounts is the sum of all weights. 571 func (t TallyResult) TotalCounts() (math.Dec, error) { 572 yesCount, err := t.GetYesCount() 573 if err != nil { 574 return math.Dec{}, errorsmod.Wrap(err, "yes count") 575 } 576 noCount, err := t.GetNoCount() 577 if err != nil { 578 return math.Dec{}, errorsmod.Wrap(err, "no count") 579 } 580 abstainCount, err := t.GetAbstainCount() 581 if err != nil { 582 return math.Dec{}, errorsmod.Wrap(err, "abstain count") 583 } 584 vetoCount, err := t.GetNoWithVetoCount() 585 if err != nil { 586 return math.Dec{}, errorsmod.Wrap(err, "veto count") 587 } 588 589 totalCounts := math.NewDecFromInt64(0) 590 totalCounts, err = totalCounts.Add(yesCount) 591 if err != nil { 592 return math.Dec{}, err 593 } 594 totalCounts, err = totalCounts.Add(noCount) 595 if err != nil { 596 return math.Dec{}, err 597 } 598 totalCounts, err = totalCounts.Add(abstainCount) 599 if err != nil { 600 return math.Dec{}, err 601 } 602 totalCounts, err = totalCounts.Add(vetoCount) 603 if err != nil { 604 return math.Dec{}, err 605 } 606 return totalCounts, nil 607 } 608 609 // DefaultTallyResult returns a TallyResult with all counts set to 0. 610 func DefaultTallyResult() TallyResult { 611 return TallyResult{ 612 YesCount: "0", 613 NoCount: "0", 614 NoWithVetoCount: "0", 615 AbstainCount: "0", 616 } 617 } 618 619 // VoteOptionFromString returns a VoteOption from a string. It returns an error 620 // if the string is invalid. 621 func VoteOptionFromString(str string) (VoteOption, error) { 622 vo, ok := VoteOption_value[str] 623 if !ok { 624 return VOTE_OPTION_UNSPECIFIED, fmt.Errorf("'%s' is not a valid vote option", str) 625 } 626 return VoteOption(vo), nil 627 }