The following documents specify the group module.This module allows the creation and management of on-chain multisig accounts and enables voting for message execution based on configurable decision policies.
A group is simply an aggregation of accounts with associated weights. It is not
an account and doesn’t have a balance. It doesn’t in and of itself have any
sort of voting or decision weight. It does have an “administrator” which has
the ability to add, remove and update members in the group. Note that a
group policy account could be an administrator of a group, and that the
administrator doesn’t necessarily have to be a member of the group.
A group policy is an account associated with a group and a decision policy.
Group policies are abstracted from groups because a single group may have
multiple decision policies for different types of actions. Managing group
membership separately from decision policies results in the least overhead
and keeps membership consistent across different policies. The pattern that
is recommended is to have a single master group policy for a given group,
and then to create separate group policies with different decision policies
and delegate the desired permissions from the master account to
those “sub-accounts” using the x/authz module.
A decision policy is the mechanism by which members of a group can vote on
proposals, as well as the rules that dictate whether a proposal should pass
or not based on its tally outcome.All decision policies generally would have a mininum execution period and a
maximum voting window. The minimum execution period is the minimum amount of time
that must pass after submission in order for a proposal to potentially be executed, and it may
be set to 0. The maximum voting window is the maximum time after submission that a proposal may
be voted on before it is tallied.The chain developer also defines an app-wide maximum execution period, which is
the maximum amount of time after a proposal’s voting period end where users are
allowed to execute a proposal.The current group module comes shipped with two decision policies: threshold
and percentage. Any chain developer can extend upon these two, by creating
custom decision policies, as long as they adhere to the DecisionPolicy
interface:
Copy
Ask AI
Final bool}// DecisionPolicy is the persistent set of rules to determine the result of election on a proposal.type DecisionPolicy interface { proto.Message // GetVotingPeriod returns the duration after proposal submission where // votes are accepted. GetVotingPeriod() time.Duration // GetMinExecutionPeriod returns the minimum duration after submission // where we can execution a proposal. It can be set to 0 or to a value // lesser than VotingPeriod to allow TRY_EXEC. GetMinExecutionPeriod() 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) (DecisionPolicyResult, error)
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).
A percentage decision policy is similar to a threshold decision policy, except
that the threshold is not defined as a constant weight, but as a percentage.
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.
Any member(s) of a group can submit a proposal for a group policy account to decide upon.
A proposal consists of a set of messages that will be executed if the proposal
passes as well as any metadata associated with the proposal.
There are four choices to choose while voting - yes, no, abstain and veto. Not
all decision policies will take the four choices into account. Votes can contain some optional metadata.
In the current implementation, the voting window begins as soon as a proposal
is submitted, and the end is defined by the group policy’s decision policy.
Proposals can be withdrawn any time before the voting period end, either by the
admin of the group policy or by one of the proposers. Once withdrawn, it is
marked as PROPOSAL_STATUS_WITHDRAWN, and no more voting or execution is
allowed on it.
If the group policy is updated during the voting period of the proposal, then
the proposal is marked as PROPOSAL_STATUS_ABORTED, and no more voting or
execution is allowed on it. This is because the group policy defines the rules
of proposal voting and execution, so if those rules change during the lifecycle
of a proposal, then the proposal should be marked as stale.
Tallying is the counting of all votes on a proposal. It happens only once in
the lifecycle of a proposal, but can be triggered by two factors, whichever
happens first:
either someone tries to execute the proposal (see next section), which can
happen on a Msg/Exec transaction, or a Msg/{SubmitProposal,Vote}
transaction with the Exec field set. When a proposal execution is attempted,
a tally is done first to make sure the proposal passes.
or on EndBlock when the proposal’s voting period end just passed.
If the tally result passes the decision policy’s rules, then the proposal is
marked as PROPOSAL_STATUS_ACCEPTED, or else it is marked as
PROPOSAL_STATUS_REJECTED. In any case, no more voting is allowed anymore, and the tally
result is persisted to state in the proposal’s FinalTallyResult.
Proposals are executed only when the tallying is done, and the group account’s
decision policy allows the proposal to pass based on the tally outcome. They
are marked by the status PROPOSAL_STATUS_ACCEPTED. Execution must happen
before a duration of MaxExecutionPeriod (set by the chain developer) after
each proposal’s voting period end.Proposals will not be automatically executed by the chain in this current design,
but rather a user must submit a Msg/Exec transaction to attempt to execute the
proposal based on the current votes and decision policy. Any user (not only the
group members) can execute proposals that have been accepted, and execution fees are
paid by the proposal executor.
It’s also possible to try to execute a proposal immediately on creation or on
new votes using the Exec field of Msg/SubmitProposal and Msg/Vote requests.
In the former case, proposers signatures are considered as yes votes.
In these cases, if the proposal can’t be executed (i.e. it didn’t pass the
decision policy’s rules), it will still be opened for new votes and
could be tallied and executed later on.A successful proposal execution will have its ExecutorResult marked as
PROPOSAL_EXECUTOR_RESULT_SUCCESS. The proposal will be automatically pruned
after execution. On the other hand, a failed proposal execution will be marked
as PROPOSAL_EXECUTOR_RESULT_FAILURE. Such a proposal can be re-executed
multiple times, until it expires after MaxExecutionPeriod after voting period
end.
Proposals and votes are automatically pruned to avoid state bloat.Votes are pruned:
either after a successful tally, i.e. a tally whose result passes the decision
policy’s rules, which can be trigged by a Msg/Exec or a
Msg/{SubmitProposal,Vote} with the Exec field set,
or on EndBlock right after the proposal’s voting period end. This applies to proposals with status aborted or withdrawn too.
whichever happens first.Proposals are pruned:
on EndBlock whose proposal status is withdrawn or aborted on proposal’s voting period end before tallying,
and either after a successful proposal execution,
or on EndBlock right after the proposal’s voting_period_end +
max_execution_period (defined as an app-wide configuration) is passed,
The group module uses the orm package which provides table storage with support for
primary keys and secondary indexes. orm also defines Sequence which is a persistent unique key generator based on a counter that can be used along with Tables.Here’s the list of tables and associated sequences and indexes stored as part of the group module.
The value of groupSeq is incremented when creating a new group and corresponds to the new GroupId: 0x1 | 0x1 -> BigEndian.The second 0x1 corresponds to the ORM sequenceStorageKey.
The groupMemberTable stores GroupMembers: 0x10 | BigEndian(GroupId) | []byte(member.Address) -> ProtocolBuffer(GroupMember).The groupMemberTable is a primary key table and its PrimaryKey is given by
BigEndian(GroupId) | []byte(member.Address) which is used by the following indexes.
groupMemberByMemberIndex allows to retrieve group members by member address:
0x12 | len([]byte(member.Address)) | []byte(member.Address) | PrimaryKey -> []byte().
The groupPolicyTable stores GroupPolicyInfo: 0x20 | len([]byte(Address)) | []byte(Address) -> ProtocolBuffer(GroupPolicyInfo).The groupPolicyTable is a primary key table and its PrimaryKey is given by
len([]byte(Address)) | []byte(Address) which is used by the following indexes.
The value of groupPolicySeq is incremented when creating a new group policy and is used to generate the new group policy account Address:
0x21 | 0x1 -> BigEndian.The second 0x1 corresponds to the ORM sequenceStorageKey.
The value of proposalSeq is incremented when creating a new proposal and corresponds to the new ProposalId: 0x31 | 0x1 -> BigEndian.The second 0x1 corresponds to the ORM sequenceStorageKey.
proposalsByVotingPeriodEndIndex allows to retrieve proposals sorted by chronological voting_period_end:
0x33 | sdk.FormatTimeBytes(proposal.VotingPeriodEnd) | BigEndian(ProposalId) -> []byte().This index is used when tallying the proposal votes at the end of the voting period, and for pruning proposals at VotingPeriodEnd + MaxExecutionPeriod.
The voteTable stores Votes: 0x40 | BigEndian(ProposalId) | []byte(voter.Address) -> ProtocolBuffer(Vote).The voteTable is a primary key table and its PrimaryKey is given by
BigEndian(ProposalId) | []byte(voter.Address) which is used by the following indexes.
A new group can be created with the MsgCreateGroup, which has an admin address, a list of members and some optional metadata.The metadata has a maximum length that is chosen by the app developer, and
passed into the group keeper as a config.
Copy
Ask AI
// MsgCreateGroup is the Msg/CreateGroup request type.message MsgCreateGroup { option (cosmos.msg.v1.signer) = "admin"; option (amino.name) = "cosmos-sdk/MsgCreateGroup"; // admin is the account address of the group admin. string admin = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // members defines the group members. repeated MemberRequest members = 2 [(gogoproto.nullable) = false, (amino.dont_omitempty) = true]; // metadata is any arbitrary metadata to attached to the group. string metadata = 3;}
It’s expected to fail if
metadata length is greater than MaxMetadataLen config
members are not correctly set (e.g. wrong address format, duplicates, or with 0 weight).
Group members can be updated with the UpdateGroupMembers.
Copy
Ask AI
// MsgUpdateGroupMembers is the Msg/UpdateGroupMembers request type.message MsgUpdateGroupMembers { option (cosmos.msg.v1.signer) = "admin"; option (amino.name) = "cosmos-sdk/MsgUpdateGroupMembers"; // admin is the account address of the group admin. string admin = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // group_id is the unique ID of the group. uint64 group_id = 2; // member_updates is the list of members to update, // set weight to 0 to remove a member. repeated MemberRequest member_updates = 3 [(gogoproto.nullable) = false, (amino.dont_omitempty) = true];}
In the list of MemberUpdates, an existing member can be removed by setting its weight to 0.It’s expected to fail if:
the signer is not the admin of the group.
for any one of the associated group policies, if its decision policy’s Validate() method fails against the updated group.
The UpdateGroupAdmin can be used to update a group admin.
Copy
Ask AI
// MsgUpdateGroupAdmin is the Msg/UpdateGroupAdmin request type.message MsgUpdateGroupAdmin { option (cosmos.msg.v1.signer) = "admin"; option (amino.name) = "cosmos-sdk/MsgUpdateGroupAdmin"; // admin is the current account address of the group admin. string admin = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // group_id is the unique ID of the group. uint64 group_id = 2; // new_admin is the group new admin account address. string new_admin = 3 [(cosmos_proto.scalar) = "cosmos.AddressString"];}
It’s expected to fail if the signer is not the admin of the group.
The UpdateGroupMetadata can be used to update a group metadata.
Copy
Ask AI
// MsgUpdateGroupMetadata is the Msg/UpdateGroupMetadata request type.message MsgUpdateGroupMetadata { option (cosmos.msg.v1.signer) = "admin"; option (amino.name) = "cosmos-sdk/MsgUpdateGroupMetadata"; // admin is the account address of the group admin. string admin = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // group_id is the unique ID of the group. uint64 group_id = 2; // metadata is the updated group's metadata. string metadata = 3;}
It’s expected to fail if:
new metadata length is greater than MaxMetadataLen config.
A new group policy can be created with the MsgCreateGroupPolicy, which has an admin address, a group id, a decision policy and some optional metadata.
Copy
Ask AI
// MsgCreateGroupPolicy is the Msg/CreateGroupPolicy request type.message MsgCreateGroupPolicy { option (cosmos.msg.v1.signer) = "admin"; option (amino.name) = "cosmos-sdk/MsgCreateGroupPolicy"; option (gogoproto.goproto_getters) = false; // admin is the account address of the group admin. string admin = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // group_id is the unique ID of the group. uint64 group_id = 2; // metadata is any arbitrary metadata attached to the group policy. string metadata = 3; // decision_policy specifies the group policy's decision policy. google.protobuf.Any decision_policy = 4 [(cosmos_proto.accepts_interface) = "cosmos.group.v1.DecisionPolicy"];}
It’s expected to fail if:
the signer is not the admin of the group.
metadata length is greater than MaxMetadataLen config.
the decision policy’s Validate() method doesn’t pass against the group.
A new group with policy can be created with the MsgCreateGroupWithPolicy, which has an admin address, a list of members, a decision policy, a group_policy_as_admin field to optionally set group and group policy admin with group policy address and some optional metadata for group and group policy.
Copy
Ask AI
// MsgCreateGroupWithPolicy is the Msg/CreateGroupWithPolicy request type.message MsgCreateGroupWithPolicy { option (cosmos.msg.v1.signer) = "admin"; option (amino.name) = "cosmos-sdk/MsgCreateGroupWithPolicy"; option (gogoproto.goproto_getters) = false; // admin is the account address of the group and group policy admin. string admin = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // members defines the group members. repeated MemberRequest members = 2 [(gogoproto.nullable) = false, (amino.dont_omitempty) = true]; // group_metadata is any arbitrary metadata attached to the group. string group_metadata = 3; // group_policy_metadata is any arbitrary metadata attached to the group policy. string group_policy_metadata = 4; // group_policy_as_admin is a boolean field, if set to true, the group policy account address will be used as group // and group policy admin. bool group_policy_as_admin = 5; // decision_policy specifies the group policy's decision policy. google.protobuf.Any decision_policy = 6 [(cosmos_proto.accepts_interface) = "cosmos.group.v1.DecisionPolicy"];}
It’s expected to fail for the same reasons as Msg/CreateGroup and Msg/CreateGroupPolicy.
The UpdateGroupPolicyAdmin can be used to update a group policy admin.
Copy
Ask AI
// MsgUpdateGroupPolicyAdmin is the Msg/UpdateGroupPolicyAdmin request type.message MsgUpdateGroupPolicyAdmin { option (cosmos.msg.v1.signer) = "admin"; option (amino.name) = "cosmos-sdk/MsgUpdateGroupPolicyAdmin"; // admin is the account address of the group admin. string admin = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // group_policy_address is the account address of the group policy. string group_policy_address = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // new_admin is the new group policy admin. string new_admin = 3 [(cosmos_proto.scalar) = "cosmos.AddressString"];}
It’s expected to fail if the signer is not the admin of the group policy.
The UpdateGroupPolicyDecisionPolicy can be used to update a decision policy.
Copy
Ask AI
// MsgUpdateGroupPolicyDecisionPolicy is the Msg/UpdateGroupPolicyDecisionPolicy request type.message MsgUpdateGroupPolicyDecisionPolicy { option (cosmos.msg.v1.signer) = "admin"; option (amino.name) = "cosmos-sdk/MsgUpdateGroupDecisionPolicy"; option (gogoproto.goproto_getters) = false; // admin is the account address of the group admin. string admin = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // group_policy_address is the account address of group policy. string group_policy_address = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // decision_policy is the updated group policy's decision policy. google.protobuf.Any decision_policy = 3 [(cosmos_proto.accepts_interface) = "cosmos.group.v1.DecisionPolicy"];}
It’s expected to fail if:
the signer is not the admin of the group policy.
the new decision policy’s Validate() method doesn’t pass against the group.
The UpdateGroupPolicyMetadata can be used to update a group policy metadata.
Copy
Ask AI
// MsgUpdateGroupPolicyMetadata is the Msg/UpdateGroupPolicyMetadata request type.message MsgUpdateGroupPolicyMetadata { option (cosmos.msg.v1.signer) = "admin"; option (amino.name) = "cosmos-sdk/MsgUpdateGroupPolicyMetadata"; // admin is the account address of the group admin. string admin = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // group_policy_address is the account address of group policy. string group_policy_address = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // metadata is the group policy metadata to be updated. string metadata = 3;}
It’s expected to fail if:
new metadata length is greater than MaxMetadataLen config.
A new proposal can be created with the MsgSubmitProposal, which has a group policy account address, a list of proposers addresses, a list of messages to execute if the proposal is accepted and some optional metadata.
An optional Exec value can be provided to try to execute the proposal immediately after proposal creation. Proposers signatures are considered as yes votes in this case.
Copy
Ask AI
// MsgSubmitProposal is the Msg/SubmitProposal request type.message MsgSubmitProposal { option (cosmos.msg.v1.signer) = "proposers"; option (amino.name) = "cosmos-sdk/group/MsgSubmitProposal"; option (gogoproto.goproto_getters) = false; // group_policy_address is the account address of group policy. string group_policy_address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // proposers are the account addresses of the proposers. // Proposers signatures will be counted as yes votes. repeated string proposers = 2; // metadata is any arbitrary metadata attached to the proposal. string metadata = 3; // messages is a list of `sdk.Msg`s that will be executed if the proposal passes. repeated google.protobuf.Any messages = 4; // exec defines the mode of execution of the proposal, // whether it should be executed immediately on creation or not. // If so, proposers signatures are considered as Yes votes. Exec exec = 5; // title is the title of the proposal. // // Since: cosmos-sdk 0.47 string title = 6; // summary is the summary of the proposal. // // Since: cosmos-sdk 0.47 string summary = 7;}
It’s expected to fail if:
metadata, title, or summary length is greater than MaxMetadataLen config.
A proposal can be withdrawn using MsgWithdrawProposal which has an address (can be either a proposer or the group policy admin) and a proposal_id (which has to be withdrawn).
Copy
Ask AI
// MsgWithdrawProposal is the Msg/WithdrawProposal request type.message MsgWithdrawProposal { option (cosmos.msg.v1.signer) = "address"; option (amino.name) = "cosmos-sdk/group/MsgWithdrawProposal"; // proposal is the unique ID of the proposal. uint64 proposal_id = 1; // address is the admin of the group policy or one of the proposer of the proposal. string address = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"];}
It’s expected to fail if:
the signer is neither the group policy admin nor proposer of the proposal.
A new vote can be created with the MsgVote, given a proposal id, a voter address, a choice (yes, no, veto or abstain) and some optional metadata.
An optional Exec value can be provided to try to execute the proposal immediately after voting.
Copy
Ask AI
// MsgVote is the Msg/Vote request type.message MsgVote { option (cosmos.msg.v1.signer) = "voter"; option (amino.name) = "cosmos-sdk/group/MsgVote"; // proposal is the unique ID of the proposal. uint64 proposal_id = 1; // voter is the voter account address. string voter = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // option is the voter's choice on the proposal. VoteOption option = 3; // metadata is any arbitrary metadata attached to the vote. string metadata = 4; // exec defines whether the proposal should be executed // immediately after voting or not. Exec exec = 5;}
It’s expected to fail if:
metadata length is greater than MaxMetadataLen config.
// MsgExec is the Msg/Exec request type.message MsgExec { option (cosmos.msg.v1.signer) = "executor"; option (amino.name) = "cosmos-sdk/group/MsgExec"; // proposal is the unique ID of the proposal. uint64 proposal_id = 1; // executor is the account address used to execute the proposal. string executor = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"];}
The messages that are part of this proposal won’t be executed if:
the proposal has not been accepted by the group policy.
the proposal has already been successfully executed.
The MsgLeaveGroup allows group member to leave a group.
Copy
Ask AI
// MsgLeaveGroup is the Msg/LeaveGroup request type.message MsgLeaveGroup { option (cosmos.msg.v1.signer) = "address"; option (amino.name) = "cosmos-sdk/group/MsgLeaveGroup"; // address is the account address of the group member. string address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // group_id is the unique ID of the group. uint64 group_id = 2;}
It’s expected to fail if:
the group member is not part of the group.
for any one of the associated group policies, if its decision policy’s Validate() method fails against the updated group.
The create-group command allows users to create a group which is an aggregation of member accounts with associated weights and
an administrator account.
Copy
Ask AI
simd tx group create-group [admin] [metadata] [members-json-file]
Example:
Copy
Ask AI
simd tx group create-group cosmos1.. "AQ==" members.json
The create-group-with-policy command allows users to create a group which is an aggregation of member accounts with associated weights and an administrator account with decision policy. If the --group-policy-as-admin flag is set to true, the group policy address becomes the group and group policy admin.
Copy
Ask AI
simd tx group create-group-with-policy [admin] [group-metadata] [group-policy-metadata] [members-json-file] [decision-policy] [flags]
The group module has four locations for metadata where users can provide further context about the on-chain actions they are taking. By default all metadata fields have a 255 character length field where metadata can be stored in json format, either on-chain or off-chain depending on the amount of data required. Here we provide a recommendation for the json structure and where the data should be stored. There are two important factors in making these recommendations. First, that the group and gov modules are consistent with one another, note the number of proposals made by all groups may be quite large. Second, that client applications such as block explorers and governance interfaces have confidence in the consistency of metadata structure across chains.
The authors field is an array of strings, this is to allow for multiple authors to be listed in the metadata.
In v0.46, the authors field is a comma-separated string. Frontends are encouraged to support both formats for backwards compatibility.