Skip to content

Commit

Permalink
feat: r/gov/dao v2 (#2581)
Browse files Browse the repository at this point in the history
## Description

This PR introduces an upgrade to the `r/gov/dao` system:
- it makes it configurable through custom implementations
    - added a `p/demo/simpledao` implementation
- the implementations are changeable through a govdao proposal
- adds weighted voting to a govdao example implementation

<details><summary>Contributors' checklist...</summary>

- [x] Added new tests, or not needed, or not feasible
- [x] Provided an example (e.g. screenshot) to aid review or the PR is
self-explanatory
- [x] Updated the official documentation or not needed
- [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message
was included in the description
- [x] Added references to related issues and PRs
- [ ] Provided any useful hints for running manual tests
- [ ] Added new benchmarks to [generated
graphs](https://gnoland.github.io/benchmarks), if any. More info
[here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md).
</details>

---------

Co-authored-by: Manfred Touron <[email protected]>
  • Loading branch information
zivkovicmilos and moul committed Oct 31, 2024
1 parent 386f8bd commit 71bc19d
Show file tree
Hide file tree
Showing 62 changed files with 3,542 additions and 1,063 deletions.
40 changes: 40 additions & 0 deletions examples/gno.land/p/demo/combinederr/combinederr.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package combinederr

import "strings"

// CombinedError is a combined execution error
type CombinedError struct {
errors []error
}

// Error returns the combined execution error
func (e *CombinedError) Error() string {
if len(e.errors) == 0 {
return ""
}

var sb strings.Builder

for _, err := range e.errors {
sb.WriteString(err.Error() + "; ")
}

// Remove the last semicolon and space
result := sb.String()

return result[:len(result)-2]
}

// Add adds a new error to the execution error
func (e *CombinedError) Add(err error) {
if err == nil {
return
}

e.errors = append(e.errors, err)
}

// Size returns a
func (e *CombinedError) Size() int {
return len(e.errors)
}
1 change: 1 addition & 0 deletions examples/gno.land/p/demo/combinederr/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module gno.land/p/demo/combinederr
33 changes: 33 additions & 0 deletions examples/gno.land/p/demo/dao/dao.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package dao

const (
ProposalAddedEvent = "ProposalAdded" // emitted when a new proposal has been added
ProposalAcceptedEvent = "ProposalAccepted" // emitted when a proposal has been accepted
ProposalNotAcceptedEvent = "ProposalNotAccepted" // emitted when a proposal has not been accepted
ProposalExecutedEvent = "ProposalExecuted" // emitted when a proposal has been executed

ProposalEventIDKey = "proposal-id"
ProposalEventAuthorKey = "proposal-author"
ProposalEventExecutionKey = "exec-status"
)

// ProposalRequest is a single govdao proposal request
// that contains the necessary information to
// log and generate a valid proposal
type ProposalRequest struct {
Description string // the description associated with the proposal
Executor Executor // the proposal executor
}

// DAO defines the DAO abstraction
type DAO interface {
// PropStore is the DAO proposal storage
PropStore

// Propose adds a new proposal to the executor-based GOVDAO.
// Returns the generated proposal ID
Propose(request ProposalRequest) (uint64, error)

// ExecuteProposal executes the proposal with the given ID
ExecuteProposal(id uint64) error
}
5 changes: 5 additions & 0 deletions examples/gno.land/p/demo/dao/doc.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Package dao houses common DAO building blocks (framework), which can be used or adopted by any
// specific DAO implementation. By design, the DAO should house the proposals it receives, but not the actual
// DAO members or proposal votes. These abstractions should be implemented by a separate entity, to keep the DAO
// agnostic of implementation details such as these (member / vote management).
package dao
56 changes: 56 additions & 0 deletions examples/gno.land/p/demo/dao/events.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package dao

import (
"std"

"gno.land/p/demo/ufmt"
)

// EmitProposalAdded emits an event signaling that
// a given proposal was added
func EmitProposalAdded(id uint64, proposer std.Address) {
std.Emit(
ProposalAddedEvent,
ProposalEventIDKey, ufmt.Sprintf("%d", id),
ProposalEventAuthorKey, proposer.String(),
)
}

// EmitProposalAccepted emits an event signaling that
// a given proposal was accepted
func EmitProposalAccepted(id uint64) {
std.Emit(
ProposalAcceptedEvent,
ProposalEventIDKey, ufmt.Sprintf("%d", id),
)
}

// EmitProposalNotAccepted emits an event signaling that
// a given proposal was not accepted
func EmitProposalNotAccepted(id uint64) {
std.Emit(
ProposalNotAcceptedEvent,
ProposalEventIDKey, ufmt.Sprintf("%d", id),
)
}

// EmitProposalExecuted emits an event signaling that
// a given proposal was executed, with the given status
func EmitProposalExecuted(id uint64, status ProposalStatus) {
std.Emit(
ProposalExecutedEvent,
ProposalEventIDKey, ufmt.Sprintf("%d", id),
ProposalEventExecutionKey, status.String(),
)
}

// EmitVoteAdded emits an event signaling that
// a vote was cast for a given proposal
func EmitVoteAdded(id uint64, voter std.Address, option VoteOption) {
std.Emit(
VoteAddedEvent,
VoteAddedIDKey, ufmt.Sprintf("%d", id),
VoteAddedAuthorKey, voter.String(),
VoteAddedOptionKey, option.String(),
)
}
9 changes: 9 additions & 0 deletions examples/gno.land/p/demo/dao/executor.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package dao

// Executor represents a minimal closure-oriented proposal design.
// It is intended to be used by a govdao governance proposal (v1, v2, etc)
type Executor interface {
// Execute executes the given proposal, and returns any error encountered
// during the execution
Execute() error
}
3 changes: 3 additions & 0 deletions examples/gno.land/p/demo/dao/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module gno.land/p/demo/dao

require gno.land/p/demo/ufmt v0.0.0-latest
62 changes: 62 additions & 0 deletions examples/gno.land/p/demo/dao/proposals.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package dao

import "std"

// ProposalStatus is the currently active proposal status,
// changed based on DAO functionality.
// Status transitions:
//
// ACTIVE -> ACCEPTED -> EXECUTION(SUCCEEDED/FAILED)
//
// ACTIVE -> NOT ACCEPTED
type ProposalStatus string

var (
Active ProposalStatus = "active" // proposal is still active
Accepted ProposalStatus = "accepted" // proposal gathered quorum
NotAccepted ProposalStatus = "not accepted" // proposal failed to gather quorum
ExecutionSuccessful ProposalStatus = "execution successful" // proposal is executed successfully
ExecutionFailed ProposalStatus = "execution failed" // proposal is failed during execution
)

func (s ProposalStatus) String() string {
return string(s)
}

// PropStore defines the proposal storage abstraction
type PropStore interface {
// Proposals returns the given paginated proposals
Proposals(offset, count uint64) []Proposal

// ProposalByID returns the proposal associated with
// the given ID, if any
ProposalByID(id uint64) (Proposal, error)

// Size returns the number of proposals in
// the proposal store
Size() int
}

// Proposal is the single proposal abstraction
type Proposal interface {
// Author returns the author of the proposal
Author() std.Address

// Description returns the description of the proposal
Description() string

// Status returns the status of the proposal
Status() ProposalStatus

// Executor returns the proposal executor
Executor() Executor

// Stats returns the voting stats of the proposal
Stats() Stats

// IsExpired returns a flag indicating if the proposal expired
IsExpired() bool

// Render renders the proposal in a readable format
Render() string
}
69 changes: 69 additions & 0 deletions examples/gno.land/p/demo/dao/vote.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package dao

// NOTE:
// This voting pods will be removed in a future version of the
// p/demo/dao package. A DAO shouldn't have to comply with or define how the voting mechanism works internally;
// it should be viewed as an entity that makes decisions
//
// The extent of "votes being enforced" in this implementation is just in the context
// of types a DAO can use (import), and in the context of "Stats", where
// there is a notion of "Yay", "Nay" and "Abstain" votes.
const (
VoteAddedEvent = "VoteAdded" // emitted when a vote was cast for a proposal

VoteAddedIDKey = "proposal-id"
VoteAddedAuthorKey = "author"
VoteAddedOptionKey = "option"
)

// VoteOption is the limited voting option for a DAO proposal
type VoteOption string

const (
YesVote VoteOption = "YES" // Proposal should be accepted
NoVote VoteOption = "NO" // Proposal should be rejected
AbstainVote VoteOption = "ABSTAIN" // Side is not chosen
)

func (v VoteOption) String() string {
return string(v)
}

// Stats encompasses the proposal voting stats
type Stats struct {
YayVotes uint64
NayVotes uint64
AbstainVotes uint64

TotalVotingPower uint64
}

// YayPercent returns the percentage (0-100) of the yay votes
// in relation to the total voting power
func (v Stats) YayPercent() uint64 {
return v.YayVotes * 100 / v.TotalVotingPower
}

// NayPercent returns the percentage (0-100) of the nay votes
// in relation to the total voting power
func (v Stats) NayPercent() uint64 {
return v.NayVotes * 100 / v.TotalVotingPower
}

// AbstainPercent returns the percentage (0-100) of the abstain votes
// in relation to the total voting power
func (v Stats) AbstainPercent() uint64 {
return v.AbstainVotes * 100 / v.TotalVotingPower
}

// MissingVotes returns the summed voting power that has not
// participated in proposal voting yet
func (v Stats) MissingVotes() uint64 {
return v.TotalVotingPower - (v.YayVotes + v.NayVotes + v.AbstainVotes)
}

// MissingVotesPercent returns the percentage (0-100) of the missing votes
// in relation to the total voting power
func (v Stats) MissingVotesPercent() uint64 {
return v.MissingVotes() * 100 / v.TotalVotingPower
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
module gno.land/r/gov/dao
module gno.land/p/demo/membstore

require (
gno.land/p/demo/avl v0.0.0-latest
gno.land/p/demo/testutils v0.0.0-latest
gno.land/p/demo/uassert v0.0.0-latest
gno.land/p/demo/ufmt v0.0.0-latest
gno.land/p/demo/urequire v0.0.0-latest
gno.land/p/gov/proposal v0.0.0-latest
)
38 changes: 38 additions & 0 deletions examples/gno.land/p/demo/membstore/members.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package membstore

import (
"std"
)

// MemberStore defines the member storage abstraction
type MemberStore interface {
// Members returns all members in the store
Members(offset, count uint64) []Member

// Size returns the current size of the store
Size() int

// IsMember returns a flag indicating if the given address
// belongs to a member
IsMember(address std.Address) bool

// TotalPower returns the total voting power of the member store
TotalPower() uint64

// Member returns the requested member
Member(address std.Address) (Member, error)

// AddMember adds a member to the store
AddMember(member Member) error

// UpdateMember updates the member in the store.
// If updating a member's voting power to 0,
// the member will be removed
UpdateMember(address std.Address, member Member) error
}

// Member holds the relevant member information
type Member struct {
Address std.Address // bech32 gno address of the member (unique)
VotingPower uint64 // the voting power of the member
}
Loading

0 comments on commit 71bc19d

Please sign in to comment.