cosmos-sdk/x/group/types.go

606 lines
18 KiB
Go

package group
import (
"fmt"
"time"
"github.com/cosmos/cosmos-sdk/codec"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/x/group/errors"
"github.com/cosmos/cosmos-sdk/x/group/internal/math"
"github.com/cosmos/cosmos-sdk/x/group/internal/orm"
)
// DecisionPolicyResult is the result of whether a proposal passes or not a
// decision policy.
type DecisionPolicyResult struct {
// Allow determines if the proposal is allowed to pass.
Allow bool
// Final determines if the tally result is final or not. If final, then
// votes are pruned, and the tally result is saved in the proposal's
// `FinalTallyResult` field.
Final bool
}
// DecisionPolicy is the persistent set of rules to determine the result of election on a proposal.
type DecisionPolicy interface {
codec.ProtoMarshaler
// GetVotingPeriod returns the duration after proposal submission where
// votes are accepted.
GetVotingPeriod() 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)
ValidateBasic() error
Validate(g GroupInfo, config Config) error
}
// Implements DecisionPolicy Interface
var _ DecisionPolicy = &ThresholdDecisionPolicy{}
// NewThresholdDecisionPolicy creates a threshold DecisionPolicy
func NewThresholdDecisionPolicy(threshold string, votingPeriod time.Duration, minExecutionPeriod time.Duration) DecisionPolicy {
return &ThresholdDecisionPolicy{threshold, &DecisionPolicyWindows{votingPeriod, minExecutionPeriod}}
}
func (p ThresholdDecisionPolicy) GetVotingPeriod() time.Duration {
return p.Windows.VotingPeriod
}
func (p ThresholdDecisionPolicy) ValidateBasic() error {
if _, err := math.NewPositiveDecFromString(p.Threshold); err != nil {
return sdkerrors.Wrap(err, "threshold")
}
if p.Windows == nil || p.Windows.VotingPeriod == 0 {
return sdkerrors.Wrap(errors.ErrInvalid, "voting period cannot be zero")
}
return nil
}
// 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)
}
threshold, err := math.NewPositiveDecFromString(p.Threshold)
if err != nil {
return DecisionPolicyResult{}, sdkerrors.Wrap(err, "threshold")
}
yesCount, err := math.NewNonNegativeDecFromString(tallyResult.YesCount)
if err != nil {
return DecisionPolicyResult{}, sdkerrors.Wrap(err, "yes count")
}
totalPowerDec, err := math.NewNonNegativeDecFromString(totalPower)
if err != nil {
return DecisionPolicyResult{}, sdkerrors.Wrap(err, "total power")
}
// 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)
if yesCount.Cmp(realThreshold) >= 0 {
return DecisionPolicyResult{Allow: true, Final: true}, nil
}
totalCounts, err := tallyResult.TotalCounts()
if err != nil {
return DecisionPolicyResult{}, err
}
undecided, err := math.SubNonNegative(totalPowerDec, totalCounts)
if err != nil {
return DecisionPolicyResult{}, err
}
// maxYesCount is the max potential number of yes count, i.e the current yes count
// plus all undecided count (supposing they all vote yes).
maxYesCount, err := yesCount.Add(undecided)
if err != nil {
return DecisionPolicyResult{}, err
}
if maxYesCount.Cmp(realThreshold) < 0 {
return DecisionPolicyResult{Allow: false, Final: true}, nil
}
return DecisionPolicyResult{Allow: false, Final: false}, nil
}
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 {
_, err := math.NewPositiveDecFromString(p.Threshold)
if err != nil {
return sdkerrors.Wrap(err, "threshold")
}
_, err = math.NewNonNegativeDecFromString(g.TotalWeight)
if err != nil {
return sdkerrors.Wrap(err, "group total weight")
}
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")
}
return nil
}
// Implements DecisionPolicy Interface
var _ DecisionPolicy = &PercentageDecisionPolicy{}
// NewPercentageDecisionPolicy creates a new percentage DecisionPolicy
func NewPercentageDecisionPolicy(percentage string, votingPeriod time.Duration, executionPeriod time.Duration) DecisionPolicy {
return &PercentageDecisionPolicy{percentage, &DecisionPolicyWindows{votingPeriod, executionPeriod}}
}
func (p PercentageDecisionPolicy) GetVotingPeriod() time.Duration {
return p.Windows.VotingPeriod
}
func (p PercentageDecisionPolicy) ValidateBasic() error {
percentage, err := math.NewPositiveDecFromString(p.Percentage)
if err != nil {
return sdkerrors.Wrap(err, "percentage threshold")
}
if percentage.Cmp(math.NewDecFromInt64(1)) == 1 {
return sdkerrors.Wrap(errors.ErrInvalid, "percentage must be > 0 and <= 1")
}
if p.Windows == nil || p.Windows.VotingPeriod == 0 {
return sdkerrors.Wrap(errors.ErrInvalid, "voting period cannot be 0")
}
return nil
}
func (p *PercentageDecisionPolicy) Validate(g GroupInfo, config Config) error {
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")
}
return nil
}
// 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)
}
percentage, err := math.NewPositiveDecFromString(p.Percentage)
if err != nil {
return DecisionPolicyResult{}, sdkerrors.Wrap(err, "percentage")
}
yesCount, err := math.NewNonNegativeDecFromString(tally.YesCount)
if err != nil {
return DecisionPolicyResult{}, sdkerrors.Wrap(err, "yes count")
}
totalPowerDec, err := math.NewNonNegativeDecFromString(totalPower)
if err != nil {
return DecisionPolicyResult{}, sdkerrors.Wrap(err, "total power")
}
yesPercentage, err := yesCount.Quo(totalPowerDec)
if err != nil {
return DecisionPolicyResult{}, err
}
if yesPercentage.Cmp(percentage) >= 0 {
return DecisionPolicyResult{Allow: true, Final: true}, nil
}
totalCounts, err := tally.TotalCounts()
if err != nil {
return DecisionPolicyResult{}, err
}
undecided, err := math.SubNonNegative(totalPowerDec, totalCounts)
if err != nil {
return DecisionPolicyResult{}, err
}
sum, err := yesCount.Add(undecided)
if err != nil {
return DecisionPolicyResult{}, err
}
sumPercentage, err := sum.Quo(totalPowerDec)
if err != nil {
return DecisionPolicyResult{}, err
}
if sumPercentage.Cmp(percentage) < 0 {
return DecisionPolicyResult{Allow: false, Final: true}, nil
}
return DecisionPolicyResult{Allow: false, Final: false}, nil
}
var _ orm.Validateable = GroupPolicyInfo{}
// NewGroupPolicyInfo creates a new GroupPolicyInfo instance
func NewGroupPolicyInfo(address sdk.AccAddress, group uint64, admin sdk.AccAddress, metadata string,
version uint64, decisionPolicy DecisionPolicy, createdAt time.Time) (GroupPolicyInfo, error) {
p := GroupPolicyInfo{
Address: address.String(),
GroupId: group,
Admin: admin.String(),
Metadata: metadata,
Version: version,
CreatedAt: createdAt,
}
err := p.SetDecisionPolicy(decisionPolicy)
if err != nil {
return GroupPolicyInfo{}, err
}
return p, nil
}
func (g *GroupPolicyInfo) SetDecisionPolicy(decisionPolicy DecisionPolicy) error {
any, err := codectypes.NewAnyWithValue(decisionPolicy)
if err != nil {
return err
}
g.DecisionPolicy = any
return nil
}
func (g GroupPolicyInfo) GetDecisionPolicy() (DecisionPolicy, error) {
decisionPolicy, ok := g.DecisionPolicy.GetCachedValue().(DecisionPolicy)
if !ok {
return nil, sdkerrors.ErrInvalidType.Wrapf("expected %T, got %T", (DecisionPolicy)(nil), g.DecisionPolicy.GetCachedValue())
}
return decisionPolicy, nil
}
// UnpackInterfaces implements UnpackInterfacesMessage.UnpackInterfaces
func (g GroupPolicyInfo) UnpackInterfaces(unpacker codectypes.AnyUnpacker) error {
var decisionPolicy DecisionPolicy
return unpacker.UnpackAny(g.DecisionPolicy, &decisionPolicy)
}
func (g GroupInfo) PrimaryKeyFields() []interface{} {
return []interface{}{g.Id}
}
func (g GroupInfo) ValidateBasic() error {
if g.Id == 0 {
return sdkerrors.Wrap(errors.ErrEmpty, "group's GroupId")
}
_, err := sdk.AccAddressFromBech32(g.Admin)
if err != nil {
return sdkerrors.Wrap(err, "admin")
}
if _, err := math.NewNonNegativeDecFromString(g.TotalWeight); err != nil {
return sdkerrors.Wrap(err, "total weight")
}
if g.Version == 0 {
return sdkerrors.Wrap(errors.ErrEmpty, "version")
}
return nil
}
func (g GroupPolicyInfo) PrimaryKeyFields() []interface{} {
addr, err := sdk.AccAddressFromBech32(g.Address)
if err != nil {
panic(err)
}
return []interface{}{addr.Bytes()}
}
func (g Proposal) PrimaryKeyFields() []interface{} {
return []interface{}{g.Id}
}
func (g GroupPolicyInfo) ValidateBasic() error {
_, err := sdk.AccAddressFromBech32(g.Admin)
if err != nil {
return sdkerrors.Wrap(err, "group policy admin")
}
_, err = sdk.AccAddressFromBech32(g.Address)
if err != nil {
return sdkerrors.Wrap(err, "group policy account address")
}
if g.GroupId == 0 {
return sdkerrors.Wrap(errors.ErrEmpty, "group policy's group id")
}
if g.Version == 0 {
return sdkerrors.Wrap(errors.ErrEmpty, "group policy version")
}
policy, err := g.GetDecisionPolicy()
if err != nil {
return sdkerrors.Wrap(err, "group policy decision policy")
}
if err := policy.ValidateBasic(); err != nil {
return sdkerrors.Wrap(err, "group policy's decision policy")
}
return nil
}
func (g GroupMember) PrimaryKeyFields() []interface{} {
addr, err := sdk.AccAddressFromBech32(g.Member.Address)
if err != nil {
panic(err)
}
return []interface{}{g.GroupId, addr.Bytes()}
}
func (g GroupMember) ValidateBasic() error {
if g.GroupId == 0 {
return sdkerrors.Wrap(errors.ErrEmpty, "group member's group id")
}
err := MemberToMemberRequest(g.Member).ValidateBasic()
if err != nil {
return sdkerrors.Wrap(err, "group member")
}
return nil
}
// MemberToMemberRequest converts a `Member` (used for storage)
// to a `MemberRequest` (used in requests). The only difference
// between the two is that `MemberRequest` doesn't have any `AddedAt` field
// since it cannot be set as part of requests.
func MemberToMemberRequest(m *Member) MemberRequest {
return MemberRequest{
Address: m.Address,
Weight: m.Weight,
Metadata: m.Metadata,
}
}
func (p Proposal) ValidateBasic() error {
if p.Id == 0 {
return sdkerrors.Wrap(errors.ErrEmpty, "proposal id")
}
_, err := sdk.AccAddressFromBech32(p.GroupPolicyAddress)
if err != nil {
return sdkerrors.Wrap(err, "proposal group policy address")
}
if p.GroupVersion == 0 {
return sdkerrors.Wrap(errors.ErrEmpty, "proposal group version")
}
if p.GroupPolicyVersion == 0 {
return sdkerrors.Wrap(errors.ErrEmpty, "proposal group policy version")
}
_, err = p.FinalTallyResult.GetYesCount()
if err != nil {
return sdkerrors.Wrap(err, "proposal FinalTallyResult yes count")
}
_, err = p.FinalTallyResult.GetNoCount()
if err != nil {
return sdkerrors.Wrap(err, "proposal FinalTallyResult no count")
}
_, err = p.FinalTallyResult.GetAbstainCount()
if err != nil {
return sdkerrors.Wrap(err, "proposal FinalTallyResult abstain count")
}
_, err = p.FinalTallyResult.GetNoWithVetoCount()
if err != nil {
return sdkerrors.Wrap(err, "proposal FinalTallyResult veto count")
}
return nil
}
func (v Vote) PrimaryKeyFields() []interface{} {
addr, err := sdk.AccAddressFromBech32(v.Voter)
if err != nil {
panic(err)
}
return []interface{}{v.ProposalId, addr.Bytes()}
}
var _ orm.Validateable = Vote{}
func (v Vote) ValidateBasic() error {
_, err := sdk.AccAddressFromBech32(v.Voter)
if err != nil {
return sdkerrors.Wrap(err, "voter")
}
if v.ProposalId == 0 {
return sdkerrors.Wrap(errors.ErrEmpty, "voter ProposalId")
}
if v.Option == VOTE_OPTION_UNSPECIFIED {
return sdkerrors.Wrap(errors.ErrEmpty, "voter vote option")
}
if _, ok := VoteOption_name[int32(v.Option)]; !ok {
return sdkerrors.Wrap(errors.ErrInvalid, "vote option")
}
return nil
}
// UnpackInterfaces implements UnpackInterfacesMessage.UnpackInterfaces
func (q QueryGroupPoliciesByGroupResponse) UnpackInterfaces(unpacker codectypes.AnyUnpacker) error {
return unpackGroupPolicies(unpacker, q.GroupPolicies)
}
// UnpackInterfaces implements UnpackInterfacesMessage.UnpackInterfaces
func (q QueryGroupPoliciesByAdminResponse) UnpackInterfaces(unpacker codectypes.AnyUnpacker) error {
return unpackGroupPolicies(unpacker, q.GroupPolicies)
}
func unpackGroupPolicies(unpacker codectypes.AnyUnpacker, accs []*GroupPolicyInfo) error {
for _, g := range accs {
err := g.UnpackInterfaces(unpacker)
if err != nil {
return err
}
}
return nil
}
type operation func(x, y math.Dec) (math.Dec, error)
func (t *TallyResult) operation(vote Vote, weight string, op operation) error {
weightDec, err := math.NewPositiveDecFromString(weight)
if err != nil {
return err
}
yesCount, err := t.GetYesCount()
if err != nil {
return sdkerrors.Wrap(err, "yes count")
}
noCount, err := t.GetNoCount()
if err != nil {
return sdkerrors.Wrap(err, "no count")
}
abstainCount, err := t.GetAbstainCount()
if err != nil {
return sdkerrors.Wrap(err, "abstain count")
}
vetoCount, err := t.GetNoWithVetoCount()
if err != nil {
return sdkerrors.Wrap(err, "veto count")
}
switch vote.Option {
case VOTE_OPTION_YES:
yesCount, err := op(yesCount, weightDec)
if err != nil {
return sdkerrors.Wrap(err, "yes count")
}
t.YesCount = yesCount.String()
case VOTE_OPTION_NO:
noCount, err := op(noCount, weightDec)
if err != nil {
return sdkerrors.Wrap(err, "no count")
}
t.NoCount = noCount.String()
case VOTE_OPTION_ABSTAIN:
abstainCount, err := op(abstainCount, weightDec)
if err != nil {
return sdkerrors.Wrap(err, "abstain count")
}
t.AbstainCount = abstainCount.String()
case VOTE_OPTION_NO_WITH_VETO:
vetoCount, err := op(vetoCount, weightDec)
if err != nil {
return sdkerrors.Wrap(err, "veto count")
}
t.NoWithVetoCount = vetoCount.String()
default:
return sdkerrors.Wrapf(errors.ErrInvalid, "unknown vote option %s", vote.Option.String())
}
return nil
}
func (t TallyResult) GetYesCount() (math.Dec, error) {
yesCount, err := math.NewNonNegativeDecFromString(t.YesCount)
if err != nil {
return math.Dec{}, err
}
return yesCount, nil
}
func (t TallyResult) GetNoCount() (math.Dec, error) {
noCount, err := math.NewNonNegativeDecFromString(t.NoCount)
if err != nil {
return math.Dec{}, err
}
return noCount, nil
}
func (t TallyResult) GetAbstainCount() (math.Dec, error) {
abstainCount, err := math.NewNonNegativeDecFromString(t.AbstainCount)
if err != nil {
return math.Dec{}, err
}
return abstainCount, nil
}
func (t TallyResult) GetNoWithVetoCount() (math.Dec, error) {
vetoCount, err := math.NewNonNegativeDecFromString(t.NoWithVetoCount)
if err != nil {
return math.Dec{}, err
}
return vetoCount, nil
}
func (t *TallyResult) Add(vote Vote, weight string) error {
if err := t.operation(vote, weight, math.Add); err != nil {
return err
}
return nil
}
// TotalCounts is the sum of all weights.
func (t TallyResult) TotalCounts() (math.Dec, error) {
yesCount, err := t.GetYesCount()
if err != nil {
return math.Dec{}, sdkerrors.Wrap(err, "yes count")
}
noCount, err := t.GetNoCount()
if err != nil {
return math.Dec{}, sdkerrors.Wrap(err, "no count")
}
abstainCount, err := t.GetAbstainCount()
if err != nil {
return math.Dec{}, sdkerrors.Wrap(err, "abstain count")
}
vetoCount, err := t.GetNoWithVetoCount()
if err != nil {
return math.Dec{}, sdkerrors.Wrap(err, "veto count")
}
totalCounts := math.NewDecFromInt64(0)
totalCounts, err = totalCounts.Add(yesCount)
if err != nil {
return math.Dec{}, err
}
totalCounts, err = totalCounts.Add(noCount)
if err != nil {
return math.Dec{}, err
}
totalCounts, err = totalCounts.Add(abstainCount)
if err != nil {
return math.Dec{}, err
}
totalCounts, err = totalCounts.Add(vetoCount)
if err != nil {
return math.Dec{}, err
}
return totalCounts, nil
}
// DefaultTallyResult returns a TallyResult with all counts set to 0.
func DefaultTallyResult() TallyResult {
return TallyResult{
YesCount: "0",
NoCount: "0",
NoWithVetoCount: "0",
AbstainCount: "0",
}
}
// VoteOptionFromString returns a VoteOption from a string. It returns an error
// if the string is invalid.
func VoteOptionFromString(str string) (VoteOption, error) {
vo, ok := VoteOption_value[str]
if !ok {
return VOTE_OPTION_UNSPECIFIED, fmt.Errorf("'%s' is not a valid vote option", str)
}
return VoteOption(vo), nil
}