README.md
CommonDAO Package
CommonDAO is a general-purpose package that provides support to implement custom Decentralized Autonomous Organizations (DAO) on Gno.land.
It offers a minimal and flexible framework for building DAOs, with customizable options that adapt across multiple use cases.
Core Types
Package contains some core types which are important in any DAO implementation, these are CommonDAO, ProposalDefinition, Proposal and Vote.
1. CommonDAO Type
CommonDAO type is the main type used to define DAOs, allowing standalone DAO creation or hierarchical tree based ones.
During creation, it accepts many optional arguments some of which are handy depending on the DAO type. For example, standalone DAOs might use IDs, a name and description to uniquely identify individual DAOs; Hierarchical ones might choose to use slugs instead of IDs, or even a mix of both.
DAO Creation Examples
Standalone DAO:
1import "gno.land/p/nt/commondao"
2
3dao := commondao.New(
4 commondao.WithID(1),
5 commondao.WithName("MyDAO"),
6 commondao.WithDescription("An example DAO"),
7 commondao.WithMember("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"),
8 commondao.WithMember("g1hy6zry03hg5d8le9s2w4fxme6236hkgd928dun"),
9)
Hierarchical DAO:
1import "gno.land/p/nt/commondao"
2
3dao := commondao.New(
4 commondao.WithSlug("parent"),
5 commondao.WithName("ParentDAO"),
6 commondao.WithMember("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"),
7)
8
9subDAO := commondao.New(
10 commondao.WithSlug("child"),
11 commondao.WithName("ChildDAO"),
12 commondao.WithParent(dao),
13)
2. ProposalDefinition Type
Proposal definitions are the way proposal types are implemented in commondao
.
Definitions are required when creating a new proposal because they define the
behavior of the proposal.
Generally speaking, proposals can be divided in two types, one are the general (a.k.a. text proposals), and the other are the executable ones. The difference is that executable ones modify the blockchain state when they are executed after they have been approved, while general ones don't, they are usually used to signal or measure sentiment, for example regarding a relevant issue.
Creating a new proposal type requires implementing the following interface:
1type ProposalDefinition interface {
2 // Title returns proposal title.
3 Title() string
4
5 // Body returns proposal's body.
6 // It usually contains description or values that are specific to
7 // the proposal, like a description of the proposal's motivation
8 // or the list of values that would be applied when the proposal
9 // is approved.
10 Body() string
11
12 // VotingPeriod returns the period where votes are allowed after
13 // proposal creation. It's used to calculate the voting deadline
14 // from the proposal's creationd date.
15 VotingPeriod() time.Duration
16
17 // Tally counts the number of votes and verifies if proposal passes.
18 // It receives a readonly record containing the votes that has been
19 // submitted for the proposal and also the list of current DAO members.
20 Tally(ReadonlyVotingRecord, MemberSet) (passes bool, _ error)
21}
This minimal interface is the one required for general proposal types. Here
the most important method is the Tally()
one. It's used to check whether a
proposal passes or not.
Within Tally()
votes can be counted using different rules depending on the
proposal type, some proposal types might decide if there is consensus by using
super majority while others might decide using plurality for example, or even
just counting that a minimum number of certain positive votes have been
submitted to approve a proposal.
CommonDAO provides a couple of helpers for this, to cover some cases:
SelectChoiceByAbsoluteMajority()
SelectChoiceBySuperMajority()
(using a 2/3s threshold)SelectChoiceByPlurality()
2.1. Executable Proposals
Proposal definitions have optional features that could be implemented to extend the proposal type behaviour. One of those is required to enable execution support.
A proposal can be executable implementing the Executable interface as part of the new proposal definition:
1type Executable interface {
2 // Execute executes the proposal.
3 Execute(realm) error
4}
The Execute()
method is where the realm changes are made once the proposal is
executed.
Other features can be enabled by implementing the Validable interface and the CustomizableVoteChoices one, as a way to separate pre-execution validation and to support proposal voting choices different than the default ones (YES, NO and ABSTAIN).
3. Proposal Type
Proposals are key for governance, they are the main mechanic that allows DAO members to engage on governance.
They are usually not created directly but though CommonDAO instances, by
calling the CommonDAO.Propose()
or CommonDAO.MustPropose()
methods. Though,
alternatively, proposals could be added to CommonDAO's active proposals storage
using CommonDAO.ActiveProposals().Add()
.
1import (
2 "std"
3
4 "gno.land/p/nt/commondao"
5 "gno.land/r/example/mydao"
6)
7
8dao := commondao.New()
9creator := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")
10propDef := mydao.NewGeneralProposalDefinition("Title", "Description")
11proposal := dao.MustPropose(creator, propDef)
3.1. Voting on Proposals
The preferred way to submit a vote, once a proposal is created, is by calling
the CommonDAO.Vote()
method because it performs sanity checks before a vote
is considered valid; Alternatively votes can be directly added without sanity
checks to the proposal's voting record by calling
Proposal.VotingRecord().AddVote()
.
3.2. Voting Record
Each proposal keeps track of their submitted votes within an internal voting record. CommonDAO package defines it as a VotingRecord type.
The voting record of a proposal can be getted by calling its
Proposal.VotingRecord()
method.
Right now proposals have a single voting record but the plan is to support multiple voting records per proposal as an optional feature, which could be used in cases where a proposal must track votes in multiple independent records, for example in cases where a proposal could be promoted to a different DAO with a different set of members.
4. Vote Type
Vote type defines the structure to store information for individual proposal
votes. Apart from the normally mandatory Address
and voting Choice
fields,
there are two optional fields that can be useful in different use cases; These
fields are Reason
which can store a string with the reason for the vote, and
Context
which can be used to store generic values related to the vote, for
example vote weight information.
It's very important to be careful when using the Context
field, in case
references/pointers are assigned to it because they could potentially be
accessed anywhere, which could lead to unwanted indirect modifications.
Vote type is defined as:
1type Vote struct {
2 // Address is the address of the user that this vote belons to.
3 Address std.Address
4
5 // Choice contains the voted choice.
6 Choice VoteChoice
7
8 // Reason contains an optional reason for the vote.
9 Reason string
10
11 // Context can store any custom voting values related to the vote.
12 Context any
13}
Secondary Types
There are other types which can be handy for some implementations which might require to store DAO members or proposals in a custom location, or that might need member grouping support.
1. MemberStorage and ProposalStorage Types
These two types allows storing and iterating DAO members and proposals. They support DAO implementations that might require storing either members or proposals in an external realm other than the DAO realm.
CommonDAO package provides implementations that use AVL trees under the hood for storage and lookup.
Custom implementations are supported though the MemberStorage and ProposalStorage interfaces:
1type MemberStorage interface {
2 // Size returns the number of members in the storage.
3 Size() int
4
5 // Has checks if a member exists in the storage.
6 Has(std.Address) bool
7
8 // Add adds a member to the storage.
9 Add(std.Address) bool
10
11 // Remove removes a member from the storage.
12 Remove(std.Address) bool
13
14 // Grouping returns member groups when supported.
15 Grouping() MemberGrouping
16
17 // IterateByOffset iterates members starting at the given offset.
18 IterateByOffset(offset, count int, fn func(std.Address) bool)
19}
20
21type ProposalStorage interface {
22 // Has checks if a proposal exists.
23 Has(id uint64) bool
24
25 // Get returns a proposal or nil when proposal doesn't exist.
26 Get(id uint64) *Proposal
27
28 // Add adds a proposal to the storage.
29 Add(*Proposal)
30
31 // Remove removes a proposal from the storage.
32 Remove(id uint64)
33
34 // Size returns the number of proposals that the storage contains.
35 Size() int
36
37 // Iterate iterates proposals.
38 Iterate(offset, count int, reverse bool, fn func(*Proposal) bool) bool
39}
2. MemberGrouping and MemberGroup Types
Members grouping is an optional feature that provides support for DAO members grouping.
Grouping can be useful for DAOs that require grouping users by roles or tiers for example.
The MemberGrouping type is a collection of member groups, while the MemberGroup is a group of members with metadata.
Grouping by Role Example
1import "gno.land/p/nt/commondao"
2
3storage := commondao.NewMemberStorageWithGrouping()
4
5// Add a member that doesn't belong to any group
6storage.Add("g1...a")
7
8// Create a member group for owners
9owners, err := storage.Grouping().Add("owners")
10if err != nil {
11 panic(err)
12}
13
14// Add a member to the owners group
15owners.Members().Add("g1...b")
16
17// Add voting power to owners group metadata
18owners.SetMeta(3)
19
20// Create a member group for moderators
21moderators, err := storage.Grouping().Add("moderators")
22if err != nil {
23 panic(err)
24}
25
26// Add voting power to moderators group metadata
27moderators.SetMeta(1)
28
29// Add members to the moderators group
30moderators.Members().Add("g1...c")
31moderators.Members().Add("g1...d")