Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: r/gov/dao v2 #2581

Merged
merged 95 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
95 commits
Select commit Hold shift + click to select a range
4dd7c58
Add standard govdao types
zivkovicmilos Jul 13, 2024
aad859b
Initial simpledao implementation
zivkovicmilos Jul 13, 2024
81396bf
Add membstore tests
zivkovicmilos Jul 13, 2024
dd5d64b
Add propstore tests
zivkovicmilos Jul 13, 2024
c342741
Add testing stubs
zivkovicmilos Jul 13, 2024
3d16ba6
Update examples/gno.land/p/gov/dao/types.gno
zivkovicmilos Jul 14, 2024
8f2e3ac
Move /nt/simpledao -> /demo/simpledao
zivkovicmilos Jul 14, 2024
ae0639a
Move DAO API to dao.gno
zivkovicmilos Jul 14, 2024
3a7ba00
Move vote API to vote.gno
zivkovicmilos Jul 14, 2024
00056a5
Add GetVoteByMember to proposal API
zivkovicmilos Jul 14, 2024
209ee06
Drop name from dao.Member
zivkovicmilos Jul 14, 2024
54c555d
Add limit to number of proposals returned
zivkovicmilos Jul 14, 2024
84aeb30
Add Size() to MembStore API
zivkovicmilos Jul 14, 2024
322bc8c
Apply Yoda wisdom to MemberStore API
zivkovicmilos Jul 14, 2024
794a353
Add execution failed ProposalStatus
zivkovicmilos Jul 14, 2024
8c45039
Drop unused PropStore method
zivkovicmilos Jul 14, 2024
268021d
Drop RemoveMember, add removal in UpdateMember
zivkovicmilos Jul 14, 2024
43e5c66
Add Size() to PropStore API
zivkovicmilos Jul 14, 2024
e1cd617
Add pagination to MembStore
zivkovicmilos Jul 14, 2024
67e4535
Rename proposal.gno -> proposals.gno
zivkovicmilos Jul 14, 2024
8fbbd5e
Start cleaning up r/gov/dao
zivkovicmilos Jul 14, 2024
fd13d02
Clean house for the govdao executor
zivkovicmilos Jul 14, 2024
ebfd3d2
Add remaining simpledao tests
zivkovicmilos Jul 15, 2024
b3856c7
Remove unsafe ctx executor usage
zivkovicmilos Jul 15, 2024
4d3d94e
Fixup prop filetests
zivkovicmilos Jul 15, 2024
1f1d5d7
Fixup prop filetests
zivkovicmilos Jul 15, 2024
7fa7530
Remove faulty test
zivkovicmilos Jul 15, 2024
ef299bd
Add pagination for vote fetching
zivkovicmilos Jul 15, 2024
3d0c056
Tidy mod
zivkovicmilos Jul 15, 2024
4396c48
Update update method
zivkovicmilos Jul 15, 2024
24e0b08
Add r/sys/vars
zivkovicmilos Jul 15, 2024
4827d0b
Version packages, add unit tests
zivkovicmilos Jul 15, 2024
26f21d8
Start integrating r/sys/vars into EndBlocker
zivkovicmilos Jul 15, 2024
71e8644
Add moniker to r/gnoland/valopers
zivkovicmilos Jul 16, 2024
93d2b70
Resolve freaky import
zivkovicmilos Jul 16, 2024
efe35cc
Restore Endblocker
zivkovicmilos Jul 16, 2024
63cf23f
Make tidy
zivkovicmilos Jul 16, 2024
4520593
Make tidy
zivkovicmilos Jul 16, 2024
af65764
Fixup vars tests
zivkovicmilos Jul 16, 2024
f567394
Merge branch 'master' into dev/zivkovicmilos/govdao
zivkovicmilos Jul 16, 2024
4ec9f3b
Export errors
zivkovicmilos Jul 16, 2024
30ac5ff
Update comment
zivkovicmilos Jul 16, 2024
c05289f
Drop useless voters methods
zivkovicmilos Jul 16, 2024
d8e5b7d
Drop excess field
zivkovicmilos Jul 16, 2024
d320493
Update voting power
zivkovicmilos Jul 16, 2024
d33b0df
Please the standards police
zivkovicmilos Jul 16, 2024
79be838
Slim down the DAO API
zivkovicmilos Jul 16, 2024
8c52893
Revert "Slim down the DAO API"
zivkovicmilos Jul 16, 2024
74948fc
Add Executor to proposal
zivkovicmilos Jul 16, 2024
c9b4cc4
Fix up old API
zivkovicmilos Jul 17, 2024
93ca6a8
Mod tidy
zivkovicmilos Jul 17, 2024
ee77102
Make the PropStore part of the DAO API
zivkovicmilos Jul 17, 2024
dbf0966
Fixup prop test
zivkovicmilos Jul 17, 2024
2f337a6
Move out memberstore
zivkovicmilos Jul 17, 2024
31d265f
Add Render to proposal
zivkovicmilos Jul 18, 2024
908b949
Add govdao executor wrapper
zivkovicmilos Jul 18, 2024
6b3b81c
Remove unused code
zivkovicmilos Jul 18, 2024
480a287
Add new prop test
zivkovicmilos Jul 18, 2024
06fe4fe
Move out memberstore
zivkovicmilos Jul 20, 2024
dba0379
Move p/gov/dao -> p/demo/dao
zivkovicmilos Jul 20, 2024
873ba9b
Drop voting fetchers from the propstore
zivkovicmilos Jul 21, 2024
6ceb72a
Rename VotingStats -> Stats
zivkovicmilos Jul 21, 2024
8198c7f
Add key name to r/sys/vars emit event
zivkovicmilos Jul 21, 2024
8a51452
Merge branch 'master' into dev/zivkovicmilos/govdao
zivkovicmilos Jul 22, 2024
b0fa6b7
Merge branch 'master' into dev/zivkovicmilos/govdao
zivkovicmilos Aug 15, 2024
676b202
Make the executor not panic due to wrong caller
zivkovicmilos Aug 15, 2024
852291c
isCallerGOVDAO -> isCallerDAORealm
zivkovicmilos Aug 15, 2024
ae650d2
Resolve nitpick
zivkovicmilos Aug 15, 2024
aa4f9ef
Fix faulty test assert
zivkovicmilos Aug 15, 2024
33343d7
Merge branch 'master' into dev/zivkovicmilos/govdao
zivkovicmilos Sep 18, 2024
c3c33a2
Merge branch 'master' into dev/zivkovicmilos/govdao
zivkovicmilos Oct 7, 2024
30b80f7
Move r/sys/vars outside the PR
zivkovicmilos Oct 7, 2024
7addbd5
Move event emissions to the dao package
zivkovicmilos Oct 7, 2024
2f2632b
Add p/demo/dao doc and new event
zivkovicmilos Oct 7, 2024
671872a
Add duplicate check for WithInitialMembers
zivkovicmilos Oct 7, 2024
3f196b2
Change valoper proposal comment
zivkovicmilos Oct 7, 2024
c4b3970
Drop unused constant from propstore
zivkovicmilos Oct 7, 2024
38e2eae
Remove unused Vote struct from vote.gno
zivkovicmilos Oct 7, 2024
82745d9
Tidy mods
zivkovicmilos Oct 7, 2024
15404f5
Move combinederr to a specific package
zivkovicmilos Oct 7, 2024
2f19233
Remove IsExpired from the executor
zivkovicmilos Oct 7, 2024
9cbc56a
Remove unused check
zivkovicmilos Oct 7, 2024
3858a3c
Update gno.land/pkg/gnoland/vals.go
zivkovicmilos Oct 7, 2024
ee989ff
Add note about votes in the DAO
zivkovicmilos Oct 7, 2024
af75547
Merge branch 'dev/zivkovicmilos/govdao' of https://github.com/gnolang…
zivkovicmilos Oct 7, 2024
d98d780
Remove /r/gov/dao/v2 dependency
zivkovicmilos Oct 9, 2024
7171651
Merge branch 'master' into dev/zivkovicmilos/govdao
zivkovicmilos Oct 9, 2024
485114b
Merge branch 'master' into dev/zivkovicmilos/govdao
zivkovicmilos Oct 23, 2024
929993d
Drop useless assign
zivkovicmilos Oct 23, 2024
a57e5e0
Merge branch 'master' into dev/zivkovicmilos/govdao
zivkovicmilos Oct 27, 2024
7c721d8
Update exported comments
zivkovicmilos Oct 27, 2024
e6acd8f
Update membstore comments
zivkovicmilos Oct 27, 2024
ed953d6
Merge branch 'master' into dev/zivkovicmilos/govdao
zivkovicmilos Oct 29, 2024
5690734
Move combinederr to demo/combinederr
zivkovicmilos Oct 29, 2024
04107ac
Remove unused error
zivkovicmilos Oct 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions examples/gno.land/p/gov/dao/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module gno.land/p/gov/dao

require gno.land/p/gov/proposal v0.0.0-latest
34 changes: 34 additions & 0 deletions examples/gno.land/p/gov/dao/members.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package dao

import (
"std"
)

// MemberStore defines the member storage abstraction
type MemberStore interface {
zivkovicmilos marked this conversation as resolved.
Show resolved Hide resolved
// GetMembers returns all members in the store
GetMembers() []Member
zivkovicmilos marked this conversation as resolved.
Show resolved Hide resolved
moul marked this conversation as resolved.
Show resolved Hide resolved

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

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

// AddMember attempts to add a member to the store
zivkovicmilos marked this conversation as resolved.
Show resolved Hide resolved
AddMember(member Member) error

// RemoveMember attempts to remove a member from the store
RemoveMember(address std.Address) error

// UpdateMember attempts to update the member in the store
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)
Name string // name associated with the member
zivkovicmilos marked this conversation as resolved.
Show resolved Hide resolved
VotingPower uint64 // the voting power of the member
}
47 changes: 47 additions & 0 deletions examples/gno.land/p/gov/dao/proposal.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package dao

import "std"

type Status string

// ACTIVE -> ACCEPTED -> EXECUTED
// ACTIVE -> NOT ACCEPTED
var (
Active Status = "active" // proposal is still active
Accepted Status = "accepted" // proposal gathered quorum
NotAccepted Status = "not accepted" // proposal failed to gather quorum
Executed Status = "executed" // proposal is executed
zivkovicmilos marked this conversation as resolved.
Show resolved Hide resolved
)

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

// PropStore defines the proposal storage abstraction
type PropStore interface {
// GetProposals returns the given paginated proposals
GetProposals(offset, count uint64) []Proposal
zivkovicmilos marked this conversation as resolved.
Show resolved Hide resolved

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

// GetProposalsByAddress returns the proposals associated
// with the given proposer address
GetProposalsByAddress(address std.Address) []Proposal
moul marked this conversation as resolved.
Show resolved Hide resolved
}

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

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

// GetStatus returns the status of the proposal
GetStatus() Status
moul marked this conversation as resolved.
Show resolved Hide resolved

// GetVotes returns the votes of the proposal
GetVotes() []Vote
moul marked this conversation as resolved.
Show resolved Hide resolved
}
zivkovicmilos marked this conversation as resolved.
Show resolved Hide resolved
43 changes: 43 additions & 0 deletions examples/gno.land/p/gov/dao/types.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package dao

import (
"std"

"gno.land/p/gov/proposal"
)

type VoteOption string

const (
YesVote VoteOption = "YES"
NoVote VoteOption = "NO"
zivkovicmilos marked this conversation as resolved.
Show resolved Hide resolved
)

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

// Vote encompasses a single GOVDAO vote
type Vote struct {
zivkovicmilos marked this conversation as resolved.
Show resolved Hide resolved
Address std.Address // the address of the voter
Option VoteOption // the voting option
}

// DAO defines the DAO abstraction
type DAO interface {
zivkovicmilos marked this conversation as resolved.
Show resolved Hide resolved
// Propose adds a new proposal to the executor-based GOVDAO.
// Returns the generated proposal ID
Propose(comment string, executor proposal.Executor) (uint64, error)

// VoteOnProposal adds a vote to the given proposal ID
VoteOnProposal(id uint64, option VoteOption) error

// ExecuteProposal executes the proposal with the given ID
ExecuteProposal(id uint64) error

// GetMembStore returns the member store associated with the DAO
GetMembStore() MemberStore

// GetPropStore returns the proposal store associated with the DAO
GetPropStore() PropStore
zivkovicmilos marked this conversation as resolved.
Show resolved Hide resolved
}
206 changes: 206 additions & 0 deletions examples/gno.land/p/nt/simpledao/dao.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
package simpledao

import (
"errors"
"std"

"gno.land/p/demo/ufmt"
"gno.land/p/gov/dao"
pproposal "gno.land/p/gov/proposal"
)

var (
errInvalidExecutor = errors.New("invalid executor provided")
errInsufficientProposalFunds = errors.New("insufficient funds for proposal")
errInsufficientExecuteFunds = errors.New("insufficient funds for executing proposal")
errProposalExecuted = errors.New("proposal already executed")
errProposalInactive = errors.New("proposal is inactive")
errProposalExpired = errors.New("proposal is expired")
errProposalNotAccepted = errors.New("proposal is not accepted")

errNotGovDAO = errors.New("caller not correct govdao instance")
)

var (
minProposalFeeValue int64 = 100 * 1_000_000 // minimum gnot required for a govdao proposal (100 GNOT)
minExecuteFeeValue int64 = 500 * 1_000_000 // minimum gnot required for a govdao proposal (500 GNOT)

minProposalFee = std.NewCoin("ugnot", minProposalFeeValue)
minExecuteFee = std.NewCoin("ugnot", minExecuteFeeValue)
)

// SimpleDAO is a simple DAO implementation
type SimpleDAO struct {
membStore *MembStore
propStore *PropStore
}

// NewSimpleDAO creates a new instance of the simpledao DAO
func NewSimpleDAO(membStore *MembStore, propStore *PropStore) *SimpleDAO {
moul marked this conversation as resolved.
Show resolved Hide resolved
return &SimpleDAO{
membStore: membStore,
propStore: propStore,
}
}

func (s *SimpleDAO) Propose(description string, executor pproposal.Executor) (uint64, error) {
// Make sure the executor is set
if executor == nil {
return 0, errInvalidExecutor
}

var (
caller = getDAOCaller()
sentCoins = std.GetOrigSend() // Get the sent coins, if any
canCoverFee = sentCoins.AmountOf("ugnot") >= minProposalFee.Amount
)

// Check if the proposal is valid
if !s.membStore.IsMember(caller) && !canCoverFee {
return 0, errInsufficientProposalFunds
}

// Create the wrapped proposal
prop := &proposal{
description: description,
executor: executor,
author: caller,
votes: newVotes(),
}

// Add the proposal
id, err := s.propStore.addProposal(prop)
if err != nil {
return 0, ufmt.Errorf("unable to add proposal, %s", err)
}

return id, nil
}

func (s *SimpleDAO) VoteOnProposal(id uint64, option dao.VoteOption) error {
// Verify the GOVDAO member
member, err := s.membStore.GetMember(getDAOCaller())
if err != nil {
return ufmt.Errorf("unable to get govdao member, %s", err)
}

// Check if the proposal exists
propRaw, err := s.propStore.GetProposalByID(id)
if err != nil {
return ufmt.Errorf("unable to get proposal %d, %s", id, err)
}

prop := propRaw.(*proposal)

// Check the proposal status
if prop.GetStatus() == dao.Executed {
// Proposal was already executed, nothing to vote on anymore.
//
// In fact, the proposal should stop accepting
// votes as soon as a 2/3+ majority is reached
// on either option, but leaving the ability to vote still,
// even if a proposal is accepted, or not accepted,
// leaves room for "principle" vote decisions to be recorded
return errProposalInactive
}

// Check if the proposal executor is expired
if prop.executor.IsExpired() {
return errProposalExpired
}

// Cast the vote
if err = prop.votes.castVote(member, option); err != nil {
return ufmt.Errorf("unable to vote on proposal %d, %s", id, err)
}

// Check the votes to see if quorum is reached
var (
majorityPower = s.membStore.getMajorityPower()
yays, nays = prop.votes.getTally()
)

switch {
case yays > majorityPower:
prop.status = dao.Accepted
case nays > majorityPower:
prop.status = dao.NotAccepted
default:
// Quorum not reached
}

return nil
}

func (s *SimpleDAO) ExecuteProposal(id uint64) error {
var (
caller = getDAOCaller()
sentCoins = std.GetOrigSend() // Get the sent coins, if any
canCoverFee = sentCoins.AmountOf("ugnot") >= minExecuteFee.Amount
)

// Check if the non-DAO member can cover the execute fee
if !s.membStore.IsMember(caller) && !canCoverFee {
return errInsufficientExecuteFunds
}

// Check if the proposal exists
propRaw, err := s.propStore.GetProposalByID(id)
if err != nil {
return ufmt.Errorf("unable to get proposal %d, %s", id, err)
}

prop := propRaw.(*proposal)

// Check the proposal status
if prop.GetStatus() != dao.Accepted {
// Proposal is not accepted, cannot be executed
return errProposalNotAccepted
}

// Check if the proposal is executed
if prop.GetStatus() == dao.Executed {
// Proposal is already executed
return errProposalExecuted
}

// Check if proposal is expired
if prop.executor.IsExpired() {
return errProposalExpired
}

// Update the proposal status
prop.status = dao.Executed

// Attempt to execute the proposal
if err = prop.executor.Execute(); err != nil {
return ufmt.Errorf("error during proposal %d execution, %s", id, err)
}

return nil
}

func (s *SimpleDAO) GetMembStore() dao.MemberStore {
return s.membStore
}

func (s *SimpleDAO) GetPropStore() dao.PropStore {
return s.propStore
}

// getDAOCaller returns the DAO caller.
// XXX: This is not a great way to determine the caller, and it is very unsafe.
// However, the current MsgRun context does not persist escaping the main() scope.
// Until a better solution is developed, this enables proposals to be made through a package deployment + init()
func getDAOCaller() std.Address {
return std.GetOrigCaller()
}

// TODO change to r/sys/vars
const daoPkgPath = "gno.land/r/gov/dao"

// isCallerGOVDAO returns a flag indicating if the
// current caller context is the active GOVDAO
func isCallerGOVDAO() bool {
moul marked this conversation as resolved.
Show resolved Hide resolved
return std.CurrentRealm().PkgPath() == daoPkgPath
}
Loading
Loading