From 4dd7c58e7351afe9b31106895c25df39063833cc Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Sat, 13 Jul 2024 11:40:03 +0200 Subject: [PATCH 01/80] Add standard govdao types --- examples/gno.land/p/gov/dao/gno.mod | 3 ++ examples/gno.land/p/gov/dao/members.gno | 27 ++++++++++++ examples/gno.land/p/gov/dao/proposal.gno | 52 ++++++++++++++++++++++++ examples/gno.land/p/gov/dao/types.gno | 38 +++++++++++++++++ 4 files changed, 120 insertions(+) create mode 100644 examples/gno.land/p/gov/dao/gno.mod create mode 100644 examples/gno.land/p/gov/dao/members.gno create mode 100644 examples/gno.land/p/gov/dao/proposal.gno create mode 100644 examples/gno.land/p/gov/dao/types.gno diff --git a/examples/gno.land/p/gov/dao/gno.mod b/examples/gno.land/p/gov/dao/gno.mod new file mode 100644 index 00000000000..80d25134a0a --- /dev/null +++ b/examples/gno.land/p/gov/dao/gno.mod @@ -0,0 +1,3 @@ +module gno.land/p/gov/dao + +require gno.land/p/gov/proposal v0.0.0-latest diff --git a/examples/gno.land/p/gov/dao/members.gno b/examples/gno.land/p/gov/dao/members.gno new file mode 100644 index 00000000000..bb380b50dd6 --- /dev/null +++ b/examples/gno.land/p/gov/dao/members.gno @@ -0,0 +1,27 @@ +package dao + +import ( + "std" +) + +// MemberStore defines the member storage abstraction +type MemberStore interface { + // GetMembers returns all members in the store + GetMembers() []Member + + // AddMember attempts to add a member to the store + AddMember() error + + // RemoveMember attempts to remove a member from the store + RemoveMember() error + + // IsMember returns a flag indicating if the given address + // belongs to a member + IsMember(address std.Address) bool +} + +// 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 +} diff --git a/examples/gno.land/p/gov/dao/proposal.gno b/examples/gno.land/p/gov/dao/proposal.gno new file mode 100644 index 00000000000..3440b1d0515 --- /dev/null +++ b/examples/gno.land/p/gov/dao/proposal.gno @@ -0,0 +1,52 @@ +package dao + +import "std" + +type Status string + +var ( + Accepted Status = "accepted" + Active Status = "active" + NotAccepted Status = "not accepted" + Expired Status = "expired" + Succeeded Status = "succeeded" +) + +func (s Status) String() string { + return string(s) +} + +// PropStore defines the proposal storage abstraction +type PropStore interface { + // AddProposal adds a new proposal to the store + AddProposal(proposal Proposal) error + + // RemoveProposal removes a proposal from the store, if any + RemoveProposal(id uint64) error + + // GetProposals returns the given paginated proposals + GetProposals(offset, count uint64) []Proposal + + // 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 +} + +// 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 + + // GetVoters returns the voters of the proposal + GetVoters() []std.Address +} diff --git a/examples/gno.land/p/gov/dao/types.gno b/examples/gno.land/p/gov/dao/types.gno new file mode 100644 index 00000000000..7c253e87c31 --- /dev/null +++ b/examples/gno.land/p/gov/dao/types.gno @@ -0,0 +1,38 @@ +package dao + +import "gno.land/p/gov/proposal" + +type VoteOption string + +const ( + YesVote VoteOption = "YES" + NoVote VoteOption = "NO" +) + +func (v VoteOption) String() string { + return string(v) +} + +// DAO defines the DAO abstraction +type DAO interface { + // Propose adds a new proposal to the executor-based GOVDAO + Propose(comment string, executor proposal.Executor) 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 + + // GetMembersStore returns the member store associated with the DAO + GetMembersStore() MemberStore + + // SetMembersStore sets the member store for the DAO + SetMembersStore(store MemberStore) + + // GetPropStore returns the proposal store associated with the DAO + GetPropStore() PropStore + + // SetPropStore sets the proposal store for the DAO + SetPropStore(store PropStore) +} From aad859b58311813c3245aab7e098892da637d0dd Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Sat, 13 Jul 2024 17:12:57 +0200 Subject: [PATCH 02/80] Initial simpledao implementation --- examples/gno.land/p/gov/dao/members.gno | 21 +- examples/gno.land/p/gov/dao/proposal.gno | 21 +- examples/gno.land/p/gov/dao/types.gno | 27 ++- examples/gno.land/p/nt/simpledao/dao.gno | 206 ++++++++++++++++++ examples/gno.land/p/nt/simpledao/gno.mod | 9 + .../gno.land/p/nt/simpledao/membstore.gno | 154 +++++++++++++ .../gno.land/p/nt/simpledao/propstore.gno | 136 ++++++++++++ examples/gno.land/p/nt/simpledao/vote.gno | 76 +++++++ 8 files changed, 619 insertions(+), 31 deletions(-) create mode 100644 examples/gno.land/p/nt/simpledao/dao.gno create mode 100644 examples/gno.land/p/nt/simpledao/gno.mod create mode 100644 examples/gno.land/p/nt/simpledao/membstore.gno create mode 100644 examples/gno.land/p/nt/simpledao/propstore.gno create mode 100644 examples/gno.land/p/nt/simpledao/vote.gno diff --git a/examples/gno.land/p/gov/dao/members.gno b/examples/gno.land/p/gov/dao/members.gno index bb380b50dd6..965d13eea00 100644 --- a/examples/gno.land/p/gov/dao/members.gno +++ b/examples/gno.land/p/gov/dao/members.gno @@ -9,19 +9,26 @@ type MemberStore interface { // GetMembers returns all members in the store GetMembers() []Member + // 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 - AddMember() error + AddMember(member Member) error // RemoveMember attempts to remove a member from the store - RemoveMember() error + RemoveMember(address std.Address) error - // IsMember returns a flag indicating if the given address - // belongs to a member - IsMember(address std.Address) bool + // 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 + Address std.Address // bech32 gno address of the member (unique) + Name string // name associated with the member + VotingPower uint64 // the voting power of the member } diff --git a/examples/gno.land/p/gov/dao/proposal.gno b/examples/gno.land/p/gov/dao/proposal.gno index 3440b1d0515..67339427a0c 100644 --- a/examples/gno.land/p/gov/dao/proposal.gno +++ b/examples/gno.land/p/gov/dao/proposal.gno @@ -4,12 +4,13 @@ import "std" type Status string +// ACTIVE -> ACCEPTED -> EXECUTED +// ACTIVE -> NOT ACCEPTED var ( - Accepted Status = "accepted" - Active Status = "active" - NotAccepted Status = "not accepted" - Expired Status = "expired" - Succeeded Status = "succeeded" + 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 ) func (s Status) String() string { @@ -18,12 +19,6 @@ func (s Status) String() string { // PropStore defines the proposal storage abstraction type PropStore interface { - // AddProposal adds a new proposal to the store - AddProposal(proposal Proposal) error - - // RemoveProposal removes a proposal from the store, if any - RemoveProposal(id uint64) error - // GetProposals returns the given paginated proposals GetProposals(offset, count uint64) []Proposal @@ -47,6 +42,6 @@ type Proposal interface { // GetStatus returns the status of the proposal GetStatus() Status - // GetVoters returns the voters of the proposal - GetVoters() []std.Address + // GetVotes returns the votes of the proposal + GetVotes() []Vote } diff --git a/examples/gno.land/p/gov/dao/types.gno b/examples/gno.land/p/gov/dao/types.gno index 7c253e87c31..872fd97d4f6 100644 --- a/examples/gno.land/p/gov/dao/types.gno +++ b/examples/gno.land/p/gov/dao/types.gno @@ -1,6 +1,10 @@ package dao -import "gno.land/p/gov/proposal" +import ( + "std" + + "gno.land/p/gov/proposal" +) type VoteOption string @@ -13,10 +17,17 @@ func (v VoteOption) String() string { return string(v) } +// Vote encompasses a single GOVDAO vote +type Vote struct { + Address std.Address // the address of the voter + Option VoteOption // the voting option +} + // DAO defines the DAO abstraction type DAO interface { - // Propose adds a new proposal to the executor-based GOVDAO - Propose(comment string, executor proposal.Executor) error + // 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 @@ -24,15 +35,9 @@ type DAO interface { // ExecuteProposal executes the proposal with the given ID ExecuteProposal(id uint64) error - // GetMembersStore returns the member store associated with the DAO - GetMembersStore() MemberStore - - // SetMembersStore sets the member store for the DAO - SetMembersStore(store MemberStore) + // GetMembStore returns the member store associated with the DAO + GetMembStore() MemberStore // GetPropStore returns the proposal store associated with the DAO GetPropStore() PropStore - - // SetPropStore sets the proposal store for the DAO - SetPropStore(store PropStore) } diff --git a/examples/gno.land/p/nt/simpledao/dao.gno b/examples/gno.land/p/nt/simpledao/dao.gno new file mode 100644 index 00000000000..4b604d6c137 --- /dev/null +++ b/examples/gno.land/p/nt/simpledao/dao.gno @@ -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 dao.MembStore, propStore dao.PropStore) *SimpleDAO { + 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 + } + + // Attempt to execute the proposal + if err = prop.executor.Execute(); err != nil { + return ufmt.Errorf("failed to execute proposal %d, %s", id, err) + } + + // Update the proposal status + prop.status = dao.Executed + + 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 { + return std.PrevRealm().Addr() == daoPkgPath +} diff --git a/examples/gno.land/p/nt/simpledao/gno.mod b/examples/gno.land/p/nt/simpledao/gno.mod new file mode 100644 index 00000000000..1f0b7ef929f --- /dev/null +++ b/examples/gno.land/p/nt/simpledao/gno.mod @@ -0,0 +1,9 @@ +module gno.land/p/nt/simpledao + +require ( + gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/seqid v0.0.0-latest + gno.land/p/demo/ufmt v0.0.0-latest + gno.land/p/gov/dao v0.0.0-latest + gno.land/p/gov/proposal v0.0.0-latest +) diff --git a/examples/gno.land/p/nt/simpledao/membstore.gno b/examples/gno.land/p/nt/simpledao/membstore.gno new file mode 100644 index 00000000000..f35ef380a1d --- /dev/null +++ b/examples/gno.land/p/nt/simpledao/membstore.gno @@ -0,0 +1,154 @@ +package simpledao + +import ( + "errors" + + "std" + + "gno.land/p/demo/avl" + "gno.land/p/gov/dao" +) + +var ( + errAlreadyMember = errors.New("address is already a member") + errMissingMember = errors.New("address is not a member") + errInvalidAddressUpdate = errors.New("invalid address update") +) + +type MembStoreOption func(*MembStore) + +// WithInitialMembers initializes the simpledao member store +// with an initial member list +func WithInitialMembers(members []dao.Member) MembStoreOption { + return func(store *MembStore) { + for _, m := range members { + members.Set(m.Address.String(), m) + } + } +} + +// MembStore implements the dao.MembStore abstraction +type MembStore struct { + members *avl.Tree // std.Address -> dao.Member + + totalVotingPower uint64 // cached value for quick lookups + majorityPower uint64 // cached value for quick lookups +} + +// NewMembStore creates a new simpledao member store +func NewMembStore() *MembStore { + // TODO add option + return &MembStore{ + members: avl.NewTree(), + totalVotingPower: 0, + majorityPower: 0, + } +} + +func (m *MembStore) GetMembers() []dao.Member { + members := make([]dao.Member, 0, m.members.Size()) + + m.members.Iterate("", "", func(_ string, val interface{}) bool { + member := val.(dao.Member) + + // Save the member + members = append(members, member) + + return false + }) + + return members +} + +func (m *MembStore) AddMember(member Member) error { + if !isCallerGOVDAO() { + return errNotGovDAO + } + + // Check if the member exists + if m.IsMember(member.Address) { + return errAlreadyMember + } + + // Add the member + m.members.Set(member.Address.String(), member) + + // Update the total voting power + m.totalVotingPower += member.VotingPower + m.majorityPower = (2 * m.totalVotingPower) / 3 + + return nil +} + +func (m *MembStore) RemoveMember(address std.Address) error { + if !isCallerGOVDAO() { + return errNotGovDAO + } + + // Check if the member exists + memberRaw, exists := m.members.Get(address) + if !exists { + return errMissingMember + } + + member := memberRaw.(dao.Member) + + // Remove the member + m.members.Remove(address.String()) + + m.totalVotingPower -= member.VotingPower + m.majorityPower = (2 * m.totalVotingPower) / 3 + + return nil +} + +func (m *MembStore) UpdateMember(address std.Address, member dao.Member) error { + if !isCallerGOVDAO() { + return errNotGovDAO + } + + // Check if the member exists + memberRaw, exists := m.members.Get(address) + if !exists { + return errMissingMember + } + + // Check that the member wouldn't be + // overwriting an existing one + isAddressUpdate := address != member.Address + if isAddressUpdate && m.IsMember(member.Address) { + return errInvalidAddressUpdate + } + + // Remove the old member info + // in case the address changed + if address != member.Address { + m.members.Remove(address.String()) + } + + // Save the new member info + m.members.Set(member.Address.String(), member) + + return nil +} + +func (m *MembStore) IsMember(address std.Address) bool { + _, exists := m.members.Get(address.String()) + + return exists +} + +func (m *MembStore) GetMember(address std.Address) (dao.Member, error) { + member, exists := m.members.Get(address.String()) + if !exists { + return dao.Member{}, errMissingMember + } + + return member, nil +} + +// getMajorityPower returns the majority voting power +// of the member store +func (m *MembStore) getMajorityPower() uint64 { + return m.majorityPower +} diff --git a/examples/gno.land/p/nt/simpledao/propstore.gno b/examples/gno.land/p/nt/simpledao/propstore.gno new file mode 100644 index 00000000000..5a13e9b3fcd --- /dev/null +++ b/examples/gno.land/p/nt/simpledao/propstore.gno @@ -0,0 +1,136 @@ +package simpledao + +import ( + "errors" + "std" + + "gno.land/p/demo/avl" + "gno.land/p/demo/seqid" + "gno.land/p/gov/dao" + pproposal "gno.land/p/gov/proposal" +) + +var errMissingProposal = errors.New("proposal is missing") + +// proposal is the internal simpledao proposal implementation +type proposal struct { + author std.Address // initiator of the proposal + description string // description of the proposal + + executor pproposal.Executor // executor for the proposal + status dao.Status // status of the proposal + + votes *votes // voting mechanism +} + +func (p *proposal) GetAuthor() std.Address { + return p.author +} + +func (p *proposal) GetDescription() string { + return p.description +} + +func (p *proposal) GetStatus() dao.Status { + return p.status +} + +func (p *proposal) GetVotes() []dao.Vote { + return p.votes.getVotes() +} + +// PropStore implements the dao.PropStore abstraction +type PropStore struct { + proposals *avl.Tree // seqid.ID -> proposal +} + +// NewPropStore returns a new simpledao PropStore +func NewPropStore() *PropStore { + return &PropStore{ + proposals: avl.NewTree(), + } +} + +// addProposal adds a new simpledao proposal to the store +func (p *PropStore) addProposal(proposal *proposal) (uint64, error) { + // See what the next proposal number should be + nextID := uint64(p.proposals.Size()) + + // Save the proposal + p.proposals.Set(getProposalID(nextID), proposal) + + return nextID, nil +} + +func (p *PropStore) GetProposals(givenOffset, count uint64) []dao.Proposal { + // Calculate the left and right bounds + offset := givenOffset + if offset < 1 { + offset = 1 + } + + var ( + startIndex = (offset - 1) * count + endIndex = startIndex + count + + numProposals = uint64(p.proposals.Size()) + ) + + // Check if the current offset has any proposals + if startIndex >= numProposals { + return []dao.Proposal{} + } + + // Check if the right bound is good + if endIndex >= numProposals { + endIndex = numProposals - 1 + } + + props := make([]dao.Proposal, 0) + p.proposals.Iterate( + getProposalID(startIndex), + getProposalID(endIndex), + func(_ string, val interface{}) bool { + prop := val.(*proposal) + + // Save the proposal + props = append(props, prop) + + return false + }, + ) + + return props +} + +func (p *PropStore) GetProposalByID(id uint64) (dao.Proposal, error) { + prop, exists := p.proposals.Get(getProposalID(id)) + if !exists { + return dao.Proposal{}, errMissingProposal + } + + return prop.(*proposal), nil +} + +func (p *PropStore) GetProposalsByAddress(address std.Address) []dao.Proposal { + props := make([]dao.Proposal, 0, p.proposals.Size()) + + p.proposals.Iterate("", "", func(_ string, val interface{}) bool { + prop := val.(*proposal) + + // Check the author + if prop.GetAuthor() != address { + props = append(props, prop) + } + + return false + }) + + return props +} + +// getProposalID generates a sequential proposal ID +// from the given ID number +func getProposalID(id uint64) string { + return seqid.ID(id).String() +} diff --git a/examples/gno.land/p/nt/simpledao/vote.gno b/examples/gno.land/p/nt/simpledao/vote.gno new file mode 100644 index 00000000000..2f5fa913dd3 --- /dev/null +++ b/examples/gno.land/p/nt/simpledao/vote.gno @@ -0,0 +1,76 @@ +package simpledao + +import ( + "errors" + + "std" + + "gno.land/p/demo/avl" + "gno.land/p/gov/dao" +) + +var errAlreadyVoted = errors.New("vote already cast") + +// votes is a simple weighted voting system +type votes struct { + // tally cache to keep track of active for / against votes + yays uint64 + nays uint64 + + voters *avl.Tree // std.Address -> dao.VoteOption +} + +// newVotes creates a new weighted voting system instance +func newVotes() *votes { + return &votes{ + voters: avl.NewTree(), + } +} + +// castVote casts a single vote in the name of the given member +func (v *votes) castVote(member dao.Member, option dao.VoteOption) error { + // Check if the member voted already + address := member.Address.String() + + _, voted := v.voters.Get(address) + if voted { + return errAlreadyVoted + } + + // Update the tally + if option == dao.YesVote { + v.yays += member.VotingPower + } else { + v.nays += member.VotingPower + } + + // Save the voting status + v.voters.Set(address, option) + + return nil +} + +// getTally returns the yay, and nay count, respectively +func (v *votes) getTally() (uint64, uint64) { + return v.yays, v.nays +} + +// getVotes fetches the currently active vote set +func (v *votes) getVotes() []dao.Vote { + votes := make([]dao.Vote, 0, v.voters.Size()) + + v.voters.Iterate("", "", func(key string, val interface{}) bool { + option := val.(dao.VoteOption) + + vote := dao.Vote{ + Address: std.Address(key), + Option: option, + } + + votes = append(votes, vote) + + return false + }) + + return votes +} From 81396bfd5e7c9be232010a8dc55761f549295b5c Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Sat, 13 Jul 2024 18:18:42 +0200 Subject: [PATCH 03/80] Add membstore tests --- examples/gno.land/p/nt/simpledao/dao.gno | 4 +- examples/gno.land/p/nt/simpledao/gno.mod | 3 + .../gno.land/p/nt/simpledao/membstore.gno | 48 +-- .../p/nt/simpledao/membstore_test.gno | 280 ++++++++++++++++++ .../gno.land/p/nt/simpledao/propstore.gno | 2 +- 5 files changed, 313 insertions(+), 24 deletions(-) create mode 100644 examples/gno.land/p/nt/simpledao/membstore_test.gno diff --git a/examples/gno.land/p/nt/simpledao/dao.gno b/examples/gno.land/p/nt/simpledao/dao.gno index 4b604d6c137..1f88766724a 100644 --- a/examples/gno.land/p/nt/simpledao/dao.gno +++ b/examples/gno.land/p/nt/simpledao/dao.gno @@ -36,7 +36,7 @@ type SimpleDAO struct { } // NewSimpleDAO creates a new instance of the simpledao DAO -func NewSimpleDAO(membStore dao.MembStore, propStore dao.PropStore) *SimpleDAO { +func NewSimpleDAO(membStore *MembStore, propStore *PropStore) *SimpleDAO { return &SimpleDAO{ membStore: membStore, propStore: propStore, @@ -202,5 +202,5 @@ const daoPkgPath = "gno.land/r/gov/dao" // isCallerGOVDAO returns a flag indicating if the // current caller context is the active GOVDAO func isCallerGOVDAO() bool { - return std.PrevRealm().Addr() == daoPkgPath + return std.CurrentRealm().PkgPath() == daoPkgPath } diff --git a/examples/gno.land/p/nt/simpledao/gno.mod b/examples/gno.land/p/nt/simpledao/gno.mod index 1f0b7ef929f..5fd5151b044 100644 --- a/examples/gno.land/p/nt/simpledao/gno.mod +++ b/examples/gno.land/p/nt/simpledao/gno.mod @@ -3,7 +3,10 @@ module gno.land/p/nt/simpledao require ( gno.land/p/demo/avl v0.0.0-latest gno.land/p/demo/seqid 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/dao v0.0.0-latest gno.land/p/gov/proposal v0.0.0-latest ) diff --git a/examples/gno.land/p/nt/simpledao/membstore.gno b/examples/gno.land/p/nt/simpledao/membstore.gno index f35ef380a1d..4b152db9cc7 100644 --- a/examples/gno.land/p/nt/simpledao/membstore.gno +++ b/examples/gno.land/p/nt/simpledao/membstore.gno @@ -22,7 +22,7 @@ type MembStoreOption func(*MembStore) func WithInitialMembers(members []dao.Member) MembStoreOption { return func(store *MembStore) { for _, m := range members { - members.Set(m.Address.String(), m) + store.members.Set(m.Address.String(), m) } } } @@ -36,31 +36,22 @@ type MembStore struct { } // NewMembStore creates a new simpledao member store -func NewMembStore() *MembStore { - // TODO add option - return &MembStore{ +func NewMembStore(opts ...MembStoreOption) *MembStore { + m := &MembStore{ members: avl.NewTree(), totalVotingPower: 0, majorityPower: 0, } -} - -func (m *MembStore) GetMembers() []dao.Member { - members := make([]dao.Member, 0, m.members.Size()) - m.members.Iterate("", "", func(_ string, val interface{}) bool { - member := val.(dao.Member) - - // Save the member - members = append(members, member) - - return false - }) + // Apply the options + for _, opt := range opts { + opt(m) + } - return members + return m } -func (m *MembStore) AddMember(member Member) error { +func (m *MembStore) AddMember(member dao.Member) error { if !isCallerGOVDAO() { return errNotGovDAO } @@ -86,7 +77,7 @@ func (m *MembStore) RemoveMember(address std.Address) error { } // Check if the member exists - memberRaw, exists := m.members.Get(address) + memberRaw, exists := m.members.Get(address.String()) if !exists { return errMissingMember } @@ -108,7 +99,7 @@ func (m *MembStore) UpdateMember(address std.Address, member dao.Member) error { } // Check if the member exists - memberRaw, exists := m.members.Get(address) + memberRaw, exists := m.members.Get(address.String()) if !exists { return errMissingMember } @@ -144,7 +135,22 @@ func (m *MembStore) GetMember(address std.Address) (dao.Member, error) { return dao.Member{}, errMissingMember } - return member, nil + return member.(dao.Member), nil +} + +func (m *MembStore) GetMembers() []dao.Member { + members := make([]dao.Member, 0, m.members.Size()) + + m.members.Iterate("", "", func(_ string, val interface{}) bool { + member := val.(dao.Member) + + // Save the member + members = append(members, member) + + return false + }) + + return members } // getMajorityPower returns the majority voting power diff --git a/examples/gno.land/p/nt/simpledao/membstore_test.gno b/examples/gno.land/p/nt/simpledao/membstore_test.gno new file mode 100644 index 00000000000..21a46652a5b --- /dev/null +++ b/examples/gno.land/p/nt/simpledao/membstore_test.gno @@ -0,0 +1,280 @@ +package simpledao + +import ( + "testing" + + "std" + + "gno.land/p/demo/testutils" + "gno.land/p/demo/uassert" + "gno.land/p/demo/ufmt" + "gno.land/p/demo/urequire" + "gno.land/p/gov/dao" +) + +// generateMembers generates dummy govdao members +func generateMembers(t *testing.T, count int) []dao.Member { + t.Helper() + + members := make([]dao.Member, 0, count) + + for i := 0; i < count; i++ { + members = append(members, dao.Member{ + Address: testutils.TestAddress(ufmt.Sprintf("member %d", i)), + Name: "test member", + VotingPower: 10, + }) + } + + return members +} + +func TestMembStore_GetMember(t *testing.T) { + t.Parallel() + + t.Run("member not found", func(t *testing.T) { + t.Parallel() + + // Create an empty store + m := NewMembStore() + + _, err := m.GetMember(testutils.TestAddress("random")) + uassert.ErrorIs(t, err, errMissingMember) + }) + + t.Run("valid member fetched", func(t *testing.T) { + t.Parallel() + + // Create a non-empty store + members := generateMembers(t, 1) + + m := NewMembStore(WithInitialMembers(members)) + + _, err := m.GetMember(members[0].Address) + uassert.NoError(t, err) + }) +} + +func TestMembStore_GetMembers(t *testing.T) { + t.Parallel() + + t.Run("no members", func(t *testing.T) { + t.Parallel() + + // Create an empty store + m := NewMembStore() + + members := m.GetMembers() + uassert.Equal(t, 0, len(members)) + }) + + t.Run("valid members fetched", func(t *testing.T) { + t.Parallel() + + // Create a non-empty store + members := generateMembers(t, 50) + + m := NewMembStore(WithInitialMembers(members)) + + fetchedMembers := m.GetMembers() + + urequire.Equal(t, len(members), len(fetchedMembers)) + + for _, fetchedMember := range fetchedMembers { + for _, member := range members { + if member.Address != fetchedMember.Address { + continue + } + + uassert.Equal(t, member.VotingPower, fetchedMember.VotingPower) + uassert.Equal(t, member.Name, fetchedMember.Name) + } + } + }) +} + +func TestMembStore_IsMember(t *testing.T) { + t.Parallel() + + t.Run("non-existing member", func(t *testing.T) { + t.Parallel() + + // Create an empty store + m := NewMembStore() + + uassert.False(t, m.IsMember(testutils.TestAddress("random"))) + }) + + t.Run("existing member", func(t *testing.T) { + t.Parallel() + + // Create a non-empty store + members := generateMembers(t, 50) + + m := NewMembStore(WithInitialMembers(members)) + + for _, member := range members { + uassert.True(t, m.IsMember(member.Address)) + } + }) +} + +func TestMembStore_AddMember(t *testing.T) { + t.Parallel() + + t.Run("caller not govdao", func(t *testing.T) { + t.Parallel() + + // Create an empty store + m := NewMembStore() + + // Attempt to add a member + member := generateMembers(t, 1)[0] + uassert.ErrorIs(t, m.AddMember(member), errNotGovDAO) + }) + + t.Run("member already exists", func(t *testing.T) { + t.Parallel() + + // Execute as the /r/gov/dao caller + r := std.NewCodeRealm(daoPkgPath) + std.TestSetRealm(r) + + // Create a non-empty store + members := generateMembers(t, 1) + m := NewMembStore(WithInitialMembers(members)) + + // Attempt to add a member + uassert.ErrorIs(t, m.AddMember(members[0]), errAlreadyMember) + }) + + t.Run("new member added", func(t *testing.T) { + t.Parallel() + + // Execute as the /r/gov/dao caller + r := std.NewCodeRealm(daoPkgPath) + std.TestSetRealm(r) + + // Create an empty store + members := generateMembers(t, 1) + m := NewMembStore() + + // Attempt to add a member + urequire.NoError(t, m.AddMember(members[0])) + + // Make sure the member is added + uassert.True(t, m.IsMember(members[0].Address)) + }) +} + +func TestMembStore_RemoveMember(t *testing.T) { + t.Parallel() + + t.Run("caller not govdao", func(t *testing.T) { + t.Parallel() + + // Create an empty store + m := NewMembStore() + + // Attempt to add a member + member := generateMembers(t, 1)[0] + uassert.ErrorIs(t, m.RemoveMember(member.Address), errNotGovDAO) + }) + + t.Run("member does not exist", func(t *testing.T) { + t.Parallel() + + // Execute as the /r/gov/dao caller + r := std.NewCodeRealm(daoPkgPath) + std.TestSetRealm(r) + + // Create an empty store + m := NewMembStore() + + // Attempt to add a member + member := generateMembers(t, 1)[0] + uassert.ErrorIs(t, m.RemoveMember(member.Address), errMissingMember) + }) + + t.Run("member removed", func(t *testing.T) { + t.Parallel() + + // Execute as the /r/gov/dao caller + r := std.NewCodeRealm(daoPkgPath) + std.TestSetRealm(r) + + // Create a non-empty store + members := generateMembers(t, 1) + m := NewMembStore(WithInitialMembers(members)) + + // Attempt to remove a member + urequire.NoError(t, m.RemoveMember(members[0].Address)) + + // Make sure the member is removed + uassert.False(t, m.IsMember(members[0].Address)) + }) +} + +func TestMembStore_UpdateMember(t *testing.T) { + t.Parallel() + + t.Run("caller not govdao", func(t *testing.T) { + t.Parallel() + + // Create an empty store + m := NewMembStore() + + // Attempt to update a member + member := generateMembers(t, 1)[0] + uassert.ErrorIs(t, m.UpdateMember(member.Address, member), errNotGovDAO) + }) + + t.Run("non-existing member", func(t *testing.T) { + t.Parallel() + + // Execute as the /r/gov/dao caller + r := std.NewCodeRealm(daoPkgPath) + std.TestSetRealm(r) + + // Create an empty store + members := generateMembers(t, 1) + m := NewMembStore() + + // Attempt to update a member + uassert.ErrorIs(t, m.UpdateMember(members[0].Address, members[0]), errMissingMember) + }) + + t.Run("overwrite member attempt", func(t *testing.T) { + t.Parallel() + + // Execute as the /r/gov/dao caller + r := std.NewCodeRealm(daoPkgPath) + std.TestSetRealm(r) + + // Create a non-empty store + members := generateMembers(t, 2) + m := NewMembStore(WithInitialMembers(members)) + + // Attempt to update a member + uassert.ErrorIs(t, m.UpdateMember(members[0].Address, members[1]), errInvalidAddressUpdate) + }) + + t.Run("successful update", func(t *testing.T) { + t.Parallel() + + // Execute as the /r/gov/dao caller + r := std.NewCodeRealm(daoPkgPath) + std.TestSetRealm(r) + + // Create a non-empty store + members := generateMembers(t, 1) + m := NewMembStore(WithInitialMembers(members)) + + name := "new name!" + members[0].Name = name + + // Attempt to update a member + uassert.NoError(t, m.UpdateMember(members[0].Address, members[0])) + uassert.Equal(t, name, m.GetMembers()[0].Name) + }) +} diff --git a/examples/gno.land/p/nt/simpledao/propstore.gno b/examples/gno.land/p/nt/simpledao/propstore.gno index 5a13e9b3fcd..9c6beaaaf3e 100644 --- a/examples/gno.land/p/nt/simpledao/propstore.gno +++ b/examples/gno.land/p/nt/simpledao/propstore.gno @@ -106,7 +106,7 @@ func (p *PropStore) GetProposals(givenOffset, count uint64) []dao.Proposal { func (p *PropStore) GetProposalByID(id uint64) (dao.Proposal, error) { prop, exists := p.proposals.Get(getProposalID(id)) if !exists { - return dao.Proposal{}, errMissingProposal + return nil, errMissingProposal } return prop.(*proposal), nil From dd5d64bdff7f148ce45c585785e7798d92111434 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Sat, 13 Jul 2024 19:15:43 +0200 Subject: [PATCH 04/80] Add propstore tests --- .../gno.land/p/nt/simpledao/propstore.gno | 15 +- .../p/nt/simpledao/propstore_test.gno | 266 ++++++++++++++++++ 2 files changed, 273 insertions(+), 8 deletions(-) create mode 100644 examples/gno.land/p/nt/simpledao/propstore_test.gno diff --git a/examples/gno.land/p/nt/simpledao/propstore.gno b/examples/gno.land/p/nt/simpledao/propstore.gno index 9c6beaaaf3e..67a3ba1ed02 100644 --- a/examples/gno.land/p/nt/simpledao/propstore.gno +++ b/examples/gno.land/p/nt/simpledao/propstore.gno @@ -62,15 +62,14 @@ func (p *PropStore) addProposal(proposal *proposal) (uint64, error) { return nextID, nil } -func (p *PropStore) GetProposals(givenOffset, count uint64) []dao.Proposal { +func (p *PropStore) GetProposals(offset, count uint64) []dao.Proposal { // Calculate the left and right bounds - offset := givenOffset - if offset < 1 { - offset = 1 + if count < 1 { + return []dao.Proposal{} } var ( - startIndex = (offset - 1) * count + startIndex = offset endIndex = startIndex + count numProposals = uint64(p.proposals.Size()) @@ -82,8 +81,8 @@ func (p *PropStore) GetProposals(givenOffset, count uint64) []dao.Proposal { } // Check if the right bound is good - if endIndex >= numProposals { - endIndex = numProposals - 1 + if endIndex > numProposals { + endIndex = numProposals } props := make([]dao.Proposal, 0) @@ -119,7 +118,7 @@ func (p *PropStore) GetProposalsByAddress(address std.Address) []dao.Proposal { prop := val.(*proposal) // Check the author - if prop.GetAuthor() != address { + if prop.GetAuthor() == address { props = append(props, prop) } diff --git a/examples/gno.land/p/nt/simpledao/propstore_test.gno b/examples/gno.land/p/nt/simpledao/propstore_test.gno new file mode 100644 index 00000000000..e76b2fc014d --- /dev/null +++ b/examples/gno.land/p/nt/simpledao/propstore_test.gno @@ -0,0 +1,266 @@ +package simpledao + +import ( + "testing" + + "gno.land/p/demo/testutils" + "gno.land/p/demo/uassert" + "gno.land/p/demo/ufmt" + "gno.land/p/demo/urequire" + "gno.land/p/gov/dao" +) + +// generateProposals generates dummy proposals +func generateProposals(t *testing.T, count int) []*proposal { + t.Helper() + + var ( + members = generateMembers(t, count) + proposals = make([]*proposal, 0, count) + ) + + for i := 0; i < count; i++ { + proposal := &proposal{ + author: members[i].Address, + description: ufmt.Sprintf("proposal %d", i), + status: dao.Active, + votes: newVotes(), + executor: nil, + } + + proposals = append(proposals, proposal) + } + + return proposals +} + +func equalProposals(t *testing.T, p1, p2 dao.Proposal) { + t.Helper() + + uassert.Equal( + t, + p1.GetAuthor().String(), + p2.GetAuthor().String(), + ) + + uassert.Equal( + t, + p1.GetDescription(), + p2.GetDescription(), + ) + + uassert.Equal( + t, + p1.GetStatus().String(), + p2.GetStatus().String(), + ) + + uassert.Equal( + t, + len(p1.GetVotes()), + len(p2.GetVotes()), + ) + + p1Votes := p1.GetVotes() + for index, v2 := range p2.GetVotes() { + uassert.Equal( + t, + p1Votes[index].Address.String(), + v2.Address.String(), + ) + + uassert.Equal( + t, + p1Votes[index].Option.String(), + v2.Option.String(), + ) + } +} + +func TestProposal_Data(t *testing.T) { + t.Parallel() + + t.Run("author", func(t *testing.T) { + t.Parallel() + + p := &proposal{ + author: testutils.TestAddress("address"), + } + + uassert.Equal(t, p.author, p.GetAuthor()) + }) + + t.Run("description", func(t *testing.T) { + t.Parallel() + + p := &proposal{ + description: "example proposal description", + } + + uassert.Equal(t, p.description, p.GetDescription()) + }) + + t.Run("status", func(t *testing.T) { + t.Parallel() + + p := &proposal{ + status: dao.Executed, + } + + uassert.Equal(t, p.status.String(), p.GetStatus().String()) + }) + + t.Run("no votes", func(t *testing.T) { + t.Parallel() + + p := &proposal{ + votes: newVotes(), + } + + uassert.Equal(t, 0, len(p.GetVotes())) + }) + + t.Run("existing votes", func(t *testing.T) { + t.Parallel() + + var ( + members = generateMembers(t, 50) + p = &proposal{ + votes: newVotes(), + } + ) + + for _, m := range members { + urequire.NoError(t, p.votes.castVote(m, dao.YesVote)) + } + + votes := p.GetVotes() + + urequire.Equal(t, len(members), len(votes)) + + for _, v := range votes { + for _, m := range members { + if m.Address != v.Address { + continue + } + + uassert.Equal(t, dao.YesVote.String(), v.Option.String()) + } + } + }) +} + +func TestPropStore_GetProposals(t *testing.T) { + t.Parallel() + + t.Run("no proposals", func(t *testing.T) { + t.Parallel() + + p := NewPropStore() + + proposals := p.GetProposals(0, 0) + + uassert.Equal(t, 0, len(proposals)) + }) + + t.Run("proper pagination", func(t *testing.T) { + t.Parallel() + + var ( + numProposals = 50 + halfRange = numProposals / 2 + + p = NewPropStore() + proposals = generateProposals(t, numProposals) + ) + + // Add initial proposals + for _, proposal := range proposals { + _, err := p.addProposal(proposal) + + urequire.NoError(t, err) + } + + fetchedProposals := p.GetProposals(0, uint64(halfRange)) + urequire.Equal(t, halfRange, len(fetchedProposals)) + + for index, fetchedProposal := range fetchedProposals { + equalProposals(t, proposals[index], fetchedProposal) + } + + // Fetch the other half + fetchedProposals = p.GetProposals(uint64(halfRange), uint64(halfRange)) + urequire.Equal(t, halfRange, len(fetchedProposals)) + + for index, fetchedProposal := range fetchedProposals { + equalProposals(t, proposals[index+halfRange], fetchedProposal) + } + }) +} + +func TestPropStore_GetProposalByID(t *testing.T) { + t.Parallel() + + t.Run("missing proposal", func(t *testing.T) { + t.Parallel() + + p := NewPropStore() + + _, err := p.GetProposalByID(0) + uassert.ErrorIs(t, err, errMissingProposal) + }) + + t.Run("proposal found", func(t *testing.T) { + t.Parallel() + + var ( + p = NewPropStore() + proposal = generateProposals(t, 1)[0] + ) + + // Add the initial proposal + _, err := p.addProposal(proposal) + urequire.NoError(t, err) + + // Fetch the proposal + fetchedProposal, err := p.GetProposalByID(0) + urequire.NoError(t, err) + + equalProposals(t, proposal, fetchedProposal) + }) +} + +func TestPropStore_GetProposalByAddress(t *testing.T) { + t.Parallel() + + t.Run("no proposals", func(t *testing.T) { + t.Parallel() + + p := NewPropStore() + + proposals := p.GetProposalsByAddress(testutils.TestAddress("address")) + + uassert.Equal(t, 0, len(proposals)) + }) + + t.Run("proposal found", func(t *testing.T) { + t.Parallel() + + var ( + p = NewPropStore() + proposals = generateProposals(t, 10) + ) + + // Add the initial proposals + for _, proposal := range proposals { + _, err := p.addProposal(proposal) + urequire.NoError(t, err) + } + + // Fetch the proposal + fetchedProposals := p.GetProposalsByAddress(proposals[0].GetAuthor()) + uassert.Equal(t, 1, len(fetchedProposals)) + + equalProposals(t, proposals[0], fetchedProposals[0]) + }) +} From c342741b3212a376ad15b33b44af5373a6b17999 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Sat, 13 Jul 2024 19:24:45 +0200 Subject: [PATCH 05/80] Add testing stubs --- examples/gno.land/p/nt/simpledao/dao.gno | 8 +- examples/gno.land/p/nt/simpledao/dao_test.gno | 133 ++++++++++++++++++ 2 files changed, 137 insertions(+), 4 deletions(-) create mode 100644 examples/gno.land/p/nt/simpledao/dao_test.gno diff --git a/examples/gno.land/p/nt/simpledao/dao.gno b/examples/gno.land/p/nt/simpledao/dao.gno index 1f88766724a..b6aed37dcf7 100644 --- a/examples/gno.land/p/nt/simpledao/dao.gno +++ b/examples/gno.land/p/nt/simpledao/dao.gno @@ -169,14 +169,14 @@ func (s *SimpleDAO) ExecuteProposal(id uint64) error { 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("failed to execute proposal %d, %s", id, err) + return ufmt.Errorf("error during proposal %d execution, %s", id, err) } - // Update the proposal status - prop.status = dao.Executed - return nil } diff --git a/examples/gno.land/p/nt/simpledao/dao_test.gno b/examples/gno.land/p/nt/simpledao/dao_test.gno new file mode 100644 index 00000000000..f1ab0f2c0bd --- /dev/null +++ b/examples/gno.land/p/nt/simpledao/dao_test.gno @@ -0,0 +1,133 @@ +package simpledao + +import "testing" + +func TestSimpleDAO_GetMembStore(t *testing.T) { + t.Parallel() + + t.Run("empty store", func(t *testing.T) { + t.Parallel() + + }) + + t.Run("store with members", func(t *testing.T) { + t.Parallel() + + }) +} + +func TestSimpleDAO_GetPropStore(t *testing.T) { + t.Parallel() + + t.Run("empty store", func(t *testing.T) { + t.Parallel() + + }) + + t.Run("store with proposals", func(t *testing.T) { + t.Parallel() + + }) +} + +func TestSimpleDAO_Propose(t *testing.T) { + t.Parallel() + + t.Run("invalid executor", func(t *testing.T) { + t.Parallel() + + }) + + t.Run("caller cannot cover fee", func(t *testing.T) { + t.Parallel() + + }) + + t.Run("proposal added", func(t *testing.T) { + t.Parallel() + + }) +} + +func TestSimpleDAO_VoteOnProposal(t *testing.T) { + t.Parallel() + + t.Run("not govdao member", func(t *testing.T) { + t.Parallel() + + }) + + t.Run("missing proposal", func(t *testing.T) { + t.Parallel() + + }) + + t.Run("proposal executed", func(t *testing.T) { + t.Parallel() + + }) + + t.Run("proposal expired", func(t *testing.T) { + t.Parallel() + + }) + + t.Run("double vote on proposal", func(t *testing.T) { + t.Parallel() + + }) + + t.Run("majority accepted", func(t *testing.T) { + t.Parallel() + + }) + + t.Run("majority rejected", func(t *testing.T) { + t.Parallel() + + }) + + t.Run("majority undecided", func(t *testing.T) { + t.Parallel() + + }) +} + +func TestSimpleDAO_ExecuteProposal(t *testing.T) { + t.Parallel() + + t.Run("caller cannot cover fee", func(t *testing.T) { + t.Parallel() + + }) + + t.Run("missing proposal", func(t *testing.T) { + t.Parallel() + + }) + + t.Run("proposal not accepted", func(t *testing.T) { + t.Parallel() + + }) + + t.Run("proposal executed", func(t *testing.T) { + t.Parallel() + + }) + + t.Run("proposal expired", func(t *testing.T) { + t.Parallel() + + }) + + t.Run("execution error", func(t *testing.T) { + t.Parallel() + + }) + + t.Run("successful execution", func(t *testing.T) { + t.Parallel() + + }) +} From 3d16ba6532fdc541001e80409f12db0486c09285 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20=C5=BDivkovi=C4=87?= Date: Sun, 14 Jul 2024 10:54:19 +0200 Subject: [PATCH 06/80] Update examples/gno.land/p/gov/dao/types.gno Co-authored-by: Manfred Touron <94029+moul@users.noreply.github.com> --- examples/gno.land/p/gov/dao/types.gno | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/gno.land/p/gov/dao/types.gno b/examples/gno.land/p/gov/dao/types.gno index 872fd97d4f6..74930415740 100644 --- a/examples/gno.land/p/gov/dao/types.gno +++ b/examples/gno.land/p/gov/dao/types.gno @@ -11,6 +11,7 @@ type VoteOption string const ( YesVote VoteOption = "YES" NoVote VoteOption = "NO" + Abstain VoteOption = "ABSTAIN" ) func (v VoteOption) String() string { From 8f2e3ac737c295e2db07d03b354a95c89f9df0de Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Sun, 14 Jul 2024 11:19:50 +0200 Subject: [PATCH 07/80] Move /nt/simpledao -> /demo/simpledao --- examples/gno.land/p/{nt => demo}/simpledao/dao.gno | 0 examples/gno.land/p/{nt => demo}/simpledao/dao_test.gno | 0 examples/gno.land/p/{nt => demo}/simpledao/gno.mod | 2 +- examples/gno.land/p/{nt => demo}/simpledao/membstore.gno | 0 examples/gno.land/p/{nt => demo}/simpledao/membstore_test.gno | 0 examples/gno.land/p/{nt => demo}/simpledao/propstore.gno | 0 examples/gno.land/p/{nt => demo}/simpledao/propstore_test.gno | 0 examples/gno.land/p/{nt => demo}/simpledao/vote.gno | 0 8 files changed, 1 insertion(+), 1 deletion(-) rename examples/gno.land/p/{nt => demo}/simpledao/dao.gno (100%) rename examples/gno.land/p/{nt => demo}/simpledao/dao_test.gno (100%) rename examples/gno.land/p/{nt => demo}/simpledao/gno.mod (90%) rename examples/gno.land/p/{nt => demo}/simpledao/membstore.gno (100%) rename examples/gno.land/p/{nt => demo}/simpledao/membstore_test.gno (100%) rename examples/gno.land/p/{nt => demo}/simpledao/propstore.gno (100%) rename examples/gno.land/p/{nt => demo}/simpledao/propstore_test.gno (100%) rename examples/gno.land/p/{nt => demo}/simpledao/vote.gno (100%) diff --git a/examples/gno.land/p/nt/simpledao/dao.gno b/examples/gno.land/p/demo/simpledao/dao.gno similarity index 100% rename from examples/gno.land/p/nt/simpledao/dao.gno rename to examples/gno.land/p/demo/simpledao/dao.gno diff --git a/examples/gno.land/p/nt/simpledao/dao_test.gno b/examples/gno.land/p/demo/simpledao/dao_test.gno similarity index 100% rename from examples/gno.land/p/nt/simpledao/dao_test.gno rename to examples/gno.land/p/demo/simpledao/dao_test.gno diff --git a/examples/gno.land/p/nt/simpledao/gno.mod b/examples/gno.land/p/demo/simpledao/gno.mod similarity index 90% rename from examples/gno.land/p/nt/simpledao/gno.mod rename to examples/gno.land/p/demo/simpledao/gno.mod index 5fd5151b044..feace5b8d6d 100644 --- a/examples/gno.land/p/nt/simpledao/gno.mod +++ b/examples/gno.land/p/demo/simpledao/gno.mod @@ -1,4 +1,4 @@ -module gno.land/p/nt/simpledao +module gno.land/p/demo/simpledao require ( gno.land/p/demo/avl v0.0.0-latest diff --git a/examples/gno.land/p/nt/simpledao/membstore.gno b/examples/gno.land/p/demo/simpledao/membstore.gno similarity index 100% rename from examples/gno.land/p/nt/simpledao/membstore.gno rename to examples/gno.land/p/demo/simpledao/membstore.gno diff --git a/examples/gno.land/p/nt/simpledao/membstore_test.gno b/examples/gno.land/p/demo/simpledao/membstore_test.gno similarity index 100% rename from examples/gno.land/p/nt/simpledao/membstore_test.gno rename to examples/gno.land/p/demo/simpledao/membstore_test.gno diff --git a/examples/gno.land/p/nt/simpledao/propstore.gno b/examples/gno.land/p/demo/simpledao/propstore.gno similarity index 100% rename from examples/gno.land/p/nt/simpledao/propstore.gno rename to examples/gno.land/p/demo/simpledao/propstore.gno diff --git a/examples/gno.land/p/nt/simpledao/propstore_test.gno b/examples/gno.land/p/demo/simpledao/propstore_test.gno similarity index 100% rename from examples/gno.land/p/nt/simpledao/propstore_test.gno rename to examples/gno.land/p/demo/simpledao/propstore_test.gno diff --git a/examples/gno.land/p/nt/simpledao/vote.gno b/examples/gno.land/p/demo/simpledao/vote.gno similarity index 100% rename from examples/gno.land/p/nt/simpledao/vote.gno rename to examples/gno.land/p/demo/simpledao/vote.gno From ae0639a304fbc48cd4404cc968492943fb9dcb28 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Sun, 14 Jul 2024 11:43:38 +0200 Subject: [PATCH 08/80] Move DAO API to dao.gno --- examples/gno.land/p/gov/dao/dao.gno | 22 ++++++++++++++++++++++ examples/gno.land/p/gov/dao/types.gno | 23 +---------------------- 2 files changed, 23 insertions(+), 22 deletions(-) create mode 100644 examples/gno.land/p/gov/dao/dao.gno diff --git a/examples/gno.land/p/gov/dao/dao.gno b/examples/gno.land/p/gov/dao/dao.gno new file mode 100644 index 00000000000..af36c6079ae --- /dev/null +++ b/examples/gno.land/p/gov/dao/dao.gno @@ -0,0 +1,22 @@ +package dao + +import "gno.land/p/gov/proposal" + +// DAO defines the DAO abstraction +type DAO interface { + // 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 +} diff --git a/examples/gno.land/p/gov/dao/types.gno b/examples/gno.land/p/gov/dao/types.gno index 74930415740..4222b981ed7 100644 --- a/examples/gno.land/p/gov/dao/types.gno +++ b/examples/gno.land/p/gov/dao/types.gno @@ -2,8 +2,6 @@ package dao import ( "std" - - "gno.land/p/gov/proposal" ) type VoteOption string @@ -11,7 +9,7 @@ type VoteOption string const ( YesVote VoteOption = "YES" NoVote VoteOption = "NO" - Abstain VoteOption = "ABSTAIN" + Abstain VoteOption = "ABSTAIN" ) func (v VoteOption) String() string { @@ -23,22 +21,3 @@ type Vote struct { Address std.Address // the address of the voter Option VoteOption // the voting option } - -// DAO defines the DAO abstraction -type DAO interface { - // 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 -} From 3a7ba0041c0a8c1fe8375911c77ee2eb9ab951ed Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Sun, 14 Jul 2024 11:43:53 +0200 Subject: [PATCH 09/80] Move vote API to vote.gno --- examples/gno.land/p/gov/dao/{types.gno => vote.gno} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/gno.land/p/gov/dao/{types.gno => vote.gno} (100%) diff --git a/examples/gno.land/p/gov/dao/types.gno b/examples/gno.land/p/gov/dao/vote.gno similarity index 100% rename from examples/gno.land/p/gov/dao/types.gno rename to examples/gno.land/p/gov/dao/vote.gno From 00056a50cf52b9019b1c81d3a814b0f4174f0fcd Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Sun, 14 Jul 2024 12:21:58 +0200 Subject: [PATCH 10/80] Add GetVoteByMember to proposal API --- .../gno.land/p/demo/simpledao/propstore.gno | 41 ++++++++++++++++++- .../p/demo/simpledao/propstore_test.gno | 38 +++++++++++++++++ examples/gno.land/p/demo/simpledao/vote.gno | 23 ++--------- examples/gno.land/p/gov/dao/proposal.gno | 3 ++ 4 files changed, 83 insertions(+), 22 deletions(-) diff --git a/examples/gno.land/p/demo/simpledao/propstore.gno b/examples/gno.land/p/demo/simpledao/propstore.gno index 67a3ba1ed02..127f64e75b7 100644 --- a/examples/gno.land/p/demo/simpledao/propstore.gno +++ b/examples/gno.land/p/demo/simpledao/propstore.gno @@ -10,7 +10,10 @@ import ( pproposal "gno.land/p/gov/proposal" ) -var errMissingProposal = errors.New("proposal is missing") +var ( + errMissingProposal = errors.New("proposal is missing") + errMissingVote = errors.New("member has not voted") +) // proposal is the internal simpledao proposal implementation type proposal struct { @@ -36,7 +39,41 @@ func (p *proposal) GetStatus() dao.Status { } func (p *proposal) GetVotes() []dao.Vote { - return p.votes.getVotes() + var ( + voters = p.votes.getVoters() + votes = make([]dao.Vote, 0, voters.Size()) + ) + + voters.Iterate("", "", func(key string, val interface{}) bool { + option := val.(dao.VoteOption) + + vote := dao.Vote{ + Address: std.Address(key), + Option: option, + } + + votes = append(votes, vote) + + return false + }) + + return votes +} + +func (p *proposal) GetVoteByMember(address std.Address) (dao.Vote, error) { + voters := p.votes.getVoters() + + optionRaw, exists := voters.Get(address.String()) + if !exists { + return dao.Vote{}, errMissingVote + } + + vote := dao.Vote{ + Address: address, + Option: optionRaw.(dao.VoteOption), + } + + return vote, nil } // PropStore implements the dao.PropStore abstraction diff --git a/examples/gno.land/p/demo/simpledao/propstore_test.gno b/examples/gno.land/p/demo/simpledao/propstore_test.gno index e76b2fc014d..93b96c8a28b 100644 --- a/examples/gno.land/p/demo/simpledao/propstore_test.gno +++ b/examples/gno.land/p/demo/simpledao/propstore_test.gno @@ -148,6 +148,44 @@ func TestProposal_Data(t *testing.T) { } } }) + + t.Run("vote by member", func(t *testing.T) { + t.Parallel() + + var ( + members = generateMembers(t, 50) + p = &proposal{ + votes: newVotes(), + } + ) + + for _, m := range members { + urequire.NoError(t, p.votes.castVote(m, dao.YesVote)) + } + + votes := p.GetVotes() + urequire.Equal(t, len(members), len(votes)) + + vote, err := p.GetVoteByMember(members[0].Address) + urequire.NoError(t, err) + + uassert.Equal(t, dao.YesVote.String(), vote.Option.String()) + uassert.Equal(t, members[0].Address.String(), vote.Address.String()) + }) + + t.Run("missing vote by member", func(t *testing.T) { + t.Parallel() + + p := &proposal{ + votes: newVotes(), + } + + votes := p.GetVotes() + urequire.Equal(t, 0, len(votes)) + + _, err := p.GetVoteByMember(testutils.TestAddress("dummy")) + uassert.ErrorIs(t, err, errMissingVote) + }) } func TestPropStore_GetProposals(t *testing.T) { diff --git a/examples/gno.land/p/demo/simpledao/vote.gno b/examples/gno.land/p/demo/simpledao/vote.gno index 2f5fa913dd3..7d06aab3876 100644 --- a/examples/gno.land/p/demo/simpledao/vote.gno +++ b/examples/gno.land/p/demo/simpledao/vote.gno @@ -3,8 +3,6 @@ package simpledao import ( "errors" - "std" - "gno.land/p/demo/avl" "gno.land/p/gov/dao" ) @@ -55,22 +53,7 @@ func (v *votes) getTally() (uint64, uint64) { return v.yays, v.nays } -// getVotes fetches the currently active vote set -func (v *votes) getVotes() []dao.Vote { - votes := make([]dao.Vote, 0, v.voters.Size()) - - v.voters.Iterate("", "", func(key string, val interface{}) bool { - option := val.(dao.VoteOption) - - vote := dao.Vote{ - Address: std.Address(key), - Option: option, - } - - votes = append(votes, vote) - - return false - }) - - return votes +// getVoters returns the current voter set +func (v *votes) getVoters() *avl.Tree { + return v.voters } diff --git a/examples/gno.land/p/gov/dao/proposal.gno b/examples/gno.land/p/gov/dao/proposal.gno index 67339427a0c..855ce387b7e 100644 --- a/examples/gno.land/p/gov/dao/proposal.gno +++ b/examples/gno.land/p/gov/dao/proposal.gno @@ -44,4 +44,7 @@ type Proposal interface { // GetVotes returns the votes of the proposal GetVotes() []Vote + + // GetVoteByMember returns the proposal vote by the member, if any + GetVoteByMember(address std.Address) (Vote, error) } From 209ee060375598eb9ba1c8739310bbe84abd4e1a Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Sun, 14 Jul 2024 12:24:41 +0200 Subject: [PATCH 11/80] Drop name from dao.Member --- examples/gno.land/p/demo/simpledao/membstore_test.gno | 8 +++----- examples/gno.land/p/gov/dao/members.gno | 1 - 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/examples/gno.land/p/demo/simpledao/membstore_test.gno b/examples/gno.land/p/demo/simpledao/membstore_test.gno index 21a46652a5b..e9c46032eac 100644 --- a/examples/gno.land/p/demo/simpledao/membstore_test.gno +++ b/examples/gno.land/p/demo/simpledao/membstore_test.gno @@ -21,7 +21,6 @@ func generateMembers(t *testing.T, count int) []dao.Member { for i := 0; i < count; i++ { members = append(members, dao.Member{ Address: testutils.TestAddress(ufmt.Sprintf("member %d", i)), - Name: "test member", VotingPower: 10, }) } @@ -87,7 +86,6 @@ func TestMembStore_GetMembers(t *testing.T) { } uassert.Equal(t, member.VotingPower, fetchedMember.VotingPower) - uassert.Equal(t, member.Name, fetchedMember.Name) } } }) @@ -270,11 +268,11 @@ func TestMembStore_UpdateMember(t *testing.T) { members := generateMembers(t, 1) m := NewMembStore(WithInitialMembers(members)) - name := "new name!" - members[0].Name = name + votingPower := uint64(300) + members[0].VotingPower = votingPower // Attempt to update a member uassert.NoError(t, m.UpdateMember(members[0].Address, members[0])) - uassert.Equal(t, name, m.GetMembers()[0].Name) + uassert.Equal(t, votingPower, m.GetMembers()[0].VotingPower) }) } diff --git a/examples/gno.land/p/gov/dao/members.gno b/examples/gno.land/p/gov/dao/members.gno index 965d13eea00..7c3728204d9 100644 --- a/examples/gno.land/p/gov/dao/members.gno +++ b/examples/gno.land/p/gov/dao/members.gno @@ -29,6 +29,5 @@ type MemberStore interface { // 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 VotingPower uint64 // the voting power of the member } From 54c555d923cdd823c547f5ab41343eca7699b8fe Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Sun, 14 Jul 2024 12:28:32 +0200 Subject: [PATCH 12/80] Add limit to number of proposals returned --- examples/gno.land/p/demo/simpledao/propstore.gno | 9 +++++++++ examples/gno.land/p/demo/simpledao/propstore_test.gno | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/examples/gno.land/p/demo/simpledao/propstore.gno b/examples/gno.land/p/demo/simpledao/propstore.gno index 127f64e75b7..99f584888fe 100644 --- a/examples/gno.land/p/demo/simpledao/propstore.gno +++ b/examples/gno.land/p/demo/simpledao/propstore.gno @@ -15,6 +15,10 @@ var ( errMissingVote = errors.New("member has not voted") ) +// maxRequestProposals is the maximum number of +// paginated proposals that can be requested +const maxRequestProposals = 10 + // proposal is the internal simpledao proposal implementation type proposal struct { author std.Address // initiator of the proposal @@ -105,6 +109,11 @@ func (p *PropStore) GetProposals(offset, count uint64) []dao.Proposal { return []dao.Proposal{} } + // Limit the maximum number of returned proposals + if count > maxRequestProposals { + count = maxRequestProposals + } + var ( startIndex = offset endIndex = startIndex + count diff --git a/examples/gno.land/p/demo/simpledao/propstore_test.gno b/examples/gno.land/p/demo/simpledao/propstore_test.gno index 93b96c8a28b..efc8ebc1fe6 100644 --- a/examples/gno.land/p/demo/simpledao/propstore_test.gno +++ b/examples/gno.land/p/demo/simpledao/propstore_test.gno @@ -205,7 +205,7 @@ func TestPropStore_GetProposals(t *testing.T) { t.Parallel() var ( - numProposals = 50 + numProposals = 20 halfRange = numProposals / 2 p = NewPropStore() From 84aeb30aeb2b0cee152f6d076831375994eb8c03 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Sun, 14 Jul 2024 12:32:23 +0200 Subject: [PATCH 13/80] Add Size() to MembStore API --- .../gno.land/p/demo/simpledao/membstore.gno | 4 ++++ .../p/demo/simpledao/membstore_test.gno | 23 +++++++++++++++++++ examples/gno.land/p/gov/dao/members.gno | 3 +++ 3 files changed, 30 insertions(+) diff --git a/examples/gno.land/p/demo/simpledao/membstore.gno b/examples/gno.land/p/demo/simpledao/membstore.gno index 4b152db9cc7..b76052f65e4 100644 --- a/examples/gno.land/p/demo/simpledao/membstore.gno +++ b/examples/gno.land/p/demo/simpledao/membstore.gno @@ -153,6 +153,10 @@ func (m *MembStore) GetMembers() []dao.Member { return members } +func (m *MembStore) Size() int { + return m.members.Size() +} + // getMajorityPower returns the majority voting power // of the member store func (m *MembStore) getMajorityPower() uint64 { diff --git a/examples/gno.land/p/demo/simpledao/membstore_test.gno b/examples/gno.land/p/demo/simpledao/membstore_test.gno index e9c46032eac..38780a71366 100644 --- a/examples/gno.land/p/demo/simpledao/membstore_test.gno +++ b/examples/gno.land/p/demo/simpledao/membstore_test.gno @@ -165,6 +165,29 @@ func TestMembStore_AddMember(t *testing.T) { }) } +func TestMembStore_Size(t *testing.T) { + t.Parallel() + + t.Run("empty govdao", func(t *testing.T) { + t.Parallel() + + // Create an empty store + m := NewMembStore() + + uassert.Equal(t, 0, m.Size()) + }) + + t.Run("non-empty govdao", func(t *testing.T) { + t.Parallel() + + // Create a non-empty store + members := generateMembers(t, 50) + m := NewMembStore(WithInitialMembers(members)) + + uassert.Equal(t, len(members), m.Size()) + }) +} + func TestMembStore_RemoveMember(t *testing.T) { t.Parallel() diff --git a/examples/gno.land/p/gov/dao/members.gno b/examples/gno.land/p/gov/dao/members.gno index 7c3728204d9..e2fdd03e529 100644 --- a/examples/gno.land/p/gov/dao/members.gno +++ b/examples/gno.land/p/gov/dao/members.gno @@ -9,6 +9,9 @@ type MemberStore interface { // GetMembers returns all members in the store GetMembers() []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 From 322bc8ca88bc3760f54520bf0ac780a33c537764 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Sun, 14 Jul 2024 12:33:30 +0200 Subject: [PATCH 14/80] Apply Yoda wisdom to MemberStore API --- examples/gno.land/p/gov/dao/members.gno | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/gno.land/p/gov/dao/members.gno b/examples/gno.land/p/gov/dao/members.gno index e2fdd03e529..11dde265f39 100644 --- a/examples/gno.land/p/gov/dao/members.gno +++ b/examples/gno.land/p/gov/dao/members.gno @@ -19,13 +19,13 @@ type MemberStore interface { // GetMember returns the requested member GetMember(address std.Address) (Member, error) - // AddMember attempts to add a member to the store + // AddMember adds a member to the store AddMember(member Member) error - // RemoveMember attempts to remove a member from the store + // RemoveMember removes a member from the store RemoveMember(address std.Address) error - // UpdateMember attempts to update the member in the store + // UpdateMember updates the member in the store UpdateMember(address std.Address, member Member) error } From 794a353907029c192c1aa1ee4b991f100affc0d3 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Sun, 14 Jul 2024 13:16:29 +0200 Subject: [PATCH 15/80] Add execution failed ProposalStatus --- examples/gno.land/p/demo/simpledao/dao.gno | 14 +++++++++----- .../gno.land/p/demo/simpledao/propstore.gno | 4 ++-- .../p/demo/simpledao/propstore_test.gno | 2 +- examples/gno.land/p/gov/dao/proposal.gno | 17 +++++++++-------- 4 files changed, 21 insertions(+), 16 deletions(-) diff --git a/examples/gno.land/p/demo/simpledao/dao.gno b/examples/gno.land/p/demo/simpledao/dao.gno index b6aed37dcf7..74d3e4f16e1 100644 --- a/examples/gno.land/p/demo/simpledao/dao.gno +++ b/examples/gno.land/p/demo/simpledao/dao.gno @@ -93,7 +93,8 @@ func (s *SimpleDAO) VoteOnProposal(id uint64, option dao.VoteOption) error { prop := propRaw.(*proposal) // Check the proposal status - if prop.GetStatus() == dao.Executed { + if prop.GetStatus() == dao.ExecutionSuccessful || + prop.GetStatus() == dao.ExecutionFailed { // Proposal was already executed, nothing to vote on anymore. // // In fact, the proposal should stop accepting @@ -159,7 +160,8 @@ func (s *SimpleDAO) ExecuteProposal(id uint64) error { } // Check if the proposal is executed - if prop.GetStatus() == dao.Executed { + if prop.GetStatus() == dao.ExecutionSuccessful || + prop.GetStatus() == dao.ExecutionFailed { // Proposal is already executed return errProposalExecuted } @@ -169,14 +171,16 @@ func (s *SimpleDAO) ExecuteProposal(id uint64) error { return errProposalExpired } - // Update the proposal status - prop.status = dao.Executed - // Attempt to execute the proposal if err = prop.executor.Execute(); err != nil { + prop.status = dao.ExecutionFailed + return ufmt.Errorf("error during proposal %d execution, %s", id, err) } + // Update the proposal status + prop.status = dao.ExecutionSuccessful + return nil } diff --git a/examples/gno.land/p/demo/simpledao/propstore.gno b/examples/gno.land/p/demo/simpledao/propstore.gno index 99f584888fe..670dc68e2cb 100644 --- a/examples/gno.land/p/demo/simpledao/propstore.gno +++ b/examples/gno.land/p/demo/simpledao/propstore.gno @@ -25,7 +25,7 @@ type proposal struct { description string // description of the proposal executor pproposal.Executor // executor for the proposal - status dao.Status // status of the proposal + status dao.ProposalStatus // status of the proposal votes *votes // voting mechanism } @@ -38,7 +38,7 @@ func (p *proposal) GetDescription() string { return p.description } -func (p *proposal) GetStatus() dao.Status { +func (p *proposal) GetStatus() dao.ProposalStatus { return p.status } diff --git a/examples/gno.land/p/demo/simpledao/propstore_test.gno b/examples/gno.land/p/demo/simpledao/propstore_test.gno index efc8ebc1fe6..aca39fb9707 100644 --- a/examples/gno.land/p/demo/simpledao/propstore_test.gno +++ b/examples/gno.land/p/demo/simpledao/propstore_test.gno @@ -104,7 +104,7 @@ func TestProposal_Data(t *testing.T) { t.Parallel() p := &proposal{ - status: dao.Executed, + status: dao.ExecutionSuccessful, } uassert.Equal(t, p.status.String(), p.GetStatus().String()) diff --git a/examples/gno.land/p/gov/dao/proposal.gno b/examples/gno.land/p/gov/dao/proposal.gno index 855ce387b7e..13eff2c0f17 100644 --- a/examples/gno.land/p/gov/dao/proposal.gno +++ b/examples/gno.land/p/gov/dao/proposal.gno @@ -2,18 +2,19 @@ package dao import "std" -type Status string +type ProposalStatus string -// ACTIVE -> ACCEPTED -> EXECUTED +// ACTIVE -> ACCEPTED -> EXECUTION(SUCCEEDED/FAILED) // 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 + 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 Status) String() string { +func (s ProposalStatus) String() string { return string(s) } @@ -40,7 +41,7 @@ type Proposal interface { GetDescription() string // GetStatus returns the status of the proposal - GetStatus() Status + GetStatus() ProposalStatus // GetVotes returns the votes of the proposal GetVotes() []Vote From 8c45039a8c165b5dd4232acc410e601eea3908eb Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Sun, 14 Jul 2024 13:18:36 +0200 Subject: [PATCH 16/80] Drop unused PropStore method --- .../gno.land/p/demo/simpledao/propstore.gno | 17 --------- .../p/demo/simpledao/propstore_test.gno | 35 ------------------- examples/gno.land/p/gov/dao/proposal.gno | 4 --- 3 files changed, 56 deletions(-) diff --git a/examples/gno.land/p/demo/simpledao/propstore.gno b/examples/gno.land/p/demo/simpledao/propstore.gno index 670dc68e2cb..a9838b07ca9 100644 --- a/examples/gno.land/p/demo/simpledao/propstore.gno +++ b/examples/gno.land/p/demo/simpledao/propstore.gno @@ -157,23 +157,6 @@ func (p *PropStore) GetProposalByID(id uint64) (dao.Proposal, error) { return prop.(*proposal), nil } -func (p *PropStore) GetProposalsByAddress(address std.Address) []dao.Proposal { - props := make([]dao.Proposal, 0, p.proposals.Size()) - - p.proposals.Iterate("", "", func(_ string, val interface{}) bool { - prop := val.(*proposal) - - // Check the author - if prop.GetAuthor() == address { - props = append(props, prop) - } - - return false - }) - - return props -} - // getProposalID generates a sequential proposal ID // from the given ID number func getProposalID(id uint64) string { diff --git a/examples/gno.land/p/demo/simpledao/propstore_test.gno b/examples/gno.land/p/demo/simpledao/propstore_test.gno index aca39fb9707..d2d67270999 100644 --- a/examples/gno.land/p/demo/simpledao/propstore_test.gno +++ b/examples/gno.land/p/demo/simpledao/propstore_test.gno @@ -267,38 +267,3 @@ func TestPropStore_GetProposalByID(t *testing.T) { equalProposals(t, proposal, fetchedProposal) }) } - -func TestPropStore_GetProposalByAddress(t *testing.T) { - t.Parallel() - - t.Run("no proposals", func(t *testing.T) { - t.Parallel() - - p := NewPropStore() - - proposals := p.GetProposalsByAddress(testutils.TestAddress("address")) - - uassert.Equal(t, 0, len(proposals)) - }) - - t.Run("proposal found", func(t *testing.T) { - t.Parallel() - - var ( - p = NewPropStore() - proposals = generateProposals(t, 10) - ) - - // Add the initial proposals - for _, proposal := range proposals { - _, err := p.addProposal(proposal) - urequire.NoError(t, err) - } - - // Fetch the proposal - fetchedProposals := p.GetProposalsByAddress(proposals[0].GetAuthor()) - uassert.Equal(t, 1, len(fetchedProposals)) - - equalProposals(t, proposals[0], fetchedProposals[0]) - }) -} diff --git a/examples/gno.land/p/gov/dao/proposal.gno b/examples/gno.land/p/gov/dao/proposal.gno index 13eff2c0f17..d7ffc3e3bd0 100644 --- a/examples/gno.land/p/gov/dao/proposal.gno +++ b/examples/gno.land/p/gov/dao/proposal.gno @@ -26,10 +26,6 @@ type PropStore interface { // 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 } // Proposal is the single proposal abstraction From 268021dcf8d9d73e3d547ee058b15fa3dfeca2a8 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Sun, 14 Jul 2024 13:23:33 +0200 Subject: [PATCH 17/80] Drop RemoveMember, add removal in UpdateMember --- .../gno.land/p/demo/simpledao/membstore.gno | 25 ++----- .../p/demo/simpledao/membstore_test.gno | 69 ++++++------------- examples/gno.land/p/gov/dao/members.gno | 7 +- 3 files changed, 29 insertions(+), 72 deletions(-) diff --git a/examples/gno.land/p/demo/simpledao/membstore.gno b/examples/gno.land/p/demo/simpledao/membstore.gno index b76052f65e4..fc687292282 100644 --- a/examples/gno.land/p/demo/simpledao/membstore.gno +++ b/examples/gno.land/p/demo/simpledao/membstore.gno @@ -71,7 +71,7 @@ func (m *MembStore) AddMember(member dao.Member) error { return nil } -func (m *MembStore) RemoveMember(address std.Address) error { +func (m *MembStore) UpdateMember(address std.Address, member dao.Member) error { if !isCallerGOVDAO() { return errNotGovDAO } @@ -82,26 +82,11 @@ func (m *MembStore) RemoveMember(address std.Address) error { return errMissingMember } - member := memberRaw.(dao.Member) - - // Remove the member - m.members.Remove(address.String()) - - m.totalVotingPower -= member.VotingPower - m.majorityPower = (2 * m.totalVotingPower) / 3 - - return nil -} - -func (m *MembStore) UpdateMember(address std.Address, member dao.Member) error { - if !isCallerGOVDAO() { - return errNotGovDAO - } + // Check if this is a removal request + if member.VotingPower == 0 { + m.members.Remove(address.String()) - // Check if the member exists - memberRaw, exists := m.members.Get(address.String()) - if !exists { - return errMissingMember + return nil } // Check that the member wouldn't be diff --git a/examples/gno.land/p/demo/simpledao/membstore_test.gno b/examples/gno.land/p/demo/simpledao/membstore_test.gno index 38780a71366..fa27dc021b5 100644 --- a/examples/gno.land/p/demo/simpledao/membstore_test.gno +++ b/examples/gno.land/p/demo/simpledao/membstore_test.gno @@ -188,54 +188,6 @@ func TestMembStore_Size(t *testing.T) { }) } -func TestMembStore_RemoveMember(t *testing.T) { - t.Parallel() - - t.Run("caller not govdao", func(t *testing.T) { - t.Parallel() - - // Create an empty store - m := NewMembStore() - - // Attempt to add a member - member := generateMembers(t, 1)[0] - uassert.ErrorIs(t, m.RemoveMember(member.Address), errNotGovDAO) - }) - - t.Run("member does not exist", func(t *testing.T) { - t.Parallel() - - // Execute as the /r/gov/dao caller - r := std.NewCodeRealm(daoPkgPath) - std.TestSetRealm(r) - - // Create an empty store - m := NewMembStore() - - // Attempt to add a member - member := generateMembers(t, 1)[0] - uassert.ErrorIs(t, m.RemoveMember(member.Address), errMissingMember) - }) - - t.Run("member removed", func(t *testing.T) { - t.Parallel() - - // Execute as the /r/gov/dao caller - r := std.NewCodeRealm(daoPkgPath) - std.TestSetRealm(r) - - // Create a non-empty store - members := generateMembers(t, 1) - m := NewMembStore(WithInitialMembers(members)) - - // Attempt to remove a member - urequire.NoError(t, m.RemoveMember(members[0].Address)) - - // Make sure the member is removed - uassert.False(t, m.IsMember(members[0].Address)) - }) -} - func TestMembStore_UpdateMember(t *testing.T) { t.Parallel() @@ -298,4 +250,25 @@ func TestMembStore_UpdateMember(t *testing.T) { uassert.NoError(t, m.UpdateMember(members[0].Address, members[0])) uassert.Equal(t, votingPower, m.GetMembers()[0].VotingPower) }) + + t.Run("member removed", func(t *testing.T) { + t.Parallel() + + // Execute as the /r/gov/dao caller + r := std.NewCodeRealm(daoPkgPath) + std.TestSetRealm(r) + + // Create a non-empty store + members := generateMembers(t, 1) + m := NewMembStore(WithInitialMembers(members)) + + votingPower := uint64(0) + members[0].VotingPower = votingPower + + // Attempt to update a member + uassert.NoError(t, m.UpdateMember(members[0].Address, members[0])) + + // Make sure the member was removed + uassert.False(t, m.IsMember(members[0].Address)) + }) } diff --git a/examples/gno.land/p/gov/dao/members.gno b/examples/gno.land/p/gov/dao/members.gno index 11dde265f39..5a120d5879e 100644 --- a/examples/gno.land/p/gov/dao/members.gno +++ b/examples/gno.land/p/gov/dao/members.gno @@ -22,10 +22,9 @@ type MemberStore interface { // AddMember adds a member to the store AddMember(member Member) error - // RemoveMember removes a member from the store - RemoveMember(address std.Address) error - - // UpdateMember updates the member in the store + // 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 } From 43e5c66e2ec8cdd9a977d995d395e2742413e139 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Sun, 14 Jul 2024 13:33:21 +0200 Subject: [PATCH 18/80] Add Size() to PropStore API --- examples/gno.land/p/demo/simpledao/propstore.gno | 4 ++++ examples/gno.land/p/demo/simpledao/propstore_test.gno | 3 +++ examples/gno.land/p/gov/dao/proposal.gno | 4 ++++ 3 files changed, 11 insertions(+) diff --git a/examples/gno.land/p/demo/simpledao/propstore.gno b/examples/gno.land/p/demo/simpledao/propstore.gno index a9838b07ca9..bd4331a581e 100644 --- a/examples/gno.land/p/demo/simpledao/propstore.gno +++ b/examples/gno.land/p/demo/simpledao/propstore.gno @@ -157,6 +157,10 @@ func (p *PropStore) GetProposalByID(id uint64) (dao.Proposal, error) { return prop.(*proposal), nil } +func (p *PropStore) Size() int { + return p.proposals.Size() +} + // getProposalID generates a sequential proposal ID // from the given ID number func getProposalID(id uint64) string { diff --git a/examples/gno.land/p/demo/simpledao/propstore_test.gno b/examples/gno.land/p/demo/simpledao/propstore_test.gno index d2d67270999..99c5ba09de6 100644 --- a/examples/gno.land/p/demo/simpledao/propstore_test.gno +++ b/examples/gno.land/p/demo/simpledao/propstore_test.gno @@ -196,6 +196,7 @@ func TestPropStore_GetProposals(t *testing.T) { p := NewPropStore() + uassert.Equal(t, 0, p.Size()) proposals := p.GetProposals(0, 0) uassert.Equal(t, 0, len(proposals)) @@ -219,6 +220,8 @@ func TestPropStore_GetProposals(t *testing.T) { urequire.NoError(t, err) } + uassert.Equal(t, numProposals, p.Size()) + fetchedProposals := p.GetProposals(0, uint64(halfRange)) urequire.Equal(t, halfRange, len(fetchedProposals)) diff --git a/examples/gno.land/p/gov/dao/proposal.gno b/examples/gno.land/p/gov/dao/proposal.gno index d7ffc3e3bd0..69ebcee3491 100644 --- a/examples/gno.land/p/gov/dao/proposal.gno +++ b/examples/gno.land/p/gov/dao/proposal.gno @@ -26,6 +26,10 @@ type PropStore interface { // GetProposalByID returns the proposal associated with // the given ID, if any GetProposalByID(id uint64) (Proposal, error) + + // Size returns the number of proposals in + // the proposal store + Size() int } // Proposal is the single proposal abstraction From e1cd61724bfa5e01987f4537b993b81c1db0b000 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Sun, 14 Jul 2024 14:13:04 +0200 Subject: [PATCH 19/80] Add pagination to MembStore --- .../gno.land/p/demo/simpledao/membstore.gno | 29 +++++++---- .../p/demo/simpledao/membstore_test.gno | 48 ++++++++++++------- .../gno.land/p/demo/simpledao/propstore.gno | 12 +++-- examples/gno.land/p/gov/dao/members.gno | 2 +- 4 files changed, 63 insertions(+), 28 deletions(-) diff --git a/examples/gno.land/p/demo/simpledao/membstore.gno b/examples/gno.land/p/demo/simpledao/membstore.gno index fc687292282..2b97a229564 100644 --- a/examples/gno.land/p/demo/simpledao/membstore.gno +++ b/examples/gno.land/p/demo/simpledao/membstore.gno @@ -123,17 +123,30 @@ func (m *MembStore) GetMember(address std.Address) (dao.Member, error) { return member.(dao.Member), nil } -func (m *MembStore) GetMembers() []dao.Member { - members := make([]dao.Member, 0, m.members.Size()) +func (m *MembStore) GetMembers(offset, count uint64) []dao.Member { + // Calculate the left and right bounds + if count < 1 || offset >= uint64(m.members.Size()) { + return []dao.Member{} + } + + // Limit the maximum number of returned members + if count > maxRequestMembers { + count = maxRequestMembers + } - m.members.Iterate("", "", func(_ string, val interface{}) bool { - member := val.(dao.Member) + // Gather the members + members := make([]dao.Member, 0) + m.members.IterateByOffset( + int(offset), + int(count), + func(_ string, val interface{}) bool { + member := val.(dao.Member) - // Save the member - members = append(members, member) + // Save the member + members = append(members, member) - return false - }) + return false + }) return members } diff --git a/examples/gno.land/p/demo/simpledao/membstore_test.gno b/examples/gno.land/p/demo/simpledao/membstore_test.gno index fa27dc021b5..836473c2366 100644 --- a/examples/gno.land/p/demo/simpledao/membstore_test.gno +++ b/examples/gno.land/p/demo/simpledao/membstore_test.gno @@ -63,31 +63,47 @@ func TestMembStore_GetMembers(t *testing.T) { // Create an empty store m := NewMembStore() - members := m.GetMembers() + members := m.GetMembers(0, 10) uassert.Equal(t, 0, len(members)) }) - t.Run("valid members fetched", func(t *testing.T) { + t.Run("proper pagination", func(t *testing.T) { t.Parallel() - // Create a non-empty store - members := generateMembers(t, 50) - - m := NewMembStore(WithInitialMembers(members)) + var ( + numMembers = maxRequestMembers * 2 + halfRange = numMembers / 2 - fetchedMembers := m.GetMembers() + members = generateMembers(t, numMembers) + m = NewMembStore(WithInitialMembers(members)) - urequire.Equal(t, len(members), len(fetchedMembers)) + verifyMembersPresent = func(members, fetchedMembers []dao.Member) { + for _, fetchedMember := range fetchedMembers { + for _, member := range members { + if member.Address != fetchedMember.Address { + continue + } - for _, fetchedMember := range fetchedMembers { - for _, member := range members { - if member.Address != fetchedMember.Address { - continue + uassert.Equal(t, member.VotingPower, fetchedMember.VotingPower) + } } - - uassert.Equal(t, member.VotingPower, fetchedMember.VotingPower) } - } + ) + + urequire.Equal(t, numMembers, m.Size()) + + fetchedMembers := m.GetMembers(0, uint64(halfRange)) + urequire.Equal(t, halfRange, len(fetchedMembers)) + + // Verify the members + verifyMembersPresent(members, fetchedMembers) + + // Fetch the other half + fetchedMembers = m.GetMembers(uint64(halfRange), uint64(halfRange)) + urequire.Equal(t, halfRange, len(fetchedMembers)) + + // Verify the members + verifyMembersPresent(members, fetchedMembers) }) } @@ -248,7 +264,7 @@ func TestMembStore_UpdateMember(t *testing.T) { // Attempt to update a member uassert.NoError(t, m.UpdateMember(members[0].Address, members[0])) - uassert.Equal(t, votingPower, m.GetMembers()[0].VotingPower) + uassert.Equal(t, votingPower, m.GetMembers(0, 10)[0].VotingPower) }) t.Run("member removed", func(t *testing.T) { diff --git a/examples/gno.land/p/demo/simpledao/propstore.gno b/examples/gno.land/p/demo/simpledao/propstore.gno index bd4331a581e..8d5e125d857 100644 --- a/examples/gno.land/p/demo/simpledao/propstore.gno +++ b/examples/gno.land/p/demo/simpledao/propstore.gno @@ -15,9 +15,15 @@ var ( errMissingVote = errors.New("member has not voted") ) -// maxRequestProposals is the maximum number of -// paginated proposals that can be requested -const maxRequestProposals = 10 +const ( + // maxRequestProposals is the maximum number of + // paginated proposals that can be requested + maxRequestProposals = 10 + + // maxRequestMembers is the maximum number of + // paginated members that can be requested + maxRequestMembers = 50 +) // proposal is the internal simpledao proposal implementation type proposal struct { diff --git a/examples/gno.land/p/gov/dao/members.gno b/examples/gno.land/p/gov/dao/members.gno index 5a120d5879e..0d15422c60c 100644 --- a/examples/gno.land/p/gov/dao/members.gno +++ b/examples/gno.land/p/gov/dao/members.gno @@ -7,7 +7,7 @@ import ( // MemberStore defines the member storage abstraction type MemberStore interface { // GetMembers returns all members in the store - GetMembers() []Member + GetMembers(offset, count uint64) []Member // Size returns the current size of the store Size() int From 67e4535f9da1e8e5cb67634966ed8dc8503039d9 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Sun, 14 Jul 2024 15:27:00 +0200 Subject: [PATCH 20/80] Rename proposal.gno -> proposals.gno --- examples/gno.land/p/gov/dao/{proposal.gno => proposals.gno} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/gno.land/p/gov/dao/{proposal.gno => proposals.gno} (100%) diff --git a/examples/gno.land/p/gov/dao/proposal.gno b/examples/gno.land/p/gov/dao/proposals.gno similarity index 100% rename from examples/gno.land/p/gov/dao/proposal.gno rename to examples/gno.land/p/gov/dao/proposals.gno From 8fbbd5e28b14afb64618369651816ec0a4f8c47c Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Sun, 14 Jul 2024 19:39:26 +0200 Subject: [PATCH 21/80] Start cleaning up r/gov/dao --- examples/gno.land/p/demo/simpledao/dao.gno | 8 - examples/gno.land/p/gov/dao/dao.gno | 6 - examples/gno.land/p/gov/proposal/error.gno | 40 +++++ examples/gno.land/r/gov/dao/dao.gno | 160 +++--------------- examples/gno.land/r/gov/dao/memberset.gno | 40 ----- examples/gno.land/r/gov/dao/poc.gno | 90 ++++++++++ .../gno.land/r/gov/dao/prop1_filetest.gno | 10 +- examples/gno.land/r/gov/dao/types.gno | 32 ---- examples/gno.land/r/gov/dao/voter.gno | 91 ---------- 9 files changed, 160 insertions(+), 317 deletions(-) create mode 100644 examples/gno.land/p/gov/proposal/error.gno delete mode 100644 examples/gno.land/r/gov/dao/memberset.gno create mode 100644 examples/gno.land/r/gov/dao/poc.gno delete mode 100644 examples/gno.land/r/gov/dao/types.gno delete mode 100644 examples/gno.land/r/gov/dao/voter.gno diff --git a/examples/gno.land/p/demo/simpledao/dao.gno b/examples/gno.land/p/demo/simpledao/dao.gno index 74d3e4f16e1..3d42f37e31b 100644 --- a/examples/gno.land/p/demo/simpledao/dao.gno +++ b/examples/gno.land/p/demo/simpledao/dao.gno @@ -184,14 +184,6 @@ func (s *SimpleDAO) ExecuteProposal(id uint64) error { 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. diff --git a/examples/gno.land/p/gov/dao/dao.gno b/examples/gno.land/p/gov/dao/dao.gno index af36c6079ae..88c78684ab5 100644 --- a/examples/gno.land/p/gov/dao/dao.gno +++ b/examples/gno.land/p/gov/dao/dao.gno @@ -13,10 +13,4 @@ type DAO interface { // 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 } diff --git a/examples/gno.land/p/gov/proposal/error.gno b/examples/gno.land/p/gov/proposal/error.gno new file mode 100644 index 00000000000..58a520ceeb9 --- /dev/null +++ b/examples/gno.land/p/gov/proposal/error.gno @@ -0,0 +1,40 @@ +package proposal + +import "strings" + +// CombinedErrors is a combined execution error +type CombinedErrors struct { + errors []error +} + +// Error returns the combined execution error +func (e *CombinedErrors) 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 *CombinedErrors) Add(errs ...error) { + if len(errs) == 0 { + return + } + + e.errors = append(e.errors, errs...) +} + +// Size returns a +func (e *CombinedErrors) Size() int { + return len(e.errors) +} diff --git a/examples/gno.land/r/gov/dao/dao.gno b/examples/gno.land/r/gov/dao/dao.gno index 632935dafed..940a2e9100f 100644 --- a/examples/gno.land/r/gov/dao/dao.gno +++ b/examples/gno.land/r/gov/dao/dao.gno @@ -1,146 +1,54 @@ package govdao import ( - "std" "strconv" + "github.com/gnolang/gno/examples/gno.land/p/demo/simpledao" "gno.land/p/demo/ufmt" + "gno.land/p/gov/dao" pproposal "gno.land/p/gov/proposal" ) var ( - proposals = make([]*proposal, 0) - members = make([]std.Address, 0) // XXX: these should be pointers to avoid data duplication. Not possible due to VM bugs + d dao.DAO // the current active DAO implementation + proposals dao.PropStore // the proposal store + members dao.MembStore // the member store ) -const ( - msgMissingExecutor = "missing proposal executor" - msgPropExecuted = "prop already executed" - msgPropExpired = "prop is expired" - msgPropInactive = "prop is not active anymore" - msgPropActive = "prop is still active" - msgPropNotAccepted = "prop is not accepted" +func init() { + var ( + propStore = simpledao.NewPropStore() + membStore = simpledao.NewMembStore() + ) - msgCallerNotAMember = "caller is not member of govdao" - msgProposalNotFound = "proposal not found" -) + // Set the stores + proposals = propStore + members = membStore -type proposal struct { - author std.Address - comment string - executor pproposal.Executor - voter Voter - executed bool - voters []std.Address // XXX: these should be pointers to avoid data duplication. Not possible due to VM bugs. -} - -func (p proposal) Status() Status { - if p.executor.IsExpired() { - return Expired - } - - if p.executor.IsDone() { - return Succeeded - } - - if !p.voter.IsFinished(members) { - return Active - } - - if p.voter.IsAccepted(members) { - return Accepted - } - - return NotAccepted + // Set the DAO implementation + d = simpledao.NewSimpleDAO(membStore, propStore) } // Propose is designed to be called by another contract or with // `maketx run`, not by a `maketx call`. -func Propose(comment string, executor pproposal.Executor) int { - // XXX: require payment? - if executor == nil { - panic(msgMissingExecutor) - } - caller := std.GetOrigCaller() // XXX: CHANGE THIS WHEN MSGRUN PERSIST CODE ESCAPING THE main() SCOPE! IT IS UNSAFE! - AssertIsMember(caller) - - prop := &proposal{ - comment: comment, - executor: executor, - author: caller, - voter: NewPercentageVoter(66), // at least 2/3 must say yes - } - - proposals = append(proposals, prop) - - return len(proposals) - 1 +func Propose(comment string, executor pproposal.Executor) (uint64, error) { + return d.Propose(comment, executor) } -func VoteOnProposal(idx int, option string) { - assertProposalExists(idx) - caller := std.GetOrigCaller() // XXX: CHANGE THIS WHEN MSGRUN PERSIST CODE ESCAPING THE main() SCOPE! IT IS UNSAFE! - AssertIsMember(caller) - - prop := getProposal(idx) - - if prop.executed { - panic(msgPropExecuted) - } - - if prop.executor.IsExpired() { - panic(msgPropExpired) - } - - if prop.voter.IsFinished(members) { - panic(msgPropInactive) - } - - prop.voter.Vote(members, caller, option) +func VoteOnProposal(id uint64, option VoteOption) error { + return d.VoteOnProposal(id, option) } -func ExecuteProposal(idx int) { - assertProposalExists(idx) - prop := getProposal(idx) - - if prop.executed { - panic(msgPropExecuted) - } - - if prop.executor.IsExpired() { - panic(msgPropExpired) - } - - if !prop.voter.IsFinished(members) { - panic(msgPropActive) - } - - if !prop.voter.IsAccepted(members) { - panic(msgPropNotAccepted) - } - - prop.executor.Execute() - prop.voters = members - prop.executed = true +func ExecuteProposal(id uint64) error { + return d.ExecuteProposal(id) } -func IsMember(addr std.Address) bool { - if len(members) == 0 { // special case for initial execution - return true - } - - for _, v := range members { - if v == addr { - return true - } - } - - return false +func GetPropStore() dao.PropStore { + return proposals } -func AssertIsMember(addr std.Address) { - if !IsMember(addr) { - panic(msgCallerNotAMember) - } +func GetMembStore() dao.MembStore { + return members } func Render(path string) string { @@ -187,21 +95,3 @@ func Render(path string) string { return output } - -func getProposal(idx int) *proposal { - if idx > len(proposals)-1 { - panic(msgProposalNotFound) - } - - return proposals[idx] -} - -func proposalExists(idx int) bool { - return idx >= 0 && idx <= len(proposals) -} - -func assertProposalExists(idx int) { - if !proposalExists(idx) { - panic("invalid proposal id") - } -} diff --git a/examples/gno.land/r/gov/dao/memberset.gno b/examples/gno.land/r/gov/dao/memberset.gno deleted file mode 100644 index 3abd52ae99d..00000000000 --- a/examples/gno.land/r/gov/dao/memberset.gno +++ /dev/null @@ -1,40 +0,0 @@ -package govdao - -import ( - "std" - - pproposal "gno.land/p/gov/proposal" -) - -const daoPkgPath = "gno.land/r/gov/dao" - -const ( - errNoChangesProposed = "no set changes proposed" - errNotGovDAO = "caller not govdao executor" -) - -func NewPropExecutor(changesFn func() []std.Address) pproposal.Executor { - if changesFn == nil { - panic(errNoChangesProposed) - } - - callback := func() error { - // Make sure the GovDAO executor runs the valset changes - assertGovDAOCaller() - - for _, addr := range changesFn() { - members = append(members, addr) - } - - return nil - } - - return pproposal.NewExecutor(callback) -} - -// assertGovDAOCaller verifies the caller is the GovDAO executor -func assertGovDAOCaller() { - if std.CurrentRealm().PkgPath() != daoPkgPath { - panic(errNotGovDAO) - } -} diff --git a/examples/gno.land/r/gov/dao/poc.gno b/examples/gno.land/r/gov/dao/poc.gno new file mode 100644 index 00000000000..55ec03675a8 --- /dev/null +++ b/examples/gno.land/r/gov/dao/poc.gno @@ -0,0 +1,90 @@ +package govdao + +import ( + "std" + + "gno.land/p/demo/context" + "gno.land/p/gov/dao" + "gno.land/p/gov/proposal" + pproposal "gno.land/p/gov/proposal" +) + +const daoPkgPath = "gno.land/r/gov/dao" + +const ( + errNoChangesProposed = "no set changes proposed" + errNotGovDAO = "caller not govdao executor" +) + +// NewMemberPropExecutor returns the GOVDAO member change executor +func NewMemberPropExecutor(changesFn func() []dao.Member) pproposal.Executor { + if changesFn == nil { + panic(errNoChangesProposed) + } + + callback := func() error { + // Make sure the GovDAO executor runs the member changes + assertGovDAOCaller() + + errs := &pproposal.CombinedErrors{} + for _, member := range changesFn() { + switch { + case !members.IsMember(member.Address): + // Addition request + err := members.AddMember(member) + + errs.Add(err) + case member.VotingPower == 0: + // Remove request + err := members.UpdateMember(member.Address, dao.Member{ + Address: member.Address, + VotingPower: 0, // 0 indicated removal + }) + + errs.Add(err) + default: + // Update request + err := members.UpdateMember(member.Address, member) + + errs.Add(err) + } + } + + // Check if there were any execution errors + if errs.Size() == 0 { + return nil + } + + return errs + } + + return pproposal.NewExecutor(callback) +} + +// SetDAOImpl sets a new DAO implementation +func SetDAOImpl(ctx context.Context, impl dao.DAO) { + proposal.AssertContextApprovedByGovDAO(ctx) + + d = impl +} + +// SetMembStoreImpl sets a new dao.MembStore implementation +func SetMembStoreImpl(ctx context.Context, impl dao.MembStore) { + proposal.AssertContextApprovedByGovDAO(ctx) + + members = impl +} + +// SetPropStoreImpl sets a new dao.PropStore implementation +func SetPropStoreImpl(ctx context.Context, impl dao.PropStore) { + proposal.AssertContextApprovedByGovDAO(ctx) + + proposals = impl +} + +// assertGovDAOCaller verifies the caller is the GovDAO executor +func assertGovDAOCaller() { + if std.CurrentRealm().PkgPath() != daoPkgPath { + panic(errNotGovDAO) + } +} diff --git a/examples/gno.land/r/gov/dao/prop1_filetest.gno b/examples/gno.land/r/gov/dao/prop1_filetest.gno index 49a200fd561..3ca0df7f4f7 100644 --- a/examples/gno.land/r/gov/dao/prop1_filetest.gno +++ b/examples/gno.land/r/gov/dao/prop1_filetest.gno @@ -10,17 +10,17 @@ package main import ( "std" + "github.com/gnolang/gno/examples/gno.land/p/gov/dao" pVals "gno.land/p/sys/validators" govdao "gno.land/r/gov/dao" "gno.land/r/sys/validators" ) -const daoPkgPath = "gno.land/r/gov/dao" - func init() { - membersFn := func() []std.Address { - return []std.Address{ - std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"), + membersFn := func() []dao.Member { + return []dao.Member{ + Address: std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"), + VotingPower: 10, } } diff --git a/examples/gno.land/r/gov/dao/types.gno b/examples/gno.land/r/gov/dao/types.gno deleted file mode 100644 index 123fc489075..00000000000 --- a/examples/gno.land/r/gov/dao/types.gno +++ /dev/null @@ -1,32 +0,0 @@ -package govdao - -import ( - "std" -) - -// Status enum. -type Status string - -var ( - Accepted Status = "accepted" - Active Status = "active" - NotAccepted Status = "not accepted" - Expired Status = "expired" - Succeeded Status = "succeeded" -) - -// Voter defines the needed methods for a voting system -type Voter interface { - - // IsAccepted indicates if the voting process had been accepted - IsAccepted(voters []std.Address) bool - - // IsFinished indicates if the voting process is finished - IsFinished(voters []std.Address) bool - - // Vote adds a new vote to the voting system - Vote(voters []std.Address, caller std.Address, flag string) - - // Status returns a human friendly string describing how the voting process is going - Status(voters []std.Address) string -} diff --git a/examples/gno.land/r/gov/dao/voter.gno b/examples/gno.land/r/gov/dao/voter.gno deleted file mode 100644 index 99223210791..00000000000 --- a/examples/gno.land/r/gov/dao/voter.gno +++ /dev/null @@ -1,91 +0,0 @@ -package govdao - -import ( - "std" - - "gno.land/p/demo/ufmt" -) - -const ( - yay = "YES" - nay = "NO" - - msgNoMoreVotesAllowed = "no more votes allowed" - msgAlreadyVoted = "caller already voted" - msgWrongVotingValue = "voting values must be YES or NO" -) - -func NewPercentageVoter(percent int) *PercentageVoter { - if percent < 0 || percent > 100 { - panic("percent value must be between 0 and 100") - } - - return &PercentageVoter{ - percentage: percent, - } -} - -// PercentageVoter is a system based on the amount of received votes. -// When the specified treshold is reached, the voting process finishes. -type PercentageVoter struct { - percentage int - - voters []std.Address - yes int - no int -} - -func (pv *PercentageVoter) IsAccepted(voters []std.Address) bool { - if len(voters) == 0 { - return true // special case - } - - return pv.percent(voters) >= pv.percentage -} - -func (pv *PercentageVoter) IsFinished(voters []std.Address) bool { - return pv.yes+pv.no >= len(voters) -} - -func (pv *PercentageVoter) Status(voters []std.Address) string { - return ufmt.Sprintf("YES: %d, NO: %d, percent: %d, members: %d", pv.yes, pv.no, pv.percent(voters), len(voters)) -} - -func (pv *PercentageVoter) Vote(voters []std.Address, caller std.Address, flag string) { - if pv.IsFinished(voters) { - panic(msgNoMoreVotesAllowed) - } - - if pv.alreadyVoted(caller) { - panic(msgAlreadyVoted) - } - - switch flag { - case yay: - pv.yes++ - pv.voters = append(pv.voters, caller) - case nay: - pv.no++ - pv.voters = append(pv.voters, caller) - default: - panic(msgWrongVotingValue) - } -} - -func (pv *PercentageVoter) percent(voters []std.Address) int { - if len(voters) == 0 { - return 0 - } - - return int((float32(pv.yes) / float32(len(voters))) * 100) -} - -func (pv *PercentageVoter) alreadyVoted(addr std.Address) bool { - for _, v := range pv.voters { - if v == addr { - return true - } - } - - return false -} From fd13d024ac7dc915515c8e0c232ea34f2bb0dece Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Sun, 14 Jul 2024 20:37:53 +0200 Subject: [PATCH 22/80] Clean house for the govdao executor --- examples/gno.land/p/demo/simpledao/dao.gno | 4 +- examples/gno.land/p/demo/simpledao/gno.mod | 2 +- .../gno.land/p/demo/simpledao/propstore.gno | 4 +- examples/gno.land/p/gov/dao/dao.gno | 4 +- examples/gno.land/p/gov/dao/executor.gno | 9 ++ examples/gno.land/p/gov/dao/gno.mod | 2 +- examples/gno.land/p/gov/executor/callback.gno | 45 +++++++ examples/gno.land/p/gov/executor/context.gno | 69 +++++++++++ .../error.gno => executor/errors.gno} | 12 +- .../p/gov/{proposal => executor}/gno.mod | 3 +- .../{proposal => executor}/proposal_test.gno | 110 ++++++++++++------ examples/gno.land/p/gov/proposal/proposal.gno | 106 ----------------- examples/gno.land/p/gov/proposal/types.gno | 37 ------ examples/gno.land/r/gnoland/blog/admin.gno | 4 +- examples/gno.land/r/gnoland/blog/gno.mod | 2 +- examples/gno.land/r/gov/dao/dao.gno | 6 +- examples/gno.land/r/gov/dao/dao_test.gno | 4 +- examples/gno.land/r/gov/dao/gno.mod | 4 +- examples/gno.land/r/gov/dao/poc.gno | 74 +++++++++--- .../gno.land/r/gov/dao/prop1_filetest.gno | 2 +- .../gno.land/r/gov/dao/prop2_filetest.gno | 6 +- examples/gno.land/r/sys/validators/gno.mod | 2 +- examples/gno.land/r/sys/validators/poc.gno | 6 +- 23 files changed, 289 insertions(+), 228 deletions(-) create mode 100644 examples/gno.land/p/gov/dao/executor.gno create mode 100644 examples/gno.land/p/gov/executor/callback.gno create mode 100644 examples/gno.land/p/gov/executor/context.gno rename examples/gno.land/p/gov/{proposal/error.gno => executor/errors.gno} (68%) rename examples/gno.land/p/gov/{proposal => executor}/gno.mod (56%) rename examples/gno.land/p/gov/{proposal => executor}/proposal_test.gno (53%) delete mode 100644 examples/gno.land/p/gov/proposal/proposal.gno delete mode 100644 examples/gno.land/p/gov/proposal/types.gno diff --git a/examples/gno.land/p/demo/simpledao/dao.gno b/examples/gno.land/p/demo/simpledao/dao.gno index 3d42f37e31b..b80e2c4ca78 100644 --- a/examples/gno.land/p/demo/simpledao/dao.gno +++ b/examples/gno.land/p/demo/simpledao/dao.gno @@ -6,7 +6,7 @@ import ( "gno.land/p/demo/ufmt" "gno.land/p/gov/dao" - pproposal "gno.land/p/gov/proposal" + "gno.land/p/gov/executor" ) var ( @@ -43,7 +43,7 @@ func NewSimpleDAO(membStore *MembStore, propStore *PropStore) *SimpleDAO { } } -func (s *SimpleDAO) Propose(description string, executor pproposal.Executor) (uint64, error) { +func (s *SimpleDAO) Propose(description string, executor executor.Executor) (uint64, error) { // Make sure the executor is set if executor == nil { return 0, errInvalidExecutor diff --git a/examples/gno.land/p/demo/simpledao/gno.mod b/examples/gno.land/p/demo/simpledao/gno.mod index feace5b8d6d..c6e6ad15723 100644 --- a/examples/gno.land/p/demo/simpledao/gno.mod +++ b/examples/gno.land/p/demo/simpledao/gno.mod @@ -8,5 +8,5 @@ require ( gno.land/p/demo/ufmt v0.0.0-latest gno.land/p/demo/urequire v0.0.0-latest gno.land/p/gov/dao v0.0.0-latest - gno.land/p/gov/proposal v0.0.0-latest + gno.land/p/gov/executor v0.0.0-latest ) diff --git a/examples/gno.land/p/demo/simpledao/propstore.gno b/examples/gno.land/p/demo/simpledao/propstore.gno index 8d5e125d857..15a2db132d1 100644 --- a/examples/gno.land/p/demo/simpledao/propstore.gno +++ b/examples/gno.land/p/demo/simpledao/propstore.gno @@ -7,7 +7,7 @@ import ( "gno.land/p/demo/avl" "gno.land/p/demo/seqid" "gno.land/p/gov/dao" - pproposal "gno.land/p/gov/proposal" + "gno.land/p/gov/executor" ) var ( @@ -30,7 +30,7 @@ type proposal struct { author std.Address // initiator of the proposal description string // description of the proposal - executor pproposal.Executor // executor for the proposal + executor executor.Executor // executor for the proposal status dao.ProposalStatus // status of the proposal votes *votes // voting mechanism diff --git a/examples/gno.land/p/gov/dao/dao.gno b/examples/gno.land/p/gov/dao/dao.gno index 88c78684ab5..0730393e023 100644 --- a/examples/gno.land/p/gov/dao/dao.gno +++ b/examples/gno.land/p/gov/dao/dao.gno @@ -1,12 +1,12 @@ package dao -import "gno.land/p/gov/proposal" +import "gno.land/p/gov/executor" // DAO defines the DAO abstraction type DAO interface { // Propose adds a new proposal to the executor-based GOVDAO. // Returns the generated proposal ID - Propose(comment string, executor proposal.Executor) (uint64, error) + Propose(comment string, executor executor.Executor) (uint64, error) // VoteOnProposal adds a vote to the given proposal ID VoteOnProposal(id uint64, option VoteOption) error diff --git a/examples/gno.land/p/gov/dao/executor.gno b/examples/gno.land/p/gov/dao/executor.gno new file mode 100644 index 00000000000..9291c2c53c5 --- /dev/null +++ b/examples/gno.land/p/gov/dao/executor.gno @@ -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 +} diff --git a/examples/gno.land/p/gov/dao/gno.mod b/examples/gno.land/p/gov/dao/gno.mod index 80d25134a0a..da442b205fd 100644 --- a/examples/gno.land/p/gov/dao/gno.mod +++ b/examples/gno.land/p/gov/dao/gno.mod @@ -1,3 +1,3 @@ module gno.land/p/gov/dao -require gno.land/p/gov/proposal v0.0.0-latest +require gno.land/p/gov/executor v0.0.0-latest diff --git a/examples/gno.land/p/gov/executor/callback.gno b/examples/gno.land/p/gov/executor/callback.gno new file mode 100644 index 00000000000..cddd0f3fc38 --- /dev/null +++ b/examples/gno.land/p/gov/executor/callback.gno @@ -0,0 +1,45 @@ +package executor + +import ( + "errors" + "std" +) + +const daoPkgPath = "gno.land/r/gov/dao" // TODO: make sure this is configurable through r/sys/vars + +var errNotGovDAO = errors.New("only r/gov/dao can be the caller") + +// NewCallbackExecutor creates a new callback executor with the provided callback function +func NewCallbackExecutor(callback func() error) *CallbackExecutor { + return &CallbackExecutor{ + callback: callback, + } +} + +// CallbackExecutor is an implementation of the dao.Executor interface, +// based on a specific callback. +// The given callback should verify the validity of the govdao call +type CallbackExecutor struct { + callback func() error // the callback to be executed +} + +// Execute runs the executor's callback function. +func (exec *CallbackExecutor) Execute() error { + // Verify the executor is r/gov/dao + assertCalledByGovdao() + + if exec.callback != nil { + return exec.callback() + } + + return nil +} + +// assertCalledByGovdao asserts that the calling Realm is /r/gov/dao +func assertCalledByGovdao() { + caller := std.CurrentRealm().PkgPath() + + if caller != daoPkgPath { + panic(errNotGovDAO) + } +} diff --git a/examples/gno.land/p/gov/executor/context.gno b/examples/gno.land/p/gov/executor/context.gno new file mode 100644 index 00000000000..b7460a0f4f9 --- /dev/null +++ b/examples/gno.land/p/gov/executor/context.gno @@ -0,0 +1,69 @@ +package executor + +import ( + "errors" + + "gno.land/p/demo/context" +) + +type propContextKey string + +func (k propContextKey) String() string { return string(k) } + +const ( + statusContextKey = propContextKey("govdao-prop-status") + approvedStatus = "approved" +) + +var errNotApproved = errors.New("not approved by govdao") + +// CtxExecutor is an implementation of the dao.Executor interface, +// based on the given context. +// It utilizes the given context to assert the validity of the govdao call +type CtxExecutor struct { + callbackCtx func(ctx context.Context) error // the callback ctx fn, if any +} + +// NewCtxExecutor creates a new executor with the provided callback function. +func NewCtxExecutor(callback func(ctx context.Context) error) *CtxExecutor { + return &CtxExecutor{ + callbackCtx: callback, + } +} + +// Execute runs the executor's callback function +func (exec *CtxExecutor) Execute() error { + // Verify the executor is r/gov/dao + assertCalledByGovdao() + + // Create the context + ctx := context.WithValue( + context.Empty(), + statusContextKey, + approvedStatus, + ) + + return exec.callbackCtx(ctx) +} + +// IsApprovedByGovdaoContext asserts that the govdao approved the context +func IsApprovedByGovdaoContext(ctx context.Context) bool { + v := ctx.Value(statusContextKey) + if v == nil { + return false + } + + vs, ok := v.(string) + + return ok && vs == approvedStatus +} + +// AssertContextApprovedByGovDAO asserts the given context +// was approved by GOVDAO +func AssertContextApprovedByGovDAO(ctx context.Context) { + if IsApprovedByGovdaoContext(ctx) { + return + } + + panic(errNotApproved) +} diff --git a/examples/gno.land/p/gov/proposal/error.gno b/examples/gno.land/p/gov/executor/errors.gno similarity index 68% rename from examples/gno.land/p/gov/proposal/error.gno rename to examples/gno.land/p/gov/executor/errors.gno index 58a520ceeb9..187fdc97a9e 100644 --- a/examples/gno.land/p/gov/proposal/error.gno +++ b/examples/gno.land/p/gov/executor/errors.gno @@ -1,14 +1,14 @@ -package proposal +package executor import "strings" -// CombinedErrors is a combined execution error -type CombinedErrors struct { +// CombinedError is a combined execution error +type CombinedError struct { errors []error } // Error returns the combined execution error -func (e *CombinedErrors) Error() string { +func (e *CombinedError) Error() string { if len(e.errors) == 0 { return "" } @@ -26,7 +26,7 @@ func (e *CombinedErrors) Error() string { } // Add adds a new error to the execution error -func (e *CombinedErrors) Add(errs ...error) { +func (e *CombinedError) Add(errs ...error) { if len(errs) == 0 { return } @@ -35,6 +35,6 @@ func (e *CombinedErrors) Add(errs ...error) { } // Size returns a -func (e *CombinedErrors) Size() int { +func (e *CombinedError) Size() int { return len(e.errors) } diff --git a/examples/gno.land/p/gov/proposal/gno.mod b/examples/gno.land/p/gov/executor/gno.mod similarity index 56% rename from examples/gno.land/p/gov/proposal/gno.mod rename to examples/gno.land/p/gov/executor/gno.mod index 3f6ef34a759..3ae47397665 100644 --- a/examples/gno.land/p/gov/proposal/gno.mod +++ b/examples/gno.land/p/gov/executor/gno.mod @@ -1,7 +1,6 @@ -module gno.land/p/gov/proposal +module gno.land/p/gov/executor require ( gno.land/p/demo/context v0.0.0-latest gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest ) diff --git a/examples/gno.land/p/gov/proposal/proposal_test.gno b/examples/gno.land/p/gov/executor/proposal_test.gno similarity index 53% rename from examples/gno.land/p/gov/proposal/proposal_test.gno rename to examples/gno.land/p/gov/executor/proposal_test.gno index 536871e644d..eb77a84b211 100644 --- a/examples/gno.land/p/gov/proposal/proposal_test.gno +++ b/examples/gno.land/p/gov/executor/proposal_test.gno @@ -1,27 +1,17 @@ -package proposal +package executor import ( "errors" "std" "testing" + "gno.land/p/demo/context" "gno.land/p/demo/uassert" - "gno.land/p/demo/urequire" ) -func TestExecutor(t *testing.T) { +func TestExecutor_Callback(t *testing.T) { t.Parallel() - verifyProposalFailed := func(e Executor) { - uassert.True(t, e.IsDone(), "expected proposal to be done") - uassert.False(t, e.IsSuccessful(), "expected proposal to fail") - } - - verifyProposalSucceeded := func(e Executor) { - uassert.True(t, e.IsDone(), "expected proposal to be done") - uassert.True(t, e.IsSuccessful(), "expected proposal to be successful") - } - t.Run("govdao not caller", func(t *testing.T) { t.Parallel() @@ -36,9 +26,7 @@ func TestExecutor(t *testing.T) { ) // Create the executor - e := NewExecutor(cb) - - urequire.False(t, e.IsDone(), "expected status to be NotExecuted") + e := NewCallbackExecutor(cb) // Execute as not the /r/gov/dao caller uassert.PanicsWithMessage(t, errNotGovDAO.Error(), func() { @@ -62,9 +50,7 @@ func TestExecutor(t *testing.T) { ) // Create the executor - e := NewExecutor(cb) - - urequire.False(t, e.IsDone(), "expected status to be NotExecuted") + e := NewCallbackExecutor(cb) // Execute as the /r/gov/dao caller r := std.NewCodeRealm(daoPkgPath) @@ -77,9 +63,6 @@ func TestExecutor(t *testing.T) { }) uassert.True(t, called, "expected proposal to execute") - - // Make sure the execution params are correct - verifyProposalSucceeded(e) }) t.Run("execution unsuccessful", func(t *testing.T) { @@ -97,7 +80,7 @@ func TestExecutor(t *testing.T) { ) // Create the executor - e := NewExecutor(cb) + e := NewCallbackExecutor(cb) // Execute as the /r/gov/dao caller r := std.NewCodeRealm(daoPkgPath) @@ -110,18 +93,23 @@ func TestExecutor(t *testing.T) { }) uassert.True(t, called, "expected proposal to execute") - - // Make sure the execution params are correct - verifyProposalFailed(e) }) +} - t.Run("proposal already executed", func(t *testing.T) { +func TestExecutor_Context(t *testing.T) { + t.Parallel() + + t.Run("govdao not caller", func(t *testing.T) { t.Parallel() var ( called = false - cb = func() error { + cb = func(ctx context.Context) error { + if !IsApprovedByGovdaoContext(ctx) { + t.Fatal("not govdao caller") + } + called = true return nil @@ -129,28 +117,80 @@ func TestExecutor(t *testing.T) { ) // Create the executor - e := NewExecutor(cb) + e := NewCtxExecutor(cb) + + // Execute as not the /r/gov/dao caller + uassert.PanicsWithMessage(t, errNotGovDAO.Error(), func() { + _ = e.Execute() + }) + + uassert.False(t, called, "expected proposal to not execute") + }) + + t.Run("execution successful", func(t *testing.T) { + t.Parallel() + + var ( + called = false + + cb = func(ctx context.Context) error { + if !IsApprovedByGovdaoContext(ctx) { + t.Fatal("not govdao caller") + } + + called = true + + return nil + } + ) - urequire.False(t, e.IsDone(), "expected status to be NotExecuted") + // Create the executor + e := NewCtxExecutor(cb) // Execute as the /r/gov/dao caller r := std.NewCodeRealm(daoPkgPath) std.TestSetRealm(r) uassert.NotPanics(t, func() { - uassert.NoError(t, e.Execute()) + err := e.Execute() + + uassert.NoError(t, err) }) uassert.True(t, called, "expected proposal to execute") + }) + + t.Run("execution unsuccessful", func(t *testing.T) { + t.Parallel() + + var ( + called = false + expectedErr = errors.New("unexpected") - // Make sure the execution params are correct - verifyProposalSucceeded(e) + cb = func(ctx context.Context) error { + if !IsApprovedByGovdaoContext(ctx) { + t.Fatal("not govdao caller") + } + + called = true + + return expectedErr + } + ) + + // Create the executor + e := NewCtxExecutor(cb) + + // Execute as the /r/gov/dao caller + r := std.NewCodeRealm(daoPkgPath) + std.TestSetRealm(r) - // Attempt to execute the proposal again uassert.NotPanics(t, func() { err := e.Execute() - uassert.ErrorIs(t, err, ErrAlreadyDone) + uassert.ErrorIs(t, err, expectedErr) }) + + uassert.True(t, called, "expected proposal to execute") }) } diff --git a/examples/gno.land/p/gov/proposal/proposal.gno b/examples/gno.land/p/gov/proposal/proposal.gno deleted file mode 100644 index ca1767228c9..00000000000 --- a/examples/gno.land/p/gov/proposal/proposal.gno +++ /dev/null @@ -1,106 +0,0 @@ -// Package proposal provides a structure for executing proposals. -package proposal - -import ( - "errors" - "std" - - "gno.land/p/demo/context" -) - -var errNotGovDAO = errors.New("only r/gov/dao can be the caller") - -// NewExecutor creates a new executor with the provided callback function. -func NewExecutor(callback func() error) Executor { - return &executorImpl{ - callback: callback, - done: false, - } -} - -// NewCtxExecutor creates a new executor with the provided callback function. -func NewCtxExecutor(callback func(ctx context.Context) error) Executor { - return &executorImpl{ - callbackCtx: callback, - done: false, - } -} - -// executorImpl is an implementation of the Executor interface. -type executorImpl struct { - callback func() error - callbackCtx func(ctx context.Context) error - done bool - success bool -} - -// Execute runs the executor's callback function. -func (exec *executorImpl) Execute() error { - if exec.done { - return ErrAlreadyDone - } - - // Verify the executor is r/gov/dao - assertCalledByGovdao() - - var err error - if exec.callback != nil { - err = exec.callback() - } else if exec.callbackCtx != nil { - ctx := context.WithValue(context.Empty(), statusContextKey, approvedStatus) - err = exec.callbackCtx(ctx) - } - exec.done = true - exec.success = err == nil - - return err -} - -// IsDone returns whether the executor has been executed. -func (exec *executorImpl) IsDone() bool { - return exec.done -} - -// IsSuccessful returns whether the execution was successful. -func (exec *executorImpl) IsSuccessful() bool { - return exec.success -} - -// IsExpired returns whether the execution had expired or not. -// This implementation never expires. -func (exec *executorImpl) IsExpired() bool { - return false -} - -func IsApprovedByGovdaoContext(ctx context.Context) bool { - v := ctx.Value(statusContextKey) - if v == nil { - return false - } - vs, ok := v.(string) - return ok && vs == approvedStatus -} - -func AssertContextApprovedByGovDAO(ctx context.Context) { - if !IsApprovedByGovdaoContext(ctx) { - panic("not approved by govdao") - } -} - -// assertCalledByGovdao asserts that the calling Realm is /r/gov/dao -func assertCalledByGovdao() { - caller := std.CurrentRealm().PkgPath() - - if caller != daoPkgPath { - panic(errNotGovDAO) - } -} - -type propContextKey string - -func (k propContextKey) String() string { return string(k) } - -const ( - statusContextKey = propContextKey("govdao-prop-status") - approvedStatus = "approved" -) diff --git a/examples/gno.land/p/gov/proposal/types.gno b/examples/gno.land/p/gov/proposal/types.gno deleted file mode 100644 index 6cd2da9ccfe..00000000000 --- a/examples/gno.land/p/gov/proposal/types.gno +++ /dev/null @@ -1,37 +0,0 @@ -// Package proposal defines types for proposal execution. -package proposal - -import "errors" - -// 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 - - // IsDone returns a flag indicating if the proposal was executed - IsDone() bool - - // IsSuccessful returns a flag indicating if the proposal was executed - // and is successful - IsSuccessful() bool // IsDone() && !err - - // IsExpired returns whether the execution had expired or not. - IsExpired() bool -} - -// ErrAlreadyDone is the error returned when trying to execute an already -// executed proposal. -var ErrAlreadyDone = errors.New("already executed") - -// Status enum. -type Status string - -const ( - NotExecuted Status = "not_executed" - Succeeded Status = "succeeded" - Failed Status = "failed" -) - -const daoPkgPath = "gno.land/r/gov/dao" // TODO: make sure this is configurable through r/sys/vars diff --git a/examples/gno.land/r/gnoland/blog/admin.gno b/examples/gno.land/r/gnoland/blog/admin.gno index 08b0911cf24..526331eee02 100644 --- a/examples/gno.land/r/gnoland/blog/admin.gno +++ b/examples/gno.land/r/gnoland/blog/admin.gno @@ -6,7 +6,7 @@ import ( "gno.land/p/demo/avl" "gno.land/p/demo/context" - "gno.land/p/gov/proposal" + "gno.land/p/gov/executor" ) var ( @@ -42,7 +42,7 @@ func AdminRemoveModerator(addr std.Address) { } func DaoAddPost(ctx context.Context, slug, title, body, publicationDate, authors, tags string) { - proposal.AssertContextApprovedByGovDAO(ctx) + executor.AssertContextApprovedByGovDAO(ctx) caller := std.DerivePkgAddr("gno.land/r/gov/dao") addPost(caller, slug, title, body, publicationDate, authors, tags) } diff --git a/examples/gno.land/r/gnoland/blog/gno.mod b/examples/gno.land/r/gnoland/blog/gno.mod index 17c17e0cfa6..3d03b4f02fe 100644 --- a/examples/gno.land/r/gnoland/blog/gno.mod +++ b/examples/gno.land/r/gnoland/blog/gno.mod @@ -4,5 +4,5 @@ require ( gno.land/p/demo/avl v0.0.0-latest gno.land/p/demo/blog v0.0.0-latest gno.land/p/demo/context v0.0.0-latest - gno.land/p/gov/proposal v0.0.0-latest + gno.land/p/gov/executor v0.0.0-latest ) diff --git a/examples/gno.land/r/gov/dao/dao.gno b/examples/gno.land/r/gov/dao/dao.gno index 940a2e9100f..24c65a2a471 100644 --- a/examples/gno.land/r/gov/dao/dao.gno +++ b/examples/gno.land/r/gov/dao/dao.gno @@ -3,10 +3,10 @@ package govdao import ( "strconv" - "github.com/gnolang/gno/examples/gno.land/p/demo/simpledao" + "gno.land/p/demo/simpledao" "gno.land/p/demo/ufmt" "gno.land/p/gov/dao" - pproposal "gno.land/p/gov/proposal" + "gno.land/p/gov/executor" ) var ( @@ -31,7 +31,7 @@ func init() { // Propose is designed to be called by another contract or with // `maketx run`, not by a `maketx call`. -func Propose(comment string, executor pproposal.Executor) (uint64, error) { +func Propose(comment string, executor executor.Executor) (uint64, error) { return d.Propose(comment, executor) } diff --git a/examples/gno.land/r/gov/dao/dao_test.gno b/examples/gno.land/r/gov/dao/dao_test.gno index 96eaba7f5e9..fa2b3fc6aa4 100644 --- a/examples/gno.land/r/gov/dao/dao_test.gno +++ b/examples/gno.land/r/gov/dao/dao_test.gno @@ -6,7 +6,7 @@ import ( "gno.land/p/demo/testutils" "gno.land/p/demo/urequire" - pproposal "gno.land/p/gov/proposal" + "gno.land/p/gov/executor" ) func TestPackage(t *testing.T) { @@ -26,7 +26,7 @@ func TestPackage(t *testing.T) { urequire.Equal(t, expected, out) var called bool - ex := pproposal.NewExecutor(func() error { + ex := executor.NewCallbackExecutor(func() error { called = true return nil }) diff --git a/examples/gno.land/r/gov/dao/gno.mod b/examples/gno.land/r/gov/dao/gno.mod index f3c0bae990e..88456caff7d 100644 --- a/examples/gno.land/r/gov/dao/gno.mod +++ b/examples/gno.land/r/gov/dao/gno.mod @@ -1,8 +1,10 @@ module gno.land/r/gov/dao require ( + gno.land/p/demo/simpledao v0.0.0-latest gno.land/p/demo/testutils 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 + gno.land/p/gov/dao v0.0.0-latest + gno.land/p/gov/executor v0.0.0-latest ) diff --git a/examples/gno.land/r/gov/dao/poc.gno b/examples/gno.land/r/gov/dao/poc.gno index 55ec03675a8..f68ef7789f6 100644 --- a/examples/gno.land/r/gov/dao/poc.gno +++ b/examples/gno.land/r/gov/dao/poc.gno @@ -3,10 +3,8 @@ package govdao import ( "std" - "gno.land/p/demo/context" "gno.land/p/gov/dao" - "gno.land/p/gov/proposal" - pproposal "gno.land/p/gov/proposal" + "gno.land/p/gov/executor" ) const daoPkgPath = "gno.land/r/gov/dao" @@ -17,7 +15,7 @@ const ( ) // NewMemberPropExecutor returns the GOVDAO member change executor -func NewMemberPropExecutor(changesFn func() []dao.Member) pproposal.Executor { +func NewMemberPropExecutor(changesFn func() []dao.Member) executor.Executor { if changesFn == nil { panic(errNoChangesProposed) } @@ -26,7 +24,7 @@ func NewMemberPropExecutor(changesFn func() []dao.Member) pproposal.Executor { // Make sure the GovDAO executor runs the member changes assertGovDAOCaller() - errs := &pproposal.CombinedErrors{} + errs := &executor.CombinedError{} for _, member := range changesFn() { switch { case !members.IsMember(member.Address): @@ -58,27 +56,69 @@ func NewMemberPropExecutor(changesFn func() []dao.Member) pproposal.Executor { return errs } - return pproposal.NewExecutor(callback) + return executor.NewCallbackExecutor(callback) } -// SetDAOImpl sets a new DAO implementation -func SetDAOImpl(ctx context.Context, impl dao.DAO) { - proposal.AssertContextApprovedByGovDAO(ctx) +func NewDAOImplExecutor(changeFn func() dao.DAO) executor.Executor { + if changeFn == nil { + panic(errNoChangesProposed) + } - d = impl + callback := func() error { + assertGovDAOCaller() + + setDAOImpl(changeFn()) + + return nil + } + + return executor.NewCallbackExecutor(callback) } -// SetMembStoreImpl sets a new dao.MembStore implementation -func SetMembStoreImpl(ctx context.Context, impl dao.MembStore) { - proposal.AssertContextApprovedByGovDAO(ctx) +func NewMembStoreImplExecutor(changeFn func() dao.MembStore) executor.Executor { + if changeFn == nil { + panic(errNoChangesProposed) + } - members = impl + callback := func() error { + assertGovDAOCaller() + + setMembStoreImpl(changeFn()) + + return nil + } + + return executor.NewCallbackExecutor(callback) } -// SetPropStoreImpl sets a new dao.PropStore implementation -func SetPropStoreImpl(ctx context.Context, impl dao.PropStore) { - proposal.AssertContextApprovedByGovDAO(ctx) +func NewPropStoreImplExecutor(changeFn func() dao.PropStore) executor.Executor { + if changeFn == nil { + panic(errNoChangesProposed) + } + + callback := func() error { + assertGovDAOCaller() + + setPropStoreImpl(changeFn()) + + return nil + } + + return executor.NewCallbackExecutor(callback) +} + +// setDAOImpl sets a new DAO implementation +func setDAOImpl(impl dao.DAO) { + d = impl +} + +// setMembStoreImpl sets a new dao.MembStore implementation +func setMembStoreImpl(impl dao.MembStore) { + members = impl +} +// setPropStoreImpl sets a new dao.PropStore implementation +func setPropStoreImpl(impl dao.PropStore) { proposals = impl } diff --git a/examples/gno.land/r/gov/dao/prop1_filetest.gno b/examples/gno.land/r/gov/dao/prop1_filetest.gno index 3ca0df7f4f7..cd4bd7d7063 100644 --- a/examples/gno.land/r/gov/dao/prop1_filetest.gno +++ b/examples/gno.land/r/gov/dao/prop1_filetest.gno @@ -10,7 +10,7 @@ package main import ( "std" - "github.com/gnolang/gno/examples/gno.land/p/gov/dao" + "gno.land/p/gov/dao" pVals "gno.land/p/sys/validators" govdao "gno.land/r/gov/dao" "gno.land/r/sys/validators" diff --git a/examples/gno.land/r/gov/dao/prop2_filetest.gno b/examples/gno.land/r/gov/dao/prop2_filetest.gno index 047709cc45f..311b12e7915 100644 --- a/examples/gno.land/r/gov/dao/prop2_filetest.gno +++ b/examples/gno.land/r/gov/dao/prop2_filetest.gno @@ -5,7 +5,7 @@ import ( "time" "gno.land/p/demo/context" - "gno.land/p/gov/proposal" + "gno.land/p/gov/executor" gnoblog "gno.land/r/gnoland/blog" govdao "gno.land/r/gov/dao" ) @@ -25,7 +25,7 @@ func init() { govdao.ExecuteProposal(id) - executor := proposal.NewCtxExecutor(func(ctx context.Context) error { + ex := executor.NewCtxExecutor(func(ctx context.Context) error { gnoblog.DaoAddPost( ctx, "hello-from-govdao", // slug @@ -41,7 +41,7 @@ func init() { // Create a proposal. // XXX: payment comment = "post a new blogpost about govdao" - govdao.Propose(comment, executor) + govdao.Propose(comment, ex) } func main() { diff --git a/examples/gno.land/r/sys/validators/gno.mod b/examples/gno.land/r/sys/validators/gno.mod index d9d129dd543..cc4e6b72236 100644 --- a/examples/gno.land/r/sys/validators/gno.mod +++ b/examples/gno.land/r/sys/validators/gno.mod @@ -6,7 +6,7 @@ require ( 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/gov/proposal v0.0.0-latest + gno.land/p/gov/executor v0.0.0-latest gno.land/p/nt/poa v0.0.0-latest gno.land/p/sys/validators v0.0.0-latest ) diff --git a/examples/gno.land/r/sys/validators/poc.gno b/examples/gno.land/r/sys/validators/poc.gno index e088b3b4293..d39a83a2786 100644 --- a/examples/gno.land/r/sys/validators/poc.gno +++ b/examples/gno.land/r/sys/validators/poc.gno @@ -3,7 +3,7 @@ package validators import ( "std" - "gno.land/p/gov/proposal" + "gno.land/p/gov/executor" "gno.land/p/sys/validators" ) @@ -20,7 +20,7 @@ const ( // // Concept adapted from: // https://github.com/gnolang/gno/pull/1945 -func NewPropExecutor(changesFn func() []validators.Validator) proposal.Executor { +func NewPropExecutor(changesFn func() []validators.Validator) executor.Executor { if changesFn == nil { panic(errNoChangesProposed) } @@ -44,7 +44,7 @@ func NewPropExecutor(changesFn func() []validators.Validator) proposal.Executor return nil } - return proposal.NewExecutor(callback) + return executor.NewExecutor(callback) } // assertGovDAOCaller verifies the caller is the GovDAO executor From ebfd3d224297f133037ad2aef97e9e90290fec79 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Mon, 15 Jul 2024 11:28:17 +0200 Subject: [PATCH 23/80] Add remaining simpledao tests --- examples/gno.land/p/demo/simpledao/dao.gno | 69 +- .../gno.land/p/demo/simpledao/dao_test.gno | 700 +++++++++++++++++- .../gno.land/p/demo/simpledao/membstore.gno | 19 + .../gno.land/p/demo/simpledao/mock_test.gno | 27 + .../gno.land/p/demo/simpledao/propstore.gno | 3 +- examples/gno.land/p/demo/simpledao/vote.gno | 19 +- examples/gno.land/p/gov/dao/dao.gno | 4 +- examples/gno.land/p/gov/dao/executor.gno | 4 + examples/gno.land/p/gov/dao/gno.mod | 2 - examples/gno.land/p/gov/dao/vote.gno | 6 +- examples/gno.land/p/gov/executor/callback.gno | 6 + examples/gno.land/p/gov/executor/context.gno | 6 + examples/gno.land/r/gov/dao/dao.gno | 3 +- examples/gno.land/r/gov/dao/poc.gno | 8 +- examples/gno.land/r/sys/validators/poc.gno | 2 +- 15 files changed, 789 insertions(+), 89 deletions(-) create mode 100644 examples/gno.land/p/demo/simpledao/mock_test.gno diff --git a/examples/gno.land/p/demo/simpledao/dao.gno b/examples/gno.land/p/demo/simpledao/dao.gno index b80e2c4ca78..6e13fa1f368 100644 --- a/examples/gno.land/p/demo/simpledao/dao.gno +++ b/examples/gno.land/p/demo/simpledao/dao.gno @@ -6,7 +6,6 @@ import ( "gno.land/p/demo/ufmt" "gno.land/p/gov/dao" - "gno.land/p/gov/executor" ) var ( @@ -15,10 +14,9 @@ var ( 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") + errExecutorExpired = errors.New("executor is expired") + errNotGovDAO = errors.New("caller not correct govdao instance") ) var ( @@ -35,15 +33,15 @@ type SimpleDAO struct { propStore *PropStore } -// NewSimpleDAO creates a new instance of the simpledao DAO -func NewSimpleDAO(membStore *MembStore, propStore *PropStore) *SimpleDAO { +// New creates a new instance of the simpledao DAO +func New(membStore *MembStore, propStore *PropStore) *SimpleDAO { return &SimpleDAO{ membStore: membStore, propStore: propStore, } } -func (s *SimpleDAO) Propose(description string, executor executor.Executor) (uint64, error) { +func (s *SimpleDAO) Propose(description string, executor dao.Executor) (uint64, error) { // Make sure the executor is set if executor == nil { return 0, errInvalidExecutor @@ -62,16 +60,17 @@ func (s *SimpleDAO) Propose(description string, executor executor.Executor) (uin // Create the wrapped proposal prop := &proposal{ + author: caller, description: description, executor: executor, - author: caller, + status: dao.Active, 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 0, ufmt.Errorf("unable to add proposal, %s", err.Error()) } return id, nil @@ -81,13 +80,13 @@ 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) + return ufmt.Errorf("unable to get govdao member, %s", err.Error()) } // 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) + return ufmt.Errorf("unable to get proposal %d, %s", id, err.Error()) } prop := propRaw.(*proposal) @@ -105,26 +104,37 @@ func (s *SimpleDAO) VoteOnProposal(id uint64, option dao.VoteOption) error { return errProposalInactive } - // Check if the proposal executor is expired + // Check if the executor is expired if prop.executor.IsExpired() { - return errProposalExpired + return errExecutorExpired } // Cast the vote if err = prop.votes.castVote(member, option); err != nil { - return ufmt.Errorf("unable to vote on proposal %d, %s", id, err) + return ufmt.Errorf("unable to vote on proposal %d, %s", id, err.Error()) } // Check the votes to see if quorum is reached var ( majorityPower = s.membStore.getMajorityPower() - yays, nays = prop.votes.getTally() + totalPower = s.membStore.getTotalPower() + + yays, nays, abstains = prop.votes.getTally() ) switch { case yays > majorityPower: + // 2/3+ voted YES prop.status = dao.Accepted case nays > majorityPower: + // 2/3+ voted NO + prop.status = dao.NotAccepted + case abstains > majorityPower: + // 2/3+ voted ABSTAIN + prop.status = dao.NotAccepted + case yays+nays+abstains >= totalPower: + // Everyone voted, but it's undecided, + // hence the proposal can't go through prop.status = dao.NotAccepted default: // Quorum not reached @@ -148,17 +158,11 @@ func (s *SimpleDAO) ExecuteProposal(id uint64) error { // 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) + return ufmt.Errorf("unable to get proposal %d, %s", id, err.Error()) } 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.ExecutionSuccessful || prop.GetStatus() == dao.ExecutionFailed { @@ -166,16 +170,22 @@ func (s *SimpleDAO) ExecuteProposal(id uint64) error { return errProposalExecuted } - // Check if proposal is expired + // Check the proposal status + if prop.GetStatus() != dao.Accepted { + // Proposal is not accepted, cannot be executed + return errProposalNotAccepted + } + + // Check if the executor is expired if prop.executor.IsExpired() { - return errProposalExpired + return errExecutorExpired } // Attempt to execute the proposal if err = prop.executor.Execute(); err != nil { prop.status = dao.ExecutionFailed - return ufmt.Errorf("error during proposal %d execution, %s", id, err) + return ufmt.Errorf("error during proposal %d execution, %s", id, err.Error()) } // Update the proposal status @@ -191,12 +201,3 @@ func (s *SimpleDAO) ExecuteProposal(id uint64) error { 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 { - return std.CurrentRealm().PkgPath() == daoPkgPath -} diff --git a/examples/gno.land/p/demo/simpledao/dao_test.gno b/examples/gno.land/p/demo/simpledao/dao_test.gno index f1ab0f2c0bd..0416b6a2af6 100644 --- a/examples/gno.land/p/demo/simpledao/dao_test.gno +++ b/examples/gno.land/p/demo/simpledao/dao_test.gno @@ -1,34 +1,16 @@ package simpledao -import "testing" - -func TestSimpleDAO_GetMembStore(t *testing.T) { - t.Parallel() - - t.Run("empty store", func(t *testing.T) { - t.Parallel() - - }) - - t.Run("store with members", func(t *testing.T) { - t.Parallel() - - }) -} - -func TestSimpleDAO_GetPropStore(t *testing.T) { - t.Parallel() - - t.Run("empty store", func(t *testing.T) { - t.Parallel() - - }) - - t.Run("store with proposals", func(t *testing.T) { - t.Parallel() - - }) -} +import ( + "errors" + "std" + "testing" + + "gno.land/p/demo/testutils" + "gno.land/p/demo/uassert" + "gno.land/p/demo/urequire" + "gno.land/p/gov/dao" + "gno.land/p/gov/executor" +) func TestSimpleDAO_Propose(t *testing.T) { t.Parallel() @@ -36,16 +18,100 @@ func TestSimpleDAO_Propose(t *testing.T) { t.Run("invalid executor", func(t *testing.T) { t.Parallel() + s := &SimpleDAO{} + + _, err := s.Propose("", nil) + uassert.ErrorIs( + t, + err, + errInvalidExecutor, + ) }) t.Run("caller cannot cover fee", func(t *testing.T) { t.Parallel() + var ( + called = false + cb = func() error { + called = true + + return nil + } + ex = executor.NewCallbackExecutor(cb) + + sentCoins = std.NewCoins( + std.NewCoin( + "ugnot", + minProposalFeeValue-1, + ), + ) + + s = &SimpleDAO{ + membStore: NewMembStore(), // empty member store + } + ) + + // Set the sent coins to be lower + // than the proposal fee + std.TestSetOrigSend(sentCoins, std.Coins{}) + + _, err := s.Propose("", ex) + uassert.ErrorIs( + t, + err, + errInsufficientProposalFunds, + ) + + uassert.False(t, called) }) t.Run("proposal added", func(t *testing.T) { t.Parallel() + var ( + called = false + cb = func() error { + called = true + + return nil + } + + ex = executor.NewCallbackExecutor(cb) + description = "Proposal description" + + proposer = testutils.TestAddress("proposer") + sentCoins = std.NewCoins( + std.NewCoin( + "ugnot", + minProposalFeeValue, // enough to cover + ), + ) + + s = &SimpleDAO{ + membStore: NewMembStore(), // empty member store + propStore: NewPropStore(), + } + ) + + // Set the sent coins to enough + // to cover the fee + std.TestSetOrigSend(sentCoins, std.Coins{}) + std.TestSetOrigCaller(proposer) + + // Make sure the proposal was added + id, err := s.Propose(description, ex) + uassert.NoError(t, err) + uassert.False(t, called) + + // Make sure the proposal exists + prop, err := s.propStore.GetProposalByID(id) + uassert.NoError(t, err) + + uassert.Equal(t, proposer.String(), prop.GetAuthor().String()) + uassert.Equal(t, description, prop.GetDescription()) + uassert.Equal(t, dao.Active.String(), prop.GetStatus().String()) + uassert.Equal(t, 0, len(prop.GetVotes())) }) } @@ -55,41 +121,375 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { t.Run("not govdao member", func(t *testing.T) { t.Parallel() + var ( + voter = testutils.TestAddress("voter") + + s = &SimpleDAO{ + membStore: NewMembStore(), + } + ) + + std.TestSetOrigCaller(voter) + + // Attempt to vote on the proposal + uassert.ErrorContains( + t, + s.VoteOnProposal(0, dao.YesVote), + errMissingMember.Error(), + ) }) t.Run("missing proposal", func(t *testing.T) { t.Parallel() + var ( + voter = testutils.TestAddress("voter") + + s = &SimpleDAO{ + membStore: NewMembStore( + WithInitialMembers([]dao.Member{ + {Address: voter}, + }), + ), + propStore: NewPropStore(), + } + ) + + std.TestSetOrigCaller(voter) + + // Attempt to vote on the proposal + uassert.ErrorContains( + t, + s.VoteOnProposal(0, dao.YesVote), + errMissingProposal.Error(), + ) }) t.Run("proposal executed", func(t *testing.T) { t.Parallel() + var ( + voter = testutils.TestAddress("voter") + + s = &SimpleDAO{ + membStore: NewMembStore( + WithInitialMembers([]dao.Member{ + {Address: voter}, + }), + ), + propStore: NewPropStore(), + } + + prop = &proposal{ + status: dao.ExecutionSuccessful, + } + ) + + std.TestSetOrigCaller(voter) + + // Add an initial proposal + id, err := s.propStore.addProposal(prop) + urequire.NoError(t, err) + + // Attempt to vote on the proposal + uassert.ErrorIs( + t, + s.VoteOnProposal(id, dao.YesVote), + errProposalInactive, + ) }) t.Run("proposal expired", func(t *testing.T) { t.Parallel() + var ( + voter = testutils.TestAddress("voter") + + s = &SimpleDAO{ + membStore: NewMembStore( + WithInitialMembers([]dao.Member{ + {Address: voter}, + }), + ), + propStore: NewPropStore(), + } + + mockExecutor = &mockExecutor{ + isExpiredFn: func() bool { + return true + }, + } + + prop = &proposal{ + status: dao.Active, + executor: mockExecutor, + } + ) + + std.TestSetOrigCaller(voter) + + // Add an initial proposal + id, err := s.propStore.addProposal(prop) + urequire.NoError(t, err) + + // Attempt to vote on the proposal + uassert.ErrorIs( + t, + s.VoteOnProposal(id, dao.YesVote), + errExecutorExpired, + ) }) t.Run("double vote on proposal", func(t *testing.T) { t.Parallel() + var ( + voter = testutils.TestAddress("voter") + member = dao.Member{ + Address: voter, + VotingPower: 10, + } + + s = &SimpleDAO{ + membStore: NewMembStore( + WithInitialMembers([]dao.Member{ + member, + }), + ), + propStore: NewPropStore(), + } + + prop = &proposal{ + status: dao.Active, + executor: &mockExecutor{}, + votes: newVotes(), + } + ) + + std.TestSetOrigCaller(voter) + + // Cast the initial vote + urequire.NoError(t, prop.votes.castVote(member, dao.YesVote)) + + // Add an initial proposal + id, err := s.propStore.addProposal(prop) + urequire.NoError(t, err) + + // Attempt to vote on the proposal + uassert.ErrorContains( + t, + s.VoteOnProposal(id, dao.YesVote), + errAlreadyVoted.Error(), + ) }) t.Run("majority accepted", func(t *testing.T) { t.Parallel() + var ( + members = generateMembers(t, 50) + + s = &SimpleDAO{ + membStore: NewMembStore(WithInitialMembers(members)), + propStore: NewPropStore(), + } + + prop = &proposal{ + status: dao.Active, + executor: &mockExecutor{}, + votes: newVotes(), + } + ) + + // Add an initial proposal + id, err := s.propStore.addProposal(prop) + urequire.NoError(t, err) + + majorityIndex := (len(members)*2)/3 + 1 // 2/3+ + for _, m := range members[:majorityIndex] { + std.TestSetOrigCaller(m.Address) + + // Attempt to vote on the proposal + urequire.NoError( + t, + s.VoteOnProposal(id, dao.YesVote), + ) + } + + // Make sure the proposal was accepted + uassert.Equal(t, dao.Accepted.String(), prop.status.String()) }) t.Run("majority rejected", func(t *testing.T) { t.Parallel() + var ( + members = generateMembers(t, 50) + + s = &SimpleDAO{ + membStore: NewMembStore(WithInitialMembers(members)), + propStore: NewPropStore(), + } + + prop = &proposal{ + status: dao.Active, + executor: &mockExecutor{}, + votes: newVotes(), + } + ) + + // Add an initial proposal + id, err := s.propStore.addProposal(prop) + urequire.NoError(t, err) + + majorityIndex := (len(members)*2)/3 + 1 // 2/3+ + for _, m := range members[:majorityIndex] { + std.TestSetOrigCaller(m.Address) + + // Attempt to vote on the proposal + urequire.NoError( + t, + s.VoteOnProposal(id, dao.NoVote), + ) + } + + // Make sure the proposal was not accepted + uassert.Equal(t, dao.NotAccepted.String(), prop.status.String()) }) - t.Run("majority undecided", func(t *testing.T) { + t.Run("majority abstained", func(t *testing.T) { t.Parallel() + var ( + members = generateMembers(t, 50) + + s = &SimpleDAO{ + membStore: NewMembStore(WithInitialMembers(members)), + propStore: NewPropStore(), + } + + prop = &proposal{ + status: dao.Active, + executor: &mockExecutor{}, + votes: newVotes(), + } + ) + + // Add an initial proposal + id, err := s.propStore.addProposal(prop) + urequire.NoError(t, err) + + majorityIndex := (len(members)*2)/3 + 1 // 2/3+ + for _, m := range members[:majorityIndex] { + std.TestSetOrigCaller(m.Address) + + // Attempt to vote on the proposal + urequire.NoError( + t, + s.VoteOnProposal(id, dao.AbstainVote), + ) + } + + // Make sure the proposal was not accepted + uassert.Equal(t, dao.NotAccepted.String(), prop.status.String()) + }) + + t.Run("everyone voted, undecided", func(t *testing.T) { + t.Parallel() + + var ( + members = generateMembers(t, 50) + + s = &SimpleDAO{ + membStore: NewMembStore(WithInitialMembers(members)), + propStore: NewPropStore(), + } + + prop = &proposal{ + status: dao.Active, + executor: &mockExecutor{}, + votes: newVotes(), + } + ) + + // Add an initial proposal + id, err := s.propStore.addProposal(prop) + urequire.NoError(t, err) + + // The first half votes yes + for _, m := range members[:len(members)/2] { + std.TestSetOrigCaller(m.Address) + + // Attempt to vote on the proposal + urequire.NoError( + t, + s.VoteOnProposal(id, dao.YesVote), + ) + } + + // The other half votes no + for _, m := range members[len(members)/2:] { + std.TestSetOrigCaller(m.Address) + + // Attempt to vote on the proposal + urequire.NoError( + t, + s.VoteOnProposal(id, dao.NoVote), + ) + } + + // Make sure the proposal is not active, + // since everyone voted, and it was undecided + uassert.Equal(t, dao.NotAccepted.String(), prop.status.String()) + }) + + t.Run("proposal undecided", func(t *testing.T) { + t.Parallel() + + var ( + members = generateMembers(t, 50) + + s = &SimpleDAO{ + membStore: NewMembStore(WithInitialMembers(members)), + propStore: NewPropStore(), + } + + prop = &proposal{ + status: dao.Active, + executor: &mockExecutor{}, + votes: newVotes(), + } + ) + + // Add an initial proposal + id, err := s.propStore.addProposal(prop) + urequire.NoError(t, err) + + // The first quarter votes yes + for _, m := range members[:len(members)/4] { + std.TestSetOrigCaller(m.Address) + + // Attempt to vote on the proposal + urequire.NoError( + t, + s.VoteOnProposal(id, dao.YesVote), + ) + } + + // The second quarter votes no + for _, m := range members[len(members)/4 : len(members)/2] { + std.TestSetOrigCaller(m.Address) + + // Attempt to vote on the proposal + urequire.NoError( + t, + s.VoteOnProposal(id, dao.NoVote), + ) + } + + // Make sure the proposal is still active, + // since there wasn't quorum reached on any decision + uassert.Equal(t, dao.Active.String(), prop.status.String()) }) } @@ -99,35 +499,273 @@ func TestSimpleDAO_ExecuteProposal(t *testing.T) { t.Run("caller cannot cover fee", func(t *testing.T) { t.Parallel() + var ( + sentCoins = std.NewCoins( + std.NewCoin( + "ugnot", + minExecuteFeeValue-1, + ), + ) + + s = &SimpleDAO{ + membStore: NewMembStore(), // empty member store + } + ) + + // Set the sent coins to be lower + // than the execute fee + std.TestSetOrigSend(sentCoins, std.Coins{}) + + uassert.ErrorIs( + t, + s.ExecuteProposal(0), + errInsufficientExecuteFunds, + ) }) t.Run("missing proposal", func(t *testing.T) { t.Parallel() + var ( + sentCoins = std.NewCoins( + std.NewCoin( + "ugnot", + minExecuteFeeValue, + ), + ) + + s = &SimpleDAO{ + membStore: NewMembStore(), // empty member store + propStore: NewPropStore(), // empty prop store + } + ) + + // Set the sent coins to be enough + // so the execution can take place + std.TestSetOrigSend(sentCoins, std.Coins{}) + + uassert.ErrorContains( + t, + s.ExecuteProposal(0), + errMissingProposal.Error(), + ) }) t.Run("proposal not accepted", func(t *testing.T) { t.Parallel() + var ( + voter = testutils.TestAddress("voter") + + s = &SimpleDAO{ + membStore: NewMembStore( + WithInitialMembers([]dao.Member{ + {Address: voter}, + }), + ), + propStore: NewPropStore(), + } + + prop = &proposal{ + status: dao.NotAccepted, + } + ) + + std.TestSetOrigCaller(voter) + + // Add an initial proposal + id, err := s.propStore.addProposal(prop) + urequire.NoError(t, err) + + // Attempt to vote on the proposal + uassert.ErrorIs( + t, + s.ExecuteProposal(id), + errProposalNotAccepted, + ) }) - t.Run("proposal executed", func(t *testing.T) { + t.Run("proposal already executed", func(t *testing.T) { t.Parallel() + testTable := []struct { + name string + status dao.ProposalStatus + }{ + { + "execution was successful", + dao.ExecutionSuccessful, + }, + { + "execution failed", + dao.ExecutionFailed, + }, + } + + for _, testCase := range testTable { + testCase := testCase + + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + var ( + voter = testutils.TestAddress("voter") + + s = &SimpleDAO{ + membStore: NewMembStore( + WithInitialMembers([]dao.Member{ + {Address: voter}, + }), + ), + propStore: NewPropStore(), + } + + prop = &proposal{ + status: testCase.status, + } + ) + + std.TestSetOrigCaller(voter) + + // Add an initial proposal + id, err := s.propStore.addProposal(prop) + urequire.NoError(t, err) + + // Attempt to vote on the proposal + uassert.ErrorIs( + t, + s.ExecuteProposal(id), + errProposalExecuted, + ) + }) + } }) t.Run("proposal expired", func(t *testing.T) { t.Parallel() + var ( + voter = testutils.TestAddress("voter") + + s = &SimpleDAO{ + membStore: NewMembStore( + WithInitialMembers([]dao.Member{ + {Address: voter}, + }), + ), + propStore: NewPropStore(), + } + + mockExecutor = &mockExecutor{ + isExpiredFn: func() bool { + return true + }, + } + + prop = &proposal{ + status: dao.Accepted, + executor: mockExecutor, + } + ) + + std.TestSetOrigCaller(voter) + + // Add an initial proposal + id, err := s.propStore.addProposal(prop) + urequire.NoError(t, err) + + // Attempt to vote on the proposal + uassert.ErrorIs( + t, + s.ExecuteProposal(id), + errExecutorExpired, + ) }) t.Run("execution error", func(t *testing.T) { t.Parallel() + var ( + voter = testutils.TestAddress("voter") + + s = &SimpleDAO{ + membStore: NewMembStore( + WithInitialMembers([]dao.Member{ + {Address: voter}, + }), + ), + propStore: NewPropStore(), + } + + execError = errors.New("exec error") + + mockExecutor = &mockExecutor{ + executeFn: func() error { + return execError + }, + } + + prop = &proposal{ + status: dao.Accepted, + executor: mockExecutor, + } + ) + + std.TestSetOrigCaller(voter) + + // Add an initial proposal + id, err := s.propStore.addProposal(prop) + urequire.NoError(t, err) + + // Attempt to vote on the proposal + uassert.ErrorContains( + t, + s.ExecuteProposal(id), + execError.Error(), + ) + + uassert.Equal(t, dao.ExecutionFailed.String(), prop.status.String()) }) t.Run("successful execution", func(t *testing.T) { t.Parallel() + var ( + voter = testutils.TestAddress("voter") + + s = &SimpleDAO{ + membStore: NewMembStore( + WithInitialMembers([]dao.Member{ + {Address: voter}, + }), + ), + propStore: NewPropStore(), + } + + called = false + mockExecutor = &mockExecutor{ + executeFn: func() error { + called = true + + return nil + }, + } + + prop = &proposal{ + status: dao.Accepted, + executor: mockExecutor, + } + ) + + std.TestSetOrigCaller(voter) + + // Add an initial proposal + id, err := s.propStore.addProposal(prop) + urequire.NoError(t, err) + + // Attempt to vote on the proposal + uassert.NoError(t, s.ExecuteProposal(id)) + uassert.Equal(t, dao.ExecutionSuccessful.String(), prop.status.String()) + uassert.True(t, called) }) } diff --git a/examples/gno.land/p/demo/simpledao/membstore.gno b/examples/gno.land/p/demo/simpledao/membstore.gno index 2b97a229564..202eb732a2f 100644 --- a/examples/gno.land/p/demo/simpledao/membstore.gno +++ b/examples/gno.land/p/demo/simpledao/membstore.gno @@ -23,7 +23,11 @@ func WithInitialMembers(members []dao.Member) MembStoreOption { return func(store *MembStore) { for _, m := range members { store.members.Set(m.Address.String(), m) + + store.totalVotingPower += m.VotingPower } + + store.majorityPower = (2 * store.totalVotingPower) / 3 } } @@ -160,3 +164,18 @@ func (m *MembStore) Size() int { func (m *MembStore) getMajorityPower() uint64 { return m.majorityPower } + +// getTotalPower returns the total voting power +// of the member store +func (m *MembStore) getTotalPower() uint64 { + return m.totalVotingPower +} + +// 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 { + return std.CurrentRealm().PkgPath() == daoPkgPath +} diff --git a/examples/gno.land/p/demo/simpledao/mock_test.gno b/examples/gno.land/p/demo/simpledao/mock_test.gno new file mode 100644 index 00000000000..2a48bb9134b --- /dev/null +++ b/examples/gno.land/p/demo/simpledao/mock_test.gno @@ -0,0 +1,27 @@ +package simpledao + +type ( + executeDelegate func() error + isExpiredDelegate func() bool +) + +type mockExecutor struct { + executeFn executeDelegate + isExpiredFn isExpiredDelegate +} + +func (m *mockExecutor) Execute() error { + if m.executeFn != nil { + return m.executeFn() + } + + return nil +} + +func (m *mockExecutor) IsExpired() bool { + if m.isExpiredFn != nil { + return m.isExpiredFn() + } + + return false +} diff --git a/examples/gno.land/p/demo/simpledao/propstore.gno b/examples/gno.land/p/demo/simpledao/propstore.gno index 15a2db132d1..93590d2f92c 100644 --- a/examples/gno.land/p/demo/simpledao/propstore.gno +++ b/examples/gno.land/p/demo/simpledao/propstore.gno @@ -7,7 +7,6 @@ import ( "gno.land/p/demo/avl" "gno.land/p/demo/seqid" "gno.land/p/gov/dao" - "gno.land/p/gov/executor" ) var ( @@ -30,7 +29,7 @@ type proposal struct { author std.Address // initiator of the proposal description string // description of the proposal - executor executor.Executor // executor for the proposal + executor dao.Executor // executor for the proposal status dao.ProposalStatus // status of the proposal votes *votes // voting mechanism diff --git a/examples/gno.land/p/demo/simpledao/vote.gno b/examples/gno.land/p/demo/simpledao/vote.gno index 7d06aab3876..bfa95ffbd67 100644 --- a/examples/gno.land/p/demo/simpledao/vote.gno +++ b/examples/gno.land/p/demo/simpledao/vote.gno @@ -11,9 +11,11 @@ var errAlreadyVoted = errors.New("vote already cast") // votes is a simple weighted voting system type votes struct { - // tally cache to keep track of active for / against votes - yays uint64 - nays uint64 + // tally cache to keep track of active + // yes / no / abstain votes + yays uint64 + nays uint64 + abstains uint64 voters *avl.Tree // std.Address -> dao.VoteOption } @@ -36,9 +38,12 @@ func (v *votes) castVote(member dao.Member, option dao.VoteOption) error { } // Update the tally - if option == dao.YesVote { + switch option { + case dao.YesVote: v.yays += member.VotingPower - } else { + case dao.AbstainVote: + v.abstains += member.VotingPower + default: v.nays += member.VotingPower } @@ -49,8 +54,8 @@ func (v *votes) castVote(member dao.Member, option dao.VoteOption) error { } // getTally returns the yay, and nay count, respectively -func (v *votes) getTally() (uint64, uint64) { - return v.yays, v.nays +func (v *votes) getTally() (uint64, uint64, uint64) { + return v.yays, v.nays, v.abstains } // getVoters returns the current voter set diff --git a/examples/gno.land/p/gov/dao/dao.gno b/examples/gno.land/p/gov/dao/dao.gno index 0730393e023..2452f01e2cd 100644 --- a/examples/gno.land/p/gov/dao/dao.gno +++ b/examples/gno.land/p/gov/dao/dao.gno @@ -1,12 +1,10 @@ package dao -import "gno.land/p/gov/executor" - // DAO defines the DAO abstraction type DAO interface { // Propose adds a new proposal to the executor-based GOVDAO. // Returns the generated proposal ID - Propose(comment string, executor executor.Executor) (uint64, error) + Propose(comment string, executor Executor) (uint64, error) // VoteOnProposal adds a vote to the given proposal ID VoteOnProposal(id uint64, option VoteOption) error diff --git a/examples/gno.land/p/gov/dao/executor.gno b/examples/gno.land/p/gov/dao/executor.gno index 9291c2c53c5..410d3e5b288 100644 --- a/examples/gno.land/p/gov/dao/executor.gno +++ b/examples/gno.land/p/gov/dao/executor.gno @@ -6,4 +6,8 @@ type Executor interface { // Execute executes the given proposal, and returns any error encountered // during the execution Execute() error + + // IsExpired returns a flag indicating + // if the executor is expired + IsExpired() bool } diff --git a/examples/gno.land/p/gov/dao/gno.mod b/examples/gno.land/p/gov/dao/gno.mod index da442b205fd..66a285c4465 100644 --- a/examples/gno.land/p/gov/dao/gno.mod +++ b/examples/gno.land/p/gov/dao/gno.mod @@ -1,3 +1 @@ module gno.land/p/gov/dao - -require gno.land/p/gov/executor v0.0.0-latest diff --git a/examples/gno.land/p/gov/dao/vote.gno b/examples/gno.land/p/gov/dao/vote.gno index 4222b981ed7..4990e72e2b1 100644 --- a/examples/gno.land/p/gov/dao/vote.gno +++ b/examples/gno.land/p/gov/dao/vote.gno @@ -7,9 +7,9 @@ import ( type VoteOption string const ( - YesVote VoteOption = "YES" - NoVote VoteOption = "NO" - Abstain VoteOption = "ABSTAIN" + YesVote VoteOption = "YES" + NoVote VoteOption = "NO" + AbstainVote VoteOption = "ABSTAIN" ) func (v VoteOption) String() string { diff --git a/examples/gno.land/p/gov/executor/callback.gno b/examples/gno.land/p/gov/executor/callback.gno index cddd0f3fc38..b885571c0e1 100644 --- a/examples/gno.land/p/gov/executor/callback.gno +++ b/examples/gno.land/p/gov/executor/callback.gno @@ -35,6 +35,12 @@ func (exec *CallbackExecutor) Execute() error { return nil } +// IsExpired returns a flag indicating if the executor is expired. +// The CallbackExecutor never expires +func (exec *CallbackExecutor) IsExpired() bool { + return false +} + // assertCalledByGovdao asserts that the calling Realm is /r/gov/dao func assertCalledByGovdao() { caller := std.CurrentRealm().PkgPath() diff --git a/examples/gno.land/p/gov/executor/context.gno b/examples/gno.land/p/gov/executor/context.gno index b7460a0f4f9..73548aea778 100644 --- a/examples/gno.land/p/gov/executor/context.gno +++ b/examples/gno.land/p/gov/executor/context.gno @@ -46,6 +46,12 @@ func (exec *CtxExecutor) Execute() error { return exec.callbackCtx(ctx) } +// IsExpired returns a flag indicating if the executor is expired. +// The CtxExecutor never expires +func (exec *CtxExecutor) IsExpired() bool { + return false +} + // IsApprovedByGovdaoContext asserts that the govdao approved the context func IsApprovedByGovdaoContext(ctx context.Context) bool { v := ctx.Value(statusContextKey) diff --git a/examples/gno.land/r/gov/dao/dao.gno b/examples/gno.land/r/gov/dao/dao.gno index 24c65a2a471..86ef85a0c2c 100644 --- a/examples/gno.land/r/gov/dao/dao.gno +++ b/examples/gno.land/r/gov/dao/dao.gno @@ -6,7 +6,6 @@ import ( "gno.land/p/demo/simpledao" "gno.land/p/demo/ufmt" "gno.land/p/gov/dao" - "gno.land/p/gov/executor" ) var ( @@ -31,7 +30,7 @@ func init() { // Propose is designed to be called by another contract or with // `maketx run`, not by a `maketx call`. -func Propose(comment string, executor executor.Executor) (uint64, error) { +func Propose(comment string, executor dao.Executor) (uint64, error) { return d.Propose(comment, executor) } diff --git a/examples/gno.land/r/gov/dao/poc.gno b/examples/gno.land/r/gov/dao/poc.gno index f68ef7789f6..101c2657c1a 100644 --- a/examples/gno.land/r/gov/dao/poc.gno +++ b/examples/gno.land/r/gov/dao/poc.gno @@ -15,7 +15,7 @@ const ( ) // NewMemberPropExecutor returns the GOVDAO member change executor -func NewMemberPropExecutor(changesFn func() []dao.Member) executor.Executor { +func NewMemberPropExecutor(changesFn func() []dao.Member) dao.Executor { if changesFn == nil { panic(errNoChangesProposed) } @@ -59,7 +59,7 @@ func NewMemberPropExecutor(changesFn func() []dao.Member) executor.Executor { return executor.NewCallbackExecutor(callback) } -func NewDAOImplExecutor(changeFn func() dao.DAO) executor.Executor { +func NewDAOImplExecutor(changeFn func() dao.DAO) dao.Executor { if changeFn == nil { panic(errNoChangesProposed) } @@ -75,7 +75,7 @@ func NewDAOImplExecutor(changeFn func() dao.DAO) executor.Executor { return executor.NewCallbackExecutor(callback) } -func NewMembStoreImplExecutor(changeFn func() dao.MembStore) executor.Executor { +func NewMembStoreImplExecutor(changeFn func() dao.MembStore) dao.Executor { if changeFn == nil { panic(errNoChangesProposed) } @@ -91,7 +91,7 @@ func NewMembStoreImplExecutor(changeFn func() dao.MembStore) executor.Executor { return executor.NewCallbackExecutor(callback) } -func NewPropStoreImplExecutor(changeFn func() dao.PropStore) executor.Executor { +func NewPropStoreImplExecutor(changeFn func() dao.PropStore) dao.Executor { if changeFn == nil { panic(errNoChangesProposed) } diff --git a/examples/gno.land/r/sys/validators/poc.gno b/examples/gno.land/r/sys/validators/poc.gno index d39a83a2786..548ddd5367a 100644 --- a/examples/gno.land/r/sys/validators/poc.gno +++ b/examples/gno.land/r/sys/validators/poc.gno @@ -20,7 +20,7 @@ const ( // // Concept adapted from: // https://github.com/gnolang/gno/pull/1945 -func NewPropExecutor(changesFn func() []validators.Validator) executor.Executor { +func NewPropExecutor(changesFn func() []validators.Validator) dao.Executor { if changesFn == nil { panic(errNoChangesProposed) } From b3856c74da6886ec074c28c244f5d6e75520409d Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Mon, 15 Jul 2024 11:33:10 +0200 Subject: [PATCH 24/80] Remove unsafe ctx executor usage --- examples/gno.land/r/gnoland/blog/admin.gno | 27 ++++++++++++++++++---- examples/gno.land/r/gnoland/blog/gno.mod | 2 +- examples/gno.land/r/sys/validators/poc.gno | 3 ++- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/examples/gno.land/r/gnoland/blog/admin.gno b/examples/gno.land/r/gnoland/blog/admin.gno index 526331eee02..38f0b1227c8 100644 --- a/examples/gno.land/r/gnoland/blog/admin.gno +++ b/examples/gno.land/r/gnoland/blog/admin.gno @@ -5,10 +5,13 @@ import ( "strings" "gno.land/p/demo/avl" - "gno.land/p/demo/context" + "gno.land/p/gov/dao" "gno.land/p/gov/executor" ) +const daoPkgPath = "gno.land/r/gov/dao" // TODO change to r/sys/vars +const errNotGovDAO = "caller not govdao executor" + var ( adminAddr std.Address moderatorList avl.Tree @@ -41,10 +44,24 @@ func AdminRemoveModerator(addr std.Address) { moderatorList.Set(addr.String(), false) // FIXME: delete instead? } -func DaoAddPost(ctx context.Context, slug, title, body, publicationDate, authors, tags string) { - executor.AssertContextApprovedByGovDAO(ctx) - caller := std.DerivePkgAddr("gno.land/r/gov/dao") - addPost(caller, slug, title, body, publicationDate, authors, tags) +func NewPostExecutor(slug, title, body, publicationDate, authors, tags string) dao.Executor { + callback := func() error { + assertGovDAOCaller() + caller := std.DerivePkgAddr("gno.land/r/gov/dao") // TODO use r/sys/vars + + addPost(caller, slug, title, body, publicationDate, authors, tags) + + return nil + } + + return executor.NewCallbackExecutor(callback) +} + +// assertGovDAOCaller verifies the caller is the GovDAO executor +func assertGovDAOCaller() { + if std.PrevRealm().PkgPath() != daoPkgPath { + panic(errNotGovDAO) + } } func ModAddPost(slug, title, body, publicationDate, authors, tags string) { diff --git a/examples/gno.land/r/gnoland/blog/gno.mod b/examples/gno.land/r/gnoland/blog/gno.mod index 3d03b4f02fe..c1fda8f7d6f 100644 --- a/examples/gno.land/r/gnoland/blog/gno.mod +++ b/examples/gno.land/r/gnoland/blog/gno.mod @@ -3,6 +3,6 @@ module gno.land/r/gnoland/blog require ( gno.land/p/demo/avl v0.0.0-latest gno.land/p/demo/blog v0.0.0-latest - gno.land/p/demo/context v0.0.0-latest + gno.land/p/gov/dao v0.0.0-latest gno.land/p/gov/executor v0.0.0-latest ) diff --git a/examples/gno.land/r/sys/validators/poc.gno b/examples/gno.land/r/sys/validators/poc.gno index 548ddd5367a..62b84d1232b 100644 --- a/examples/gno.land/r/sys/validators/poc.gno +++ b/examples/gno.land/r/sys/validators/poc.gno @@ -3,11 +3,12 @@ package validators import ( "std" + "github.com/gnolang/gno/examples/gno.land/p/gov/dao" "gno.land/p/gov/executor" "gno.land/p/sys/validators" ) -const daoPkgPath = "gno.land/r/gov/dao" +const daoPkgPath = "gno.land/r/gov/dao" // TODO change to r/sys/vars const ( errNoChangesProposed = "no set changes proposed" From 4d3d94e3b869a50791705c41203e5b103acaae22 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Mon, 15 Jul 2024 13:48:57 +0200 Subject: [PATCH 25/80] Fixup prop filetests --- .../gno.land/p/demo/simpledao/propstore.gno | 5 + examples/gno.land/p/gov/dao/proposals.gno | 3 + examples/gno.land/p/gov/dao/vote.gno | 30 ++ examples/gno.land/r/gov/dao/dao.gno | 112 ++++-- examples/gno.land/r/gov/dao/dao_test.gno | 363 +++++++++--------- examples/gno.land/r/gov/dao/poc.gno | 4 +- .../gno.land/r/gov/dao/prop1_filetest.gno | 55 ++- .../gno.land/r/gov/dao/prop2_filetest.gno | 79 ++-- examples/gno.land/r/sys/validators/gno.mod | 1 + examples/gno.land/r/sys/validators/poc.gno | 4 +- 10 files changed, 361 insertions(+), 295 deletions(-) diff --git a/examples/gno.land/p/demo/simpledao/propstore.gno b/examples/gno.land/p/demo/simpledao/propstore.gno index 93590d2f92c..85caaf2cdf2 100644 --- a/examples/gno.land/p/demo/simpledao/propstore.gno +++ b/examples/gno.land/p/demo/simpledao/propstore.gno @@ -69,6 +69,11 @@ func (p *proposal) GetVotes() []dao.Vote { return votes } +func (p *proposal) GetVotingStats() dao.VotingStats { + // TODO implement + return dao.VotingStats{} +} + func (p *proposal) GetVoteByMember(address std.Address) (dao.Vote, error) { voters := p.votes.getVoters() diff --git a/examples/gno.land/p/gov/dao/proposals.gno b/examples/gno.land/p/gov/dao/proposals.gno index 69ebcee3491..cf447075640 100644 --- a/examples/gno.land/p/gov/dao/proposals.gno +++ b/examples/gno.land/p/gov/dao/proposals.gno @@ -46,6 +46,9 @@ type Proposal interface { // GetVotes returns the votes of the proposal GetVotes() []Vote + // GetVotingStats returns the voting stats of the proposal + GetVotingStats() VotingStats + // GetVoteByMember returns the proposal vote by the member, if any GetVoteByMember(address std.Address) (Vote, error) } diff --git a/examples/gno.land/p/gov/dao/vote.gno b/examples/gno.land/p/gov/dao/vote.gno index 4990e72e2b1..d031ffaff63 100644 --- a/examples/gno.land/p/gov/dao/vote.gno +++ b/examples/gno.land/p/gov/dao/vote.gno @@ -21,3 +21,33 @@ type Vote struct { Address std.Address // the address of the voter Option VoteOption // the voting option } + +// VotingStats encompasses the proposal voting stats +type VotingStats struct { + YayVotes uint64 + NayVotes uint64 + AbstainVotes uint64 + MissingVotes uint64 + + ThresholdMet bool +} + +func (v VotingStats) GetYayPercent() uint64 { + // TODO implement + return 0 +} + +func (v VotingStats) GetNayPercent() uint64 { + // TODO implement + return 0 +} + +func (v VotingStats) GetAbstainPercent() uint64 { + // TODO implement + return 0 +} + +func (v VotingStats) GetMissingPercent() uint64 { + // TODO implement + return 0 +} diff --git a/examples/gno.land/r/gov/dao/dao.gno b/examples/gno.land/r/gov/dao/dao.gno index 86ef85a0c2c..0c1417333cf 100644 --- a/examples/gno.land/r/gov/dao/dao.gno +++ b/examples/gno.land/r/gov/dao/dao.gno @@ -1,7 +1,9 @@ package govdao import ( + "std" "strconv" + "strings" "gno.land/p/demo/simpledao" "gno.land/p/demo/ufmt" @@ -9,15 +11,23 @@ import ( ) var ( - d dao.DAO // the current active DAO implementation - proposals dao.PropStore // the proposal store - members dao.MembStore // the member store + d dao.DAO // the current active DAO implementation + proposals dao.PropStore // the proposal store + members dao.MemberStore // the member store ) func init() { + // Example initial member set (just test addresses) + set := []dao.Member{ + { + Address: std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"), + VotingPower: 10, + }, + } + var ( propStore = simpledao.NewPropStore() - membStore = simpledao.NewMembStore() + membStore = simpledao.NewMembStore(simpledao.WithInitialMembers(set)) ) // Set the stores @@ -25,72 +35,118 @@ func init() { members = membStore // Set the DAO implementation - d = simpledao.NewSimpleDAO(membStore, propStore) + d = simpledao.New(membStore, propStore) } // Propose is designed to be called by another contract or with // `maketx run`, not by a `maketx call`. -func Propose(comment string, executor dao.Executor) (uint64, error) { - return d.Propose(comment, executor) +func Propose(comment string, executor dao.Executor) uint64 { + idx, err := d.Propose(comment, executor) + if err != nil { + panic(err) + } + + return idx } -func VoteOnProposal(id uint64, option VoteOption) error { - return d.VoteOnProposal(id, option) +// VoteOnProposal casts a vote for the given proposal +func VoteOnProposal(id uint64, option dao.VoteOption) { + if err := d.VoteOnProposal(id, option); err != nil { + panic(err) + } } -func ExecuteProposal(id uint64) error { - return d.ExecuteProposal(id) +// ExecuteProposal executes the proposal +func ExecuteProposal(id uint64) { + if err := d.ExecuteProposal(id); err != nil { + panic(err) + } } +// GetPropStore returns the active proposal store func GetPropStore() dao.PropStore { return proposals } -func GetMembStore() dao.MembStore { +// GetMembStore returns the active member store +func GetMembStore() dao.MemberStore { return members } func Render(path string) string { if path == "" { - if len(proposals) == 0 { + numProposals := proposals.Size() + + if numProposals == 0 { return "No proposals found :(" // corner case } output := "" - for idx, prop := range proposals { - output += ufmt.Sprintf("- [%d](/r/gov/dao:%d) - %s (**%s**)(by %s)\n", idx, idx, prop.comment, string(prop.Status()), prop.author) + + offset := uint64(0) + if numProposals >= 10 { + offset = uint64(numProposals) - 10 + } + + // Fetch the last 10 proposals + for idx, prop := range proposals.GetProposals(offset, uint64(10)) { + output += ufmt.Sprintf( + "- [Proposal #%d](%s:%d) - (**%s**)(by %s)\n", + idx, + strings.TrimPrefix(getGOVDAOPath(), "gno.land"), + idx, + prop.GetStatus().String(), + prop.GetAuthor().String(), + ) } return output } - // else display the proposal + // Display the detailed proposal idx, err := strconv.Atoi(path) if err != nil { - return "404" + return "404: Invalid proposal ID" } - if !proposalExists(idx) { - return "404" + // Fetch the proposal + prop, err := proposals.GetProposalByID(uint64(idx)) + if err != nil { + return ufmt.Sprintf("unable to fetch proposal, %s", err.Error()) } - prop := getProposal(idx) - vs := members - if prop.executed { - vs = prop.voters - } + // Fetch the voting stats + stats := prop.GetVotingStats() output := "" output += ufmt.Sprintf("# Prop #%d", idx) output += "\n\n" - output += prop.comment + output += ufmt.Sprintf("Author: %s", prop.GetAuthor().String()) output += "\n\n" - output += ufmt.Sprintf("Status: %s", string(prop.Status())) + output += prop.GetDescription() output += "\n\n" - output += ufmt.Sprintf("Voting status: %s", prop.voter.Status(vs)) + output += ufmt.Sprintf("Status: %s", prop.GetStatus().String()) output += "\n\n" - output += ufmt.Sprintf("Author: %s", string(prop.author)) + output += ufmt.Sprintf( + "Voting stats: YAY %d (%d%%), NAY %d (%d%%), ABSTAIN %d (%d%%), HAVEN'T VOTED %d (%d%%)", + stats.YayVotes, + stats.GetYayPercent(), + stats.NayVotes, + stats.GetNayPercent(), + stats.AbstainVotes, + stats.GetAbstainPercent(), + stats.MissingVotes, + stats.GetMissingPercent(), + ) + output += "\n\n" + output += ufmt.Sprintf("Threshold met: %t", stats.ThresholdMet) output += "\n\n" return output } + +// getGOVDAOPath returns the active govdao Realm path +func getGOVDAOPath() string { + // TODO use r/sys/vars + return "gno.land/r/gov/dao" +} diff --git a/examples/gno.land/r/gov/dao/dao_test.gno b/examples/gno.land/r/gov/dao/dao_test.gno index fa2b3fc6aa4..6c50b827c11 100644 --- a/examples/gno.land/r/gov/dao/dao_test.gno +++ b/examples/gno.land/r/gov/dao/dao_test.gno @@ -1,192 +1,189 @@ package govdao import ( - "std" "testing" - - "gno.land/p/demo/testutils" - "gno.land/p/demo/urequire" - "gno.land/p/gov/executor" ) func TestPackage(t *testing.T) { - u1 := testutils.TestAddress("u1") - u2 := testutils.TestAddress("u2") - u3 := testutils.TestAddress("u3") - - members = append(members, u1) - members = append(members, u2) - members = append(members, u3) - - nu1 := testutils.TestAddress("random1") - - out := Render("") - - expected := "No proposals found :(" - urequire.Equal(t, expected, out) - - var called bool - ex := executor.NewCallbackExecutor(func() error { - called = true - return nil - }) - - std.TestSetOrigCaller(u1) - pid := Propose("dummy proposal", ex) - - // try to vote not being a member - std.TestSetOrigCaller(nu1) - - urequire.PanicsWithMessage(t, msgCallerNotAMember, func() { - VoteOnProposal(pid, "YES") - }) - - // try to vote several times - std.TestSetOrigCaller(u1) - urequire.NotPanics(t, func() { - VoteOnProposal(pid, "YES") - }) - urequire.PanicsWithMessage(t, msgAlreadyVoted, func() { - VoteOnProposal(pid, "YES") - }) - - out = Render("0") - expected = `# Prop #0 - -dummy proposal - -Status: active - -Voting status: YES: 1, NO: 0, percent: 33, members: 3 - -Author: g1w5c47h6lta047h6lta047h6lta047h6ly5kscr - -` - - urequire.Equal(t, expected, out) - - std.TestSetOrigCaller(u2) - urequire.PanicsWithMessage(t, msgWrongVotingValue, func() { - VoteOnProposal(pid, "INCORRECT") - }) - urequire.NotPanics(t, func() { - VoteOnProposal(pid, "NO") - }) - - out = Render("0") - expected = `# Prop #0 - -dummy proposal - -Status: active - -Voting status: YES: 1, NO: 1, percent: 33, members: 3 - -Author: g1w5c47h6lta047h6lta047h6lta047h6ly5kscr - -` - - urequire.Equal(t, expected, out) - - std.TestSetOrigCaller(u3) - urequire.NotPanics(t, func() { - VoteOnProposal(pid, "YES") - }) - - out = Render("0") - expected = `# Prop #0 - -dummy proposal - -Status: accepted - -Voting status: YES: 2, NO: 1, percent: 66, members: 3 - -Author: g1w5c47h6lta047h6lta047h6lta047h6ly5kscr - -` - - urequire.Equal(t, expected, out) - - // Add a new member, so non-executed proposals will change the voting status - u4 := testutils.TestAddress("u4") - members = append(members, u4) - - out = Render("0") - expected = `# Prop #0 - -dummy proposal - -Status: active - -Voting status: YES: 2, NO: 1, percent: 50, members: 4 - -Author: g1w5c47h6lta047h6lta047h6lta047h6ly5kscr - -` - - urequire.Equal(t, expected, out) - - std.TestSetOrigCaller(u4) - urequire.NotPanics(t, func() { - VoteOnProposal(pid, "YES") - }) - - out = Render("0") - expected = `# Prop #0 - -dummy proposal - -Status: accepted - -Voting status: YES: 3, NO: 1, percent: 75, members: 4 - -Author: g1w5c47h6lta047h6lta047h6lta047h6ly5kscr - -` - - urequire.Equal(t, expected, out) - - ExecuteProposal(pid) - urequire.True(t, called) - - out = Render("0") - expected = `# Prop #0 - -dummy proposal - -Status: succeeded - -Voting status: YES: 3, NO: 1, percent: 75, members: 4 - -Author: g1w5c47h6lta047h6lta047h6lta047h6ly5kscr - -` - - urequire.Equal(t, expected, out) - - // Add a new member and try to vote an already executed proposal - u5 := testutils.TestAddress("u5") - members = append(members, u5) - std.TestSetOrigCaller(u5) - urequire.PanicsWithMessage(t, msgPropExecuted, func() { - ExecuteProposal(pid) - }) - - // even if we added a new member the executed proposal is showing correctly the members that voted on it - out = Render("0") - expected = `# Prop #0 - -dummy proposal - -Status: succeeded - -Voting status: YES: 3, NO: 1, percent: 75, members: 4 - -Author: g1w5c47h6lta047h6lta047h6lta047h6ly5kscr - -` - - urequire.Equal(t, expected, out) + t.SkipNow() // TODO fix + + // u1 := testutils.TestAddress("u1") + // u2 := testutils.TestAddress("u2") + // u3 := testutils.TestAddress("u3") + // + // members = append(members, u1) + // members = append(members, u2) + // members = append(members, u3) + // + // nu1 := testutils.TestAddress("random1") + // + // out := Render("") + // + // expected := "No proposals found :(" + // urequire.Equal(t, expected, out) + // + // var called bool + // ex := executor.NewCallbackExecutor(func() error { + // called = true + // return nil + // }) + // + // std.TestSetOrigCaller(u1) + // pid := Propose("dummy proposal", ex) + // + // // try to vote not being a member + // std.TestSetOrigCaller(nu1) + // + // urequire.PanicsWithMessage(t, msgCallerNotAMember, func() { + // VoteOnProposal(pid, "YES") + // }) + // + // // try to vote several times + // std.TestSetOrigCaller(u1) + // urequire.NotPanics(t, func() { + // VoteOnProposal(pid, "YES") + // }) + // urequire.PanicsWithMessage(t, msgAlreadyVoted, func() { + // VoteOnProposal(pid, "YES") + // }) + // + // out = Render("0") + // expected = `# Prop #0 + // + // dummy proposal + // + // Status: active + // + // Voting status: YES: 1, NO: 0, percent: 33, members: 3 + // + // Author: g1w5c47h6lta047h6lta047h6lta047h6ly5kscr + // + // ` + // + // urequire.Equal(t, expected, out) + // + // std.TestSetOrigCaller(u2) + // urequire.PanicsWithMessage(t, msgWrongVotingValue, func() { + // VoteOnProposal(pid, "INCORRECT") + // }) + // urequire.NotPanics(t, func() { + // VoteOnProposal(pid, "NO") + // }) + // + // out = Render("0") + // expected = `# Prop #0 + // + // dummy proposal + // + // Status: active + // + // Voting status: YES: 1, NO: 1, percent: 33, members: 3 + // + // Author: g1w5c47h6lta047h6lta047h6lta047h6ly5kscr + // + // ` + // + // urequire.Equal(t, expected, out) + // + // std.TestSetOrigCaller(u3) + // urequire.NotPanics(t, func() { + // VoteOnProposal(pid, "YES") + // }) + // + // out = Render("0") + // expected = `# Prop #0 + // + // dummy proposal + // + // Status: accepted + // + // Voting status: YES: 2, NO: 1, percent: 66, members: 3 + // + // Author: g1w5c47h6lta047h6lta047h6lta047h6ly5kscr + // + // ` + // + // urequire.Equal(t, expected, out) + // + // // Add a new member, so non-executed proposals will change the voting status + // u4 := testutils.TestAddress("u4") + // members = append(members, u4) + // + // out = Render("0") + // expected = `# Prop #0 + // + // dummy proposal + // + // Status: active + // + // Voting status: YES: 2, NO: 1, percent: 50, members: 4 + // + // Author: g1w5c47h6lta047h6lta047h6lta047h6ly5kscr + // + // ` + // + // urequire.Equal(t, expected, out) + // + // std.TestSetOrigCaller(u4) + // urequire.NotPanics(t, func() { + // VoteOnProposal(pid, "YES") + // }) + // + // out = Render("0") + // expected = `# Prop #0 + // + // dummy proposal + // + // Status: accepted + // + // Voting status: YES: 3, NO: 1, percent: 75, members: 4 + // + // Author: g1w5c47h6lta047h6lta047h6lta047h6ly5kscr + // + // ` + // + // urequire.Equal(t, expected, out) + // + // ExecuteProposal(pid) + // urequire.True(t, called) + // + // out = Render("0") + // expected = `# Prop #0 + // + // dummy proposal + // + // Status: succeeded + // + // Voting status: YES: 3, NO: 1, percent: 75, members: 4 + // + // Author: g1w5c47h6lta047h6lta047h6lta047h6ly5kscr + // + // ` + // + // urequire.Equal(t, expected, out) + // + // // Add a new member and try to vote an already executed proposal + // u5 := testutils.TestAddress("u5") + // members = append(members, u5) + // std.TestSetOrigCaller(u5) + // urequire.PanicsWithMessage(t, msgPropExecuted, func() { + // ExecuteProposal(pid) + // }) + // + // // even if we added a new member the executed proposal is showing correctly the members that voted on it + // out = Render("0") + // expected = `# Prop #0 + // + // dummy proposal + // + // Status: succeeded + // + // Voting status: YES: 3, NO: 1, percent: 75, members: 4 + // + // Author: g1w5c47h6lta047h6lta047h6lta047h6ly5kscr + // + // ` + // + // urequire.Equal(t, expected, out) } diff --git a/examples/gno.land/r/gov/dao/poc.gno b/examples/gno.land/r/gov/dao/poc.gno index 101c2657c1a..c08a2fbef0d 100644 --- a/examples/gno.land/r/gov/dao/poc.gno +++ b/examples/gno.land/r/gov/dao/poc.gno @@ -75,7 +75,7 @@ func NewDAOImplExecutor(changeFn func() dao.DAO) dao.Executor { return executor.NewCallbackExecutor(callback) } -func NewMembStoreImplExecutor(changeFn func() dao.MembStore) dao.Executor { +func NewMembStoreImplExecutor(changeFn func() dao.MemberStore) dao.Executor { if changeFn == nil { panic(errNoChangesProposed) } @@ -113,7 +113,7 @@ func setDAOImpl(impl dao.DAO) { } // setMembStoreImpl sets a new dao.MembStore implementation -func setMembStoreImpl(impl dao.MembStore) { +func setMembStoreImpl(impl dao.MemberStore) { members = impl } diff --git a/examples/gno.land/r/gov/dao/prop1_filetest.gno b/examples/gno.land/r/gov/dao/prop1_filetest.gno index cd4bd7d7063..f756d7b2f5e 100644 --- a/examples/gno.land/r/gov/dao/prop1_filetest.gno +++ b/examples/gno.land/r/gov/dao/prop1_filetest.gno @@ -17,19 +17,6 @@ import ( ) func init() { - membersFn := func() []dao.Member { - return []dao.Member{ - Address: std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"), - VotingPower: 10, - } - } - - mExec := govdao.NewPropExecutor(membersFn) - - comment := "adding someone to vote" - id := govdao.Propose(comment, mExec) - govdao.ExecuteProposal(id) - changesFn := func() []pVals.Validator { return []pVals.Validator{ { @@ -55,8 +42,7 @@ func init() { executor := validators.NewPropExecutor(changesFn) // Create a proposal. - // XXX: payment - comment = "manual valset changes proposal example" + comment := "manual valset changes proposal example" govdao.Propose(comment, executor) } @@ -64,64 +50,69 @@ func main() { println("--") println(govdao.Render("")) println("--") - println(govdao.Render("1")) + println(govdao.Render("0")) println("--") - govdao.VoteOnProposal(1, "YES") + govdao.VoteOnProposal(0, dao.YesVote) println("--") - println(govdao.Render("1")) + println(govdao.Render("0")) println("--") println(validators.Render("")) println("--") - govdao.ExecuteProposal(1) + govdao.ExecuteProposal(0) println("--") - println(govdao.Render("1")) + println(govdao.Render("0")) println("--") println(validators.Render("")) } // Output: // -- -// - [0](/r/gov/dao:0) - adding someone to vote (**succeeded**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm) -// - [1](/r/gov/dao:1) - manual valset changes proposal example (**active**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm) +// - [Proposal #0](/r/gov/dao:0) - (**active**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm) // // -- -// # Prop #1 +// # Prop #0 +// +// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm // // manual valset changes proposal example // // Status: active // -// Voting status: YES: 0, NO: 0, percent: 0, members: 1 +// Voting stats: YAY 0 (0%), NAY 0 (0%), ABSTAIN 0 (0%), HAVEN'T VOTED 0 (0%) // -// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm +// Threshold met: false // // // -- // -- -// # Prop #1 +// # Prop #0 +// +// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm // // manual valset changes proposal example // // Status: accepted // -// Voting status: YES: 1, NO: 0, percent: 100, members: 1 +// Voting stats: YAY 0 (0%), NAY 0 (0%), ABSTAIN 0 (0%), HAVEN'T VOTED 0 (0%) // -// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm +// Threshold met: false // // // -- // No valset changes to apply. // -- // -- -// # Prop #1 +// # Prop #0 +// +// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm // // manual valset changes proposal example // -// Status: succeeded +// Status: execution successful // -// Voting status: YES: 1, NO: 0, percent: 100, members: 1 +// Voting stats: YAY 0 (0%), NAY 0 (0%), ABSTAIN 0 (0%), HAVEN'T VOTED 0 (0%) // -// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm +// Threshold met: false // // // -- diff --git a/examples/gno.land/r/gov/dao/prop2_filetest.gno b/examples/gno.land/r/gov/dao/prop2_filetest.gno index 311b12e7915..aa5b8a65078 100644 --- a/examples/gno.land/r/gov/dao/prop2_filetest.gno +++ b/examples/gno.land/r/gov/dao/prop2_filetest.gno @@ -1,46 +1,24 @@ package main import ( - "std" "time" - "gno.land/p/demo/context" - "gno.land/p/gov/executor" gnoblog "gno.land/r/gnoland/blog" govdao "gno.land/r/gov/dao" ) func init() { - membersFn := func() []std.Address { - return []std.Address{ - std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"), - } - } - - mExec := govdao.NewPropExecutor(membersFn) - - comment := "adding someone to vote" - - id := govdao.Propose(comment, mExec) - - govdao.ExecuteProposal(id) - - ex := executor.NewCtxExecutor(func(ctx context.Context) error { - gnoblog.DaoAddPost( - ctx, - "hello-from-govdao", // slug - "Hello from GovDAO!", // title - "This post was published by a GovDAO proposal.", // body - time.Now().Format(time.RFC3339), // publidation date - "moul", // authors - "govdao,example", // tags - ) - return nil - }) + ex := gnoblog.NewPostExecutor( + "hello-from-govdao", // slug + "Hello from GovDAO!", // title + "This post was published by a GovDAO proposal.", // body + time.Now().Format(time.RFC3339), // publidation date + "moul", // authors + "govdao,example", // tags + ) // Create a proposal. - // XXX: payment - comment = "post a new blogpost about govdao" + comment := "post a new blogpost about govdao" govdao.Propose(comment, ex) } @@ -48,49 +26,52 @@ func main() { println("--") println(govdao.Render("")) println("--") - println(govdao.Render("1")) + println(govdao.Render("0")) println("--") - govdao.VoteOnProposal(1, "YES") + govdao.VoteOnProposal(0, "YES") println("--") - println(govdao.Render("1")) + println(govdao.Render("0")) println("--") println(gnoblog.Render("")) println("--") - govdao.ExecuteProposal(1) + govdao.ExecuteProposal(0) println("--") - println(govdao.Render("1")) + println(govdao.Render("0")) println("--") println(gnoblog.Render("")) } // Output: // -- -// - [0](/r/gov/dao:0) - adding someone to vote (**succeeded**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm) -// - [1](/r/gov/dao:1) - post a new blogpost about govdao (**active**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm) +// - [Proposal #0](/r/gov/dao:0) - (**active**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm) // // -- -// # Prop #1 +// # Prop #0 +// +// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm // // post a new blogpost about govdao // // Status: active // -// Voting status: YES: 0, NO: 0, percent: 0, members: 1 +// Voting stats: YAY 0 (0%), NAY 0 (0%), ABSTAIN 0 (0%), HAVEN'T VOTED 0 (0%) // -// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm +// Threshold met: false // // // -- // -- -// # Prop #1 +// # Prop #0 +// +// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm // // post a new blogpost about govdao // // Status: accepted // -// Voting status: YES: 1, NO: 0, percent: 100, members: 1 +// Voting stats: YAY 0 (0%), NAY 0 (0%), ABSTAIN 0 (0%), HAVEN'T VOTED 0 (0%) // -// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm +// Threshold met: false // // // -- @@ -99,15 +80,17 @@ func main() { // No posts. // -- // -- -// # Prop #1 +// # Prop #0 +// +// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm // // post a new blogpost about govdao // -// Status: succeeded +// Status: execution successful // -// Voting status: YES: 1, NO: 0, percent: 100, members: 1 +// Voting stats: YAY 0 (0%), NAY 0 (0%), ABSTAIN 0 (0%), HAVEN'T VOTED 0 (0%) // -// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm +// Threshold met: false // // // -- diff --git a/examples/gno.land/r/sys/validators/gno.mod b/examples/gno.land/r/sys/validators/gno.mod index cc4e6b72236..89e49ba2250 100644 --- a/examples/gno.land/r/sys/validators/gno.mod +++ b/examples/gno.land/r/sys/validators/gno.mod @@ -6,6 +6,7 @@ require ( 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/gov/dao v0.0.0-latest gno.land/p/gov/executor v0.0.0-latest gno.land/p/nt/poa v0.0.0-latest gno.land/p/sys/validators v0.0.0-latest diff --git a/examples/gno.land/r/sys/validators/poc.gno b/examples/gno.land/r/sys/validators/poc.gno index 62b84d1232b..d888a11f7f4 100644 --- a/examples/gno.land/r/sys/validators/poc.gno +++ b/examples/gno.land/r/sys/validators/poc.gno @@ -3,7 +3,7 @@ package validators import ( "std" - "github.com/gnolang/gno/examples/gno.land/p/gov/dao" + "gno.land/p/gov/dao" "gno.land/p/gov/executor" "gno.land/p/sys/validators" ) @@ -45,7 +45,7 @@ func NewPropExecutor(changesFn func() []validators.Validator) dao.Executor { return nil } - return executor.NewExecutor(callback) + return executor.NewCallbackExecutor(callback) } // assertGovDAOCaller verifies the caller is the GovDAO executor From 1f1d5d726993ef7dfd42ba42fdfefa19576d2ed3 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Mon, 15 Jul 2024 14:22:21 +0200 Subject: [PATCH 26/80] Fixup prop filetests --- examples/gno.land/p/demo/simpledao/dao.gno | 11 +++++----- .../gno.land/p/demo/simpledao/propstore.gno | 17 ++++++++++++--- examples/gno.land/p/gov/dao/vote.gno | 21 +++++++++---------- examples/gno.land/r/gov/dao/dao.gno | 6 +++--- .../gno.land/r/gov/dao/prop1_filetest.gno | 10 ++++----- .../gno.land/r/gov/dao/prop2_filetest.gno | 10 ++++----- 6 files changed, 43 insertions(+), 32 deletions(-) diff --git a/examples/gno.land/p/demo/simpledao/dao.gno b/examples/gno.land/p/demo/simpledao/dao.gno index 6e13fa1f368..0c85279057f 100644 --- a/examples/gno.land/p/demo/simpledao/dao.gno +++ b/examples/gno.land/p/demo/simpledao/dao.gno @@ -60,11 +60,12 @@ func (s *SimpleDAO) Propose(description string, executor dao.Executor) (uint64, // Create the wrapped proposal prop := &proposal{ - author: caller, - description: description, - executor: executor, - status: dao.Active, - votes: newVotes(), + author: caller, + description: description, + executor: executor, + status: dao.Active, + votes: newVotes(), + getTotalVotingPowerFn: s.membStore.getTotalPower, } // Add the proposal diff --git a/examples/gno.land/p/demo/simpledao/propstore.gno b/examples/gno.land/p/demo/simpledao/propstore.gno index 85caaf2cdf2..7a41c57e5f2 100644 --- a/examples/gno.land/p/demo/simpledao/propstore.gno +++ b/examples/gno.land/p/demo/simpledao/propstore.gno @@ -32,7 +32,8 @@ type proposal struct { executor dao.Executor // executor for the proposal status dao.ProposalStatus // status of the proposal - votes *votes // voting mechanism + votes *votes // voting mechanism + getTotalVotingPowerFn func() uint64 // total voting power of the voting body } func (p *proposal) GetAuthor() std.Address { @@ -70,8 +71,18 @@ func (p *proposal) GetVotes() []dao.Vote { } func (p *proposal) GetVotingStats() dao.VotingStats { - // TODO implement - return dao.VotingStats{} + // Get the tally + yays, nays, abstains := p.votes.getTally() + + // Get the total voting power of the body + totalPower := p.getTotalVotingPowerFn() + + return dao.VotingStats{ + YayVotes: yays, + NayVotes: nays, + AbstainVotes: abstains, + TotalVotingPower: totalPower, + } } func (p *proposal) GetVoteByMember(address std.Address) (dao.Vote, error) { diff --git a/examples/gno.land/p/gov/dao/vote.gno b/examples/gno.land/p/gov/dao/vote.gno index d031ffaff63..7ca923d001f 100644 --- a/examples/gno.land/p/gov/dao/vote.gno +++ b/examples/gno.land/p/gov/dao/vote.gno @@ -27,27 +27,26 @@ type VotingStats struct { YayVotes uint64 NayVotes uint64 AbstainVotes uint64 - MissingVotes uint64 - ThresholdMet bool + TotalVotingPower uint64 } func (v VotingStats) GetYayPercent() uint64 { - // TODO implement - return 0 + return (v.YayVotes / v.TotalVotingPower) * 100 } func (v VotingStats) GetNayPercent() uint64 { - // TODO implement - return 0 + return (v.NayVotes / v.TotalVotingPower) * 100 } func (v VotingStats) GetAbstainPercent() uint64 { - // TODO implement - return 0 + return (v.AbstainVotes / v.TotalVotingPower) * 100 } -func (v VotingStats) GetMissingPercent() uint64 { - // TODO implement - return 0 +func (v VotingStats) GetMissingVotes() uint64 { + return v.TotalVotingPower - (v.YayVotes + v.NayVotes + v.AbstainVotes) +} + +func (v VotingStats) GetMissingVotesPercent() uint64 { + return (v.GetMissingVotes() / v.TotalVotingPower) * 100 } diff --git a/examples/gno.land/r/gov/dao/dao.gno b/examples/gno.land/r/gov/dao/dao.gno index 0c1417333cf..219ef7791db 100644 --- a/examples/gno.land/r/gov/dao/dao.gno +++ b/examples/gno.land/r/gov/dao/dao.gno @@ -135,11 +135,11 @@ func Render(path string) string { stats.GetNayPercent(), stats.AbstainVotes, stats.GetAbstainPercent(), - stats.MissingVotes, - stats.GetMissingPercent(), + stats.GetMissingVotes(), + stats.GetMissingVotesPercent(), ) output += "\n\n" - output += ufmt.Sprintf("Threshold met: %t", stats.ThresholdMet) + output += ufmt.Sprintf("Threshold met: %t", stats.YayVotes > (2*stats.TotalVotingPower)/3) output += "\n\n" return output diff --git a/examples/gno.land/r/gov/dao/prop1_filetest.gno b/examples/gno.land/r/gov/dao/prop1_filetest.gno index f756d7b2f5e..cbdb9b81c0c 100644 --- a/examples/gno.land/r/gov/dao/prop1_filetest.gno +++ b/examples/gno.land/r/gov/dao/prop1_filetest.gno @@ -78,7 +78,7 @@ func main() { // // Status: active // -// Voting stats: YAY 0 (0%), NAY 0 (0%), ABSTAIN 0 (0%), HAVEN'T VOTED 0 (0%) +// Voting stats: YAY 0 (0%), NAY 0 (0%), ABSTAIN 0 (0%), HAVEN'T VOTED 10 (100%) // // Threshold met: false // @@ -93,9 +93,9 @@ func main() { // // Status: accepted // -// Voting stats: YAY 0 (0%), NAY 0 (0%), ABSTAIN 0 (0%), HAVEN'T VOTED 0 (0%) +// Voting stats: YAY 10 (100%), NAY 0 (0%), ABSTAIN 0 (0%), HAVEN'T VOTED 0 (0%) // -// Threshold met: false +// Threshold met: true // // // -- @@ -110,9 +110,9 @@ func main() { // // Status: execution successful // -// Voting stats: YAY 0 (0%), NAY 0 (0%), ABSTAIN 0 (0%), HAVEN'T VOTED 0 (0%) +// Voting stats: YAY 10 (100%), NAY 0 (0%), ABSTAIN 0 (0%), HAVEN'T VOTED 0 (0%) // -// Threshold met: false +// Threshold met: true // // // -- diff --git a/examples/gno.land/r/gov/dao/prop2_filetest.gno b/examples/gno.land/r/gov/dao/prop2_filetest.gno index aa5b8a65078..4e1f492c68a 100644 --- a/examples/gno.land/r/gov/dao/prop2_filetest.gno +++ b/examples/gno.land/r/gov/dao/prop2_filetest.gno @@ -54,7 +54,7 @@ func main() { // // Status: active // -// Voting stats: YAY 0 (0%), NAY 0 (0%), ABSTAIN 0 (0%), HAVEN'T VOTED 0 (0%) +// Voting stats: YAY 0 (0%), NAY 0 (0%), ABSTAIN 0 (0%), HAVEN'T VOTED 10 (100%) // // Threshold met: false // @@ -69,9 +69,9 @@ func main() { // // Status: accepted // -// Voting stats: YAY 0 (0%), NAY 0 (0%), ABSTAIN 0 (0%), HAVEN'T VOTED 0 (0%) +// Voting stats: YAY 10 (100%), NAY 0 (0%), ABSTAIN 0 (0%), HAVEN'T VOTED 0 (0%) // -// Threshold met: false +// Threshold met: true // // // -- @@ -88,9 +88,9 @@ func main() { // // Status: execution successful // -// Voting stats: YAY 0 (0%), NAY 0 (0%), ABSTAIN 0 (0%), HAVEN'T VOTED 0 (0%) +// Voting stats: YAY 10 (100%), NAY 0 (0%), ABSTAIN 0 (0%), HAVEN'T VOTED 0 (0%) // -// Threshold met: false +// Threshold met: true // // // -- From 7fa7530099f34d38ab823c4ef80040755c2918a8 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Mon, 15 Jul 2024 14:26:03 +0200 Subject: [PATCH 27/80] Remove faulty test --- examples/gno.land/r/gov/dao/dao_test.gno | 189 ----------------------- 1 file changed, 189 deletions(-) delete mode 100644 examples/gno.land/r/gov/dao/dao_test.gno diff --git a/examples/gno.land/r/gov/dao/dao_test.gno b/examples/gno.land/r/gov/dao/dao_test.gno deleted file mode 100644 index 6c50b827c11..00000000000 --- a/examples/gno.land/r/gov/dao/dao_test.gno +++ /dev/null @@ -1,189 +0,0 @@ -package govdao - -import ( - "testing" -) - -func TestPackage(t *testing.T) { - t.SkipNow() // TODO fix - - // u1 := testutils.TestAddress("u1") - // u2 := testutils.TestAddress("u2") - // u3 := testutils.TestAddress("u3") - // - // members = append(members, u1) - // members = append(members, u2) - // members = append(members, u3) - // - // nu1 := testutils.TestAddress("random1") - // - // out := Render("") - // - // expected := "No proposals found :(" - // urequire.Equal(t, expected, out) - // - // var called bool - // ex := executor.NewCallbackExecutor(func() error { - // called = true - // return nil - // }) - // - // std.TestSetOrigCaller(u1) - // pid := Propose("dummy proposal", ex) - // - // // try to vote not being a member - // std.TestSetOrigCaller(nu1) - // - // urequire.PanicsWithMessage(t, msgCallerNotAMember, func() { - // VoteOnProposal(pid, "YES") - // }) - // - // // try to vote several times - // std.TestSetOrigCaller(u1) - // urequire.NotPanics(t, func() { - // VoteOnProposal(pid, "YES") - // }) - // urequire.PanicsWithMessage(t, msgAlreadyVoted, func() { - // VoteOnProposal(pid, "YES") - // }) - // - // out = Render("0") - // expected = `# Prop #0 - // - // dummy proposal - // - // Status: active - // - // Voting status: YES: 1, NO: 0, percent: 33, members: 3 - // - // Author: g1w5c47h6lta047h6lta047h6lta047h6ly5kscr - // - // ` - // - // urequire.Equal(t, expected, out) - // - // std.TestSetOrigCaller(u2) - // urequire.PanicsWithMessage(t, msgWrongVotingValue, func() { - // VoteOnProposal(pid, "INCORRECT") - // }) - // urequire.NotPanics(t, func() { - // VoteOnProposal(pid, "NO") - // }) - // - // out = Render("0") - // expected = `# Prop #0 - // - // dummy proposal - // - // Status: active - // - // Voting status: YES: 1, NO: 1, percent: 33, members: 3 - // - // Author: g1w5c47h6lta047h6lta047h6lta047h6ly5kscr - // - // ` - // - // urequire.Equal(t, expected, out) - // - // std.TestSetOrigCaller(u3) - // urequire.NotPanics(t, func() { - // VoteOnProposal(pid, "YES") - // }) - // - // out = Render("0") - // expected = `# Prop #0 - // - // dummy proposal - // - // Status: accepted - // - // Voting status: YES: 2, NO: 1, percent: 66, members: 3 - // - // Author: g1w5c47h6lta047h6lta047h6lta047h6ly5kscr - // - // ` - // - // urequire.Equal(t, expected, out) - // - // // Add a new member, so non-executed proposals will change the voting status - // u4 := testutils.TestAddress("u4") - // members = append(members, u4) - // - // out = Render("0") - // expected = `# Prop #0 - // - // dummy proposal - // - // Status: active - // - // Voting status: YES: 2, NO: 1, percent: 50, members: 4 - // - // Author: g1w5c47h6lta047h6lta047h6lta047h6ly5kscr - // - // ` - // - // urequire.Equal(t, expected, out) - // - // std.TestSetOrigCaller(u4) - // urequire.NotPanics(t, func() { - // VoteOnProposal(pid, "YES") - // }) - // - // out = Render("0") - // expected = `# Prop #0 - // - // dummy proposal - // - // Status: accepted - // - // Voting status: YES: 3, NO: 1, percent: 75, members: 4 - // - // Author: g1w5c47h6lta047h6lta047h6lta047h6ly5kscr - // - // ` - // - // urequire.Equal(t, expected, out) - // - // ExecuteProposal(pid) - // urequire.True(t, called) - // - // out = Render("0") - // expected = `# Prop #0 - // - // dummy proposal - // - // Status: succeeded - // - // Voting status: YES: 3, NO: 1, percent: 75, members: 4 - // - // Author: g1w5c47h6lta047h6lta047h6lta047h6ly5kscr - // - // ` - // - // urequire.Equal(t, expected, out) - // - // // Add a new member and try to vote an already executed proposal - // u5 := testutils.TestAddress("u5") - // members = append(members, u5) - // std.TestSetOrigCaller(u5) - // urequire.PanicsWithMessage(t, msgPropExecuted, func() { - // ExecuteProposal(pid) - // }) - // - // // even if we added a new member the executed proposal is showing correctly the members that voted on it - // out = Render("0") - // expected = `# Prop #0 - // - // dummy proposal - // - // Status: succeeded - // - // Voting status: YES: 3, NO: 1, percent: 75, members: 4 - // - // Author: g1w5c47h6lta047h6lta047h6lta047h6ly5kscr - // - // ` - // - // urequire.Equal(t, expected, out) - -} From ef299bd6fa56833e5d9cf690d5daa5d5ac45e996 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Mon, 15 Jul 2024 14:32:14 +0200 Subject: [PATCH 28/80] Add pagination for vote fetching --- .../gno.land/p/demo/simpledao/dao_test.gno | 2 +- .../gno.land/p/demo/simpledao/propstore.gno | 43 +++++++++++++------ .../p/demo/simpledao/propstore_test.gno | 16 +++---- examples/gno.land/p/gov/dao/proposals.gno | 2 +- 4 files changed, 39 insertions(+), 24 deletions(-) diff --git a/examples/gno.land/p/demo/simpledao/dao_test.gno b/examples/gno.land/p/demo/simpledao/dao_test.gno index 0416b6a2af6..5e4c273f01c 100644 --- a/examples/gno.land/p/demo/simpledao/dao_test.gno +++ b/examples/gno.land/p/demo/simpledao/dao_test.gno @@ -111,7 +111,7 @@ func TestSimpleDAO_Propose(t *testing.T) { uassert.Equal(t, proposer.String(), prop.GetAuthor().String()) uassert.Equal(t, description, prop.GetDescription()) uassert.Equal(t, dao.Active.String(), prop.GetStatus().String()) - uassert.Equal(t, 0, len(prop.GetVotes())) + uassert.Equal(t, 0, len(prop.GetVotes(0, maxRequestVotes))) }) } diff --git a/examples/gno.land/p/demo/simpledao/propstore.gno b/examples/gno.land/p/demo/simpledao/propstore.gno index 7a41c57e5f2..8760d8d96b1 100644 --- a/examples/gno.land/p/demo/simpledao/propstore.gno +++ b/examples/gno.land/p/demo/simpledao/propstore.gno @@ -22,6 +22,10 @@ const ( // maxRequestMembers is the maximum number of // paginated members that can be requested maxRequestMembers = 50 + + // maxRequestVotes is the maximum number of + // paginated votes that can be requested + maxRequestVotes = 50 ) // proposal is the internal simpledao proposal implementation @@ -48,24 +52,35 @@ func (p *proposal) GetStatus() dao.ProposalStatus { return p.status } -func (p *proposal) GetVotes() []dao.Vote { - var ( - voters = p.votes.getVoters() - votes = make([]dao.Vote, 0, voters.Size()) - ) +func (p *proposal) GetVotes(offset, count uint64) []dao.Vote { + voters := p.votes.getVoters() + + // Calculate the left and right bounds + if count < 1 || offset >= uint64(voters.Size()) { + return []dao.Vote{} + } - voters.Iterate("", "", func(key string, val interface{}) bool { - option := val.(dao.VoteOption) + // Limit the maximum number of returned votes + if count > maxRequestVotes { + count = maxRequestVotes + } - vote := dao.Vote{ - Address: std.Address(key), - Option: option, - } + votes := make([]dao.Vote, 0, voters.Size()) + voters.IterateByOffset( + int(offset), + int(count), + func(key string, val interface{}) bool { + option := val.(dao.VoteOption) - votes = append(votes, vote) + vote := dao.Vote{ + Address: std.Address(key), + Option: option, + } - return false - }) + votes = append(votes, vote) + + return false + }) return votes } diff --git a/examples/gno.land/p/demo/simpledao/propstore_test.gno b/examples/gno.land/p/demo/simpledao/propstore_test.gno index 99c5ba09de6..cb17bfc34ef 100644 --- a/examples/gno.land/p/demo/simpledao/propstore_test.gno +++ b/examples/gno.land/p/demo/simpledao/propstore_test.gno @@ -57,12 +57,12 @@ func equalProposals(t *testing.T, p1, p2 dao.Proposal) { uassert.Equal( t, - len(p1.GetVotes()), - len(p2.GetVotes()), + len(p1.GetVotes(0, maxRequestVotes)), + len(p2.GetVotes(0, maxRequestVotes)), ) - p1Votes := p1.GetVotes() - for index, v2 := range p2.GetVotes() { + p1Votes := p1.GetVotes(0, maxRequestVotes) + for index, v2 := range p2.GetVotes(0, maxRequestVotes) { uassert.Equal( t, p1Votes[index].Address.String(), @@ -117,7 +117,7 @@ func TestProposal_Data(t *testing.T) { votes: newVotes(), } - uassert.Equal(t, 0, len(p.GetVotes())) + uassert.Equal(t, 0, len(p.GetVotes(0, maxRequestVotes))) }) t.Run("existing votes", func(t *testing.T) { @@ -134,7 +134,7 @@ func TestProposal_Data(t *testing.T) { urequire.NoError(t, p.votes.castVote(m, dao.YesVote)) } - votes := p.GetVotes() + votes := p.GetVotes(0, maxRequestVotes) urequire.Equal(t, len(members), len(votes)) @@ -163,7 +163,7 @@ func TestProposal_Data(t *testing.T) { urequire.NoError(t, p.votes.castVote(m, dao.YesVote)) } - votes := p.GetVotes() + votes := p.GetVotes(0, maxRequestVotes) urequire.Equal(t, len(members), len(votes)) vote, err := p.GetVoteByMember(members[0].Address) @@ -180,7 +180,7 @@ func TestProposal_Data(t *testing.T) { votes: newVotes(), } - votes := p.GetVotes() + votes := p.GetVotes(0, maxRequestVotes) urequire.Equal(t, 0, len(votes)) _, err := p.GetVoteByMember(testutils.TestAddress("dummy")) diff --git a/examples/gno.land/p/gov/dao/proposals.gno b/examples/gno.land/p/gov/dao/proposals.gno index cf447075640..2c3e9fc29b9 100644 --- a/examples/gno.land/p/gov/dao/proposals.gno +++ b/examples/gno.land/p/gov/dao/proposals.gno @@ -44,7 +44,7 @@ type Proposal interface { GetStatus() ProposalStatus // GetVotes returns the votes of the proposal - GetVotes() []Vote + GetVotes(offset, count uint64) []Vote // GetVotingStats returns the voting stats of the proposal GetVotingStats() VotingStats From 3d0c05691cfa6e0a69ee44956830fbea80cef8db Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Mon, 15 Jul 2024 14:47:15 +0200 Subject: [PATCH 29/80] Tidy mod --- examples/gno.land/r/gov/dao/gno.mod | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/gno.land/r/gov/dao/gno.mod b/examples/gno.land/r/gov/dao/gno.mod index 88456caff7d..a4c896eee6b 100644 --- a/examples/gno.land/r/gov/dao/gno.mod +++ b/examples/gno.land/r/gov/dao/gno.mod @@ -2,9 +2,7 @@ module gno.land/r/gov/dao require ( gno.land/p/demo/simpledao v0.0.0-latest - gno.land/p/demo/testutils 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/dao v0.0.0-latest gno.land/p/gov/executor v0.0.0-latest ) From 4396c4852664448a6127e52b3bb674c1230a8c58 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Mon, 15 Jul 2024 14:56:06 +0200 Subject: [PATCH 30/80] Update update method --- examples/gno.land/p/demo/simpledao/membstore.gno | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/gno.land/p/demo/simpledao/membstore.gno b/examples/gno.land/p/demo/simpledao/membstore.gno index 202eb732a2f..436b275d789 100644 --- a/examples/gno.land/p/demo/simpledao/membstore.gno +++ b/examples/gno.land/p/demo/simpledao/membstore.gno @@ -81,8 +81,7 @@ func (m *MembStore) UpdateMember(address std.Address, member dao.Member) error { } // Check if the member exists - memberRaw, exists := m.members.Get(address.String()) - if !exists { + if !m.IsMember(address) { return errMissingMember } From 24e0b084ae29d4a923980ce9413184f9454de920 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Mon, 15 Jul 2024 16:53:24 +0200 Subject: [PATCH 31/80] Add r/sys/vars --- examples/gno.land/p/gov/executor/callback.gno | 6 +- examples/gno.land/p/gov/executor/gno.mod | 1 + .../gno.land/p/gov/executor/proposal_test.gno | 9 +- examples/gno.land/r/gnoland/blog/admin.gno | 5 +- examples/gno.land/r/gnoland/blog/gno.mod | 1 + examples/gno.land/r/sys/vars/gno.mod | 3 + examples/gno.land/r/sys/vars/poc.gno | 48 ++++++ examples/gno.land/r/sys/vars/vars.gno | 149 ++++++++++++++++++ 8 files changed, 213 insertions(+), 9 deletions(-) create mode 100644 examples/gno.land/r/sys/vars/gno.mod create mode 100644 examples/gno.land/r/sys/vars/poc.gno create mode 100644 examples/gno.land/r/sys/vars/vars.gno diff --git a/examples/gno.land/p/gov/executor/callback.gno b/examples/gno.land/p/gov/executor/callback.gno index b885571c0e1..53e1d69de6f 100644 --- a/examples/gno.land/p/gov/executor/callback.gno +++ b/examples/gno.land/p/gov/executor/callback.gno @@ -3,9 +3,9 @@ package executor import ( "errors" "std" -) -const daoPkgPath = "gno.land/r/gov/dao" // TODO: make sure this is configurable through r/sys/vars + "gno.land/r/sys/vars" +) var errNotGovDAO = errors.New("only r/gov/dao can be the caller") @@ -45,7 +45,7 @@ func (exec *CallbackExecutor) IsExpired() bool { func assertCalledByGovdao() { caller := std.CurrentRealm().PkgPath() - if caller != daoPkgPath { + if caller != vars.GetStringValue(vars.GovdaoRealm) { panic(errNotGovDAO) } } diff --git a/examples/gno.land/p/gov/executor/gno.mod b/examples/gno.land/p/gov/executor/gno.mod index 3ae47397665..5b117d8c008 100644 --- a/examples/gno.land/p/gov/executor/gno.mod +++ b/examples/gno.land/p/gov/executor/gno.mod @@ -3,4 +3,5 @@ module gno.land/p/gov/executor require ( gno.land/p/demo/context v0.0.0-latest gno.land/p/demo/uassert v0.0.0-latest + gno.land/r/sys/vars v0.0.0-latest ) diff --git a/examples/gno.land/p/gov/executor/proposal_test.gno b/examples/gno.land/p/gov/executor/proposal_test.gno index eb77a84b211..c634e6361bc 100644 --- a/examples/gno.land/p/gov/executor/proposal_test.gno +++ b/examples/gno.land/p/gov/executor/proposal_test.gno @@ -5,6 +5,7 @@ import ( "std" "testing" + "github.com/gnolang/gno/examples/gno.land/r/sys/vars" "gno.land/p/demo/context" "gno.land/p/demo/uassert" ) @@ -53,7 +54,7 @@ func TestExecutor_Callback(t *testing.T) { e := NewCallbackExecutor(cb) // Execute as the /r/gov/dao caller - r := std.NewCodeRealm(daoPkgPath) + r := std.NewCodeRealm(vars.GetStringValue(vars.GovdaoRealm)) std.TestSetRealm(r) uassert.NotPanics(t, func() { @@ -83,7 +84,7 @@ func TestExecutor_Callback(t *testing.T) { e := NewCallbackExecutor(cb) // Execute as the /r/gov/dao caller - r := std.NewCodeRealm(daoPkgPath) + r := std.NewCodeRealm(vars.GetStringValue(vars.GovdaoRealm)) std.TestSetRealm(r) uassert.NotPanics(t, func() { @@ -148,7 +149,7 @@ func TestExecutor_Context(t *testing.T) { e := NewCtxExecutor(cb) // Execute as the /r/gov/dao caller - r := std.NewCodeRealm(daoPkgPath) + r := std.NewCodeRealm(vars.GetStringValue(vars.GovdaoRealm)) std.TestSetRealm(r) uassert.NotPanics(t, func() { @@ -182,7 +183,7 @@ func TestExecutor_Context(t *testing.T) { e := NewCtxExecutor(cb) // Execute as the /r/gov/dao caller - r := std.NewCodeRealm(daoPkgPath) + r := std.NewCodeRealm(vars.GetStringValue(vars.GovdaoRealm)) std.TestSetRealm(r) uassert.NotPanics(t, func() { diff --git a/examples/gno.land/r/gnoland/blog/admin.gno b/examples/gno.land/r/gnoland/blog/admin.gno index 38f0b1227c8..8635d151645 100644 --- a/examples/gno.land/r/gnoland/blog/admin.gno +++ b/examples/gno.land/r/gnoland/blog/admin.gno @@ -7,6 +7,7 @@ import ( "gno.land/p/demo/avl" "gno.land/p/gov/dao" "gno.land/p/gov/executor" + "gno.land/r/sys/vars" ) const daoPkgPath = "gno.land/r/gov/dao" // TODO change to r/sys/vars @@ -47,7 +48,7 @@ func AdminRemoveModerator(addr std.Address) { func NewPostExecutor(slug, title, body, publicationDate, authors, tags string) dao.Executor { callback := func() error { assertGovDAOCaller() - caller := std.DerivePkgAddr("gno.land/r/gov/dao") // TODO use r/sys/vars + caller := std.DerivePkgAddr(vars.GetStringValue(vars.GovdaoRealm)) addPost(caller, slug, title, body, publicationDate, authors, tags) @@ -59,7 +60,7 @@ func NewPostExecutor(slug, title, body, publicationDate, authors, tags string) d // assertGovDAOCaller verifies the caller is the GovDAO executor func assertGovDAOCaller() { - if std.PrevRealm().PkgPath() != daoPkgPath { + if std.PrevRealm().PkgPath() != vars.GetStringValue(vars.GovdaoRealm) { panic(errNotGovDAO) } } diff --git a/examples/gno.land/r/gnoland/blog/gno.mod b/examples/gno.land/r/gnoland/blog/gno.mod index c1fda8f7d6f..d5e2f690e9f 100644 --- a/examples/gno.land/r/gnoland/blog/gno.mod +++ b/examples/gno.land/r/gnoland/blog/gno.mod @@ -5,4 +5,5 @@ require ( gno.land/p/demo/blog v0.0.0-latest gno.land/p/gov/dao v0.0.0-latest gno.land/p/gov/executor v0.0.0-latest + gno.land/r/sys/vars v0.0.0-latest ) diff --git a/examples/gno.land/r/sys/vars/gno.mod b/examples/gno.land/r/sys/vars/gno.mod new file mode 100644 index 00000000000..1e94fd8d192 --- /dev/null +++ b/examples/gno.land/r/sys/vars/gno.mod @@ -0,0 +1,3 @@ +module gno.land/r/sys/vars + +require gno.land/p/demo/avl v0.0.0-latest diff --git a/examples/gno.land/r/sys/vars/poc.gno b/examples/gno.land/r/sys/vars/poc.gno new file mode 100644 index 00000000000..bcbba701137 --- /dev/null +++ b/examples/gno.land/r/sys/vars/poc.gno @@ -0,0 +1,48 @@ +package vars + +import ( + "std" + + "gno.land/p/gov/dao" + "gno.land/p/gov/executor" +) + +var errNotGovDAO = "caller not govdao executor" + +type KV struct { + Key string + Value interface{} +} + +// NewVarPropExecutor creates a new proposal executor for +// changing r/sys/vars values through governance proposals +func NewVarPropExecutor(changesFn func() []KV) dao.Executor { + cb := func() error { + assertGovDAOCaller() + + for _, kv := range changesFn() { + if kv.Value == nil { + // Removal request + vars.Remove(kv.Key) + + continue + } + + // Add / update request + vars.Set(kv.Key, kv.Value) + } + + return nil + } + + return executor.NewCallbackExecutor(cb) +} + +// assertGovDAOCaller verifies the caller is the GovDAO executor +func assertGovDAOCaller() { + govdaoPath := GetStringValue(GovdaoRealm) + + if std.PrevRealm().PkgPath() != govdaoPath { + panic(errNotGovDAO) + } +} diff --git a/examples/gno.land/r/sys/vars/vars.gno b/examples/gno.land/r/sys/vars/vars.gno new file mode 100644 index 00000000000..f423663ff41 --- /dev/null +++ b/examples/gno.land/r/sys/vars/vars.gno @@ -0,0 +1,149 @@ +package vars + +import ( + "errors" + "strings" + + "gno.land/p/demo/avl" +) + +// maxRequestKeys is the maximum number of keys +// that can be returned on request +const maxRequestKeys = 50 + +// Prefixes + +// GovernancePrefix is used for goveranance-related vars +const GovernancePrefix = "gov-" + +// Initial keys + +const GovdaoRealm = GovernancePrefix + "govdao-realm" + +var ( + ErrMissingValue = errors.New("missing value") + ErrInvalidValueType = errors.New("invalid value type") +) + +// vars is a simple KV store +var vars *avl.Tree + +func init() { + // Initial KV set + initialSet := []struct { + key string + value string + }{ + { + GovdaoRealm, + "gno.land/r/gov/dao", + }, + } + + // Save the initial KV set + vars = avl.NewTree() + + for _, v := range initialSet { + vars.Set(v.key, v.value) + } +} + +// GetValue fetches the given value from the store, if any +func GetValue(key string) interface{} { + val, exists := vars.Get(key) + if !exists { + panic(ErrMissingValue) + } + + return val +} + +// GetStringValue returns the string value associated +// with the given key, if any +func GetStringValue(key string) string { + // Get the value + val := GetValue(key) + + // Make sure it's actually a string + valStr, ok := val.(string) + if !ok { + panic(ErrInvalidValueType) + } + + return valStr +} + +// GetKeys returns the paginated key values +func GetKeys(offset, count uint64) []string { + // Calculate the left and right bounds + if count < 1 || offset >= uint64(vars.Size()) { + return []string{} + } + + // Limit the maximum number of returned keys + if count > maxRequestKeys { + count = maxRequestKeys + } + + // Gather the members + keys := make([]string, 0) + vars.IterateByOffset( + int(offset), + int(count), + func(key string, _ interface{}) bool { + // Save the key + keys = append(keys, key) + + return false + }, + ) + + return keys +} + +// GetKeysWithPrefix returns the paginated key values with the specified prefix +func GetKeysWithPrefix( + prefix string, + offset, + count uint64, +) []string { + // Calculate the left and right bounds + if count < 1 || offset >= uint64(vars.Size()) { + return []string{} + } + + // Limit the maximum number of returned keys + if count > maxRequestKeys { + count = maxRequestKeys + } + + var ( + keys = make([]string, 0) + currOffset = uint64(0) + currCount = uint64(0) + ) + + vars.Iterate("", "", func(key string, _ interface{}) bool { + // Skip over specific elements + if currOffset < offset { + currOffset++ + + return false + } + + // Check if the key matches + if !strings.HasPrefix(key, prefix) { + return false + } + + // Save the key + keys = append(keys, key) + + // Check if the count elements + // have been gathered + currCount++ + return currCount == count + }) + + return keys +} From 4827d0bbd75a72fe62d6b1e27feda35e0d099043 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Mon, 15 Jul 2024 21:22:43 +0200 Subject: [PATCH 32/80] Version packages, add unit tests --- .../gno.land/p/demo/simpledao/membstore.gno | 7 +- examples/gno.land/p/gov/executor/callback.gno | 6 +- examples/gno.land/p/gov/executor/gno.mod | 1 - examples/gno.land/r/gnoland/blog/admin.gno | 14 +-- examples/gno.land/r/gnoland/blog/gno.mod | 1 - examples/gno.land/r/gnoland/home/home.gno | 2 +- .../gno.land/r/gnoland/home/home_filetest.gno | 2 +- .../r/gnoland/valopers/{ => v2}/gno.mod | 6 +- .../r/gnoland/valopers/{ => v2}/init.gno | 0 .../r/gnoland/valopers/{ => v2}/valopers.gno | 3 +- .../valopers/{ => v2}/valopers_test.gno | 0 examples/gno.land/r/gov/dao/{ => v2}/dao.gno | 9 +- examples/gno.land/r/gov/dao/{ => v2}/gno.mod | 2 +- examples/gno.land/r/gov/dao/{ => v2}/poc.gno | 25 +---- .../r/gov/dao/{ => v2}/prop1_filetest.gno | 5 +- .../r/gov/dao/{ => v2}/prop2_filetest.gno | 4 +- .../r/sys/validators/{ => v2}/doc.gno | 0 .../r/sys/validators/{ => v2}/gno.mod | 2 +- .../r/sys/validators/{ => v2}/gnosdk.gno | 0 .../r/sys/validators/{ => v2}/init.gno | 0 .../r/sys/validators/{ => v2}/poc.gno | 17 +-- .../r/sys/validators/{ => v2}/validators.gno | 0 .../validators/{ => v2}/validators_test.gno | 0 examples/gno.land/r/sys/vars/gno.mod | 8 +- .../gno.land/r/sys/vars/{poc.gno => prop.gno} | 15 --- .../gno.land/r/sys/vars/prop_filetest.gno | 101 ++++++++++++++++++ examples/gno.land/r/sys/vars/vars.gno | 58 +--------- examples/gno.land/r/sys/vars/vars_test.gno | 58 ++++++++++ 28 files changed, 195 insertions(+), 151 deletions(-) rename examples/gno.land/r/gnoland/valopers/{ => v2}/gno.mod (63%) rename examples/gno.land/r/gnoland/valopers/{ => v2}/init.gno (100%) rename examples/gno.land/r/gnoland/valopers/{ => v2}/valopers.gno (98%) rename examples/gno.land/r/gnoland/valopers/{ => v2}/valopers_test.gno (100%) rename examples/gno.land/r/gov/dao/{ => v2}/dao.gno (94%) rename examples/gno.land/r/gov/dao/{ => v2}/gno.mod (84%) rename examples/gno.land/r/gov/dao/{ => v2}/poc.gno (81%) rename examples/gno.land/r/gov/dao/{ => v2}/prop1_filetest.gno (94%) rename examples/gno.land/r/gov/dao/{ => v2}/prop2_filetest.gno (93%) rename examples/gno.land/r/sys/validators/{ => v2}/doc.gno (100%) rename examples/gno.land/r/sys/validators/{ => v2}/gno.mod (90%) rename examples/gno.land/r/sys/validators/{ => v2}/gnosdk.gno (100%) rename examples/gno.land/r/sys/validators/{ => v2}/init.gno (100%) rename examples/gno.land/r/sys/validators/{ => v2}/poc.gno (72%) rename examples/gno.land/r/sys/validators/{ => v2}/validators.gno (100%) rename examples/gno.land/r/sys/validators/{ => v2}/validators_test.gno (100%) rename examples/gno.land/r/sys/vars/{poc.gno => prop.gno} (66%) create mode 100644 examples/gno.land/r/sys/vars/prop_filetest.gno create mode 100644 examples/gno.land/r/sys/vars/vars_test.gno diff --git a/examples/gno.land/p/demo/simpledao/membstore.gno b/examples/gno.land/p/demo/simpledao/membstore.gno index 436b275d789..248b315959e 100644 --- a/examples/gno.land/p/demo/simpledao/membstore.gno +++ b/examples/gno.land/p/demo/simpledao/membstore.gno @@ -170,8 +170,11 @@ func (m *MembStore) getTotalPower() uint64 { return m.totalVotingPower } -// TODO change to r/sys/vars -const daoPkgPath = "gno.land/r/gov/dao" +// We need to include a govdao guard here, even if the +// executor guarantees it, because +// the API of the member store is public and callable +// by anyone who has a reference to the member store instance. +const daoPkgPath = "gno.land/r/gov/dao/v2" // isCallerGOVDAO returns a flag indicating if the // current caller context is the active GOVDAO diff --git a/examples/gno.land/p/gov/executor/callback.gno b/examples/gno.land/p/gov/executor/callback.gno index 53e1d69de6f..064a862f2d5 100644 --- a/examples/gno.land/p/gov/executor/callback.gno +++ b/examples/gno.land/p/gov/executor/callback.gno @@ -3,10 +3,10 @@ package executor import ( "errors" "std" - - "gno.land/r/sys/vars" ) +const daoPkgPath = "gno.land/r/gov/dao/v2" + var errNotGovDAO = errors.New("only r/gov/dao can be the caller") // NewCallbackExecutor creates a new callback executor with the provided callback function @@ -45,7 +45,7 @@ func (exec *CallbackExecutor) IsExpired() bool { func assertCalledByGovdao() { caller := std.CurrentRealm().PkgPath() - if caller != vars.GetStringValue(vars.GovdaoRealm) { + if caller != daoPkgPath { panic(errNotGovDAO) } } diff --git a/examples/gno.land/p/gov/executor/gno.mod b/examples/gno.land/p/gov/executor/gno.mod index 5b117d8c008..3ae47397665 100644 --- a/examples/gno.land/p/gov/executor/gno.mod +++ b/examples/gno.land/p/gov/executor/gno.mod @@ -3,5 +3,4 @@ module gno.land/p/gov/executor require ( gno.land/p/demo/context v0.0.0-latest gno.land/p/demo/uassert v0.0.0-latest - gno.land/r/sys/vars v0.0.0-latest ) diff --git a/examples/gno.land/r/gnoland/blog/admin.gno b/examples/gno.land/r/gnoland/blog/admin.gno index 8635d151645..7fea3359a57 100644 --- a/examples/gno.land/r/gnoland/blog/admin.gno +++ b/examples/gno.land/r/gnoland/blog/admin.gno @@ -7,12 +7,8 @@ import ( "gno.land/p/demo/avl" "gno.land/p/gov/dao" "gno.land/p/gov/executor" - "gno.land/r/sys/vars" ) -const daoPkgPath = "gno.land/r/gov/dao" // TODO change to r/sys/vars -const errNotGovDAO = "caller not govdao executor" - var ( adminAddr std.Address moderatorList avl.Tree @@ -47,8 +43,7 @@ func AdminRemoveModerator(addr std.Address) { func NewPostExecutor(slug, title, body, publicationDate, authors, tags string) dao.Executor { callback := func() error { - assertGovDAOCaller() - caller := std.DerivePkgAddr(vars.GetStringValue(vars.GovdaoRealm)) + caller := std.DerivePkgAddr("gno.land/r/gov/dao/v2") addPost(caller, slug, title, body, publicationDate, authors, tags) @@ -58,13 +53,6 @@ func NewPostExecutor(slug, title, body, publicationDate, authors, tags string) d return executor.NewCallbackExecutor(callback) } -// assertGovDAOCaller verifies the caller is the GovDAO executor -func assertGovDAOCaller() { - if std.PrevRealm().PkgPath() != vars.GetStringValue(vars.GovdaoRealm) { - panic(errNotGovDAO) - } -} - func ModAddPost(slug, title, body, publicationDate, authors, tags string) { assertIsModerator() caller := std.GetOrigCaller() diff --git a/examples/gno.land/r/gnoland/blog/gno.mod b/examples/gno.land/r/gnoland/blog/gno.mod index d5e2f690e9f..c1fda8f7d6f 100644 --- a/examples/gno.land/r/gnoland/blog/gno.mod +++ b/examples/gno.land/r/gnoland/blog/gno.mod @@ -5,5 +5,4 @@ require ( gno.land/p/demo/blog v0.0.0-latest gno.land/p/gov/dao v0.0.0-latest gno.land/p/gov/executor v0.0.0-latest - gno.land/r/sys/vars v0.0.0-latest ) diff --git a/examples/gno.land/r/gnoland/home/home.gno b/examples/gno.land/r/gnoland/home/home.gno index aca1007036e..42d344444b8 100644 --- a/examples/gno.land/r/gnoland/home/home.gno +++ b/examples/gno.land/r/gnoland/home/home.gno @@ -182,7 +182,7 @@ func packageStaffPicks() ui.Element { ui.BulletList{ ui.Link{URL: "r/sys/names"}, ui.Link{URL: "r/sys/rewards"}, - ui.Link{URL: "r/sys/validators"}, + ui.Link{URL: "/r/sys/validators/v2"}, }, }, { ui.H4("[r/demo](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/demo)"), diff --git a/examples/gno.land/r/gnoland/home/home_filetest.gno b/examples/gno.land/r/gnoland/home/home_filetest.gno index 2b0a802718f..03785eb664f 100644 --- a/examples/gno.land/r/gnoland/home/home_filetest.gno +++ b/examples/gno.land/r/gnoland/home/home_filetest.gno @@ -119,7 +119,7 @@ func main() { // // - [r/sys/names](r/sys/names) // - [r/sys/rewards](r/sys/rewards) -// - [r/sys/validators](r/sys/validators) +// - [/r/sys/validators/v2](/r/sys/validators/v2) // // //
diff --git a/examples/gno.land/r/gnoland/valopers/gno.mod b/examples/gno.land/r/gnoland/valopers/v2/gno.mod similarity index 63% rename from examples/gno.land/r/gnoland/valopers/gno.mod rename to examples/gno.land/r/gnoland/valopers/v2/gno.mod index 2d24fb27952..12cf11f3b3f 100644 --- a/examples/gno.land/r/gnoland/valopers/gno.mod +++ b/examples/gno.land/r/gnoland/valopers/v2/gno.mod @@ -1,4 +1,4 @@ -module gno.land/r/gnoland/valopers +module gno.land/r/gnoland/valopers/v2 require ( gno.land/p/demo/avl v0.0.0-latest @@ -6,6 +6,6 @@ require ( gno.land/p/demo/uassert v0.0.0-latest gno.land/p/demo/ufmt v0.0.0-latest gno.land/p/sys/validators v0.0.0-latest - gno.land/r/gov/dao v0.0.0-latest - gno.land/r/sys/validators v0.0.0-latest + gno.land/r/gov/dao/v2 v0.0.0-latest + gno.land//r/sys/validators/v2 v0.0.0-latest ) diff --git a/examples/gno.land/r/gnoland/valopers/init.gno b/examples/gno.land/r/gnoland/valopers/v2/init.gno similarity index 100% rename from examples/gno.land/r/gnoland/valopers/init.gno rename to examples/gno.land/r/gnoland/valopers/v2/init.gno diff --git a/examples/gno.land/r/gnoland/valopers/valopers.gno b/examples/gno.land/r/gnoland/valopers/v2/valopers.gno similarity index 98% rename from examples/gno.land/r/gnoland/valopers/valopers.gno rename to examples/gno.land/r/gnoland/valopers/v2/valopers.gno index 74cec941e0d..9f2cad57971 100644 --- a/examples/gno.land/r/gnoland/valopers/valopers.gno +++ b/examples/gno.land/r/gnoland/valopers/v2/valopers.gno @@ -8,8 +8,7 @@ import ( "gno.land/p/demo/avl" "gno.land/p/demo/ufmt" pVals "gno.land/p/sys/validators" - govdao "gno.land/r/gov/dao" - "gno.land/r/sys/validators" + govdao "gno.land/r/gov/dao/v2" ) const ( diff --git a/examples/gno.land/r/gnoland/valopers/valopers_test.gno b/examples/gno.land/r/gnoland/valopers/v2/valopers_test.gno similarity index 100% rename from examples/gno.land/r/gnoland/valopers/valopers_test.gno rename to examples/gno.land/r/gnoland/valopers/v2/valopers_test.gno diff --git a/examples/gno.land/r/gov/dao/dao.gno b/examples/gno.land/r/gov/dao/v2/dao.gno similarity index 94% rename from examples/gno.land/r/gov/dao/dao.gno rename to examples/gno.land/r/gov/dao/v2/dao.gno index 219ef7791db..c12ad025405 100644 --- a/examples/gno.land/r/gov/dao/dao.gno +++ b/examples/gno.land/r/gov/dao/v2/dao.gno @@ -3,7 +3,6 @@ package govdao import ( "std" "strconv" - "strings" "gno.land/p/demo/simpledao" "gno.land/p/demo/ufmt" @@ -93,7 +92,7 @@ func Render(path string) string { output += ufmt.Sprintf( "- [Proposal #%d](%s:%d) - (**%s**)(by %s)\n", idx, - strings.TrimPrefix(getGOVDAOPath(), "gno.land"), + "/r/gov/dao/v2", idx, prop.GetStatus().String(), prop.GetAuthor().String(), @@ -144,9 +143,3 @@ func Render(path string) string { return output } - -// getGOVDAOPath returns the active govdao Realm path -func getGOVDAOPath() string { - // TODO use r/sys/vars - return "gno.land/r/gov/dao" -} diff --git a/examples/gno.land/r/gov/dao/gno.mod b/examples/gno.land/r/gov/dao/v2/gno.mod similarity index 84% rename from examples/gno.land/r/gov/dao/gno.mod rename to examples/gno.land/r/gov/dao/v2/gno.mod index a4c896eee6b..265a974c90f 100644 --- a/examples/gno.land/r/gov/dao/gno.mod +++ b/examples/gno.land/r/gov/dao/v2/gno.mod @@ -1,4 +1,4 @@ -module gno.land/r/gov/dao +module gno.land/r/gov/dao/v2 require ( gno.land/p/demo/simpledao v0.0.0-latest diff --git a/examples/gno.land/r/gov/dao/poc.gno b/examples/gno.land/r/gov/dao/v2/poc.gno similarity index 81% rename from examples/gno.land/r/gov/dao/poc.gno rename to examples/gno.land/r/gov/dao/v2/poc.gno index c08a2fbef0d..bd79235665b 100644 --- a/examples/gno.land/r/gov/dao/poc.gno +++ b/examples/gno.land/r/gov/dao/v2/poc.gno @@ -1,18 +1,11 @@ package govdao import ( - "std" - "gno.land/p/gov/dao" "gno.land/p/gov/executor" ) -const daoPkgPath = "gno.land/r/gov/dao" - -const ( - errNoChangesProposed = "no set changes proposed" - errNotGovDAO = "caller not govdao executor" -) +const errNoChangesProposed = "no set changes proposed" // NewMemberPropExecutor returns the GOVDAO member change executor func NewMemberPropExecutor(changesFn func() []dao.Member) dao.Executor { @@ -21,9 +14,6 @@ func NewMemberPropExecutor(changesFn func() []dao.Member) dao.Executor { } callback := func() error { - // Make sure the GovDAO executor runs the member changes - assertGovDAOCaller() - errs := &executor.CombinedError{} for _, member := range changesFn() { switch { @@ -65,8 +55,6 @@ func NewDAOImplExecutor(changeFn func() dao.DAO) dao.Executor { } callback := func() error { - assertGovDAOCaller() - setDAOImpl(changeFn()) return nil @@ -81,8 +69,6 @@ func NewMembStoreImplExecutor(changeFn func() dao.MemberStore) dao.Executor { } callback := func() error { - assertGovDAOCaller() - setMembStoreImpl(changeFn()) return nil @@ -97,8 +83,6 @@ func NewPropStoreImplExecutor(changeFn func() dao.PropStore) dao.Executor { } callback := func() error { - assertGovDAOCaller() - setPropStoreImpl(changeFn()) return nil @@ -121,10 +105,3 @@ func setMembStoreImpl(impl dao.MemberStore) { func setPropStoreImpl(impl dao.PropStore) { proposals = impl } - -// assertGovDAOCaller verifies the caller is the GovDAO executor -func assertGovDAOCaller() { - if std.CurrentRealm().PkgPath() != daoPkgPath { - panic(errNotGovDAO) - } -} diff --git a/examples/gno.land/r/gov/dao/prop1_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno similarity index 94% rename from examples/gno.land/r/gov/dao/prop1_filetest.gno rename to examples/gno.land/r/gov/dao/v2/prop1_filetest.gno index cbdb9b81c0c..e417df15de1 100644 --- a/examples/gno.land/r/gov/dao/prop1_filetest.gno +++ b/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno @@ -12,8 +12,7 @@ import ( "gno.land/p/gov/dao" pVals "gno.land/p/sys/validators" - govdao "gno.land/r/gov/dao" - "gno.land/r/sys/validators" + govdao "gno.land/r/gov/dao/v2" ) func init() { @@ -67,7 +66,7 @@ func main() { // Output: // -- -// - [Proposal #0](/r/gov/dao:0) - (**active**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm) +// - [Proposal #0](/r/gov/dao/v2:0) - (**active**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm) // // -- // # Prop #0 diff --git a/examples/gno.land/r/gov/dao/prop2_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno similarity index 93% rename from examples/gno.land/r/gov/dao/prop2_filetest.gno rename to examples/gno.land/r/gov/dao/v2/prop2_filetest.gno index 4e1f492c68a..5afe5eeeca2 100644 --- a/examples/gno.land/r/gov/dao/prop2_filetest.gno +++ b/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno @@ -4,7 +4,7 @@ import ( "time" gnoblog "gno.land/r/gnoland/blog" - govdao "gno.land/r/gov/dao" + govdao "gno.land/r/gov/dao/v2" ) func init() { @@ -43,7 +43,7 @@ func main() { // Output: // -- -// - [Proposal #0](/r/gov/dao:0) - (**active**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm) +// - [Proposal #0](/r/gov/dao/v2:0) - (**active**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm) // // -- // # Prop #0 diff --git a/examples/gno.land/r/sys/validators/doc.gno b/examples/gno.land/r/sys/validators/v2/doc.gno similarity index 100% rename from examples/gno.land/r/sys/validators/doc.gno rename to examples/gno.land/r/sys/validators/v2/doc.gno diff --git a/examples/gno.land/r/sys/validators/gno.mod b/examples/gno.land/r/sys/validators/v2/gno.mod similarity index 90% rename from examples/gno.land/r/sys/validators/gno.mod rename to examples/gno.land/r/sys/validators/v2/gno.mod index 89e49ba2250..326f39533fb 100644 --- a/examples/gno.land/r/sys/validators/gno.mod +++ b/examples/gno.land/r/sys/validators/v2/gno.mod @@ -1,4 +1,4 @@ -module gno.land/r/sys/validators +module gno.land/r/sys/validators/v2 require ( gno.land/p/demo/avl v0.0.0-latest diff --git a/examples/gno.land/r/sys/validators/gnosdk.gno b/examples/gno.land/r/sys/validators/v2/gnosdk.gno similarity index 100% rename from examples/gno.land/r/sys/validators/gnosdk.gno rename to examples/gno.land/r/sys/validators/v2/gnosdk.gno diff --git a/examples/gno.land/r/sys/validators/init.gno b/examples/gno.land/r/sys/validators/v2/init.gno similarity index 100% rename from examples/gno.land/r/sys/validators/init.gno rename to examples/gno.land/r/sys/validators/v2/init.gno diff --git a/examples/gno.land/r/sys/validators/poc.gno b/examples/gno.land/r/sys/validators/v2/poc.gno similarity index 72% rename from examples/gno.land/r/sys/validators/poc.gno rename to examples/gno.land/r/sys/validators/v2/poc.gno index d888a11f7f4..29812376a2d 100644 --- a/examples/gno.land/r/sys/validators/poc.gno +++ b/examples/gno.land/r/sys/validators/v2/poc.gno @@ -8,12 +8,7 @@ import ( "gno.land/p/sys/validators" ) -const daoPkgPath = "gno.land/r/gov/dao" // TODO change to r/sys/vars - -const ( - errNoChangesProposed = "no set changes proposed" - errNotGovDAO = "caller not govdao executor" -) +const errNoChangesProposed = "no set changes proposed" // NewPropExecutor creates a new executor that wraps a changes closure // proposal. This wrapper is required to ensure the GovDAO Realm actually @@ -27,9 +22,6 @@ func NewPropExecutor(changesFn func() []validators.Validator) dao.Executor { } callback := func() error { - // Make sure the GovDAO executor runs the valset changes - assertGovDAOCaller() - for _, change := range changesFn() { if change.VotingPower == 0 { // This change request is to remove the validator @@ -48,13 +40,6 @@ func NewPropExecutor(changesFn func() []validators.Validator) dao.Executor { return executor.NewCallbackExecutor(callback) } -// assertGovDAOCaller verifies the caller is the GovDAO executor -func assertGovDAOCaller() { - if std.PrevRealm().PkgPath() != daoPkgPath { - panic(errNotGovDAO) - } -} - // IsValidator returns a flag indicating if the given bech32 address // is part of the validator set func IsValidator(addr std.Address) bool { diff --git a/examples/gno.land/r/sys/validators/validators.gno b/examples/gno.land/r/sys/validators/v2/validators.gno similarity index 100% rename from examples/gno.land/r/sys/validators/validators.gno rename to examples/gno.land/r/sys/validators/v2/validators.gno diff --git a/examples/gno.land/r/sys/validators/validators_test.gno b/examples/gno.land/r/sys/validators/v2/validators_test.gno similarity index 100% rename from examples/gno.land/r/sys/validators/validators_test.gno rename to examples/gno.land/r/sys/validators/v2/validators_test.gno diff --git a/examples/gno.land/r/sys/vars/gno.mod b/examples/gno.land/r/sys/vars/gno.mod index 1e94fd8d192..e6a5bb86aff 100644 --- a/examples/gno.land/r/sys/vars/gno.mod +++ b/examples/gno.land/r/sys/vars/gno.mod @@ -1,3 +1,9 @@ module gno.land/r/sys/vars -require gno.land/p/demo/avl v0.0.0-latest +require ( + gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/uassert v0.0.0-latest + gno.land/p/demo/urequire v0.0.0-latest + gno.land/p/gov/dao v0.0.0-latest + gno.land/p/gov/executor v0.0.0-latest +) diff --git a/examples/gno.land/r/sys/vars/poc.gno b/examples/gno.land/r/sys/vars/prop.gno similarity index 66% rename from examples/gno.land/r/sys/vars/poc.gno rename to examples/gno.land/r/sys/vars/prop.gno index bcbba701137..70f8d6eb582 100644 --- a/examples/gno.land/r/sys/vars/poc.gno +++ b/examples/gno.land/r/sys/vars/prop.gno @@ -1,14 +1,10 @@ package vars import ( - "std" - "gno.land/p/gov/dao" "gno.land/p/gov/executor" ) -var errNotGovDAO = "caller not govdao executor" - type KV struct { Key string Value interface{} @@ -18,8 +14,6 @@ type KV struct { // changing r/sys/vars values through governance proposals func NewVarPropExecutor(changesFn func() []KV) dao.Executor { cb := func() error { - assertGovDAOCaller() - for _, kv := range changesFn() { if kv.Value == nil { // Removal request @@ -37,12 +31,3 @@ func NewVarPropExecutor(changesFn func() []KV) dao.Executor { return executor.NewCallbackExecutor(cb) } - -// assertGovDAOCaller verifies the caller is the GovDAO executor -func assertGovDAOCaller() { - govdaoPath := GetStringValue(GovdaoRealm) - - if std.PrevRealm().PkgPath() != govdaoPath { - panic(errNotGovDAO) - } -} diff --git a/examples/gno.land/r/sys/vars/prop_filetest.gno b/examples/gno.land/r/sys/vars/prop_filetest.gno new file mode 100644 index 00000000000..23ef5e73c0a --- /dev/null +++ b/examples/gno.land/r/sys/vars/prop_filetest.gno @@ -0,0 +1,101 @@ +package main + +import ( + "gno.land/p/gov/dao" + govdao "gno.land/r/gov/dao/v2" + "gno.land/r/sys/vars" +) + +func init() { + changesFn := func() []vars.KV { + return []vars.KV{ + { + Key: "key 1", + Value: "value 1", + }, + { + Key: "key 2", + Value: "value 2", + }, + } + } + + // Create the executor + executor := vars.NewVarPropExecutor(changesFn) + + // Create the proposal + comment := "Example value setting" + govdao.Propose(comment, executor) +} + +func main() { + println("--") + println(govdao.Render("")) + println("--") + println(govdao.Render("0")) + println("--") + govdao.VoteOnProposal(0, dao.YesVote) + println("--") + println(govdao.Render("0")) + println("--") + govdao.ExecuteProposal(0) + println("--") + println(govdao.Render("0")) + println("--") + println(vars.GetStringValue("key 1")) + println("--") + println(vars.GetStringValue("key 2")) +} + +// Output: +// -- +// - [Proposal #0](/r/gov/dao/v2:0) - (**active**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm) +// +// -- +// # Prop #0 +// +// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm +// +// Example value setting +// +// Status: active +// +// Voting stats: YAY 0 (0%), NAY 0 (0%), ABSTAIN 0 (0%), HAVEN'T VOTED 10 (100%) +// +// Threshold met: false +// +// +// -- +// -- +// # Prop #0 +// +// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm +// +// Example value setting +// +// Status: accepted +// +// Voting stats: YAY 10 (100%), NAY 0 (0%), ABSTAIN 0 (0%), HAVEN'T VOTED 0 (0%) +// +// Threshold met: true +// +// +// -- +// -- +// # Prop #0 +// +// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm +// +// Example value setting +// +// Status: execution successful +// +// Voting stats: YAY 10 (100%), NAY 0 (0%), ABSTAIN 0 (0%), HAVEN'T VOTED 0 (0%) +// +// Threshold met: true +// +// +// -- +// value 1 +// -- +// value 2 diff --git a/examples/gno.land/r/sys/vars/vars.gno b/examples/gno.land/r/sys/vars/vars.gno index f423663ff41..0c311336c74 100644 --- a/examples/gno.land/r/sys/vars/vars.gno +++ b/examples/gno.land/r/sys/vars/vars.gno @@ -2,7 +2,6 @@ package vars import ( "errors" - "strings" "gno.land/p/demo/avl" ) @@ -13,12 +12,12 @@ const maxRequestKeys = 50 // Prefixes -// GovernancePrefix is used for goveranance-related vars -const GovernancePrefix = "gov-" +// SystemPrefix is used for system-related vars +const SystemPrefix = "sys-" // Initial keys -const GovdaoRealm = GovernancePrefix + "govdao-realm" +const ValidatorsRealm = SystemPrefix + "validators-realm" var ( ErrMissingValue = errors.New("missing value") @@ -35,8 +34,8 @@ func init() { value string }{ { - GovdaoRealm, - "gno.land/r/gov/dao", + ValidatorsRealm, + "gno.land/r/sys/validators/v2", }, } @@ -100,50 +99,3 @@ func GetKeys(offset, count uint64) []string { return keys } - -// GetKeysWithPrefix returns the paginated key values with the specified prefix -func GetKeysWithPrefix( - prefix string, - offset, - count uint64, -) []string { - // Calculate the left and right bounds - if count < 1 || offset >= uint64(vars.Size()) { - return []string{} - } - - // Limit the maximum number of returned keys - if count > maxRequestKeys { - count = maxRequestKeys - } - - var ( - keys = make([]string, 0) - currOffset = uint64(0) - currCount = uint64(0) - ) - - vars.Iterate("", "", func(key string, _ interface{}) bool { - // Skip over specific elements - if currOffset < offset { - currOffset++ - - return false - } - - // Check if the key matches - if !strings.HasPrefix(key, prefix) { - return false - } - - // Save the key - keys = append(keys, key) - - // Check if the count elements - // have been gathered - currCount++ - return currCount == count - }) - - return keys -} diff --git a/examples/gno.land/r/sys/vars/vars_test.gno b/examples/gno.land/r/sys/vars/vars_test.gno new file mode 100644 index 00000000000..3a038b7db23 --- /dev/null +++ b/examples/gno.land/r/sys/vars/vars_test.gno @@ -0,0 +1,58 @@ +package vars + +import ( + "testing" + + "gno.land/p/demo/uassert" + "gno.land/p/demo/urequire" +) + +func TestVars_GetValue(t *testing.T) { + t.Parallel() + + t.Run("missing value", func(t *testing.T) { + t.Parallel() + + uassert.PanicsWithMessage(t, ErrMissingValue.Error(), func() { + GetValue("random key") + }) + }) + + t.Run("existing value", func(t *testing.T) { + t.Parallel() + + uassert.NotPanics(t, func() { + _ = GetValue(ValidatorsRealm) + }) + }) +} + +func TestVars_GetStringValue(t *testing.T) { + t.Parallel() + + t.Run("missing value", func(t *testing.T) { + t.Parallel() + + uassert.PanicsWithMessage(t, ErrMissingValue.Error(), func() { + GetStringValue("random key") + }) + }) + + t.Run("existing string value", func(t *testing.T) { + t.Parallel() + + uassert.NotPanics(t, func() { + _ = GetStringValue(ValidatorsRealm) + }) + }) +} + +func TestVars_GetKeys(t *testing.T) { + t.Parallel() + + keys := GetKeys(0, maxRequestKeys) + + // Make sure there is 1 key + urequire.Equal(t, 1, len(keys)) + uassert.Equal(t, ValidatorsRealm, keys[0]) +} From 26f21d8572bcca7183dd2c886adc9476f99b3a26 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Mon, 15 Jul 2024 21:35:38 +0200 Subject: [PATCH 33/80] Start integrating r/sys/vars into EndBlocker --- examples/gno.land/r/sys/vars/vars.gno | 24 +++++++++++++++++++- examples/gno.land/r/sys/vars/vars_test.gno | 5 ++--- gno.land/pkg/gnoland/app.go | 26 ++++++++++++++++++++++ gno.land/pkg/gnoland/vals.go | 9 ++++---- 4 files changed, 56 insertions(+), 8 deletions(-) diff --git a/examples/gno.land/r/sys/vars/vars.gno b/examples/gno.land/r/sys/vars/vars.gno index 0c311336c74..77247c94fd9 100644 --- a/examples/gno.land/r/sys/vars/vars.gno +++ b/examples/gno.land/r/sys/vars/vars.gno @@ -17,11 +17,15 @@ const SystemPrefix = "sys-" // Initial keys -const ValidatorsRealm = SystemPrefix + "validators-realm" +const ( + ValidatorsRealm = SystemPrefix + "validators-realm" + ValidatorsChangesFn = SystemPrefix + "validators-changes-fn" +) var ( ErrMissingValue = errors.New("missing value") ErrInvalidValueType = errors.New("invalid value type") + ErrTooManyValues = errors.New("too many values requested") ) // vars is a simple KV store @@ -37,6 +41,10 @@ func init() { ValidatorsRealm, "gno.land/r/sys/validators/v2", }, + { + ValidatorsChangesFn, + "GetChanges", + }, } // Save the initial KV set @@ -72,6 +80,20 @@ func GetStringValue(key string) string { return valStr } +// GetStringValues fetches a list of values +func GetStringValues(keys ...string) []string { + if len(keys) > maxRequestKeys { + panic(ErrTooManyValues) + } + + vals := make([]string, 0, len(keys)) + for _, key := range keys { + vals = append(vals, GetStringValue(key)) + } + + return vals +} + // GetKeys returns the paginated key values func GetKeys(offset, count uint64) []string { // Calculate the left and right bounds diff --git a/examples/gno.land/r/sys/vars/vars_test.gno b/examples/gno.land/r/sys/vars/vars_test.gno index 3a038b7db23..88d936e76f0 100644 --- a/examples/gno.land/r/sys/vars/vars_test.gno +++ b/examples/gno.land/r/sys/vars/vars_test.gno @@ -52,7 +52,6 @@ func TestVars_GetKeys(t *testing.T) { keys := GetKeys(0, maxRequestKeys) - // Make sure there is 1 key - urequire.Equal(t, 1, len(keys)) - uassert.Equal(t, ValidatorsRealm, keys[0]) + // Make sure there are 2 keys + urequire.Equal(t, 2, len(keys)) } diff --git a/gno.land/pkg/gnoland/app.go b/gno.land/pkg/gnoland/app.go index ac066fa98b8..0854959b841 100644 --- a/gno.land/pkg/gnoland/app.go +++ b/gno.land/pkg/gnoland/app.go @@ -283,6 +283,32 @@ func EndBlocker( } } +// getValidatorsRealm queries r/sys/vars for the +// most up-to-date validator realm information +func getValidatorsRealm(ctx sdk.Context, vmk vm.VMKeeperI) (string, string, error) { + // Run the VM to get the values from the chain + response, err := vmk.QueryEval( + ctx, + varsRealm, + fmt.Sprintf( + "%s(%s, %s)", + varsGetValue, + validatorRealmKey, + validatorsChangesFnKey, + ), + ) + if err != nil { + return "", "", fmt.Errorf("unable to call VM during EndBlocker, %w", err) + } + + // Parse the response + return extractValValuesFromResponse(response) +} + +func extractValValuesFromResponse(response string) (string, string, error) { + +} + // extractUpdatesFromResponse extracts the validator set updates // from the VM response. // diff --git a/gno.land/pkg/gnoland/vals.go b/gno.land/pkg/gnoland/vals.go index 1843dff3984..b6793ac3e04 100644 --- a/gno.land/pkg/gnoland/vals.go +++ b/gno.land/pkg/gnoland/vals.go @@ -9,15 +9,16 @@ import ( ) const ( - valRealm = "gno.land/r/sys/validators" - valChangesFn = "GetChanges" + varsRealm = "gno.land/r/sys/vars" + varsGetValue = "GetStringValues" - validatorAddedEvent = "ValidatorAdded" - validatorRemovedEvent = "ValidatorRemoved" + validatorRealmKey = "sys-validators-realm" + validatorsChangesFnKey = "sys-validators-changes-fn" ) // XXX: replace with amino-based clean approach var valRegexp = regexp.MustCompile(`{\("([^"]*)"\s[^)]+\),\("((?:[^"]|\\")*)"\s[^)]+\),\((\d+)\s[^)]+\)}`) +var varsRegexp = regexp.MustCompile(`{\("([^"]*)"\s[^)]+\),\("((?:[^"]|\\")*)"\s[^)]+\),\((\d+)\s[^)]+\)}`) // TODO change // validatorUpdate is a type being used for "notifying" // that a validator change happened on-chain. The events from `r/sys/validators` From 71e8644e0054c7255870eadb55b717ec344d6944 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Tue, 16 Jul 2024 09:32:09 +0200 Subject: [PATCH 34/80] Add moniker to r/gnoland/valopers --- examples/gno.land/r/gnoland/valopers/v2/valopers.gno | 3 ++- examples/gno.land/r/gnoland/valopers/v2/valopers_test.gno | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/gno.land/r/gnoland/valopers/v2/valopers.gno b/examples/gno.land/r/gnoland/valopers/v2/valopers.gno index 9f2cad57971..4c8b95bc89f 100644 --- a/examples/gno.land/r/gnoland/valopers/v2/valopers.gno +++ b/examples/gno.land/r/gnoland/valopers/v2/valopers.gno @@ -24,6 +24,7 @@ var valopers *avl.Tree // Address -> Valoper // Valoper represents a validator operator profile type Valoper struct { Name string // the display name of the valoper + Moniker string // the moniker of the valoper Description string // the description of the valoper Address std.Address // The bech32 gno address of the validator @@ -100,7 +101,7 @@ func Render(_ string) string { // Render renders a single valoper with their information func (v Valoper) Render() string { - output := ufmt.Sprintf("## %s\n", v.Name) + output := ufmt.Sprintf("## %s (%s)\n", v.Name, v.Moniker) output += ufmt.Sprintf("%s\n\n", v.Description) output += ufmt.Sprintf("- Address: %s\n", v.Address.String()) output += ufmt.Sprintf("- PubKey: %s\n", v.PubKey) diff --git a/examples/gno.land/r/gnoland/valopers/v2/valopers_test.gno b/examples/gno.land/r/gnoland/valopers/v2/valopers_test.gno index 89544c46ee5..b5940738769 100644 --- a/examples/gno.land/r/gnoland/valopers/v2/valopers_test.gno +++ b/examples/gno.land/r/gnoland/valopers/v2/valopers_test.gno @@ -38,6 +38,7 @@ func TestValopers_Register(t *testing.T) { v := Valoper{ Address: testutils.TestAddress("valoper"), Name: "new valoper", + Moniker: "val-1", PubKey: "pub key", } @@ -50,6 +51,7 @@ func TestValopers_Register(t *testing.T) { uassert.Equal(t, v.Address, valoper.Address) uassert.Equal(t, v.Name, valoper.Name) + uassert.Equal(t, v.Moniker, valoper.Moniker) uassert.Equal(t, v.PubKey, valoper.PubKey) }) }) From 93d2b70af558ee3019c4e80a204f2b02330a55e3 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Tue, 16 Jul 2024 11:03:38 +0200 Subject: [PATCH 35/80] Resolve freaky import --- .../gno.land/r/gnoland/valopers/v2/gno.mod | 2 +- .../r/gnoland/valopers/v2/valopers.gno | 1 + gno.land/pkg/gnoland/app.go | 62 ++++++++++--------- gno.land/pkg/gnoland/vals.go | 11 +++- 4 files changed, 43 insertions(+), 33 deletions(-) diff --git a/examples/gno.land/r/gnoland/valopers/v2/gno.mod b/examples/gno.land/r/gnoland/valopers/v2/gno.mod index 12cf11f3b3f..076a44f7737 100644 --- a/examples/gno.land/r/gnoland/valopers/v2/gno.mod +++ b/examples/gno.land/r/gnoland/valopers/v2/gno.mod @@ -7,5 +7,5 @@ require ( gno.land/p/demo/ufmt v0.0.0-latest gno.land/p/sys/validators v0.0.0-latest gno.land/r/gov/dao/v2 v0.0.0-latest - gno.land//r/sys/validators/v2 v0.0.0-latest + gno.land/r/sys/validators/v2 v0.0.0-latest ) diff --git a/examples/gno.land/r/gnoland/valopers/v2/valopers.gno b/examples/gno.land/r/gnoland/valopers/v2/valopers.gno index 4c8b95bc89f..eb70e45c020 100644 --- a/examples/gno.land/r/gnoland/valopers/v2/valopers.gno +++ b/examples/gno.land/r/gnoland/valopers/v2/valopers.gno @@ -9,6 +9,7 @@ import ( "gno.land/p/demo/ufmt" pVals "gno.land/p/sys/validators" govdao "gno.land/r/gov/dao/v2" + validators "gno.land/r/sys/validators/v2" ) const ( diff --git a/gno.land/pkg/gnoland/app.go b/gno.land/pkg/gnoland/app.go index 0854959b841..87b839d22e7 100644 --- a/gno.land/pkg/gnoland/app.go +++ b/gno.land/pkg/gnoland/app.go @@ -251,35 +251,37 @@ func EndBlocker( req abci.RequestEndBlock, ) abci.ResponseEndBlock { return func(ctx sdk.Context, _ abci.RequestEndBlock) abci.ResponseEndBlock { - // Check if there was a valset change - if len(collector.getEvents()) == 0 { - // No valset updates - return abci.ResponseEndBlock{} - } - - // Run the VM to get the updates from the chain - response, err := vmk.QueryEval( - ctx, - valRealm, - fmt.Sprintf("%s(%d)", valChangesFn, app.LastBlockHeight()), - ) - if err != nil { - app.Logger().Error("unable to call VM during EndBlocker", "err", err) - - return abci.ResponseEndBlock{} - } - - // Extract the updates from the VM response - updates, err := extractUpdatesFromResponse(response) - if err != nil { - app.Logger().Error("unable to extract updates from response", "err", err) - - return abci.ResponseEndBlock{} - } - - return abci.ResponseEndBlock{ - ValidatorUpdates: updates, - } + return abci.ResponseEndBlock{} + + // // Check if there was a valset change + // if len(collector.getEvents()) == 0 { + // // No valset updates + // return abci.ResponseEndBlock{} + // } + + // // Run the VM to get the updates from the chain + // response, err := vmk.QueryEval( + // ctx, + // valRealm, + // fmt.Sprintf("%s(%d)", valChangesFn, app.LastBlockHeight()), + // ) + // if err != nil { + // app.Logger().Error("unable to call VM during EndBlocker", "err", err) + // + // return abci.ResponseEndBlock{} + // } + // + // // Extract the updates from the VM response + // updates, err := extractUpdatesFromResponse(response) + // if err != nil { + // app.Logger().Error("unable to extract updates from response", "err", err) + // + // return abci.ResponseEndBlock{} + // } + // + // return abci.ResponseEndBlock{ + // ValidatorUpdates: updates, + // } } } @@ -306,7 +308,7 @@ func getValidatorsRealm(ctx sdk.Context, vmk vm.VMKeeperI) (string, string, erro } func extractValValuesFromResponse(response string) (string, string, error) { - + return "", "", nil } // extractUpdatesFromResponse extracts the validator set updates diff --git a/gno.land/pkg/gnoland/vals.go b/gno.land/pkg/gnoland/vals.go index b6793ac3e04..fd20cd8266f 100644 --- a/gno.land/pkg/gnoland/vals.go +++ b/gno.land/pkg/gnoland/vals.go @@ -9,16 +9,23 @@ import ( ) const ( + valRealm = "gno.land/r/sys/validators" + varsRealm = "gno.land/r/sys/vars" varsGetValue = "GetStringValues" validatorRealmKey = "sys-validators-realm" validatorsChangesFnKey = "sys-validators-changes-fn" + + validatorAddedEvent = "ValidatorAdded" + validatorRemovedEvent = "ValidatorRemoved" ) // XXX: replace with amino-based clean approach -var valRegexp = regexp.MustCompile(`{\("([^"]*)"\s[^)]+\),\("((?:[^"]|\\")*)"\s[^)]+\),\((\d+)\s[^)]+\)}`) -var varsRegexp = regexp.MustCompile(`{\("([^"]*)"\s[^)]+\),\("((?:[^"]|\\")*)"\s[^)]+\),\((\d+)\s[^)]+\)}`) // TODO change +var ( + valRegexp = regexp.MustCompile(`{\("([^"]*)"\s[^)]+\),\("((?:[^"]|\\")*)"\s[^)]+\),\((\d+)\s[^)]+\)}`) + varsRegexp = regexp.MustCompile(`{\("([^"]*)"\s[^)]+\),\("((?:[^"]|\\")*)"\s[^)]+\),\((\d+)\s[^)]+\)}`) // TODO change +) // validatorUpdate is a type being used for "notifying" // that a validator change happened on-chain. The events from `r/sys/validators` From efe35cc41e35b2dab2a564df1da5198f7b1d88a6 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Tue, 16 Jul 2024 13:25:37 +0200 Subject: [PATCH 36/80] Restore Endblocker --- examples/gno.land/r/sys/vars/prop.gno | 27 ++++++++- gno.land/pkg/gnoland/app.go | 82 +++++++++------------------ gno.land/pkg/gnoland/vals.go | 14 +---- 3 files changed, 56 insertions(+), 67 deletions(-) diff --git a/examples/gno.land/r/sys/vars/prop.gno b/examples/gno.land/r/sys/vars/prop.gno index 70f8d6eb582..581ad5b2d3b 100644 --- a/examples/gno.land/r/sys/vars/prop.gno +++ b/examples/gno.land/r/sys/vars/prop.gno @@ -1,10 +1,18 @@ package vars import ( + "std" + "gno.land/p/gov/dao" "gno.land/p/gov/executor" ) +const ( + KeyAddedEvent = "KeyAdded" // emitted when a key was added to the set + KeyRemovedEvent = "KeyRemoved" // emitted when a key was removed from the set + KeyUpdatedEvent = "KeyUpdated" // emitted when a key value was updated in the set +) + type KV struct { Key string Value interface{} @@ -19,11 +27,28 @@ func NewVarPropExecutor(changesFn func() []KV) dao.Executor { // Removal request vars.Remove(kv.Key) + // Emit the key set change + std.Emit(KeyRemovedEvent) + + continue + } + + // Check if it's an update request + if vars.Has(kv.Key) { + // Update the value + vars.Set(kv.Key, kv.Value) + + // Emit the key set change + std.Emit(KeyUpdatedEvent) + continue } - // Add / update request + // Set the new value vars.Set(kv.Key, kv.Value) + + // Emit the key set change + std.Emit(KeyAddedEvent) } return nil diff --git a/gno.land/pkg/gnoland/app.go b/gno.land/pkg/gnoland/app.go index 87b839d22e7..ac066fa98b8 100644 --- a/gno.land/pkg/gnoland/app.go +++ b/gno.land/pkg/gnoland/app.go @@ -251,64 +251,36 @@ func EndBlocker( req abci.RequestEndBlock, ) abci.ResponseEndBlock { return func(ctx sdk.Context, _ abci.RequestEndBlock) abci.ResponseEndBlock { - return abci.ResponseEndBlock{} - - // // Check if there was a valset change - // if len(collector.getEvents()) == 0 { - // // No valset updates - // return abci.ResponseEndBlock{} - // } - - // // Run the VM to get the updates from the chain - // response, err := vmk.QueryEval( - // ctx, - // valRealm, - // fmt.Sprintf("%s(%d)", valChangesFn, app.LastBlockHeight()), - // ) - // if err != nil { - // app.Logger().Error("unable to call VM during EndBlocker", "err", err) - // - // return abci.ResponseEndBlock{} - // } - // - // // Extract the updates from the VM response - // updates, err := extractUpdatesFromResponse(response) - // if err != nil { - // app.Logger().Error("unable to extract updates from response", "err", err) - // - // return abci.ResponseEndBlock{} - // } - // - // return abci.ResponseEndBlock{ - // ValidatorUpdates: updates, - // } - } -} + // Check if there was a valset change + if len(collector.getEvents()) == 0 { + // No valset updates + return abci.ResponseEndBlock{} + } -// getValidatorsRealm queries r/sys/vars for the -// most up-to-date validator realm information -func getValidatorsRealm(ctx sdk.Context, vmk vm.VMKeeperI) (string, string, error) { - // Run the VM to get the values from the chain - response, err := vmk.QueryEval( - ctx, - varsRealm, - fmt.Sprintf( - "%s(%s, %s)", - varsGetValue, - validatorRealmKey, - validatorsChangesFnKey, - ), - ) - if err != nil { - return "", "", fmt.Errorf("unable to call VM during EndBlocker, %w", err) - } + // Run the VM to get the updates from the chain + response, err := vmk.QueryEval( + ctx, + valRealm, + fmt.Sprintf("%s(%d)", valChangesFn, app.LastBlockHeight()), + ) + if err != nil { + app.Logger().Error("unable to call VM during EndBlocker", "err", err) - // Parse the response - return extractValValuesFromResponse(response) -} + return abci.ResponseEndBlock{} + } + + // Extract the updates from the VM response + updates, err := extractUpdatesFromResponse(response) + if err != nil { + app.Logger().Error("unable to extract updates from response", "err", err) -func extractValValuesFromResponse(response string) (string, string, error) { - return "", "", nil + return abci.ResponseEndBlock{} + } + + return abci.ResponseEndBlock{ + ValidatorUpdates: updates, + } + } } // extractUpdatesFromResponse extracts the validator set updates diff --git a/gno.land/pkg/gnoland/vals.go b/gno.land/pkg/gnoland/vals.go index fd20cd8266f..da92171bd08 100644 --- a/gno.land/pkg/gnoland/vals.go +++ b/gno.land/pkg/gnoland/vals.go @@ -9,23 +9,15 @@ import ( ) const ( - valRealm = "gno.land/r/sys/validators" - - varsRealm = "gno.land/r/sys/vars" - varsGetValue = "GetStringValues" - - validatorRealmKey = "sys-validators-realm" - validatorsChangesFnKey = "sys-validators-changes-fn" + valRealm = "gno.land/r/sys/validators/v2" + valChangesFn = "GetChanges" validatorAddedEvent = "ValidatorAdded" validatorRemovedEvent = "ValidatorRemoved" ) // XXX: replace with amino-based clean approach -var ( - valRegexp = regexp.MustCompile(`{\("([^"]*)"\s[^)]+\),\("((?:[^"]|\\")*)"\s[^)]+\),\((\d+)\s[^)]+\)}`) - varsRegexp = regexp.MustCompile(`{\("([^"]*)"\s[^)]+\),\("((?:[^"]|\\")*)"\s[^)]+\),\((\d+)\s[^)]+\)}`) // TODO change -) +var valRegexp = regexp.MustCompile(`{\("([^"]*)"\s[^)]+\),\("((?:[^"]|\\")*)"\s[^)]+\),\((\d+)\s[^)]+\)}`) // validatorUpdate is a type being used for "notifying" // that a validator change happened on-chain. The events from `r/sys/validators` From 63cf23f65c2592b772f0ba9415165ab9cb348eb7 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Tue, 16 Jul 2024 13:27:40 +0200 Subject: [PATCH 37/80] Make tidy --- examples/gno.land/r/gov/dao/v2/prop1_filetest.gno | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno index e417df15de1..cc959dc63c4 100644 --- a/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno +++ b/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno @@ -13,6 +13,7 @@ import ( "gno.land/p/gov/dao" pVals "gno.land/p/sys/validators" govdao "gno.land/r/gov/dao/v2" + validators "gno.land/r/sys/validators/v2" ) func init() { From 4520593968e3bd74e4357dbc419841ff068953d9 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Tue, 16 Jul 2024 13:39:36 +0200 Subject: [PATCH 38/80] Make tidy --- examples/gno.land/p/gov/executor/proposal_test.gno | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/examples/gno.land/p/gov/executor/proposal_test.gno b/examples/gno.land/p/gov/executor/proposal_test.gno index c634e6361bc..eb77a84b211 100644 --- a/examples/gno.land/p/gov/executor/proposal_test.gno +++ b/examples/gno.land/p/gov/executor/proposal_test.gno @@ -5,7 +5,6 @@ import ( "std" "testing" - "github.com/gnolang/gno/examples/gno.land/r/sys/vars" "gno.land/p/demo/context" "gno.land/p/demo/uassert" ) @@ -54,7 +53,7 @@ func TestExecutor_Callback(t *testing.T) { e := NewCallbackExecutor(cb) // Execute as the /r/gov/dao caller - r := std.NewCodeRealm(vars.GetStringValue(vars.GovdaoRealm)) + r := std.NewCodeRealm(daoPkgPath) std.TestSetRealm(r) uassert.NotPanics(t, func() { @@ -84,7 +83,7 @@ func TestExecutor_Callback(t *testing.T) { e := NewCallbackExecutor(cb) // Execute as the /r/gov/dao caller - r := std.NewCodeRealm(vars.GetStringValue(vars.GovdaoRealm)) + r := std.NewCodeRealm(daoPkgPath) std.TestSetRealm(r) uassert.NotPanics(t, func() { @@ -149,7 +148,7 @@ func TestExecutor_Context(t *testing.T) { e := NewCtxExecutor(cb) // Execute as the /r/gov/dao caller - r := std.NewCodeRealm(vars.GetStringValue(vars.GovdaoRealm)) + r := std.NewCodeRealm(daoPkgPath) std.TestSetRealm(r) uassert.NotPanics(t, func() { @@ -183,7 +182,7 @@ func TestExecutor_Context(t *testing.T) { e := NewCtxExecutor(cb) // Execute as the /r/gov/dao caller - r := std.NewCodeRealm(vars.GetStringValue(vars.GovdaoRealm)) + r := std.NewCodeRealm(daoPkgPath) std.TestSetRealm(r) uassert.NotPanics(t, func() { From af65764987ab5ee6a5292c0ec3d119b9803a33b7 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Tue, 16 Jul 2024 14:00:11 +0200 Subject: [PATCH 39/80] Fixup vars tests --- examples/gno.land/r/sys/vars/gno.mod | 1 + examples/gno.land/r/sys/vars/vars.gno | 33 +--------- examples/gno.land/r/sys/vars/vars_test.gno | 73 ++++++++++++++++++++-- 3 files changed, 70 insertions(+), 37 deletions(-) diff --git a/examples/gno.land/r/sys/vars/gno.mod b/examples/gno.land/r/sys/vars/gno.mod index e6a5bb86aff..1cbdcd41c4e 100644 --- a/examples/gno.land/r/sys/vars/gno.mod +++ b/examples/gno.land/r/sys/vars/gno.mod @@ -3,6 +3,7 @@ module gno.land/r/sys/vars require ( gno.land/p/demo/avl 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/dao v0.0.0-latest gno.land/p/gov/executor v0.0.0-latest diff --git a/examples/gno.land/r/sys/vars/vars.gno b/examples/gno.land/r/sys/vars/vars.gno index 77247c94fd9..4d85ad0cf29 100644 --- a/examples/gno.land/r/sys/vars/vars.gno +++ b/examples/gno.land/r/sys/vars/vars.gno @@ -10,18 +10,6 @@ import ( // that can be returned on request const maxRequestKeys = 50 -// Prefixes - -// SystemPrefix is used for system-related vars -const SystemPrefix = "sys-" - -// Initial keys - -const ( - ValidatorsRealm = SystemPrefix + "validators-realm" - ValidatorsChangesFn = SystemPrefix + "validators-changes-fn" -) - var ( ErrMissingValue = errors.New("missing value") ErrInvalidValueType = errors.New("invalid value type") @@ -32,27 +20,8 @@ var ( var vars *avl.Tree func init() { - // Initial KV set - initialSet := []struct { - key string - value string - }{ - { - ValidatorsRealm, - "gno.land/r/sys/validators/v2", - }, - { - ValidatorsChangesFn, - "GetChanges", - }, - } - - // Save the initial KV set + // The initial variable set is empty vars = avl.NewTree() - - for _, v := range initialSet { - vars.Set(v.key, v.value) - } } // GetValue fetches the given value from the store, if any diff --git a/examples/gno.land/r/sys/vars/vars_test.gno b/examples/gno.land/r/sys/vars/vars_test.gno index 88d936e76f0..76d02be4cc1 100644 --- a/examples/gno.land/r/sys/vars/vars_test.gno +++ b/examples/gno.land/r/sys/vars/vars_test.gno @@ -1,9 +1,12 @@ package vars import ( + "sort" "testing" + "gno.land/p/demo/avl" "gno.land/p/demo/uassert" + "gno.land/p/demo/ufmt" "gno.land/p/demo/urequire" ) @@ -21,8 +24,22 @@ func TestVars_GetValue(t *testing.T) { t.Run("existing value", func(t *testing.T) { t.Parallel() + vars = avl.NewTree() + + var ( + key = "new key" + value = "new value" + ) + + vars.Set(key, value) + uassert.NotPanics(t, func() { - _ = GetValue(ValidatorsRealm) + valRaw := GetValue(key) + + val, ok := valRaw.(string) + urequire.True(t, ok) + + uassert.Equal(t, value, val) }) }) } @@ -41,8 +58,19 @@ func TestVars_GetStringValue(t *testing.T) { t.Run("existing string value", func(t *testing.T) { t.Parallel() + vars = avl.NewTree() + + var ( + key = "new string key" + value = "new value" + ) + + vars.Set(key, value) + uassert.NotPanics(t, func() { - _ = GetStringValue(ValidatorsRealm) + val := GetStringValue(key) + + uassert.Equal(t, value, val) }) }) } @@ -50,8 +78,43 @@ func TestVars_GetStringValue(t *testing.T) { func TestVars_GetKeys(t *testing.T) { t.Parallel() - keys := GetKeys(0, maxRequestKeys) + t.Run("no keys", func(t *testing.T) { + t.Parallel() + + vars = avl.NewTree() + + keys := GetKeys(0, maxRequestKeys) - // Make sure there are 2 keys - urequire.Equal(t, 2, len(keys)) + // Make sure there are 0 keys + urequire.Equal(t, 0, len(keys)) + }) + + t.Run("existing keys", func(t *testing.T) { + t.Parallel() + + vars = avl.NewTree() + + savedKeys := make([]string, 0, maxRequestKeys) + for i := 0; i < maxRequestKeys; i++ { + kv := KV{ + Key: ufmt.Sprintf("key-%d", i), + Value: ufmt.Sprintf("value-%d", i), + } + + savedKeys = append(savedKeys, kv.Key) + + vars.Set(kv.Key, kv.Value) + } + + // Fetch the keys + keys := GetKeys(0, maxRequestKeys) + urequire.Equal(t, maxRequestKeys, len(keys)) + + sort.Strings(keys) + sort.Strings(savedKeys) + + for index, key := range keys { + uassert.Equal(t, savedKeys[index], key) + } + }) } From 4ec9f3b4b81103e862a4209db5f91d6518dcfae2 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Tue, 16 Jul 2024 14:12:09 +0200 Subject: [PATCH 40/80] Export errors --- examples/gno.land/p/demo/simpledao/dao.gno | 32 +++++++++---------- .../gno.land/p/demo/simpledao/dao_test.gno | 24 +++++++------- .../gno.land/p/demo/simpledao/membstore.gno | 18 +++++------ .../p/demo/simpledao/membstore_test.gno | 12 +++---- .../gno.land/p/demo/simpledao/propstore.gno | 8 ++--- .../p/demo/simpledao/propstore_test.gno | 4 +-- examples/gno.land/p/demo/simpledao/vote.gno | 4 +-- 7 files changed, 51 insertions(+), 51 deletions(-) diff --git a/examples/gno.land/p/demo/simpledao/dao.gno b/examples/gno.land/p/demo/simpledao/dao.gno index 0c85279057f..d70788bafc5 100644 --- a/examples/gno.land/p/demo/simpledao/dao.gno +++ b/examples/gno.land/p/demo/simpledao/dao.gno @@ -9,14 +9,14 @@ import ( ) 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") - errProposalNotAccepted = errors.New("proposal is not accepted") - errExecutorExpired = errors.New("executor is expired") - errNotGovDAO = errors.New("caller not correct govdao instance") + 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") + ErrProposalNotAccepted = errors.New("proposal is not accepted") + ErrExecutorExpired = errors.New("executor is expired") + ErrNotGovDAO = errors.New("caller not correct govdao instance") ) var ( @@ -44,7 +44,7 @@ func New(membStore *MembStore, propStore *PropStore) *SimpleDAO { func (s *SimpleDAO) Propose(description string, executor dao.Executor) (uint64, error) { // Make sure the executor is set if executor == nil { - return 0, errInvalidExecutor + return 0, ErrInvalidExecutor } var ( @@ -55,7 +55,7 @@ func (s *SimpleDAO) Propose(description string, executor dao.Executor) (uint64, // Check if the proposal is valid if !s.membStore.IsMember(caller) && !canCoverFee { - return 0, errInsufficientProposalFunds + return 0, ErrInsufficientProposalFunds } // Create the wrapped proposal @@ -102,12 +102,12 @@ func (s *SimpleDAO) VoteOnProposal(id uint64, option dao.VoteOption) error { // 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 + return ErrProposalInactive } // Check if the executor is expired if prop.executor.IsExpired() { - return errExecutorExpired + return ErrExecutorExpired } // Cast the vote @@ -153,7 +153,7 @@ func (s *SimpleDAO) ExecuteProposal(id uint64) error { // Check if the non-DAO member can cover the execute fee if !s.membStore.IsMember(caller) && !canCoverFee { - return errInsufficientExecuteFunds + return ErrInsufficientExecuteFunds } // Check if the proposal exists @@ -168,18 +168,18 @@ func (s *SimpleDAO) ExecuteProposal(id uint64) error { if prop.GetStatus() == dao.ExecutionSuccessful || prop.GetStatus() == dao.ExecutionFailed { // Proposal is already executed - return errProposalExecuted + return ErrProposalExecuted } // Check the proposal status if prop.GetStatus() != dao.Accepted { // Proposal is not accepted, cannot be executed - return errProposalNotAccepted + return ErrProposalNotAccepted } // Check if the executor is expired if prop.executor.IsExpired() { - return errExecutorExpired + return ErrExecutorExpired } // Attempt to execute the proposal diff --git a/examples/gno.land/p/demo/simpledao/dao_test.gno b/examples/gno.land/p/demo/simpledao/dao_test.gno index 5e4c273f01c..33ef0a67549 100644 --- a/examples/gno.land/p/demo/simpledao/dao_test.gno +++ b/examples/gno.land/p/demo/simpledao/dao_test.gno @@ -24,7 +24,7 @@ func TestSimpleDAO_Propose(t *testing.T) { uassert.ErrorIs( t, err, - errInvalidExecutor, + ErrInvalidExecutor, ) }) @@ -60,7 +60,7 @@ func TestSimpleDAO_Propose(t *testing.T) { uassert.ErrorIs( t, err, - errInsufficientProposalFunds, + ErrInsufficientProposalFunds, ) uassert.False(t, called) @@ -135,7 +135,7 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { uassert.ErrorContains( t, s.VoteOnProposal(0, dao.YesVote), - errMissingMember.Error(), + ErrMissingMember.Error(), ) }) @@ -161,7 +161,7 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { uassert.ErrorContains( t, s.VoteOnProposal(0, dao.YesVote), - errMissingProposal.Error(), + ErrMissingProposal.Error(), ) }) @@ -195,7 +195,7 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { uassert.ErrorIs( t, s.VoteOnProposal(id, dao.YesVote), - errProposalInactive, + ErrProposalInactive, ) }) @@ -236,7 +236,7 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { uassert.ErrorIs( t, s.VoteOnProposal(id, dao.YesVote), - errExecutorExpired, + ErrExecutorExpired, ) }) @@ -279,7 +279,7 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { uassert.ErrorContains( t, s.VoteOnProposal(id, dao.YesVote), - errAlreadyVoted.Error(), + ErrAlreadyVoted.Error(), ) }) @@ -519,7 +519,7 @@ func TestSimpleDAO_ExecuteProposal(t *testing.T) { uassert.ErrorIs( t, s.ExecuteProposal(0), - errInsufficientExecuteFunds, + ErrInsufficientExecuteFunds, ) }) @@ -547,7 +547,7 @@ func TestSimpleDAO_ExecuteProposal(t *testing.T) { uassert.ErrorContains( t, s.ExecuteProposal(0), - errMissingProposal.Error(), + ErrMissingProposal.Error(), ) }) @@ -581,7 +581,7 @@ func TestSimpleDAO_ExecuteProposal(t *testing.T) { uassert.ErrorIs( t, s.ExecuteProposal(id), - errProposalNotAccepted, + ErrProposalNotAccepted, ) }) @@ -635,7 +635,7 @@ func TestSimpleDAO_ExecuteProposal(t *testing.T) { uassert.ErrorIs( t, s.ExecuteProposal(id), - errProposalExecuted, + ErrProposalExecuted, ) }) } @@ -678,7 +678,7 @@ func TestSimpleDAO_ExecuteProposal(t *testing.T) { uassert.ErrorIs( t, s.ExecuteProposal(id), - errExecutorExpired, + ErrExecutorExpired, ) }) diff --git a/examples/gno.land/p/demo/simpledao/membstore.gno b/examples/gno.land/p/demo/simpledao/membstore.gno index 248b315959e..062c126586d 100644 --- a/examples/gno.land/p/demo/simpledao/membstore.gno +++ b/examples/gno.land/p/demo/simpledao/membstore.gno @@ -10,9 +10,9 @@ import ( ) var ( - errAlreadyMember = errors.New("address is already a member") - errMissingMember = errors.New("address is not a member") - errInvalidAddressUpdate = errors.New("invalid address update") + ErrAlreadyMember = errors.New("address is already a member") + ErrMissingMember = errors.New("address is not a member") + ErrInvalidAddressUpdate = errors.New("invalid address update") ) type MembStoreOption func(*MembStore) @@ -57,12 +57,12 @@ func NewMembStore(opts ...MembStoreOption) *MembStore { func (m *MembStore) AddMember(member dao.Member) error { if !isCallerGOVDAO() { - return errNotGovDAO + return ErrNotGovDAO } // Check if the member exists if m.IsMember(member.Address) { - return errAlreadyMember + return ErrAlreadyMember } // Add the member @@ -77,12 +77,12 @@ func (m *MembStore) AddMember(member dao.Member) error { func (m *MembStore) UpdateMember(address std.Address, member dao.Member) error { if !isCallerGOVDAO() { - return errNotGovDAO + return ErrNotGovDAO } // Check if the member exists if !m.IsMember(address) { - return errMissingMember + return ErrMissingMember } // Check if this is a removal request @@ -96,7 +96,7 @@ func (m *MembStore) UpdateMember(address std.Address, member dao.Member) error { // overwriting an existing one isAddressUpdate := address != member.Address if isAddressUpdate && m.IsMember(member.Address) { - return errInvalidAddressUpdate + return ErrInvalidAddressUpdate } // Remove the old member info @@ -120,7 +120,7 @@ func (m *MembStore) IsMember(address std.Address) bool { func (m *MembStore) GetMember(address std.Address) (dao.Member, error) { member, exists := m.members.Get(address.String()) if !exists { - return dao.Member{}, errMissingMember + return dao.Member{}, ErrMissingMember } return member.(dao.Member), nil diff --git a/examples/gno.land/p/demo/simpledao/membstore_test.gno b/examples/gno.land/p/demo/simpledao/membstore_test.gno index 836473c2366..a264579ece6 100644 --- a/examples/gno.land/p/demo/simpledao/membstore_test.gno +++ b/examples/gno.land/p/demo/simpledao/membstore_test.gno @@ -38,7 +38,7 @@ func TestMembStore_GetMember(t *testing.T) { m := NewMembStore() _, err := m.GetMember(testutils.TestAddress("random")) - uassert.ErrorIs(t, err, errMissingMember) + uassert.ErrorIs(t, err, ErrMissingMember) }) t.Run("valid member fetched", func(t *testing.T) { @@ -144,7 +144,7 @@ func TestMembStore_AddMember(t *testing.T) { // Attempt to add a member member := generateMembers(t, 1)[0] - uassert.ErrorIs(t, m.AddMember(member), errNotGovDAO) + uassert.ErrorIs(t, m.AddMember(member), ErrNotGovDAO) }) t.Run("member already exists", func(t *testing.T) { @@ -159,7 +159,7 @@ func TestMembStore_AddMember(t *testing.T) { m := NewMembStore(WithInitialMembers(members)) // Attempt to add a member - uassert.ErrorIs(t, m.AddMember(members[0]), errAlreadyMember) + uassert.ErrorIs(t, m.AddMember(members[0]), ErrAlreadyMember) }) t.Run("new member added", func(t *testing.T) { @@ -215,7 +215,7 @@ func TestMembStore_UpdateMember(t *testing.T) { // Attempt to update a member member := generateMembers(t, 1)[0] - uassert.ErrorIs(t, m.UpdateMember(member.Address, member), errNotGovDAO) + uassert.ErrorIs(t, m.UpdateMember(member.Address, member), ErrNotGovDAO) }) t.Run("non-existing member", func(t *testing.T) { @@ -230,7 +230,7 @@ func TestMembStore_UpdateMember(t *testing.T) { m := NewMembStore() // Attempt to update a member - uassert.ErrorIs(t, m.UpdateMember(members[0].Address, members[0]), errMissingMember) + uassert.ErrorIs(t, m.UpdateMember(members[0].Address, members[0]), ErrMissingMember) }) t.Run("overwrite member attempt", func(t *testing.T) { @@ -245,7 +245,7 @@ func TestMembStore_UpdateMember(t *testing.T) { m := NewMembStore(WithInitialMembers(members)) // Attempt to update a member - uassert.ErrorIs(t, m.UpdateMember(members[0].Address, members[1]), errInvalidAddressUpdate) + uassert.ErrorIs(t, m.UpdateMember(members[0].Address, members[1]), ErrInvalidAddressUpdate) }) t.Run("successful update", func(t *testing.T) { diff --git a/examples/gno.land/p/demo/simpledao/propstore.gno b/examples/gno.land/p/demo/simpledao/propstore.gno index 8760d8d96b1..b99e2e7f690 100644 --- a/examples/gno.land/p/demo/simpledao/propstore.gno +++ b/examples/gno.land/p/demo/simpledao/propstore.gno @@ -10,8 +10,8 @@ import ( ) var ( - errMissingProposal = errors.New("proposal is missing") - errMissingVote = errors.New("member has not voted") + ErrMissingProposal = errors.New("proposal is missing") + ErrMissingVote = errors.New("member has not voted") ) const ( @@ -105,7 +105,7 @@ func (p *proposal) GetVoteByMember(address std.Address) (dao.Vote, error) { optionRaw, exists := voters.Get(address.String()) if !exists { - return dao.Vote{}, errMissingVote + return dao.Vote{}, ErrMissingVote } vote := dao.Vote{ @@ -187,7 +187,7 @@ func (p *PropStore) GetProposals(offset, count uint64) []dao.Proposal { func (p *PropStore) GetProposalByID(id uint64) (dao.Proposal, error) { prop, exists := p.proposals.Get(getProposalID(id)) if !exists { - return nil, errMissingProposal + return nil, ErrMissingProposal } return prop.(*proposal), nil diff --git a/examples/gno.land/p/demo/simpledao/propstore_test.gno b/examples/gno.land/p/demo/simpledao/propstore_test.gno index cb17bfc34ef..533fe8edfc3 100644 --- a/examples/gno.land/p/demo/simpledao/propstore_test.gno +++ b/examples/gno.land/p/demo/simpledao/propstore_test.gno @@ -184,7 +184,7 @@ func TestProposal_Data(t *testing.T) { urequire.Equal(t, 0, len(votes)) _, err := p.GetVoteByMember(testutils.TestAddress("dummy")) - uassert.ErrorIs(t, err, errMissingVote) + uassert.ErrorIs(t, err, ErrMissingVote) }) } @@ -248,7 +248,7 @@ func TestPropStore_GetProposalByID(t *testing.T) { p := NewPropStore() _, err := p.GetProposalByID(0) - uassert.ErrorIs(t, err, errMissingProposal) + uassert.ErrorIs(t, err, ErrMissingProposal) }) t.Run("proposal found", func(t *testing.T) { diff --git a/examples/gno.land/p/demo/simpledao/vote.gno b/examples/gno.land/p/demo/simpledao/vote.gno index bfa95ffbd67..18fbcac9821 100644 --- a/examples/gno.land/p/demo/simpledao/vote.gno +++ b/examples/gno.land/p/demo/simpledao/vote.gno @@ -7,7 +7,7 @@ import ( "gno.land/p/gov/dao" ) -var errAlreadyVoted = errors.New("vote already cast") +var ErrAlreadyVoted = errors.New("vote already cast") // votes is a simple weighted voting system type votes struct { @@ -34,7 +34,7 @@ func (v *votes) castVote(member dao.Member, option dao.VoteOption) error { _, voted := v.voters.Get(address) if voted { - return errAlreadyVoted + return ErrAlreadyVoted } // Update the tally From 30ac5ff8b77a74082eda6b4f882a16a2e249a330 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Tue, 16 Jul 2024 14:17:33 +0200 Subject: [PATCH 41/80] Update comment --- examples/gno.land/p/demo/simpledao/propstore.gno | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/gno.land/p/demo/simpledao/propstore.gno b/examples/gno.land/p/demo/simpledao/propstore.gno index b99e2e7f690..1137d2b5a3e 100644 --- a/examples/gno.land/p/demo/simpledao/propstore.gno +++ b/examples/gno.land/p/demo/simpledao/propstore.gno @@ -140,7 +140,7 @@ func (p *PropStore) addProposal(proposal *proposal) (uint64, error) { } func (p *PropStore) GetProposals(offset, count uint64) []dao.Proposal { - // Calculate the left and right bounds + // Check the requested count if count < 1 { return []dao.Proposal{} } From c05289f18c7cd5c13ea2baf058ae9f3c8eede71d Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Tue, 16 Jul 2024 14:23:12 +0200 Subject: [PATCH 42/80] Drop useless voters methods --- examples/gno.land/p/demo/simpledao/dao.gno | 10 ++++----- .../gno.land/p/demo/simpledao/propstore.gno | 21 +++++++------------ examples/gno.land/p/demo/simpledao/vote.gno | 10 --------- 3 files changed, 11 insertions(+), 30 deletions(-) diff --git a/examples/gno.land/p/demo/simpledao/dao.gno b/examples/gno.land/p/demo/simpledao/dao.gno index d70788bafc5..726e2b78022 100644 --- a/examples/gno.land/p/demo/simpledao/dao.gno +++ b/examples/gno.land/p/demo/simpledao/dao.gno @@ -119,21 +119,19 @@ func (s *SimpleDAO) VoteOnProposal(id uint64, option dao.VoteOption) error { var ( majorityPower = s.membStore.getMajorityPower() totalPower = s.membStore.getTotalPower() - - yays, nays, abstains = prop.votes.getTally() ) switch { - case yays > majorityPower: + case prop.votes.yays > majorityPower: // 2/3+ voted YES prop.status = dao.Accepted - case nays > majorityPower: + case prop.votes.nays > majorityPower: // 2/3+ voted NO prop.status = dao.NotAccepted - case abstains > majorityPower: + case prop.votes.abstains > majorityPower: // 2/3+ voted ABSTAIN prop.status = dao.NotAccepted - case yays+nays+abstains >= totalPower: + case prop.votes.yays+prop.votes.nays+prop.votes.abstains >= totalPower: // Everyone voted, but it's undecided, // hence the proposal can't go through prop.status = dao.NotAccepted diff --git a/examples/gno.land/p/demo/simpledao/propstore.gno b/examples/gno.land/p/demo/simpledao/propstore.gno index 1137d2b5a3e..fc3fea1e2ab 100644 --- a/examples/gno.land/p/demo/simpledao/propstore.gno +++ b/examples/gno.land/p/demo/simpledao/propstore.gno @@ -53,10 +53,8 @@ func (p *proposal) GetStatus() dao.ProposalStatus { } func (p *proposal) GetVotes(offset, count uint64) []dao.Vote { - voters := p.votes.getVoters() - // Calculate the left and right bounds - if count < 1 || offset >= uint64(voters.Size()) { + if count < 1 || offset >= uint64(p.votes.voters.Size()) { return []dao.Vote{} } @@ -65,8 +63,8 @@ func (p *proposal) GetVotes(offset, count uint64) []dao.Vote { count = maxRequestVotes } - votes := make([]dao.Vote, 0, voters.Size()) - voters.IterateByOffset( + votes := make([]dao.Vote, 0, p.votes.voters.Size()) + p.votes.voters.IterateByOffset( int(offset), int(count), func(key string, val interface{}) bool { @@ -86,24 +84,19 @@ func (p *proposal) GetVotes(offset, count uint64) []dao.Vote { } func (p *proposal) GetVotingStats() dao.VotingStats { - // Get the tally - yays, nays, abstains := p.votes.getTally() - // Get the total voting power of the body totalPower := p.getTotalVotingPowerFn() return dao.VotingStats{ - YayVotes: yays, - NayVotes: nays, - AbstainVotes: abstains, + YayVotes: p.votes.yays, + NayVotes: p.votes.nays, + AbstainVotes: p.votes.abstains, TotalVotingPower: totalPower, } } func (p *proposal) GetVoteByMember(address std.Address) (dao.Vote, error) { - voters := p.votes.getVoters() - - optionRaw, exists := voters.Get(address.String()) + optionRaw, exists := p.votes.voters.Get(address.String()) if !exists { return dao.Vote{}, ErrMissingVote } diff --git a/examples/gno.land/p/demo/simpledao/vote.gno b/examples/gno.land/p/demo/simpledao/vote.gno index 18fbcac9821..ff139333460 100644 --- a/examples/gno.land/p/demo/simpledao/vote.gno +++ b/examples/gno.land/p/demo/simpledao/vote.gno @@ -52,13 +52,3 @@ func (v *votes) castVote(member dao.Member, option dao.VoteOption) error { return nil } - -// getTally returns the yay, and nay count, respectively -func (v *votes) getTally() (uint64, uint64, uint64) { - return v.yays, v.nays, v.abstains -} - -// getVoters returns the current voter set -func (v *votes) getVoters() *avl.Tree { - return v.voters -} From d8e5b7df28598d1c63609674cff665c35f9375ed Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Tue, 16 Jul 2024 14:28:13 +0200 Subject: [PATCH 43/80] Drop excess field --- examples/gno.land/p/demo/simpledao/dao.gno | 2 +- examples/gno.land/p/demo/simpledao/membstore.gno | 16 ++-------------- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/examples/gno.land/p/demo/simpledao/dao.gno b/examples/gno.land/p/demo/simpledao/dao.gno index 726e2b78022..3213082c30a 100644 --- a/examples/gno.land/p/demo/simpledao/dao.gno +++ b/examples/gno.land/p/demo/simpledao/dao.gno @@ -117,8 +117,8 @@ func (s *SimpleDAO) VoteOnProposal(id uint64, option dao.VoteOption) error { // Check the votes to see if quorum is reached var ( - majorityPower = s.membStore.getMajorityPower() totalPower = s.membStore.getTotalPower() + majorityPower = (2 * totalPower) / 3 ) switch { diff --git a/examples/gno.land/p/demo/simpledao/membstore.gno b/examples/gno.land/p/demo/simpledao/membstore.gno index 062c126586d..89754291dac 100644 --- a/examples/gno.land/p/demo/simpledao/membstore.gno +++ b/examples/gno.land/p/demo/simpledao/membstore.gno @@ -26,17 +26,13 @@ func WithInitialMembers(members []dao.Member) MembStoreOption { store.totalVotingPower += m.VotingPower } - - store.majorityPower = (2 * store.totalVotingPower) / 3 } } // MembStore implements the dao.MembStore abstraction type MembStore struct { - members *avl.Tree // std.Address -> dao.Member - - totalVotingPower uint64 // cached value for quick lookups - majorityPower uint64 // cached value for quick lookups + members *avl.Tree // std.Address -> dao.Member + totalVotingPower uint64 // cached value for quick lookups } // NewMembStore creates a new simpledao member store @@ -44,7 +40,6 @@ func NewMembStore(opts ...MembStoreOption) *MembStore { m := &MembStore{ members: avl.NewTree(), totalVotingPower: 0, - majorityPower: 0, } // Apply the options @@ -70,7 +65,6 @@ func (m *MembStore) AddMember(member dao.Member) error { // Update the total voting power m.totalVotingPower += member.VotingPower - m.majorityPower = (2 * m.totalVotingPower) / 3 return nil } @@ -158,12 +152,6 @@ func (m *MembStore) Size() int { return m.members.Size() } -// getMajorityPower returns the majority voting power -// of the member store -func (m *MembStore) getMajorityPower() uint64 { - return m.majorityPower -} - // getTotalPower returns the total voting power // of the member store func (m *MembStore) getTotalPower() uint64 { From d3204930c11c7db5a20ef8bb0cc1bdc14d19f68b Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Tue, 16 Jul 2024 14:38:27 +0200 Subject: [PATCH 44/80] Update voting power --- examples/gno.land/p/demo/simpledao/membstore.gno | 14 +++++++++++--- .../gno.land/p/demo/simpledao/membstore_test.gno | 4 ++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/examples/gno.land/p/demo/simpledao/membstore.gno b/examples/gno.land/p/demo/simpledao/membstore.gno index 89754291dac..6d65cbbc18e 100644 --- a/examples/gno.land/p/demo/simpledao/membstore.gno +++ b/examples/gno.land/p/demo/simpledao/membstore.gno @@ -74,15 +74,19 @@ func (m *MembStore) UpdateMember(address std.Address, member dao.Member) error { return ErrNotGovDAO } - // Check if the member exists - if !m.IsMember(address) { - return ErrMissingMember + // Get the member + oldMember, err := m.GetMember(address) + if err != nil { + return err } // Check if this is a removal request if member.VotingPower == 0 { m.members.Remove(address.String()) + // Update the total voting power + m.totalVotingPower -= oldMember.VotingPower + return nil } @@ -102,6 +106,10 @@ func (m *MembStore) UpdateMember(address std.Address, member dao.Member) error { // Save the new member info m.members.Set(member.Address.String(), member) + // Update the total voting power + difference := member.VotingPower - oldMember.VotingPower + m.totalVotingPower += difference + return nil } diff --git a/examples/gno.land/p/demo/simpledao/membstore_test.gno b/examples/gno.land/p/demo/simpledao/membstore_test.gno index a264579ece6..250bd3f2f9b 100644 --- a/examples/gno.land/p/demo/simpledao/membstore_test.gno +++ b/examples/gno.land/p/demo/simpledao/membstore_test.gno @@ -259,12 +259,16 @@ func TestMembStore_UpdateMember(t *testing.T) { members := generateMembers(t, 1) m := NewMembStore(WithInitialMembers(members)) + oldVotingPower := m.totalVotingPower + urequire.Equal(t, members[0].VotingPower, oldVotingPower) + votingPower := uint64(300) members[0].VotingPower = votingPower // Attempt to update a member uassert.NoError(t, m.UpdateMember(members[0].Address, members[0])) uassert.Equal(t, votingPower, m.GetMembers(0, 10)[0].VotingPower) + urequire.Equal(t, votingPower, m.totalVotingPower) }) t.Run("member removed", func(t *testing.T) { From d33b0dfcfbb3f12db2eb09ff59d4ac2bdf6c9988 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Tue, 16 Jul 2024 15:00:33 +0200 Subject: [PATCH 45/80] Please the standards police --- examples/gno.land/p/demo/simpledao/dao.gno | 16 +++---- .../gno.land/p/demo/simpledao/dao_test.gno | 10 ++-- .../gno.land/p/demo/simpledao/membstore.gno | 6 +-- .../p/demo/simpledao/membstore_test.gno | 12 ++--- .../gno.land/p/demo/simpledao/propstore.gno | 16 +++---- .../p/demo/simpledao/propstore_test.gno | 48 +++++++++---------- examples/gno.land/p/gov/dao/members.gno | 8 ++-- examples/gno.land/p/gov/dao/proposals.gno | 32 ++++++------- examples/gno.land/p/gov/dao/vote.gno | 12 ++--- examples/gno.land/r/gov/dao/v2/dao.gno | 26 +++++----- 10 files changed, 93 insertions(+), 93 deletions(-) diff --git a/examples/gno.land/p/demo/simpledao/dao.gno b/examples/gno.land/p/demo/simpledao/dao.gno index 3213082c30a..34fb3c1a8a0 100644 --- a/examples/gno.land/p/demo/simpledao/dao.gno +++ b/examples/gno.land/p/demo/simpledao/dao.gno @@ -79,13 +79,13 @@ func (s *SimpleDAO) Propose(description string, executor dao.Executor) (uint64, func (s *SimpleDAO) VoteOnProposal(id uint64, option dao.VoteOption) error { // Verify the GOVDAO member - member, err := s.membStore.GetMember(getDAOCaller()) + member, err := s.membStore.Member(getDAOCaller()) if err != nil { return ufmt.Errorf("unable to get govdao member, %s", err.Error()) } // Check if the proposal exists - propRaw, err := s.propStore.GetProposalByID(id) + propRaw, err := s.propStore.ProposalByID(id) if err != nil { return ufmt.Errorf("unable to get proposal %d, %s", id, err.Error()) } @@ -93,8 +93,8 @@ func (s *SimpleDAO) VoteOnProposal(id uint64, option dao.VoteOption) error { prop := propRaw.(*proposal) // Check the proposal status - if prop.GetStatus() == dao.ExecutionSuccessful || - prop.GetStatus() == dao.ExecutionFailed { + if prop.Status() == dao.ExecutionSuccessful || + prop.Status() == dao.ExecutionFailed { // Proposal was already executed, nothing to vote on anymore. // // In fact, the proposal should stop accepting @@ -155,7 +155,7 @@ func (s *SimpleDAO) ExecuteProposal(id uint64) error { } // Check if the proposal exists - propRaw, err := s.propStore.GetProposalByID(id) + propRaw, err := s.propStore.ProposalByID(id) if err != nil { return ufmt.Errorf("unable to get proposal %d, %s", id, err.Error()) } @@ -163,14 +163,14 @@ func (s *SimpleDAO) ExecuteProposal(id uint64) error { prop := propRaw.(*proposal) // Check if the proposal is executed - if prop.GetStatus() == dao.ExecutionSuccessful || - prop.GetStatus() == dao.ExecutionFailed { + if prop.Status() == dao.ExecutionSuccessful || + prop.Status() == dao.ExecutionFailed { // Proposal is already executed return ErrProposalExecuted } // Check the proposal status - if prop.GetStatus() != dao.Accepted { + if prop.Status() != dao.Accepted { // Proposal is not accepted, cannot be executed return ErrProposalNotAccepted } diff --git a/examples/gno.land/p/demo/simpledao/dao_test.gno b/examples/gno.land/p/demo/simpledao/dao_test.gno index 33ef0a67549..a2c3823ab40 100644 --- a/examples/gno.land/p/demo/simpledao/dao_test.gno +++ b/examples/gno.land/p/demo/simpledao/dao_test.gno @@ -105,13 +105,13 @@ func TestSimpleDAO_Propose(t *testing.T) { uassert.False(t, called) // Make sure the proposal exists - prop, err := s.propStore.GetProposalByID(id) + prop, err := s.propStore.ProposalByID(id) uassert.NoError(t, err) - uassert.Equal(t, proposer.String(), prop.GetAuthor().String()) - uassert.Equal(t, description, prop.GetDescription()) - uassert.Equal(t, dao.Active.String(), prop.GetStatus().String()) - uassert.Equal(t, 0, len(prop.GetVotes(0, maxRequestVotes))) + uassert.Equal(t, proposer.String(), prop.Author().String()) + uassert.Equal(t, description, prop.Description()) + uassert.Equal(t, dao.Active.String(), prop.Status().String()) + uassert.Equal(t, 0, len(prop.Votes(0, maxRequestVotes))) }) } diff --git a/examples/gno.land/p/demo/simpledao/membstore.gno b/examples/gno.land/p/demo/simpledao/membstore.gno index 6d65cbbc18e..f1d8ef40235 100644 --- a/examples/gno.land/p/demo/simpledao/membstore.gno +++ b/examples/gno.land/p/demo/simpledao/membstore.gno @@ -75,7 +75,7 @@ func (m *MembStore) UpdateMember(address std.Address, member dao.Member) error { } // Get the member - oldMember, err := m.GetMember(address) + oldMember, err := m.Member(address) if err != nil { return err } @@ -119,7 +119,7 @@ func (m *MembStore) IsMember(address std.Address) bool { return exists } -func (m *MembStore) GetMember(address std.Address) (dao.Member, error) { +func (m *MembStore) Member(address std.Address) (dao.Member, error) { member, exists := m.members.Get(address.String()) if !exists { return dao.Member{}, ErrMissingMember @@ -128,7 +128,7 @@ func (m *MembStore) GetMember(address std.Address) (dao.Member, error) { return member.(dao.Member), nil } -func (m *MembStore) GetMembers(offset, count uint64) []dao.Member { +func (m *MembStore) Members(offset, count uint64) []dao.Member { // Calculate the left and right bounds if count < 1 || offset >= uint64(m.members.Size()) { return []dao.Member{} diff --git a/examples/gno.land/p/demo/simpledao/membstore_test.gno b/examples/gno.land/p/demo/simpledao/membstore_test.gno index 250bd3f2f9b..8189b07e336 100644 --- a/examples/gno.land/p/demo/simpledao/membstore_test.gno +++ b/examples/gno.land/p/demo/simpledao/membstore_test.gno @@ -37,7 +37,7 @@ func TestMembStore_GetMember(t *testing.T) { // Create an empty store m := NewMembStore() - _, err := m.GetMember(testutils.TestAddress("random")) + _, err := m.Member(testutils.TestAddress("random")) uassert.ErrorIs(t, err, ErrMissingMember) }) @@ -49,7 +49,7 @@ func TestMembStore_GetMember(t *testing.T) { m := NewMembStore(WithInitialMembers(members)) - _, err := m.GetMember(members[0].Address) + _, err := m.Member(members[0].Address) uassert.NoError(t, err) }) } @@ -63,7 +63,7 @@ func TestMembStore_GetMembers(t *testing.T) { // Create an empty store m := NewMembStore() - members := m.GetMembers(0, 10) + members := m.Members(0, 10) uassert.Equal(t, 0, len(members)) }) @@ -92,14 +92,14 @@ func TestMembStore_GetMembers(t *testing.T) { urequire.Equal(t, numMembers, m.Size()) - fetchedMembers := m.GetMembers(0, uint64(halfRange)) + fetchedMembers := m.Members(0, uint64(halfRange)) urequire.Equal(t, halfRange, len(fetchedMembers)) // Verify the members verifyMembersPresent(members, fetchedMembers) // Fetch the other half - fetchedMembers = m.GetMembers(uint64(halfRange), uint64(halfRange)) + fetchedMembers = m.Members(uint64(halfRange), uint64(halfRange)) urequire.Equal(t, halfRange, len(fetchedMembers)) // Verify the members @@ -267,7 +267,7 @@ func TestMembStore_UpdateMember(t *testing.T) { // Attempt to update a member uassert.NoError(t, m.UpdateMember(members[0].Address, members[0])) - uassert.Equal(t, votingPower, m.GetMembers(0, 10)[0].VotingPower) + uassert.Equal(t, votingPower, m.Members(0, 10)[0].VotingPower) urequire.Equal(t, votingPower, m.totalVotingPower) }) diff --git a/examples/gno.land/p/demo/simpledao/propstore.gno b/examples/gno.land/p/demo/simpledao/propstore.gno index fc3fea1e2ab..67c7ae67add 100644 --- a/examples/gno.land/p/demo/simpledao/propstore.gno +++ b/examples/gno.land/p/demo/simpledao/propstore.gno @@ -40,19 +40,19 @@ type proposal struct { getTotalVotingPowerFn func() uint64 // total voting power of the voting body } -func (p *proposal) GetAuthor() std.Address { +func (p *proposal) Author() std.Address { return p.author } -func (p *proposal) GetDescription() string { +func (p *proposal) Description() string { return p.description } -func (p *proposal) GetStatus() dao.ProposalStatus { +func (p *proposal) Status() dao.ProposalStatus { return p.status } -func (p *proposal) GetVotes(offset, count uint64) []dao.Vote { +func (p *proposal) Votes(offset, count uint64) []dao.Vote { // Calculate the left and right bounds if count < 1 || offset >= uint64(p.votes.voters.Size()) { return []dao.Vote{} @@ -83,7 +83,7 @@ func (p *proposal) GetVotes(offset, count uint64) []dao.Vote { return votes } -func (p *proposal) GetVotingStats() dao.VotingStats { +func (p *proposal) VotingStats() dao.VotingStats { // Get the total voting power of the body totalPower := p.getTotalVotingPowerFn() @@ -95,7 +95,7 @@ func (p *proposal) GetVotingStats() dao.VotingStats { } } -func (p *proposal) GetVoteByMember(address std.Address) (dao.Vote, error) { +func (p *proposal) VoteByMember(address std.Address) (dao.Vote, error) { optionRaw, exists := p.votes.voters.Get(address.String()) if !exists { return dao.Vote{}, ErrMissingVote @@ -132,7 +132,7 @@ func (p *PropStore) addProposal(proposal *proposal) (uint64, error) { return nextID, nil } -func (p *PropStore) GetProposals(offset, count uint64) []dao.Proposal { +func (p *PropStore) Proposals(offset, count uint64) []dao.Proposal { // Check the requested count if count < 1 { return []dao.Proposal{} @@ -177,7 +177,7 @@ func (p *PropStore) GetProposals(offset, count uint64) []dao.Proposal { return props } -func (p *PropStore) GetProposalByID(id uint64) (dao.Proposal, error) { +func (p *PropStore) ProposalByID(id uint64) (dao.Proposal, error) { prop, exists := p.proposals.Get(getProposalID(id)) if !exists { return nil, ErrMissingProposal diff --git a/examples/gno.land/p/demo/simpledao/propstore_test.gno b/examples/gno.land/p/demo/simpledao/propstore_test.gno index 533fe8edfc3..27dbc63973b 100644 --- a/examples/gno.land/p/demo/simpledao/propstore_test.gno +++ b/examples/gno.land/p/demo/simpledao/propstore_test.gno @@ -39,30 +39,30 @@ func equalProposals(t *testing.T, p1, p2 dao.Proposal) { uassert.Equal( t, - p1.GetAuthor().String(), - p2.GetAuthor().String(), + p1.Author().String(), + p2.Author().String(), ) uassert.Equal( t, - p1.GetDescription(), - p2.GetDescription(), + p1.Description(), + p2.Description(), ) uassert.Equal( t, - p1.GetStatus().String(), - p2.GetStatus().String(), + p1.Status().String(), + p2.Status().String(), ) uassert.Equal( t, - len(p1.GetVotes(0, maxRequestVotes)), - len(p2.GetVotes(0, maxRequestVotes)), + len(p1.Votes(0, maxRequestVotes)), + len(p2.Votes(0, maxRequestVotes)), ) - p1Votes := p1.GetVotes(0, maxRequestVotes) - for index, v2 := range p2.GetVotes(0, maxRequestVotes) { + p1Votes := p1.Votes(0, maxRequestVotes) + for index, v2 := range p2.Votes(0, maxRequestVotes) { uassert.Equal( t, p1Votes[index].Address.String(), @@ -87,7 +87,7 @@ func TestProposal_Data(t *testing.T) { author: testutils.TestAddress("address"), } - uassert.Equal(t, p.author, p.GetAuthor()) + uassert.Equal(t, p.author, p.Author()) }) t.Run("description", func(t *testing.T) { @@ -97,7 +97,7 @@ func TestProposal_Data(t *testing.T) { description: "example proposal description", } - uassert.Equal(t, p.description, p.GetDescription()) + uassert.Equal(t, p.description, p.Description()) }) t.Run("status", func(t *testing.T) { @@ -107,7 +107,7 @@ func TestProposal_Data(t *testing.T) { status: dao.ExecutionSuccessful, } - uassert.Equal(t, p.status.String(), p.GetStatus().String()) + uassert.Equal(t, p.status.String(), p.Status().String()) }) t.Run("no votes", func(t *testing.T) { @@ -117,7 +117,7 @@ func TestProposal_Data(t *testing.T) { votes: newVotes(), } - uassert.Equal(t, 0, len(p.GetVotes(0, maxRequestVotes))) + uassert.Equal(t, 0, len(p.Votes(0, maxRequestVotes))) }) t.Run("existing votes", func(t *testing.T) { @@ -134,7 +134,7 @@ func TestProposal_Data(t *testing.T) { urequire.NoError(t, p.votes.castVote(m, dao.YesVote)) } - votes := p.GetVotes(0, maxRequestVotes) + votes := p.Votes(0, maxRequestVotes) urequire.Equal(t, len(members), len(votes)) @@ -163,10 +163,10 @@ func TestProposal_Data(t *testing.T) { urequire.NoError(t, p.votes.castVote(m, dao.YesVote)) } - votes := p.GetVotes(0, maxRequestVotes) + votes := p.Votes(0, maxRequestVotes) urequire.Equal(t, len(members), len(votes)) - vote, err := p.GetVoteByMember(members[0].Address) + vote, err := p.VoteByMember(members[0].Address) urequire.NoError(t, err) uassert.Equal(t, dao.YesVote.String(), vote.Option.String()) @@ -180,10 +180,10 @@ func TestProposal_Data(t *testing.T) { votes: newVotes(), } - votes := p.GetVotes(0, maxRequestVotes) + votes := p.Votes(0, maxRequestVotes) urequire.Equal(t, 0, len(votes)) - _, err := p.GetVoteByMember(testutils.TestAddress("dummy")) + _, err := p.VoteByMember(testutils.TestAddress("dummy")) uassert.ErrorIs(t, err, ErrMissingVote) }) } @@ -197,7 +197,7 @@ func TestPropStore_GetProposals(t *testing.T) { p := NewPropStore() uassert.Equal(t, 0, p.Size()) - proposals := p.GetProposals(0, 0) + proposals := p.Proposals(0, 0) uassert.Equal(t, 0, len(proposals)) }) @@ -222,7 +222,7 @@ func TestPropStore_GetProposals(t *testing.T) { uassert.Equal(t, numProposals, p.Size()) - fetchedProposals := p.GetProposals(0, uint64(halfRange)) + fetchedProposals := p.Proposals(0, uint64(halfRange)) urequire.Equal(t, halfRange, len(fetchedProposals)) for index, fetchedProposal := range fetchedProposals { @@ -230,7 +230,7 @@ func TestPropStore_GetProposals(t *testing.T) { } // Fetch the other half - fetchedProposals = p.GetProposals(uint64(halfRange), uint64(halfRange)) + fetchedProposals = p.Proposals(uint64(halfRange), uint64(halfRange)) urequire.Equal(t, halfRange, len(fetchedProposals)) for index, fetchedProposal := range fetchedProposals { @@ -247,7 +247,7 @@ func TestPropStore_GetProposalByID(t *testing.T) { p := NewPropStore() - _, err := p.GetProposalByID(0) + _, err := p.ProposalByID(0) uassert.ErrorIs(t, err, ErrMissingProposal) }) @@ -264,7 +264,7 @@ func TestPropStore_GetProposalByID(t *testing.T) { urequire.NoError(t, err) // Fetch the proposal - fetchedProposal, err := p.GetProposalByID(0) + fetchedProposal, err := p.ProposalByID(0) urequire.NoError(t, err) equalProposals(t, proposal, fetchedProposal) diff --git a/examples/gno.land/p/gov/dao/members.gno b/examples/gno.land/p/gov/dao/members.gno index 0d15422c60c..86dfe4b55f1 100644 --- a/examples/gno.land/p/gov/dao/members.gno +++ b/examples/gno.land/p/gov/dao/members.gno @@ -6,8 +6,8 @@ import ( // MemberStore defines the member storage abstraction type MemberStore interface { - // GetMembers returns all members in the store - GetMembers(offset, count uint64) []Member + // Members returns all members in the store + Members(offset, count uint64) []Member // Size returns the current size of the store Size() int @@ -16,8 +16,8 @@ type MemberStore interface { // belongs to a member IsMember(address std.Address) bool - // GetMember returns the requested member - GetMember(address std.Address) (Member, error) + // Member returns the requested member + Member(address std.Address) (Member, error) // AddMember adds a member to the store AddMember(member Member) error diff --git a/examples/gno.land/p/gov/dao/proposals.gno b/examples/gno.land/p/gov/dao/proposals.gno index 2c3e9fc29b9..a48916de46b 100644 --- a/examples/gno.land/p/gov/dao/proposals.gno +++ b/examples/gno.land/p/gov/dao/proposals.gno @@ -20,12 +20,12 @@ func (s ProposalStatus) String() string { // PropStore defines the proposal storage abstraction type PropStore interface { - // GetProposals returns the given paginated proposals - GetProposals(offset, count uint64) []Proposal + // Proposals returns the given paginated proposals + Proposals(offset, count uint64) []Proposal - // GetProposalByID returns the proposal associated with + // ProposalByID returns the proposal associated with // the given ID, if any - GetProposalByID(id uint64) (Proposal, error) + ProposalByID(id uint64) (Proposal, error) // Size returns the number of proposals in // the proposal store @@ -34,21 +34,21 @@ type PropStore interface { // Proposal is the single proposal abstraction type Proposal interface { - // GetAuthor returns the author of the proposal - GetAuthor() std.Address + // Author returns the author of the proposal + Author() std.Address - // GetDescription returns the description of the proposal - GetDescription() string + // Description returns the description of the proposal + Description() string - // GetStatus returns the status of the proposal - GetStatus() ProposalStatus + // Status returns the status of the proposal + Status() ProposalStatus - // GetVotes returns the votes of the proposal - GetVotes(offset, count uint64) []Vote + // Votes returns the votes of the proposal + Votes(offset, count uint64) []Vote - // GetVotingStats returns the voting stats of the proposal - GetVotingStats() VotingStats + // VotingStats returns the voting stats of the proposal + VotingStats() VotingStats - // GetVoteByMember returns the proposal vote by the member, if any - GetVoteByMember(address std.Address) (Vote, error) + // VoteByMember returns the proposal vote by the member, if any + VoteByMember(address std.Address) (Vote, error) } diff --git a/examples/gno.land/p/gov/dao/vote.gno b/examples/gno.land/p/gov/dao/vote.gno index 7ca923d001f..d8825ca9bcb 100644 --- a/examples/gno.land/p/gov/dao/vote.gno +++ b/examples/gno.land/p/gov/dao/vote.gno @@ -31,22 +31,22 @@ type VotingStats struct { TotalVotingPower uint64 } -func (v VotingStats) GetYayPercent() uint64 { +func (v VotingStats) YayPercent() uint64 { return (v.YayVotes / v.TotalVotingPower) * 100 } -func (v VotingStats) GetNayPercent() uint64 { +func (v VotingStats) NayPercent() uint64 { return (v.NayVotes / v.TotalVotingPower) * 100 } -func (v VotingStats) GetAbstainPercent() uint64 { +func (v VotingStats) AbstainPercent() uint64 { return (v.AbstainVotes / v.TotalVotingPower) * 100 } -func (v VotingStats) GetMissingVotes() uint64 { +func (v VotingStats) MissingVotes() uint64 { return v.TotalVotingPower - (v.YayVotes + v.NayVotes + v.AbstainVotes) } -func (v VotingStats) GetMissingVotesPercent() uint64 { - return (v.GetMissingVotes() / v.TotalVotingPower) * 100 +func (v VotingStats) MissingVotesPercent() uint64 { + return (v.MissingVotes() / v.TotalVotingPower) * 100 } diff --git a/examples/gno.land/r/gov/dao/v2/dao.gno b/examples/gno.land/r/gov/dao/v2/dao.gno index c12ad025405..f08c36f8700 100644 --- a/examples/gno.land/r/gov/dao/v2/dao.gno +++ b/examples/gno.land/r/gov/dao/v2/dao.gno @@ -88,14 +88,14 @@ func Render(path string) string { } // Fetch the last 10 proposals - for idx, prop := range proposals.GetProposals(offset, uint64(10)) { + for idx, prop := range proposals.Proposals(offset, uint64(10)) { output += ufmt.Sprintf( "- [Proposal #%d](%s:%d) - (**%s**)(by %s)\n", idx, "/r/gov/dao/v2", idx, - prop.GetStatus().String(), - prop.GetAuthor().String(), + prop.Status().String(), + prop.Author().String(), ) } @@ -109,33 +109,33 @@ func Render(path string) string { } // Fetch the proposal - prop, err := proposals.GetProposalByID(uint64(idx)) + prop, err := proposals.ProposalByID(uint64(idx)) if err != nil { return ufmt.Sprintf("unable to fetch proposal, %s", err.Error()) } // Fetch the voting stats - stats := prop.GetVotingStats() + stats := prop.VotingStats() output := "" output += ufmt.Sprintf("# Prop #%d", idx) output += "\n\n" - output += ufmt.Sprintf("Author: %s", prop.GetAuthor().String()) + output += ufmt.Sprintf("Author: %s", prop.Author().String()) output += "\n\n" - output += prop.GetDescription() + output += prop.Description() output += "\n\n" - output += ufmt.Sprintf("Status: %s", prop.GetStatus().String()) + output += ufmt.Sprintf("Status: %s", prop.Status().String()) output += "\n\n" output += ufmt.Sprintf( "Voting stats: YAY %d (%d%%), NAY %d (%d%%), ABSTAIN %d (%d%%), HAVEN'T VOTED %d (%d%%)", stats.YayVotes, - stats.GetYayPercent(), + stats.YayPercent(), stats.NayVotes, - stats.GetNayPercent(), + stats.NayPercent(), stats.AbstainVotes, - stats.GetAbstainPercent(), - stats.GetMissingVotes(), - stats.GetMissingVotesPercent(), + stats.AbstainPercent(), + stats.MissingVotes(), + stats.MissingVotesPercent(), ) output += "\n\n" output += ufmt.Sprintf("Threshold met: %t", stats.YayVotes > (2*stats.TotalVotingPower)/3) From 79be838a9872ea80913424370e5ba468c09cb8e0 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Tue, 16 Jul 2024 19:04:32 +0200 Subject: [PATCH 46/80] Slim down the DAO API --- examples/gno.land/p/gov/dao/dao.gno | 3 --- 1 file changed, 3 deletions(-) diff --git a/examples/gno.land/p/gov/dao/dao.gno b/examples/gno.land/p/gov/dao/dao.gno index 2452f01e2cd..e4fcfb73810 100644 --- a/examples/gno.land/p/gov/dao/dao.gno +++ b/examples/gno.land/p/gov/dao/dao.gno @@ -6,9 +6,6 @@ type DAO interface { // Returns the generated proposal ID Propose(comment string, executor 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 } From 8c528935aee664da048c2c03146abc8e37edf5f7 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Tue, 16 Jul 2024 19:11:27 +0200 Subject: [PATCH 47/80] Revert "Slim down the DAO API" This reverts commit 79be838a9872ea80913424370e5ba468c09cb8e0. --- examples/gno.land/p/gov/dao/dao.gno | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/gno.land/p/gov/dao/dao.gno b/examples/gno.land/p/gov/dao/dao.gno index e4fcfb73810..2452f01e2cd 100644 --- a/examples/gno.land/p/gov/dao/dao.gno +++ b/examples/gno.land/p/gov/dao/dao.gno @@ -6,6 +6,9 @@ type DAO interface { // Returns the generated proposal ID Propose(comment string, executor 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 } From 74948fcdb13b276830857359bc83a7c20d6085bf Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Tue, 16 Jul 2024 19:51:56 +0200 Subject: [PATCH 48/80] Add Executor to proposal --- examples/gno.land/p/demo/simpledao/dao.gno | 8 +++--- .../gno.land/p/demo/simpledao/dao_test.gno | 11 +++++--- .../gno.land/p/demo/simpledao/propstore.gno | 4 +++ .../p/demo/simpledao/propstore_test.gno | 26 +++++++++++++++++++ examples/gno.land/p/gov/dao/dao.gno | 10 ++++++- examples/gno.land/p/gov/dao/proposals.gno | 3 +++ examples/gno.land/r/gov/dao/v2/dao.gno | 4 +-- 7 files changed, 56 insertions(+), 10 deletions(-) diff --git a/examples/gno.land/p/demo/simpledao/dao.gno b/examples/gno.land/p/demo/simpledao/dao.gno index 34fb3c1a8a0..fa81cdbdd67 100644 --- a/examples/gno.land/p/demo/simpledao/dao.gno +++ b/examples/gno.land/p/demo/simpledao/dao.gno @@ -41,9 +41,9 @@ func New(membStore *MembStore, propStore *PropStore) *SimpleDAO { } } -func (s *SimpleDAO) Propose(description string, executor dao.Executor) (uint64, error) { +func (s *SimpleDAO) Propose(request dao.ProposalRequest) (uint64, error) { // Make sure the executor is set - if executor == nil { + if request.Executor == nil { return 0, ErrInvalidExecutor } @@ -61,8 +61,8 @@ func (s *SimpleDAO) Propose(description string, executor dao.Executor) (uint64, // Create the wrapped proposal prop := &proposal{ author: caller, - description: description, - executor: executor, + description: request.Description, + executor: request.Executor, status: dao.Active, votes: newVotes(), getTotalVotingPowerFn: s.membStore.getTotalPower, diff --git a/examples/gno.land/p/demo/simpledao/dao_test.gno b/examples/gno.land/p/demo/simpledao/dao_test.gno index a2c3823ab40..eb4174ec05c 100644 --- a/examples/gno.land/p/demo/simpledao/dao_test.gno +++ b/examples/gno.land/p/demo/simpledao/dao_test.gno @@ -20,7 +20,7 @@ func TestSimpleDAO_Propose(t *testing.T) { s := &SimpleDAO{} - _, err := s.Propose("", nil) + _, err := s.Propose(dao.ProposalRequest{}) uassert.ErrorIs( t, err, @@ -56,7 +56,9 @@ func TestSimpleDAO_Propose(t *testing.T) { // than the proposal fee std.TestSetOrigSend(sentCoins, std.Coins{}) - _, err := s.Propose("", ex) + _, err := s.Propose(dao.ProposalRequest{ + Executor: ex, + }) uassert.ErrorIs( t, err, @@ -100,7 +102,10 @@ func TestSimpleDAO_Propose(t *testing.T) { std.TestSetOrigCaller(proposer) // Make sure the proposal was added - id, err := s.Propose(description, ex) + id, err := s.Propose(dao.ProposalRequest{ + Description: description, + Executor: ex, + }) uassert.NoError(t, err) uassert.False(t, called) diff --git a/examples/gno.land/p/demo/simpledao/propstore.gno b/examples/gno.land/p/demo/simpledao/propstore.gno index 67c7ae67add..fbcb43a24e2 100644 --- a/examples/gno.land/p/demo/simpledao/propstore.gno +++ b/examples/gno.land/p/demo/simpledao/propstore.gno @@ -52,6 +52,10 @@ func (p *proposal) Status() dao.ProposalStatus { return p.status } +func (p *proposal) Executor() dao.Executor { + return p.executor +} + func (p *proposal) Votes(offset, count uint64) []dao.Vote { // Calculate the left and right bounds if count < 1 || offset >= uint64(p.votes.voters.Size()) { diff --git a/examples/gno.land/p/demo/simpledao/propstore_test.gno b/examples/gno.land/p/demo/simpledao/propstore_test.gno index 27dbc63973b..9c5b1db8d8a 100644 --- a/examples/gno.land/p/demo/simpledao/propstore_test.gno +++ b/examples/gno.land/p/demo/simpledao/propstore_test.gno @@ -110,6 +110,32 @@ func TestProposal_Data(t *testing.T) { uassert.Equal(t, p.status.String(), p.Status().String()) }) + t.Run("executor", func(t *testing.T) { + t.Parallel() + + var ( + numCalled = 0 + cb = func() error { + numCalled++ + + return nil + } + + ex = &mockExecutor{ + executeFn: cb, + } + + p = &proposal{ + executor: ex, + } + ) + + urequire.NoError(t, p.executor.Execute()) + urequire.NoError(t, p.Executor().Execute()) + + uassert.Equal(t, 2, numCalled) + }) + t.Run("no votes", func(t *testing.T) { t.Parallel() diff --git a/examples/gno.land/p/gov/dao/dao.gno b/examples/gno.land/p/gov/dao/dao.gno index 2452f01e2cd..471e0565350 100644 --- a/examples/gno.land/p/gov/dao/dao.gno +++ b/examples/gno.land/p/gov/dao/dao.gno @@ -1,10 +1,18 @@ package dao +// 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 { // Propose adds a new proposal to the executor-based GOVDAO. // Returns the generated proposal ID - Propose(comment string, executor Executor) (uint64, error) + Propose(request ProposalRequest) (uint64, error) // VoteOnProposal adds a vote to the given proposal ID VoteOnProposal(id uint64, option VoteOption) error diff --git a/examples/gno.land/p/gov/dao/proposals.gno b/examples/gno.land/p/gov/dao/proposals.gno index a48916de46b..48ce4113441 100644 --- a/examples/gno.land/p/gov/dao/proposals.gno +++ b/examples/gno.land/p/gov/dao/proposals.gno @@ -43,6 +43,9 @@ type Proposal interface { // Status returns the status of the proposal Status() ProposalStatus + // Executor returns the proposal executor + Executor() Executor + // Votes returns the votes of the proposal Votes(offset, count uint64) []Vote diff --git a/examples/gno.land/r/gov/dao/v2/dao.gno b/examples/gno.land/r/gov/dao/v2/dao.gno index f08c36f8700..66f9079c732 100644 --- a/examples/gno.land/r/gov/dao/v2/dao.gno +++ b/examples/gno.land/r/gov/dao/v2/dao.gno @@ -39,8 +39,8 @@ func init() { // Propose is designed to be called by another contract or with // `maketx run`, not by a `maketx call`. -func Propose(comment string, executor dao.Executor) uint64 { - idx, err := d.Propose(comment, executor) +func Propose(request dao.ProposalRequest) uint64 { + idx, err := d.Propose(request) if err != nil { panic(err) } From c9b4cc4d5910e96b5f884c532dc3f29bc544c89a Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Wed, 17 Jul 2024 10:42:28 +0200 Subject: [PATCH 49/80] Fix up old API --- .../gno.land/r/gnoland/valopers/v2/valopers.gno | 12 +++++++++--- examples/gno.land/r/gov/dao/v2/prop1_filetest.gno | 12 +++++++++--- examples/gno.land/r/gov/dao/v2/prop2_filetest.gno | 15 +++++++++++---- examples/gno.land/r/sys/vars/prop_filetest.gno | 2 +- 4 files changed, 30 insertions(+), 11 deletions(-) diff --git a/examples/gno.land/r/gnoland/valopers/v2/valopers.gno b/examples/gno.land/r/gnoland/valopers/v2/valopers.gno index eb70e45c020..28c25344c1d 100644 --- a/examples/gno.land/r/gnoland/valopers/v2/valopers.gno +++ b/examples/gno.land/r/gnoland/valopers/v2/valopers.gno @@ -7,6 +7,7 @@ import ( "gno.land/p/demo/avl" "gno.land/p/demo/ufmt" + "gno.land/p/gov/dao" pVals "gno.land/p/sys/validators" govdao "gno.land/r/gov/dao/v2" validators "gno.land/r/sys/validators/v2" @@ -169,14 +170,19 @@ func GovDAOProposal(address std.Address) { // Create the executor executor := validators.NewPropExecutor(changesFn) - // Craft the proposal comment - comment := ufmt.Sprintf( + // Craft the proposal description + description := ufmt.Sprintf( "Proposal to add valoper %s (Address: %s; PubKey: %s) to the valset", valoper.Name, valoper.Address.String(), valoper.PubKey, ) + prop := dao.ProposalRequest{ + Description: description, + Executor: executor, + } + // Create the govdao proposal - govdao.Propose(comment, executor) + govdao.Propose(prop) } diff --git a/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno index cc959dc63c4..f2b51cd3de8 100644 --- a/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno +++ b/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno @@ -41,9 +41,15 @@ func init() { // complete governance proposal process. executor := validators.NewPropExecutor(changesFn) - // Create a proposal. - comment := "manual valset changes proposal example" - govdao.Propose(comment, executor) + // Create a proposal + description := "manual valset changes proposal example" + + prop := dao.ProposalRequest{ + Description: description, + Executor: executor, + } + + govdao.Propose(prop) } func main() { diff --git a/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno index 5afe5eeeca2..4998bcf5d81 100644 --- a/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno +++ b/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno @@ -3,6 +3,7 @@ package main import ( "time" + "gno.land/p/gov/dao" gnoblog "gno.land/r/gnoland/blog" govdao "gno.land/r/gov/dao/v2" ) @@ -12,14 +13,20 @@ func init() { "hello-from-govdao", // slug "Hello from GovDAO!", // title "This post was published by a GovDAO proposal.", // body - time.Now().Format(time.RFC3339), // publidation date + time.Now().Format(time.RFC3339), // publication date "moul", // authors "govdao,example", // tags ) - // Create a proposal. - comment := "post a new blogpost about govdao" - govdao.Propose(comment, ex) + // Create a proposal + description := "post a new blogpost about govdao" + + prop := dao.ProposalRequest{ + Description: description, + Executor: ex, + } + + govdao.Propose(prop) } func main() { diff --git a/examples/gno.land/r/sys/vars/prop_filetest.gno b/examples/gno.land/r/sys/vars/prop_filetest.gno index 23ef5e73c0a..1663d6e5ef9 100644 --- a/examples/gno.land/r/sys/vars/prop_filetest.gno +++ b/examples/gno.land/r/sys/vars/prop_filetest.gno @@ -25,7 +25,7 @@ func init() { // Create the proposal comment := "Example value setting" - govdao.Propose(comment, executor) + govdaoMissingVotesPercent(comment, executor) } func main() { From 93ca6a821de09fef096be710045ec3dec9ee28f1 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Wed, 17 Jul 2024 10:47:49 +0200 Subject: [PATCH 50/80] Mod tidy --- examples/gno.land/r/gnoland/valopers/v2/gno.mod | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/gno.land/r/gnoland/valopers/v2/gno.mod b/examples/gno.land/r/gnoland/valopers/v2/gno.mod index 076a44f7737..03ba2638dfe 100644 --- a/examples/gno.land/r/gnoland/valopers/v2/gno.mod +++ b/examples/gno.land/r/gnoland/valopers/v2/gno.mod @@ -5,6 +5,7 @@ require ( 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/gov/dao v0.0.0-latest gno.land/p/sys/validators v0.0.0-latest gno.land/r/gov/dao/v2 v0.0.0-latest gno.land/r/sys/validators/v2 v0.0.0-latest From ee77102b2dd61bf5f059091c70b6155e68c8df93 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Wed, 17 Jul 2024 14:14:26 +0200 Subject: [PATCH 51/80] Make the PropStore part of the DAO API --- examples/gno.land/p/demo/simpledao/dao.gno | 13 +- .../gno.land/p/demo/simpledao/dao_test.gno | 194 +++++++----------- .../gno.land/p/demo/simpledao/propstore.gno | 33 +-- .../p/demo/simpledao/propstore_test.gno | 30 +-- examples/gno.land/p/gov/dao/dao.gno | 3 + examples/gno.land/r/gov/dao/v2/dao.gno | 36 ++-- examples/gno.land/r/gov/dao/v2/poc.gno | 19 -- 7 files changed, 126 insertions(+), 202 deletions(-) diff --git a/examples/gno.land/p/demo/simpledao/dao.gno b/examples/gno.land/p/demo/simpledao/dao.gno index fa81cdbdd67..3f6b5d9a6fc 100644 --- a/examples/gno.land/p/demo/simpledao/dao.gno +++ b/examples/gno.land/p/demo/simpledao/dao.gno @@ -4,6 +4,7 @@ import ( "errors" "std" + "gno.land/p/demo/avl" "gno.land/p/demo/ufmt" "gno.land/p/gov/dao" ) @@ -29,15 +30,15 @@ var ( // SimpleDAO is a simple DAO implementation type SimpleDAO struct { + proposals *avl.Tree // seqid.ID -> proposal membStore *MembStore - propStore *PropStore } // New creates a new instance of the simpledao DAO -func New(membStore *MembStore, propStore *PropStore) *SimpleDAO { +func New(membStore *MembStore) *SimpleDAO { return &SimpleDAO{ + proposals: avl.NewTree(), membStore: membStore, - propStore: propStore, } } @@ -69,7 +70,7 @@ func (s *SimpleDAO) Propose(request dao.ProposalRequest) (uint64, error) { } // Add the proposal - id, err := s.propStore.addProposal(prop) + id, err := s.addProposal(prop) if err != nil { return 0, ufmt.Errorf("unable to add proposal, %s", err.Error()) } @@ -85,7 +86,7 @@ func (s *SimpleDAO) VoteOnProposal(id uint64, option dao.VoteOption) error { } // Check if the proposal exists - propRaw, err := s.propStore.ProposalByID(id) + propRaw, err := s.ProposalByID(id) if err != nil { return ufmt.Errorf("unable to get proposal %d, %s", id, err.Error()) } @@ -155,7 +156,7 @@ func (s *SimpleDAO) ExecuteProposal(id uint64) error { } // Check if the proposal exists - propRaw, err := s.propStore.ProposalByID(id) + propRaw, err := s.ProposalByID(id) if err != nil { return ufmt.Errorf("unable to get proposal %d, %s", id, err.Error()) } diff --git a/examples/gno.land/p/demo/simpledao/dao_test.gno b/examples/gno.land/p/demo/simpledao/dao_test.gno index eb4174ec05c..e573b4b500a 100644 --- a/examples/gno.land/p/demo/simpledao/dao_test.gno +++ b/examples/gno.land/p/demo/simpledao/dao_test.gno @@ -90,10 +90,7 @@ func TestSimpleDAO_Propose(t *testing.T) { ), ) - s = &SimpleDAO{ - membStore: NewMembStore(), // empty member store - propStore: NewPropStore(), - } + s = New(NewMembStore()) ) // Set the sent coins to enough @@ -110,7 +107,7 @@ func TestSimpleDAO_Propose(t *testing.T) { uassert.False(t, called) // Make sure the proposal exists - prop, err := s.propStore.ProposalByID(id) + prop, err := s.ProposalByID(id) uassert.NoError(t, err) uassert.Equal(t, proposer.String(), prop.Author().String()) @@ -150,14 +147,12 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { var ( voter = testutils.TestAddress("voter") - s = &SimpleDAO{ - membStore: NewMembStore( - WithInitialMembers([]dao.Member{ - {Address: voter}, - }), - ), - propStore: NewPropStore(), - } + ms = NewMembStore( + WithInitialMembers([]dao.Member{ + {Address: voter}, + }), + ) + s = New(ms) ) std.TestSetOrigCaller(voter) @@ -176,14 +171,12 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { var ( voter = testutils.TestAddress("voter") - s = &SimpleDAO{ - membStore: NewMembStore( - WithInitialMembers([]dao.Member{ - {Address: voter}, - }), - ), - propStore: NewPropStore(), - } + ms = NewMembStore( + WithInitialMembers([]dao.Member{ + {Address: voter}, + }), + ) + s = New(ms) prop = &proposal{ status: dao.ExecutionSuccessful, @@ -193,7 +186,7 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { std.TestSetOrigCaller(voter) // Add an initial proposal - id, err := s.propStore.addProposal(prop) + id, err := s.addProposal(prop) urequire.NoError(t, err) // Attempt to vote on the proposal @@ -210,14 +203,12 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { var ( voter = testutils.TestAddress("voter") - s = &SimpleDAO{ - membStore: NewMembStore( - WithInitialMembers([]dao.Member{ - {Address: voter}, - }), - ), - propStore: NewPropStore(), - } + ms = NewMembStore( + WithInitialMembers([]dao.Member{ + {Address: voter}, + }), + ) + s = New(ms) mockExecutor = &mockExecutor{ isExpiredFn: func() bool { @@ -234,7 +225,7 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { std.TestSetOrigCaller(voter) // Add an initial proposal - id, err := s.propStore.addProposal(prop) + id, err := s.addProposal(prop) urequire.NoError(t, err) // Attempt to vote on the proposal @@ -255,14 +246,12 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { VotingPower: 10, } - s = &SimpleDAO{ - membStore: NewMembStore( - WithInitialMembers([]dao.Member{ - member, - }), - ), - propStore: NewPropStore(), - } + ms = NewMembStore( + WithInitialMembers([]dao.Member{ + {Address: voter}, + }), + ) + s = New(ms) prop = &proposal{ status: dao.Active, @@ -277,7 +266,7 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { urequire.NoError(t, prop.votes.castVote(member, dao.YesVote)) // Add an initial proposal - id, err := s.propStore.addProposal(prop) + id, err := s.addProposal(prop) urequire.NoError(t, err) // Attempt to vote on the proposal @@ -294,10 +283,7 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { var ( members = generateMembers(t, 50) - s = &SimpleDAO{ - membStore: NewMembStore(WithInitialMembers(members)), - propStore: NewPropStore(), - } + s = New(NewMembStore(WithInitialMembers(members))) prop = &proposal{ status: dao.Active, @@ -307,7 +293,7 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { ) // Add an initial proposal - id, err := s.propStore.addProposal(prop) + id, err := s.addProposal(prop) urequire.NoError(t, err) majorityIndex := (len(members)*2)/3 + 1 // 2/3+ @@ -331,10 +317,8 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { var ( members = generateMembers(t, 50) - s = &SimpleDAO{ - membStore: NewMembStore(WithInitialMembers(members)), - propStore: NewPropStore(), - } + ms = NewMembStore(WithInitialMembers(members)) + s = New(ms) prop = &proposal{ status: dao.Active, @@ -344,7 +328,7 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { ) // Add an initial proposal - id, err := s.propStore.addProposal(prop) + id, err := s.addProposal(prop) urequire.NoError(t, err) majorityIndex := (len(members)*2)/3 + 1 // 2/3+ @@ -368,10 +352,7 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { var ( members = generateMembers(t, 50) - s = &SimpleDAO{ - membStore: NewMembStore(WithInitialMembers(members)), - propStore: NewPropStore(), - } + s = New(NewMembStore(WithInitialMembers(members))) prop = &proposal{ status: dao.Active, @@ -381,7 +362,7 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { ) // Add an initial proposal - id, err := s.propStore.addProposal(prop) + id, err := s.addProposal(prop) urequire.NoError(t, err) majorityIndex := (len(members)*2)/3 + 1 // 2/3+ @@ -405,10 +386,7 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { var ( members = generateMembers(t, 50) - s = &SimpleDAO{ - membStore: NewMembStore(WithInitialMembers(members)), - propStore: NewPropStore(), - } + s = New(NewMembStore(WithInitialMembers(members))) prop = &proposal{ status: dao.Active, @@ -418,7 +396,7 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { ) // Add an initial proposal - id, err := s.propStore.addProposal(prop) + id, err := s.addProposal(prop) urequire.NoError(t, err) // The first half votes yes @@ -454,10 +432,7 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { var ( members = generateMembers(t, 50) - s = &SimpleDAO{ - membStore: NewMembStore(WithInitialMembers(members)), - propStore: NewPropStore(), - } + s = New(NewMembStore(WithInitialMembers(members))) prop = &proposal{ status: dao.Active, @@ -467,7 +442,7 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { ) // Add an initial proposal - id, err := s.propStore.addProposal(prop) + id, err := s.addProposal(prop) urequire.NoError(t, err) // The first quarter votes yes @@ -512,9 +487,7 @@ func TestSimpleDAO_ExecuteProposal(t *testing.T) { ), ) - s = &SimpleDAO{ - membStore: NewMembStore(), // empty member store - } + s = New(NewMembStore()) ) // Set the sent coins to be lower @@ -539,10 +512,7 @@ func TestSimpleDAO_ExecuteProposal(t *testing.T) { ), ) - s = &SimpleDAO{ - membStore: NewMembStore(), // empty member store - propStore: NewPropStore(), // empty prop store - } + s = New(NewMembStore()) ) // Set the sent coins to be enough @@ -562,14 +532,12 @@ func TestSimpleDAO_ExecuteProposal(t *testing.T) { var ( voter = testutils.TestAddress("voter") - s = &SimpleDAO{ - membStore: NewMembStore( - WithInitialMembers([]dao.Member{ - {Address: voter}, - }), - ), - propStore: NewPropStore(), - } + ms = NewMembStore( + WithInitialMembers([]dao.Member{ + {Address: voter}, + }), + ) + s = New(ms) prop = &proposal{ status: dao.NotAccepted, @@ -579,7 +547,7 @@ func TestSimpleDAO_ExecuteProposal(t *testing.T) { std.TestSetOrigCaller(voter) // Add an initial proposal - id, err := s.propStore.addProposal(prop) + id, err := s.addProposal(prop) urequire.NoError(t, err) // Attempt to vote on the proposal @@ -616,14 +584,12 @@ func TestSimpleDAO_ExecuteProposal(t *testing.T) { var ( voter = testutils.TestAddress("voter") - s = &SimpleDAO{ - membStore: NewMembStore( - WithInitialMembers([]dao.Member{ - {Address: voter}, - }), - ), - propStore: NewPropStore(), - } + ms = NewMembStore( + WithInitialMembers([]dao.Member{ + {Address: voter}, + }), + ) + s = New(ms) prop = &proposal{ status: testCase.status, @@ -633,7 +599,7 @@ func TestSimpleDAO_ExecuteProposal(t *testing.T) { std.TestSetOrigCaller(voter) // Add an initial proposal - id, err := s.propStore.addProposal(prop) + id, err := s.addProposal(prop) urequire.NoError(t, err) // Attempt to vote on the proposal @@ -652,14 +618,12 @@ func TestSimpleDAO_ExecuteProposal(t *testing.T) { var ( voter = testutils.TestAddress("voter") - s = &SimpleDAO{ - membStore: NewMembStore( - WithInitialMembers([]dao.Member{ - {Address: voter}, - }), - ), - propStore: NewPropStore(), - } + ms = NewMembStore( + WithInitialMembers([]dao.Member{ + {Address: voter}, + }), + ) + s = New(ms) mockExecutor = &mockExecutor{ isExpiredFn: func() bool { @@ -676,7 +640,7 @@ func TestSimpleDAO_ExecuteProposal(t *testing.T) { std.TestSetOrigCaller(voter) // Add an initial proposal - id, err := s.propStore.addProposal(prop) + id, err := s.addProposal(prop) urequire.NoError(t, err) // Attempt to vote on the proposal @@ -693,14 +657,12 @@ func TestSimpleDAO_ExecuteProposal(t *testing.T) { var ( voter = testutils.TestAddress("voter") - s = &SimpleDAO{ - membStore: NewMembStore( - WithInitialMembers([]dao.Member{ - {Address: voter}, - }), - ), - propStore: NewPropStore(), - } + ms = NewMembStore( + WithInitialMembers([]dao.Member{ + {Address: voter}, + }), + ) + s = New(ms) execError = errors.New("exec error") @@ -719,7 +681,7 @@ func TestSimpleDAO_ExecuteProposal(t *testing.T) { std.TestSetOrigCaller(voter) // Add an initial proposal - id, err := s.propStore.addProposal(prop) + id, err := s.addProposal(prop) urequire.NoError(t, err) // Attempt to vote on the proposal @@ -738,14 +700,12 @@ func TestSimpleDAO_ExecuteProposal(t *testing.T) { var ( voter = testutils.TestAddress("voter") - s = &SimpleDAO{ - membStore: NewMembStore( - WithInitialMembers([]dao.Member{ - {Address: voter}, - }), - ), - propStore: NewPropStore(), - } + ms = NewMembStore( + WithInitialMembers([]dao.Member{ + {Address: voter}, + }), + ) + s = New(ms) called = false mockExecutor = &mockExecutor{ @@ -765,7 +725,7 @@ func TestSimpleDAO_ExecuteProposal(t *testing.T) { std.TestSetOrigCaller(voter) // Add an initial proposal - id, err := s.propStore.addProposal(prop) + id, err := s.addProposal(prop) urequire.NoError(t, err) // Attempt to vote on the proposal diff --git a/examples/gno.land/p/demo/simpledao/propstore.gno b/examples/gno.land/p/demo/simpledao/propstore.gno index fbcb43a24e2..9cd140780a0 100644 --- a/examples/gno.land/p/demo/simpledao/propstore.gno +++ b/examples/gno.land/p/demo/simpledao/propstore.gno @@ -4,7 +4,6 @@ import ( "errors" "std" - "gno.land/p/demo/avl" "gno.land/p/demo/seqid" "gno.land/p/gov/dao" ) @@ -113,30 +112,18 @@ func (p *proposal) VoteByMember(address std.Address) (dao.Vote, error) { return vote, nil } -// PropStore implements the dao.PropStore abstraction -type PropStore struct { - proposals *avl.Tree // seqid.ID -> proposal -} - -// NewPropStore returns a new simpledao PropStore -func NewPropStore() *PropStore { - return &PropStore{ - proposals: avl.NewTree(), - } -} - // addProposal adds a new simpledao proposal to the store -func (p *PropStore) addProposal(proposal *proposal) (uint64, error) { +func (s *SimpleDAO) addProposal(proposal *proposal) (uint64, error) { // See what the next proposal number should be - nextID := uint64(p.proposals.Size()) + nextID := uint64(s.proposals.Size()) // Save the proposal - p.proposals.Set(getProposalID(nextID), proposal) + s.proposals.Set(getProposalID(nextID), proposal) return nextID, nil } -func (p *PropStore) Proposals(offset, count uint64) []dao.Proposal { +func (s *SimpleDAO) Proposals(offset, count uint64) []dao.Proposal { // Check the requested count if count < 1 { return []dao.Proposal{} @@ -151,7 +138,7 @@ func (p *PropStore) Proposals(offset, count uint64) []dao.Proposal { startIndex = offset endIndex = startIndex + count - numProposals = uint64(p.proposals.Size()) + numProposals = uint64(s.proposals.Size()) ) // Check if the current offset has any proposals @@ -165,7 +152,7 @@ func (p *PropStore) Proposals(offset, count uint64) []dao.Proposal { } props := make([]dao.Proposal, 0) - p.proposals.Iterate( + s.proposals.Iterate( getProposalID(startIndex), getProposalID(endIndex), func(_ string, val interface{}) bool { @@ -181,8 +168,8 @@ func (p *PropStore) Proposals(offset, count uint64) []dao.Proposal { return props } -func (p *PropStore) ProposalByID(id uint64) (dao.Proposal, error) { - prop, exists := p.proposals.Get(getProposalID(id)) +func (s *SimpleDAO) ProposalByID(id uint64) (dao.Proposal, error) { + prop, exists := s.proposals.Get(getProposalID(id)) if !exists { return nil, ErrMissingProposal } @@ -190,8 +177,8 @@ func (p *PropStore) ProposalByID(id uint64) (dao.Proposal, error) { return prop.(*proposal), nil } -func (p *PropStore) Size() int { - return p.proposals.Size() +func (s *SimpleDAO) Size() int { + return s.proposals.Size() } // getProposalID generates a sequential proposal ID diff --git a/examples/gno.land/p/demo/simpledao/propstore_test.gno b/examples/gno.land/p/demo/simpledao/propstore_test.gno index 9c5b1db8d8a..37c705c7aa4 100644 --- a/examples/gno.land/p/demo/simpledao/propstore_test.gno +++ b/examples/gno.land/p/demo/simpledao/propstore_test.gno @@ -214,16 +214,16 @@ func TestProposal_Data(t *testing.T) { }) } -func TestPropStore_GetProposals(t *testing.T) { +func TestSimpleDAO_GetProposals(t *testing.T) { t.Parallel() t.Run("no proposals", func(t *testing.T) { t.Parallel() - p := NewPropStore() + s := New(NewMembStore()) - uassert.Equal(t, 0, p.Size()) - proposals := p.Proposals(0, 0) + uassert.Equal(t, 0, s.Size()) + proposals := s.Proposals(0, 0) uassert.Equal(t, 0, len(proposals)) }) @@ -235,20 +235,20 @@ func TestPropStore_GetProposals(t *testing.T) { numProposals = 20 halfRange = numProposals / 2 - p = NewPropStore() + s = New(NewMembStore()) proposals = generateProposals(t, numProposals) ) // Add initial proposals for _, proposal := range proposals { - _, err := p.addProposal(proposal) + _, err := s.addProposal(proposal) urequire.NoError(t, err) } - uassert.Equal(t, numProposals, p.Size()) + uassert.Equal(t, numProposals, s.Size()) - fetchedProposals := p.Proposals(0, uint64(halfRange)) + fetchedProposals := s.Proposals(0, uint64(halfRange)) urequire.Equal(t, halfRange, len(fetchedProposals)) for index, fetchedProposal := range fetchedProposals { @@ -256,7 +256,7 @@ func TestPropStore_GetProposals(t *testing.T) { } // Fetch the other half - fetchedProposals = p.Proposals(uint64(halfRange), uint64(halfRange)) + fetchedProposals = s.Proposals(uint64(halfRange), uint64(halfRange)) urequire.Equal(t, halfRange, len(fetchedProposals)) for index, fetchedProposal := range fetchedProposals { @@ -265,15 +265,15 @@ func TestPropStore_GetProposals(t *testing.T) { }) } -func TestPropStore_GetProposalByID(t *testing.T) { +func TestSimpleDAO_GetProposalByID(t *testing.T) { t.Parallel() t.Run("missing proposal", func(t *testing.T) { t.Parallel() - p := NewPropStore() + s := New(NewMembStore()) - _, err := p.ProposalByID(0) + _, err := s.ProposalByID(0) uassert.ErrorIs(t, err, ErrMissingProposal) }) @@ -281,16 +281,16 @@ func TestPropStore_GetProposalByID(t *testing.T) { t.Parallel() var ( - p = NewPropStore() + s = New(NewMembStore()) proposal = generateProposals(t, 1)[0] ) // Add the initial proposal - _, err := p.addProposal(proposal) + _, err := s.addProposal(proposal) urequire.NoError(t, err) // Fetch the proposal - fetchedProposal, err := p.ProposalByID(0) + fetchedProposal, err := s.ProposalByID(0) urequire.NoError(t, err) equalProposals(t, proposal, fetchedProposal) diff --git a/examples/gno.land/p/gov/dao/dao.gno b/examples/gno.land/p/gov/dao/dao.gno index 471e0565350..1da6ae30b0c 100644 --- a/examples/gno.land/p/gov/dao/dao.gno +++ b/examples/gno.land/p/gov/dao/dao.gno @@ -10,6 +10,9 @@ type ProposalRequest struct { // 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) diff --git a/examples/gno.land/r/gov/dao/v2/dao.gno b/examples/gno.land/r/gov/dao/v2/dao.gno index 66f9079c732..a0cee8a846b 100644 --- a/examples/gno.land/r/gov/dao/v2/dao.gno +++ b/examples/gno.land/r/gov/dao/v2/dao.gno @@ -10,31 +10,23 @@ import ( ) var ( - d dao.DAO // the current active DAO implementation - proposals dao.PropStore // the proposal store - members dao.MemberStore // the member store + d dao.DAO // the current active DAO implementation + members dao.MemberStore // the member store ) func init() { - // Example initial member set (just test addresses) - set := []dao.Member{ - { - Address: std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"), - VotingPower: 10, - }, - } - var ( - propStore = simpledao.NewPropStore() + // Example initial member set (just test addresses) + set = []dao.Member{ + { + Address: std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"), + VotingPower: 10, + }, + } membStore = simpledao.NewMembStore(simpledao.WithInitialMembers(set)) ) - - // Set the stores - proposals = propStore - members = membStore - // Set the DAO implementation - d = simpledao.New(membStore, propStore) + d = simpledao.New(membStore) } // Propose is designed to be called by another contract or with @@ -64,7 +56,7 @@ func ExecuteProposal(id uint64) { // GetPropStore returns the active proposal store func GetPropStore() dao.PropStore { - return proposals + return d } // GetMembStore returns the active member store @@ -74,7 +66,7 @@ func GetMembStore() dao.MemberStore { func Render(path string) string { if path == "" { - numProposals := proposals.Size() + numProposals := d.Size() if numProposals == 0 { return "No proposals found :(" // corner case @@ -88,7 +80,7 @@ func Render(path string) string { } // Fetch the last 10 proposals - for idx, prop := range proposals.Proposals(offset, uint64(10)) { + for idx, prop := range d.Proposals(offset, uint64(10)) { output += ufmt.Sprintf( "- [Proposal #%d](%s:%d) - (**%s**)(by %s)\n", idx, @@ -109,7 +101,7 @@ func Render(path string) string { } // Fetch the proposal - prop, err := proposals.ProposalByID(uint64(idx)) + prop, err := d.ProposalByID(uint64(idx)) if err != nil { return ufmt.Sprintf("unable to fetch proposal, %s", err.Error()) } diff --git a/examples/gno.land/r/gov/dao/v2/poc.gno b/examples/gno.land/r/gov/dao/v2/poc.gno index bd79235665b..8cae4f22a40 100644 --- a/examples/gno.land/r/gov/dao/v2/poc.gno +++ b/examples/gno.land/r/gov/dao/v2/poc.gno @@ -77,20 +77,6 @@ func NewMembStoreImplExecutor(changeFn func() dao.MemberStore) dao.Executor { return executor.NewCallbackExecutor(callback) } -func NewPropStoreImplExecutor(changeFn func() dao.PropStore) dao.Executor { - if changeFn == nil { - panic(errNoChangesProposed) - } - - callback := func() error { - setPropStoreImpl(changeFn()) - - return nil - } - - return executor.NewCallbackExecutor(callback) -} - // setDAOImpl sets a new DAO implementation func setDAOImpl(impl dao.DAO) { d = impl @@ -100,8 +86,3 @@ func setDAOImpl(impl dao.DAO) { func setMembStoreImpl(impl dao.MemberStore) { members = impl } - -// setPropStoreImpl sets a new dao.PropStore implementation -func setPropStoreImpl(impl dao.PropStore) { - proposals = impl -} From dbf096625ae6626a5213a9e799090bee883294a1 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Wed, 17 Jul 2024 15:59:21 +0200 Subject: [PATCH 52/80] Fixup prop test --- examples/gno.land/r/sys/vars/prop_filetest.gno | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/examples/gno.land/r/sys/vars/prop_filetest.gno b/examples/gno.land/r/sys/vars/prop_filetest.gno index 1663d6e5ef9..0132e0a4404 100644 --- a/examples/gno.land/r/sys/vars/prop_filetest.gno +++ b/examples/gno.land/r/sys/vars/prop_filetest.gno @@ -24,8 +24,14 @@ func init() { executor := vars.NewVarPropExecutor(changesFn) // Create the proposal - comment := "Example value setting" - govdaoMissingVotesPercent(comment, executor) + description := "Example value setting" + + prop := dao.ProposalRequest{ + Description: description, + Executor: executor, + } + + govdao.Propose(prop) } func main() { From 2f337a665c683b476a39e782f4e8f8e409911606 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Wed, 17 Jul 2024 19:10:00 +0200 Subject: [PATCH 53/80] Move out memberstore --- examples/gno.land/p/demo/membstore/gno.mod | 10 + .../{simpledao => membstore}/membstore.gno | 48 +-- .../membstore_test.gno | 66 ++-- examples/gno.land/p/demo/simpledao/dao.gno | 9 +- .../gno.land/p/demo/simpledao/dao_test.gno | 294 ++++++++++++++---- .../gno.land/p/demo/simpledao/mock_test.gno | 82 +++++ .../gno.land/p/demo/simpledao/propstore.gno | 4 - .../p/demo/simpledao/propstore_test.gno | 8 +- examples/gno.land/p/gov/dao/members.gno | 3 + examples/gno.land/r/gov/dao/v2/dao.gno | 4 +- examples/gno.land/r/gov/dao/v2/gno.mod | 1 + 11 files changed, 414 insertions(+), 115 deletions(-) create mode 100644 examples/gno.land/p/demo/membstore/gno.mod rename examples/gno.land/p/demo/{simpledao => membstore}/membstore.gno (75%) rename examples/gno.land/p/demo/{simpledao => membstore}/membstore_test.gno (81%) diff --git a/examples/gno.land/p/demo/membstore/gno.mod b/examples/gno.land/p/demo/membstore/gno.mod new file mode 100644 index 00000000000..a85c1f480ca --- /dev/null +++ b/examples/gno.land/p/demo/membstore/gno.mod @@ -0,0 +1,10 @@ +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/dao v0.0.0-latest +) diff --git a/examples/gno.land/p/demo/simpledao/membstore.gno b/examples/gno.land/p/demo/membstore/membstore.gno similarity index 75% rename from examples/gno.land/p/demo/simpledao/membstore.gno rename to examples/gno.land/p/demo/membstore/membstore.gno index f1d8ef40235..14ad37913c5 100644 --- a/examples/gno.land/p/demo/simpledao/membstore.gno +++ b/examples/gno.land/p/demo/membstore/membstore.gno @@ -1,4 +1,4 @@ -package simpledao +package membstore import ( "errors" @@ -13,32 +13,46 @@ var ( ErrAlreadyMember = errors.New("address is already a member") ErrMissingMember = errors.New("address is not a member") ErrInvalidAddressUpdate = errors.New("invalid address update") + ErrNotGovDAO = errors.New("caller not correct govdao instance") ) -type MembStoreOption func(*MembStore) +// maxRequestMembers is the maximum number of +// paginated members that can be requested +const maxRequestMembers = 50 -// WithInitialMembers initializes the simpledao member store +type Option func(*MembStore) + +// WithInitialMembers initializes the member store // with an initial member list -func WithInitialMembers(members []dao.Member) MembStoreOption { +func WithInitialMembers(members []dao.Member) Option { return func(store *MembStore) { for _, m := range members { store.members.Set(m.Address.String(), m) - store.totalVotingPower += m.VotingPower } } } +// WithDAOPkgPath initializes the member store +// with a dao package path guard +func WithDAOPkgPath(daoPkgPath string) Option { + return func(store *MembStore) { + store.daoPkgPath = daoPkgPath + } +} + // MembStore implements the dao.MembStore abstraction type MembStore struct { + daoPkgPath string // active dao pkg path, if any members *avl.Tree // std.Address -> dao.Member totalVotingPower uint64 // cached value for quick lookups } // NewMembStore creates a new simpledao member store -func NewMembStore(opts ...MembStoreOption) *MembStore { +func NewMembStore(opts ...Option) *MembStore { m := &MembStore{ - members: avl.NewTree(), + members: avl.NewTree(), // empty set + daoPkgPath: "", // no dao guard totalVotingPower: 0, } @@ -51,7 +65,7 @@ func NewMembStore(opts ...MembStoreOption) *MembStore { } func (m *MembStore) AddMember(member dao.Member) error { - if !isCallerGOVDAO() { + if !m.isCallerGOVDAO() { return ErrNotGovDAO } @@ -70,7 +84,7 @@ func (m *MembStore) AddMember(member dao.Member) error { } func (m *MembStore) UpdateMember(address std.Address, member dao.Member) error { - if !isCallerGOVDAO() { + if !m.isCallerGOVDAO() { return ErrNotGovDAO } @@ -160,20 +174,18 @@ func (m *MembStore) Size() int { return m.members.Size() } -// getTotalPower returns the total voting power +// TotalPower returns the total voting power // of the member store -func (m *MembStore) getTotalPower() uint64 { +func (m *MembStore) TotalPower() uint64 { return m.totalVotingPower } -// We need to include a govdao guard here, even if the +// isCallerGOVDAO returns a flag indicating if the +// current caller context is the active GOVDAO. +// We need to include a govdao guard, even if the // executor guarantees it, because // the API of the member store is public and callable // by anyone who has a reference to the member store instance. -const daoPkgPath = "gno.land/r/gov/dao/v2" - -// isCallerGOVDAO returns a flag indicating if the -// current caller context is the active GOVDAO -func isCallerGOVDAO() bool { - return std.CurrentRealm().PkgPath() == daoPkgPath +func (m *MembStore) isCallerGOVDAO() bool { + return m.daoPkgPath == "" || std.CurrentRealm().PkgPath() == m.daoPkgPath } diff --git a/examples/gno.land/p/demo/simpledao/membstore_test.gno b/examples/gno.land/p/demo/membstore/membstore_test.gno similarity index 81% rename from examples/gno.land/p/demo/simpledao/membstore_test.gno rename to examples/gno.land/p/demo/membstore/membstore_test.gno index 8189b07e336..ea3a2e359f6 100644 --- a/examples/gno.land/p/demo/simpledao/membstore_test.gno +++ b/examples/gno.land/p/demo/membstore/membstore_test.gno @@ -1,4 +1,4 @@ -package simpledao +package membstore import ( "testing" @@ -140,7 +140,7 @@ func TestMembStore_AddMember(t *testing.T) { t.Parallel() // Create an empty store - m := NewMembStore() + m := NewMembStore(WithDAOPkgPath("gno.land/r/gov/dao")) // Attempt to add a member member := generateMembers(t, 1)[0] @@ -150,13 +150,17 @@ func TestMembStore_AddMember(t *testing.T) { t.Run("member already exists", func(t *testing.T) { t.Parallel() - // Execute as the /r/gov/dao caller - r := std.NewCodeRealm(daoPkgPath) + var ( + // Execute as the /r/gov/dao caller + daoPkgPath = "gno.land/r/gov/dao" + r = std.NewCodeRealm(daoPkgPath) + ) + std.TestSetRealm(r) // Create a non-empty store members := generateMembers(t, 1) - m := NewMembStore(WithInitialMembers(members)) + m := NewMembStore(WithDAOPkgPath(daoPkgPath), WithInitialMembers(members)) // Attempt to add a member uassert.ErrorIs(t, m.AddMember(members[0]), ErrAlreadyMember) @@ -165,13 +169,17 @@ func TestMembStore_AddMember(t *testing.T) { t.Run("new member added", func(t *testing.T) { t.Parallel() - // Execute as the /r/gov/dao caller - r := std.NewCodeRealm(daoPkgPath) + var ( + // Execute as the /r/gov/dao caller + daoPkgPath = "gno.land/r/gov/dao" + r = std.NewCodeRealm(daoPkgPath) + ) + std.TestSetRealm(r) // Create an empty store members := generateMembers(t, 1) - m := NewMembStore() + m := NewMembStore(WithDAOPkgPath(daoPkgPath)) // Attempt to add a member urequire.NoError(t, m.AddMember(members[0])) @@ -211,7 +219,7 @@ func TestMembStore_UpdateMember(t *testing.T) { t.Parallel() // Create an empty store - m := NewMembStore() + m := NewMembStore(WithDAOPkgPath("gno.land/r/gov/dao")) // Attempt to update a member member := generateMembers(t, 1)[0] @@ -221,13 +229,17 @@ func TestMembStore_UpdateMember(t *testing.T) { t.Run("non-existing member", func(t *testing.T) { t.Parallel() - // Execute as the /r/gov/dao caller - r := std.NewCodeRealm(daoPkgPath) + var ( + // Execute as the /r/gov/dao caller + daoPkgPath = "gno.land/r/gov/dao" + r = std.NewCodeRealm(daoPkgPath) + ) + std.TestSetRealm(r) // Create an empty store members := generateMembers(t, 1) - m := NewMembStore() + m := NewMembStore(WithDAOPkgPath(daoPkgPath)) // Attempt to update a member uassert.ErrorIs(t, m.UpdateMember(members[0].Address, members[0]), ErrMissingMember) @@ -236,13 +248,17 @@ func TestMembStore_UpdateMember(t *testing.T) { t.Run("overwrite member attempt", func(t *testing.T) { t.Parallel() - // Execute as the /r/gov/dao caller - r := std.NewCodeRealm(daoPkgPath) + var ( + // Execute as the /r/gov/dao caller + daoPkgPath = "gno.land/r/gov/dao" + r = std.NewCodeRealm(daoPkgPath) + ) + std.TestSetRealm(r) // Create a non-empty store members := generateMembers(t, 2) - m := NewMembStore(WithInitialMembers(members)) + m := NewMembStore(WithDAOPkgPath(daoPkgPath), WithInitialMembers(members)) // Attempt to update a member uassert.ErrorIs(t, m.UpdateMember(members[0].Address, members[1]), ErrInvalidAddressUpdate) @@ -251,13 +267,17 @@ func TestMembStore_UpdateMember(t *testing.T) { t.Run("successful update", func(t *testing.T) { t.Parallel() - // Execute as the /r/gov/dao caller - r := std.NewCodeRealm(daoPkgPath) + var ( + // Execute as the /r/gov/dao caller + daoPkgPath = "gno.land/r/gov/dao" + r = std.NewCodeRealm(daoPkgPath) + ) + std.TestSetRealm(r) // Create a non-empty store members := generateMembers(t, 1) - m := NewMembStore(WithInitialMembers(members)) + m := NewMembStore(WithDAOPkgPath(daoPkgPath), WithInitialMembers(members)) oldVotingPower := m.totalVotingPower urequire.Equal(t, members[0].VotingPower, oldVotingPower) @@ -274,13 +294,17 @@ func TestMembStore_UpdateMember(t *testing.T) { t.Run("member removed", func(t *testing.T) { t.Parallel() - // Execute as the /r/gov/dao caller - r := std.NewCodeRealm(daoPkgPath) + var ( + // Execute as the /r/gov/dao caller + daoPkgPath = "gno.land/r/gov/dao" + r = std.NewCodeRealm(daoPkgPath) + ) + std.TestSetRealm(r) // Create a non-empty store members := generateMembers(t, 1) - m := NewMembStore(WithInitialMembers(members)) + m := NewMembStore(WithDAOPkgPath(daoPkgPath), WithInitialMembers(members)) votingPower := uint64(0) members[0].VotingPower = votingPower diff --git a/examples/gno.land/p/demo/simpledao/dao.gno b/examples/gno.land/p/demo/simpledao/dao.gno index 3f6b5d9a6fc..20e5c3a7897 100644 --- a/examples/gno.land/p/demo/simpledao/dao.gno +++ b/examples/gno.land/p/demo/simpledao/dao.gno @@ -17,7 +17,6 @@ var ( ErrProposalInactive = errors.New("proposal is inactive") ErrProposalNotAccepted = errors.New("proposal is not accepted") ErrExecutorExpired = errors.New("executor is expired") - ErrNotGovDAO = errors.New("caller not correct govdao instance") ) var ( @@ -31,11 +30,11 @@ var ( // SimpleDAO is a simple DAO implementation type SimpleDAO struct { proposals *avl.Tree // seqid.ID -> proposal - membStore *MembStore + membStore dao.MemberStore } // New creates a new instance of the simpledao DAO -func New(membStore *MembStore) *SimpleDAO { +func New(membStore dao.MemberStore) *SimpleDAO { return &SimpleDAO{ proposals: avl.NewTree(), membStore: membStore, @@ -66,7 +65,7 @@ func (s *SimpleDAO) Propose(request dao.ProposalRequest) (uint64, error) { executor: request.Executor, status: dao.Active, votes: newVotes(), - getTotalVotingPowerFn: s.membStore.getTotalPower, + getTotalVotingPowerFn: s.membStore.TotalPower, } // Add the proposal @@ -118,7 +117,7 @@ func (s *SimpleDAO) VoteOnProposal(id uint64, option dao.VoteOption) error { // Check the votes to see if quorum is reached var ( - totalPower = s.membStore.getTotalPower() + totalPower = s.membStore.TotalPower() majorityPower = (2 * totalPower) / 3 ) diff --git a/examples/gno.land/p/demo/simpledao/dao_test.gno b/examples/gno.land/p/demo/simpledao/dao_test.gno index e573b4b500a..53616cd4ed1 100644 --- a/examples/gno.land/p/demo/simpledao/dao_test.gno +++ b/examples/gno.land/p/demo/simpledao/dao_test.gno @@ -7,18 +7,35 @@ import ( "gno.land/p/demo/testutils" "gno.land/p/demo/uassert" + "gno.land/p/demo/ufmt" "gno.land/p/demo/urequire" "gno.land/p/gov/dao" "gno.land/p/gov/executor" ) +// generateMembers generates dummy govdao members +func generateMembers(t *testing.T, count int) []dao.Member { + t.Helper() + + members := make([]dao.Member, 0, count) + + for i := 0; i < count; i++ { + members = append(members, dao.Member{ + Address: testutils.TestAddress(ufmt.Sprintf("member %d", i)), + VotingPower: 10, + }) + } + + return members +} + func TestSimpleDAO_Propose(t *testing.T) { t.Parallel() t.Run("invalid executor", func(t *testing.T) { t.Parallel() - s := &SimpleDAO{} + s := New(nil) _, err := s.Propose(dao.ProposalRequest{}) uassert.ErrorIs( @@ -47,9 +64,12 @@ func TestSimpleDAO_Propose(t *testing.T) { ), ) - s = &SimpleDAO{ - membStore: NewMembStore(), // empty member store + ms = &mockMemberStore{ + isMemberFn: func(_ std.Address) bool { + return false + }, } + s = New(ms) ) // Set the sent coins to be lower @@ -90,7 +110,12 @@ func TestSimpleDAO_Propose(t *testing.T) { ), ) - s = New(NewMembStore()) + ms = &mockMemberStore{ + isMemberFn: func(addr std.Address) bool { + return addr == proposer + }, + } + s = New(ms) ) // Set the sent coins to enough @@ -124,11 +149,17 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { t.Parallel() var ( - voter = testutils.TestAddress("voter") - - s = &SimpleDAO{ - membStore: NewMembStore(), + voter = testutils.TestAddress("voter") + fetchErr = errors.New("fetch error") + + ms = &mockMemberStore{ + memberFn: func(_ std.Address) (dao.Member, error) { + return dao.Member{ + Address: voter, + }, fetchErr + }, } + s = New(ms) ) std.TestSetOrigCaller(voter) @@ -137,7 +168,7 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { uassert.ErrorContains( t, s.VoteOnProposal(0, dao.YesVote), - ErrMissingMember.Error(), + fetchErr.Error(), ) }) @@ -146,12 +177,18 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { var ( voter = testutils.TestAddress("voter") + ms = &mockMemberStore{ + memberFn: func(a std.Address) (dao.Member, error) { + if a != voter { + return dao.Member{}, errors.New("not found") + } + + return dao.Member{ + Address: voter, + }, nil + }, + } - ms = NewMembStore( - WithInitialMembers([]dao.Member{ - {Address: voter}, - }), - ) s = New(ms) ) @@ -171,11 +208,17 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { var ( voter = testutils.TestAddress("voter") - ms = NewMembStore( - WithInitialMembers([]dao.Member{ - {Address: voter}, - }), - ) + ms = &mockMemberStore{ + memberFn: func(a std.Address) (dao.Member, error) { + if a != voter { + return dao.Member{}, errors.New("not found") + } + + return dao.Member{ + Address: voter, + }, nil + }, + } s = New(ms) prop = &proposal{ @@ -203,11 +246,17 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { var ( voter = testutils.TestAddress("voter") - ms = NewMembStore( - WithInitialMembers([]dao.Member{ - {Address: voter}, - }), - ) + ms = &mockMemberStore{ + memberFn: func(a std.Address) (dao.Member, error) { + if a != voter { + return dao.Member{}, errors.New("not found") + } + + return dao.Member{ + Address: voter, + }, nil + }, + } s = New(ms) mockExecutor = &mockExecutor{ @@ -246,11 +295,15 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { VotingPower: 10, } - ms = NewMembStore( - WithInitialMembers([]dao.Member{ - {Address: voter}, - }), - ) + ms = &mockMemberStore{ + memberFn: func(a std.Address) (dao.Member, error) { + if a != voter { + return dao.Member{}, errors.New("not found") + } + + return member, nil + }, + } s = New(ms) prop = &proposal{ @@ -283,7 +336,28 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { var ( members = generateMembers(t, 50) - s = New(NewMembStore(WithInitialMembers(members))) + ms = &mockMemberStore{ + memberFn: func(address std.Address) (dao.Member, error) { + for _, m := range members { + if m.Address == address { + return m, nil + } + } + + return dao.Member{}, errors.New("not found") + }, + + totalPowerFn: func() uint64 { + power := uint64(0) + + for _, m := range members { + power += m.VotingPower + } + + return power + }, + } + s = New(ms) prop = &proposal{ status: dao.Active, @@ -317,8 +391,28 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { var ( members = generateMembers(t, 50) - ms = NewMembStore(WithInitialMembers(members)) - s = New(ms) + ms = &mockMemberStore{ + memberFn: func(address std.Address) (dao.Member, error) { + for _, m := range members { + if m.Address == address { + return m, nil + } + } + + return dao.Member{}, errors.New("member not found") + }, + + totalPowerFn: func() uint64 { + power := uint64(0) + + for _, m := range members { + power += m.VotingPower + } + + return power + }, + } + s = New(ms) prop = &proposal{ status: dao.Active, @@ -352,7 +446,28 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { var ( members = generateMembers(t, 50) - s = New(NewMembStore(WithInitialMembers(members))) + ms = &mockMemberStore{ + memberFn: func(address std.Address) (dao.Member, error) { + for _, m := range members { + if m.Address == address { + return m, nil + } + } + + return dao.Member{}, errors.New("member not found") + }, + + totalPowerFn: func() uint64 { + power := uint64(0) + + for _, m := range members { + power += m.VotingPower + } + + return power + }, + } + s = New(ms) prop = &proposal{ status: dao.Active, @@ -386,7 +501,28 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { var ( members = generateMembers(t, 50) - s = New(NewMembStore(WithInitialMembers(members))) + ms = &mockMemberStore{ + memberFn: func(address std.Address) (dao.Member, error) { + for _, m := range members { + if m.Address == address { + return m, nil + } + } + + return dao.Member{}, errors.New("member not found") + }, + + totalPowerFn: func() uint64 { + power := uint64(0) + + for _, m := range members { + power += m.VotingPower + } + + return power + }, + } + s = New(ms) prop = &proposal{ status: dao.Active, @@ -432,7 +568,28 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { var ( members = generateMembers(t, 50) - s = New(NewMembStore(WithInitialMembers(members))) + ms = &mockMemberStore{ + memberFn: func(address std.Address) (dao.Member, error) { + for _, m := range members { + if m.Address == address { + return m, nil + } + } + + return dao.Member{}, errors.New("member not found") + }, + + totalPowerFn: func() uint64 { + power := uint64(0) + + for _, m := range members { + power += m.VotingPower + } + + return power + }, + } + s = New(ms) prop = &proposal{ status: dao.Active, @@ -487,7 +644,12 @@ func TestSimpleDAO_ExecuteProposal(t *testing.T) { ), ) - s = New(NewMembStore()) + ms = &mockMemberStore{ + isMemberFn: func(_ std.Address) bool { + return false + }, + } + s = New(ms) ) // Set the sent coins to be lower @@ -512,7 +674,13 @@ func TestSimpleDAO_ExecuteProposal(t *testing.T) { ), ) - s = New(NewMembStore()) + ms = &mockMemberStore{ + isMemberFn: func(_ std.Address) bool { + return true + }, + } + + s = New(ms) ) // Set the sent coins to be enough @@ -532,11 +700,11 @@ func TestSimpleDAO_ExecuteProposal(t *testing.T) { var ( voter = testutils.TestAddress("voter") - ms = NewMembStore( - WithInitialMembers([]dao.Member{ - {Address: voter}, - }), - ) + ms = &mockMemberStore{ + isMemberFn: func(_ std.Address) bool { + return true + }, + } s = New(ms) prop = &proposal{ @@ -584,11 +752,11 @@ func TestSimpleDAO_ExecuteProposal(t *testing.T) { var ( voter = testutils.TestAddress("voter") - ms = NewMembStore( - WithInitialMembers([]dao.Member{ - {Address: voter}, - }), - ) + ms = &mockMemberStore{ + isMemberFn: func(_ std.Address) bool { + return true + }, + } s = New(ms) prop = &proposal{ @@ -618,11 +786,12 @@ func TestSimpleDAO_ExecuteProposal(t *testing.T) { var ( voter = testutils.TestAddress("voter") - ms = NewMembStore( - WithInitialMembers([]dao.Member{ - {Address: voter}, - }), - ) + ms = &mockMemberStore{ + isMemberFn: func(_ std.Address) bool { + return true + }, + } + s = New(ms) mockExecutor = &mockExecutor{ @@ -657,11 +826,12 @@ func TestSimpleDAO_ExecuteProposal(t *testing.T) { var ( voter = testutils.TestAddress("voter") - ms = NewMembStore( - WithInitialMembers([]dao.Member{ - {Address: voter}, - }), - ) + ms = &mockMemberStore{ + isMemberFn: func(_ std.Address) bool { + return true + }, + } + s = New(ms) execError = errors.New("exec error") @@ -700,11 +870,11 @@ func TestSimpleDAO_ExecuteProposal(t *testing.T) { var ( voter = testutils.TestAddress("voter") - ms = NewMembStore( - WithInitialMembers([]dao.Member{ - {Address: voter}, - }), - ) + ms = &mockMemberStore{ + isMemberFn: func(_ std.Address) bool { + return true + }, + } s = New(ms) called = false diff --git a/examples/gno.land/p/demo/simpledao/mock_test.gno b/examples/gno.land/p/demo/simpledao/mock_test.gno index 2a48bb9134b..066a8ea4a2c 100644 --- a/examples/gno.land/p/demo/simpledao/mock_test.gno +++ b/examples/gno.land/p/demo/simpledao/mock_test.gno @@ -1,5 +1,11 @@ package simpledao +import ( + "std" + + "gno.land/p/gov/dao" +) + type ( executeDelegate func() error isExpiredDelegate func() bool @@ -25,3 +31,79 @@ func (m *mockExecutor) IsExpired() bool { return false } + +type ( + membersDelegate func(uint64, uint64) []dao.Member + sizeDelegate func() int + isMemberDelegate func(std.Address) bool + totalPowerDelegate func() uint64 + memberDelegate func(std.Address) (dao.Member, error) + addMemberDelegate func(dao.Member) error + updateMemberDelegate func(std.Address, dao.Member) error +) + +type mockMemberStore struct { + membersFn membersDelegate + sizeFn sizeDelegate + isMemberFn isMemberDelegate + totalPowerFn totalPowerDelegate + memberFn memberDelegate + addMemberFn addMemberDelegate + updateMemberFn updateMemberDelegate +} + +func (m *mockMemberStore) Members(offset, count uint64) []dao.Member { + if m.membersFn != nil { + return m.membersFn(offset, count) + } + + return nil +} + +func (m *mockMemberStore) Size() int { + if m.sizeFn != nil { + return m.sizeFn() + } + + return 0 +} + +func (m *mockMemberStore) IsMember(address std.Address) bool { + if m.isMemberFn != nil { + return m.isMemberFn(address) + } + + return false +} + +func (m *mockMemberStore) TotalPower() uint64 { + if m.totalPowerFn != nil { + return m.totalPowerFn() + } + + return 0 +} + +func (m *mockMemberStore) Member(address std.Address) (dao.Member, error) { + if m.memberFn != nil { + return m.memberFn(address) + } + + return dao.Member{}, nil +} + +func (m *mockMemberStore) AddMember(member dao.Member) error { + if m.addMemberFn != nil { + return m.addMemberFn(member) + } + + return nil +} + +func (m *mockMemberStore) UpdateMember(address std.Address, member dao.Member) error { + if m.updateMemberFn != nil { + return m.updateMemberFn(address, member) + } + + return nil +} diff --git a/examples/gno.land/p/demo/simpledao/propstore.gno b/examples/gno.land/p/demo/simpledao/propstore.gno index 9cd140780a0..5ba164133fb 100644 --- a/examples/gno.land/p/demo/simpledao/propstore.gno +++ b/examples/gno.land/p/demo/simpledao/propstore.gno @@ -18,10 +18,6 @@ const ( // paginated proposals that can be requested maxRequestProposals = 10 - // maxRequestMembers is the maximum number of - // paginated members that can be requested - maxRequestMembers = 50 - // maxRequestVotes is the maximum number of // paginated votes that can be requested maxRequestVotes = 50 diff --git a/examples/gno.land/p/demo/simpledao/propstore_test.gno b/examples/gno.land/p/demo/simpledao/propstore_test.gno index 37c705c7aa4..bb49bf4e1a0 100644 --- a/examples/gno.land/p/demo/simpledao/propstore_test.gno +++ b/examples/gno.land/p/demo/simpledao/propstore_test.gno @@ -220,7 +220,7 @@ func TestSimpleDAO_GetProposals(t *testing.T) { t.Run("no proposals", func(t *testing.T) { t.Parallel() - s := New(NewMembStore()) + s := New(nil) uassert.Equal(t, 0, s.Size()) proposals := s.Proposals(0, 0) @@ -235,7 +235,7 @@ func TestSimpleDAO_GetProposals(t *testing.T) { numProposals = 20 halfRange = numProposals / 2 - s = New(NewMembStore()) + s = New(nil) proposals = generateProposals(t, numProposals) ) @@ -271,7 +271,7 @@ func TestSimpleDAO_GetProposalByID(t *testing.T) { t.Run("missing proposal", func(t *testing.T) { t.Parallel() - s := New(NewMembStore()) + s := New(nil) _, err := s.ProposalByID(0) uassert.ErrorIs(t, err, ErrMissingProposal) @@ -281,7 +281,7 @@ func TestSimpleDAO_GetProposalByID(t *testing.T) { t.Parallel() var ( - s = New(NewMembStore()) + s = New(nil) proposal = generateProposals(t, 1)[0] ) diff --git a/examples/gno.land/p/gov/dao/members.gno b/examples/gno.land/p/gov/dao/members.gno index 86dfe4b55f1..871f2feb998 100644 --- a/examples/gno.land/p/gov/dao/members.gno +++ b/examples/gno.land/p/gov/dao/members.gno @@ -16,6 +16,9 @@ type MemberStore interface { // 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) diff --git a/examples/gno.land/r/gov/dao/v2/dao.gno b/examples/gno.land/r/gov/dao/v2/dao.gno index a0cee8a846b..48170a2b7e6 100644 --- a/examples/gno.land/r/gov/dao/v2/dao.gno +++ b/examples/gno.land/r/gov/dao/v2/dao.gno @@ -4,6 +4,7 @@ import ( "std" "strconv" + "gno.land/p/demo/membstore" "gno.land/p/demo/simpledao" "gno.land/p/demo/ufmt" "gno.land/p/gov/dao" @@ -23,7 +24,8 @@ func init() { VotingPower: 10, }, } - membStore = simpledao.NewMembStore(simpledao.WithInitialMembers(set)) + + membStore = membstore.NewMembStore(membstore.WithInitialMembers(set)) ) // Set the DAO implementation d = simpledao.New(membStore) diff --git a/examples/gno.land/r/gov/dao/v2/gno.mod b/examples/gno.land/r/gov/dao/v2/gno.mod index 265a974c90f..28a5081502d 100644 --- a/examples/gno.land/r/gov/dao/v2/gno.mod +++ b/examples/gno.land/r/gov/dao/v2/gno.mod @@ -1,6 +1,7 @@ module gno.land/r/gov/dao/v2 require ( + gno.land/p/demo/membstore v0.0.0-latest gno.land/p/demo/simpledao v0.0.0-latest gno.land/p/demo/ufmt v0.0.0-latest gno.land/p/gov/dao v0.0.0-latest From 31d265f63eb9e55aab552dbf62bcb47425f06f35 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Thu, 18 Jul 2024 13:58:19 +0200 Subject: [PATCH 54/80] Add Render to proposal --- .../gno.land/p/demo/simpledao/propstore.gno | 29 +++++++++++++++++++ examples/gno.land/p/gov/dao/proposals.gno | 3 ++ examples/gno.land/r/gov/dao/v2/dao.gno | 24 ++------------- 3 files changed, 34 insertions(+), 22 deletions(-) diff --git a/examples/gno.land/p/demo/simpledao/propstore.gno b/examples/gno.land/p/demo/simpledao/propstore.gno index 5ba164133fb..79310445d7b 100644 --- a/examples/gno.land/p/demo/simpledao/propstore.gno +++ b/examples/gno.land/p/demo/simpledao/propstore.gno @@ -5,6 +5,7 @@ import ( "std" "gno.land/p/demo/seqid" + "gno.land/p/demo/ufmt" "gno.land/p/gov/dao" ) @@ -108,6 +109,34 @@ func (p *proposal) VoteByMember(address std.Address) (dao.Vote, error) { return vote, nil } +func (p *proposal) Render() string { + // Fetch the voting stats + stats := p.VotingStats() + + output := "" + output += ufmt.Sprintf("Author: %s", p.Author().String()) + output += "\n\n" + output += p.Description() + output += "\n\n" + output += ufmt.Sprintf("Status: %s", p.Status().String()) + output += "\n\n" + output += ufmt.Sprintf( + "Voting stats: YAY %d (%d%%), NAY %d (%d%%), ABSTAIN %d (%d%%), HAVEN'T VOTED %d (%d%%)", + stats.YayVotes, + stats.YayPercent(), + stats.NayVotes, + stats.NayPercent(), + stats.AbstainVotes, + stats.AbstainPercent(), + stats.MissingVotes(), + stats.MissingVotesPercent(), + ) + output += "\n\n" + output += ufmt.Sprintf("Threshold met: %t", stats.YayVotes > (2*stats.TotalVotingPower)/3) + + return output +} + // addProposal adds a new simpledao proposal to the store func (s *SimpleDAO) addProposal(proposal *proposal) (uint64, error) { // See what the next proposal number should be diff --git a/examples/gno.land/p/gov/dao/proposals.gno b/examples/gno.land/p/gov/dao/proposals.gno index 48ce4113441..a9daa146b76 100644 --- a/examples/gno.land/p/gov/dao/proposals.gno +++ b/examples/gno.land/p/gov/dao/proposals.gno @@ -54,4 +54,7 @@ type Proposal interface { // VoteByMember returns the proposal vote by the member, if any VoteByMember(address std.Address) (Vote, error) + + // Render renders the proposal in a readable format + Render() string } diff --git a/examples/gno.land/r/gov/dao/v2/dao.gno b/examples/gno.land/r/gov/dao/v2/dao.gno index 48170a2b7e6..3ba467f95c3 100644 --- a/examples/gno.land/r/gov/dao/v2/dao.gno +++ b/examples/gno.land/r/gov/dao/v2/dao.gno @@ -108,31 +108,11 @@ func Render(path string) string { return ufmt.Sprintf("unable to fetch proposal, %s", err.Error()) } - // Fetch the voting stats - stats := prop.VotingStats() - + // Render the proposal output := "" output += ufmt.Sprintf("# Prop #%d", idx) output += "\n\n" - output += ufmt.Sprintf("Author: %s", prop.Author().String()) - output += "\n\n" - output += prop.Description() - output += "\n\n" - output += ufmt.Sprintf("Status: %s", prop.Status().String()) - output += "\n\n" - output += ufmt.Sprintf( - "Voting stats: YAY %d (%d%%), NAY %d (%d%%), ABSTAIN %d (%d%%), HAVEN'T VOTED %d (%d%%)", - stats.YayVotes, - stats.YayPercent(), - stats.NayVotes, - stats.NayPercent(), - stats.AbstainVotes, - stats.AbstainPercent(), - stats.MissingVotes(), - stats.MissingVotesPercent(), - ) - output += "\n\n" - output += ufmt.Sprintf("Threshold met: %t", stats.YayVotes > (2*stats.TotalVotingPower)/3) + output += prop.Render() output += "\n\n" return output From 908b949b2999546876085c3c853e695f7004c5a2 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Thu, 18 Jul 2024 17:39:21 +0200 Subject: [PATCH 55/80] Add govdao executor wrapper --- .../gno.land/p/demo/simpledao/dao_test.gno | 9 +++-- examples/gno.land/p/demo/simpledao/gno.mod | 1 - examples/gno.land/p/gov/executor/callback.gno | 22 +++++------ examples/gno.land/p/gov/executor/context.gno | 6 ++- .../gno.land/p/gov/executor/proposal_test.gno | 20 ++++++---- examples/gno.land/r/gnoland/blog/admin.gno | 8 ++-- examples/gno.land/r/gnoland/blog/gno.mod | 2 +- examples/gno.land/r/gov/dao/v2/poc.gno | 38 +++++++++++++++++-- examples/gno.land/r/sys/validators/v2/gno.mod | 2 +- examples/gno.land/r/sys/validators/v2/poc.gno | 4 +- examples/gno.land/r/sys/vars/gno.mod | 2 +- examples/gno.land/r/sys/vars/prop.gno | 4 +- 12 files changed, 77 insertions(+), 41 deletions(-) diff --git a/examples/gno.land/p/demo/simpledao/dao_test.gno b/examples/gno.land/p/demo/simpledao/dao_test.gno index 53616cd4ed1..8d493d99296 100644 --- a/examples/gno.land/p/demo/simpledao/dao_test.gno +++ b/examples/gno.land/p/demo/simpledao/dao_test.gno @@ -10,7 +10,6 @@ import ( "gno.land/p/demo/ufmt" "gno.land/p/demo/urequire" "gno.land/p/gov/dao" - "gno.land/p/gov/executor" ) // generateMembers generates dummy govdao members @@ -55,7 +54,9 @@ func TestSimpleDAO_Propose(t *testing.T) { return nil } - ex = executor.NewCallbackExecutor(cb) + ex = &mockExecutor{ + executeFn: cb, + } sentCoins = std.NewCoins( std.NewCoin( @@ -99,7 +100,9 @@ func TestSimpleDAO_Propose(t *testing.T) { return nil } - ex = executor.NewCallbackExecutor(cb) + ex = &mockExecutor{ + executeFn: cb, + } description = "Proposal description" proposer = testutils.TestAddress("proposer") diff --git a/examples/gno.land/p/demo/simpledao/gno.mod b/examples/gno.land/p/demo/simpledao/gno.mod index c6e6ad15723..a208a173b27 100644 --- a/examples/gno.land/p/demo/simpledao/gno.mod +++ b/examples/gno.land/p/demo/simpledao/gno.mod @@ -8,5 +8,4 @@ require ( gno.land/p/demo/ufmt v0.0.0-latest gno.land/p/demo/urequire v0.0.0-latest gno.land/p/gov/dao v0.0.0-latest - gno.land/p/gov/executor v0.0.0-latest ) diff --git a/examples/gno.land/p/gov/executor/callback.gno b/examples/gno.land/p/gov/executor/callback.gno index 064a862f2d5..1e4417c0623 100644 --- a/examples/gno.land/p/gov/executor/callback.gno +++ b/examples/gno.land/p/gov/executor/callback.gno @@ -5,14 +5,13 @@ import ( "std" ) -const daoPkgPath = "gno.land/r/gov/dao/v2" - -var errNotGovDAO = errors.New("only r/gov/dao can be the caller") +var errInvalidCaller = errors.New("invalid executor caller") // NewCallbackExecutor creates a new callback executor with the provided callback function -func NewCallbackExecutor(callback func() error) *CallbackExecutor { +func NewCallbackExecutor(callback func() error, path string) *CallbackExecutor { return &CallbackExecutor{ - callback: callback, + callback: callback, + daoPkgPath: path, } } @@ -20,13 +19,14 @@ func NewCallbackExecutor(callback func() error) *CallbackExecutor { // based on a specific callback. // The given callback should verify the validity of the govdao call type CallbackExecutor struct { - callback func() error // the callback to be executed + callback func() error // the callback to be executed + daoPkgPath string // the active pkg path of the govdao } // Execute runs the executor's callback function. func (exec *CallbackExecutor) Execute() error { // Verify the executor is r/gov/dao - assertCalledByGovdao() + assertCaller(exec.daoPkgPath) if exec.callback != nil { return exec.callback() @@ -41,11 +41,11 @@ func (exec *CallbackExecutor) IsExpired() bool { return false } -// assertCalledByGovdao asserts that the calling Realm is /r/gov/dao -func assertCalledByGovdao() { +// assertCaller asserts that the caller Realm matches the path +func assertCaller(path string) { caller := std.CurrentRealm().PkgPath() - if caller != daoPkgPath { - panic(errNotGovDAO) + if caller != path { + panic(errInvalidCaller) } } diff --git a/examples/gno.land/p/gov/executor/context.gno b/examples/gno.land/p/gov/executor/context.gno index 73548aea778..0740fb76a6a 100644 --- a/examples/gno.land/p/gov/executor/context.gno +++ b/examples/gno.land/p/gov/executor/context.gno @@ -22,19 +22,21 @@ var errNotApproved = errors.New("not approved by govdao") // It utilizes the given context to assert the validity of the govdao call type CtxExecutor struct { callbackCtx func(ctx context.Context) error // the callback ctx fn, if any + daoPkgPath string // the active pkg path of the govdao } // NewCtxExecutor creates a new executor with the provided callback function. -func NewCtxExecutor(callback func(ctx context.Context) error) *CtxExecutor { +func NewCtxExecutor(callback func(ctx context.Context) error, path string) *CtxExecutor { return &CtxExecutor{ callbackCtx: callback, + daoPkgPath: path, } } // Execute runs the executor's callback function func (exec *CtxExecutor) Execute() error { // Verify the executor is r/gov/dao - assertCalledByGovdao() + assertCaller(exec.daoPkgPath) // Create the context ctx := context.WithValue( diff --git a/examples/gno.land/p/gov/executor/proposal_test.gno b/examples/gno.land/p/gov/executor/proposal_test.gno index eb77a84b211..58a918cfaf8 100644 --- a/examples/gno.land/p/gov/executor/proposal_test.gno +++ b/examples/gno.land/p/gov/executor/proposal_test.gno @@ -26,10 +26,10 @@ func TestExecutor_Callback(t *testing.T) { ) // Create the executor - e := NewCallbackExecutor(cb) + e := NewCallbackExecutor(cb, "gno.land/r/gov/dao") // Execute as not the /r/gov/dao caller - uassert.PanicsWithMessage(t, errNotGovDAO.Error(), func() { + uassert.PanicsWithMessage(t, errInvalidCaller.Error(), func() { _ = e.Execute() }) @@ -50,7 +50,8 @@ func TestExecutor_Callback(t *testing.T) { ) // Create the executor - e := NewCallbackExecutor(cb) + daoPkgPath := "gno.land/r/gov/dao" + e := NewCallbackExecutor(cb, daoPkgPath) // Execute as the /r/gov/dao caller r := std.NewCodeRealm(daoPkgPath) @@ -80,7 +81,8 @@ func TestExecutor_Callback(t *testing.T) { ) // Create the executor - e := NewCallbackExecutor(cb) + daoPkgPath := "gno.land/r/gov/dao" + e := NewCallbackExecutor(cb, daoPkgPath) // Execute as the /r/gov/dao caller r := std.NewCodeRealm(daoPkgPath) @@ -117,10 +119,10 @@ func TestExecutor_Context(t *testing.T) { ) // Create the executor - e := NewCtxExecutor(cb) + e := NewCtxExecutor(cb, "gno.land/r/gov/dao") // Execute as not the /r/gov/dao caller - uassert.PanicsWithMessage(t, errNotGovDAO.Error(), func() { + uassert.PanicsWithMessage(t, errInvalidCaller.Error(), func() { _ = e.Execute() }) @@ -145,7 +147,8 @@ func TestExecutor_Context(t *testing.T) { ) // Create the executor - e := NewCtxExecutor(cb) + daoPkgPath := "gno.land/r/gov/dao" + e := NewCtxExecutor(cb, daoPkgPath) // Execute as the /r/gov/dao caller r := std.NewCodeRealm(daoPkgPath) @@ -179,7 +182,8 @@ func TestExecutor_Context(t *testing.T) { ) // Create the executor - e := NewCtxExecutor(cb) + daoPkgPath := "gno.land/r/gov/dao" + e := NewCtxExecutor(cb, daoPkgPath) // Execute as the /r/gov/dao caller r := std.NewCodeRealm(daoPkgPath) diff --git a/examples/gno.land/r/gnoland/blog/admin.gno b/examples/gno.land/r/gnoland/blog/admin.gno index 7fea3359a57..9ca705a8b94 100644 --- a/examples/gno.land/r/gnoland/blog/admin.gno +++ b/examples/gno.land/r/gnoland/blog/admin.gno @@ -6,7 +6,7 @@ import ( "gno.land/p/demo/avl" "gno.land/p/gov/dao" - "gno.land/p/gov/executor" + govdao "gno.land/r/gov/dao/v2" ) var ( @@ -43,14 +43,12 @@ func AdminRemoveModerator(addr std.Address) { func NewPostExecutor(slug, title, body, publicationDate, authors, tags string) dao.Executor { callback := func() error { - caller := std.DerivePkgAddr("gno.land/r/gov/dao/v2") - - addPost(caller, slug, title, body, publicationDate, authors, tags) + addPost(std.PrevRealm().Addr(), slug, title, body, publicationDate, authors, tags) return nil } - return executor.NewCallbackExecutor(callback) + return govdao.NewGovDAOExecutor(callback) } func ModAddPost(slug, title, body, publicationDate, authors, tags string) { diff --git a/examples/gno.land/r/gnoland/blog/gno.mod b/examples/gno.land/r/gnoland/blog/gno.mod index c1fda8f7d6f..7bb6f3538b7 100644 --- a/examples/gno.land/r/gnoland/blog/gno.mod +++ b/examples/gno.land/r/gnoland/blog/gno.mod @@ -4,5 +4,5 @@ require ( gno.land/p/demo/avl v0.0.0-latest gno.land/p/demo/blog v0.0.0-latest gno.land/p/gov/dao v0.0.0-latest - gno.land/p/gov/executor v0.0.0-latest + gno.land/r/gov/dao/v2 v0.0.0-latest ) diff --git a/examples/gno.land/r/gov/dao/v2/poc.gno b/examples/gno.land/r/gov/dao/v2/poc.gno index 8cae4f22a40..50adfd71799 100644 --- a/examples/gno.land/r/gov/dao/v2/poc.gno +++ b/examples/gno.land/r/gov/dao/v2/poc.gno @@ -1,11 +1,32 @@ package govdao import ( + "errors" + "std" + "gno.land/p/gov/dao" "gno.land/p/gov/executor" ) -const errNoChangesProposed = "no set changes proposed" +const ( + daoPkgPath = "gno.land/r/gov/dao/v2" + + errNoChangesProposed = "no set changes proposed" +) + +var errNotGovDAO = errors.New("only r/gov/dao can be the caller") + +// NewGovDAOExecutor creates the govdao wrapped callback executor +func NewGovDAOExecutor(cb func() error) dao.Executor { + if cb == nil { + panic(errNoChangesProposed) + } + + return executor.NewCallbackExecutor( + cb, + std.CurrentRealm().PkgPath(), + ) +} // NewMemberPropExecutor returns the GOVDAO member change executor func NewMemberPropExecutor(changesFn func() []dao.Member) dao.Executor { @@ -46,7 +67,7 @@ func NewMemberPropExecutor(changesFn func() []dao.Member) dao.Executor { return errs } - return executor.NewCallbackExecutor(callback) + return NewGovDAOExecutor(callback) } func NewDAOImplExecutor(changeFn func() dao.DAO) dao.Executor { @@ -60,7 +81,7 @@ func NewDAOImplExecutor(changeFn func() dao.DAO) dao.Executor { return nil } - return executor.NewCallbackExecutor(callback) + return NewGovDAOExecutor(callback) } func NewMembStoreImplExecutor(changeFn func() dao.MemberStore) dao.Executor { @@ -74,7 +95,7 @@ func NewMembStoreImplExecutor(changeFn func() dao.MemberStore) dao.Executor { return nil } - return executor.NewCallbackExecutor(callback) + return NewGovDAOExecutor(callback) } // setDAOImpl sets a new DAO implementation @@ -86,3 +107,12 @@ func setDAOImpl(impl dao.DAO) { func setMembStoreImpl(impl dao.MemberStore) { members = impl } + +// assertCalledByGovdao asserts that the calling Realm is /r/gov/dao +func assertCalledByGovdao() { + caller := std.CurrentRealm().PkgPath() + + if caller != daoPkgPath { + panic(errNotGovDAO) + } +} diff --git a/examples/gno.land/r/sys/validators/v2/gno.mod b/examples/gno.land/r/sys/validators/v2/gno.mod index 326f39533fb..fe3b4928c26 100644 --- a/examples/gno.land/r/sys/validators/v2/gno.mod +++ b/examples/gno.land/r/sys/validators/v2/gno.mod @@ -7,7 +7,7 @@ require ( gno.land/p/demo/uassert v0.0.0-latest gno.land/p/demo/ufmt v0.0.0-latest gno.land/p/gov/dao v0.0.0-latest - gno.land/p/gov/executor v0.0.0-latest gno.land/p/nt/poa v0.0.0-latest gno.land/p/sys/validators v0.0.0-latest + gno.land/r/gov/dao/v2 v0.0.0-latest ) diff --git a/examples/gno.land/r/sys/validators/v2/poc.gno b/examples/gno.land/r/sys/validators/v2/poc.gno index 29812376a2d..b0836dfff9e 100644 --- a/examples/gno.land/r/sys/validators/v2/poc.gno +++ b/examples/gno.land/r/sys/validators/v2/poc.gno @@ -4,8 +4,8 @@ import ( "std" "gno.land/p/gov/dao" - "gno.land/p/gov/executor" "gno.land/p/sys/validators" + govdao "gno.land/r/gov/dao/v2" ) const errNoChangesProposed = "no set changes proposed" @@ -37,7 +37,7 @@ func NewPropExecutor(changesFn func() []validators.Validator) dao.Executor { return nil } - return executor.NewCallbackExecutor(callback) + return govdao.NewGovDAOExecutor(callback) } // IsValidator returns a flag indicating if the given bech32 address diff --git a/examples/gno.land/r/sys/vars/gno.mod b/examples/gno.land/r/sys/vars/gno.mod index 1cbdcd41c4e..b92f8990b23 100644 --- a/examples/gno.land/r/sys/vars/gno.mod +++ b/examples/gno.land/r/sys/vars/gno.mod @@ -6,5 +6,5 @@ require ( gno.land/p/demo/ufmt v0.0.0-latest gno.land/p/demo/urequire v0.0.0-latest gno.land/p/gov/dao v0.0.0-latest - gno.land/p/gov/executor v0.0.0-latest + gno.land/r/gov/dao/v2 v0.0.0-latest ) diff --git a/examples/gno.land/r/sys/vars/prop.gno b/examples/gno.land/r/sys/vars/prop.gno index 581ad5b2d3b..216b0fe4b05 100644 --- a/examples/gno.land/r/sys/vars/prop.gno +++ b/examples/gno.land/r/sys/vars/prop.gno @@ -4,7 +4,7 @@ import ( "std" "gno.land/p/gov/dao" - "gno.land/p/gov/executor" + govdao "gno.land/r/gov/dao/v2" ) const ( @@ -54,5 +54,5 @@ func NewVarPropExecutor(changesFn func() []KV) dao.Executor { return nil } - return executor.NewCallbackExecutor(cb) + return govdao.NewGovDAOExecutor(cb) } From 6b3b81c3ffefd79a5fa3e63653f5b8e81a55ba77 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Thu, 18 Jul 2024 17:39:59 +0200 Subject: [PATCH 56/80] Remove unused code --- examples/gno.land/r/gov/dao/v2/poc.gno | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/examples/gno.land/r/gov/dao/v2/poc.gno b/examples/gno.land/r/gov/dao/v2/poc.gno index 50adfd71799..531ed9903f1 100644 --- a/examples/gno.land/r/gov/dao/v2/poc.gno +++ b/examples/gno.land/r/gov/dao/v2/poc.gno @@ -1,21 +1,12 @@ package govdao import ( - "errors" "std" "gno.land/p/gov/dao" "gno.land/p/gov/executor" ) -const ( - daoPkgPath = "gno.land/r/gov/dao/v2" - - errNoChangesProposed = "no set changes proposed" -) - -var errNotGovDAO = errors.New("only r/gov/dao can be the caller") - // NewGovDAOExecutor creates the govdao wrapped callback executor func NewGovDAOExecutor(cb func() error) dao.Executor { if cb == nil { @@ -107,12 +98,3 @@ func setDAOImpl(impl dao.DAO) { func setMembStoreImpl(impl dao.MemberStore) { members = impl } - -// assertCalledByGovdao asserts that the calling Realm is /r/gov/dao -func assertCalledByGovdao() { - caller := std.CurrentRealm().PkgPath() - - if caller != daoPkgPath { - panic(errNotGovDAO) - } -} From 480a287fe651d78a0929f983972c62701c92ffc3 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Thu, 18 Jul 2024 18:15:11 +0200 Subject: [PATCH 57/80] Add new prop test --- examples/gno.land/p/gov/executor/errors.gno | 6 +- examples/gno.land/r/gov/dao/v2/dao.gno | 8 +- examples/gno.land/r/gov/dao/v2/poc.gno | 15 ++- .../gno.land/r/gov/dao/v2/prop3_filetest.gno | 119 ++++++++++++++++++ 4 files changed, 141 insertions(+), 7 deletions(-) create mode 100644 examples/gno.land/r/gov/dao/v2/prop3_filetest.gno diff --git a/examples/gno.land/p/gov/executor/errors.gno b/examples/gno.land/p/gov/executor/errors.gno index 187fdc97a9e..5a26384a43f 100644 --- a/examples/gno.land/p/gov/executor/errors.gno +++ b/examples/gno.land/p/gov/executor/errors.gno @@ -26,12 +26,12 @@ func (e *CombinedError) Error() string { } // Add adds a new error to the execution error -func (e *CombinedError) Add(errs ...error) { - if len(errs) == 0 { +func (e *CombinedError) Add(err error) { + if err == nil { return } - e.errors = append(e.errors, errs...) + e.errors = append(e.errors, err) } // Size returns a diff --git a/examples/gno.land/r/gov/dao/v2/dao.gno b/examples/gno.land/r/gov/dao/v2/dao.gno index 3ba467f95c3..06c422385ea 100644 --- a/examples/gno.land/r/gov/dao/v2/dao.gno +++ b/examples/gno.land/r/gov/dao/v2/dao.gno @@ -24,11 +24,13 @@ func init() { VotingPower: 10, }, } - - membStore = membstore.NewMembStore(membstore.WithInitialMembers(set)) ) + + // Set the member store + members = membstore.NewMembStore(membstore.WithInitialMembers(set)) + // Set the DAO implementation - d = simpledao.New(membStore) + d = simpledao.New(members) } // Propose is designed to be called by another contract or with diff --git a/examples/gno.land/r/gov/dao/v2/poc.gno b/examples/gno.land/r/gov/dao/v2/poc.gno index 531ed9903f1..fe286e52786 100644 --- a/examples/gno.land/r/gov/dao/v2/poc.gno +++ b/examples/gno.land/r/gov/dao/v2/poc.gno @@ -1,12 +1,15 @@ package govdao import ( + "errors" "std" "gno.land/p/gov/dao" "gno.land/p/gov/executor" ) +var errNoChangesProposed = errors.New("no set changes proposed") + // NewGovDAOExecutor creates the govdao wrapped callback executor func NewGovDAOExecutor(cb func() error) dao.Executor { if cb == nil { @@ -27,7 +30,9 @@ func NewMemberPropExecutor(changesFn func() []dao.Member) dao.Executor { callback := func() error { errs := &executor.CombinedError{} - for _, member := range changesFn() { + cbMembers := changesFn() + + for _, member := range cbMembers { switch { case !members.IsMember(member.Address): // Addition request @@ -91,10 +96,18 @@ func NewMembStoreImplExecutor(changeFn func() dao.MemberStore) dao.Executor { // setDAOImpl sets a new DAO implementation func setDAOImpl(impl dao.DAO) { + if impl == nil { + panic("invalid member store") + } + d = impl } // setMembStoreImpl sets a new dao.MembStore implementation func setMembStoreImpl(impl dao.MemberStore) { + if impl == nil { + panic("invalid member store") + } + members = impl } diff --git a/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno new file mode 100644 index 00000000000..928ed9b83f8 --- /dev/null +++ b/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno @@ -0,0 +1,119 @@ +package main + +import ( + "std" + + "gno.land/p/gov/dao" + govdao "gno.land/r/gov/dao/v2" +) + +func init() { + memberFn := func() []dao.Member { + return []dao.Member{ + { + Address: std.Address("g123"), + VotingPower: 10, + }, + { + Address: std.Address("g456"), + VotingPower: 10, + }, + { + Address: std.Address("g789"), + VotingPower: 10, + }, + } + } + + // Create a proposal + description := "add new members to the govdao" + + prop := dao.ProposalRequest{ + Description: description, + Executor: govdao.NewMemberPropExecutor(memberFn), + } + + govdao.Propose(prop) +} + +func main() { + println("--") + println(govdao.GetMembStore().Size()) + println("--") + println(govdao.Render("")) + println("--") + println(govdao.Render("0")) + println("--") + govdao.VoteOnProposal(0, "YES") + println("--") + println(govdao.Render("0")) + println("--") + println(govdao.Render("")) + println("--") + govdao.ExecuteProposal(0) + println("--") + println(govdao.Render("0")) + println("--") + println(govdao.Render("")) + println("--") + println(govdao.GetMembStore().Size()) +} + +// Output: +// -- +// 1 +// -- +// - [Proposal #0](/r/gov/dao/v2:0) - (**active**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm) +// +// -- +// # Prop #0 +// +// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm +// +// add new members to the govdao +// +// Status: active +// +// Voting stats: YAY 0 (0%), NAY 0 (0%), ABSTAIN 0 (0%), HAVEN'T VOTED 10 (100%) +// +// Threshold met: false +// +// +// -- +// -- +// # Prop #0 +// +// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm +// +// add new members to the govdao +// +// Status: accepted +// +// Voting stats: YAY 10 (100%), NAY 0 (0%), ABSTAIN 0 (0%), HAVEN'T VOTED 0 (0%) +// +// Threshold met: true +// +// +// -- +// - [Proposal #0](/r/gov/dao/v2:0) - (**accepted**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm) +// +// -- +// -- +// # Prop #0 +// +// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm +// +// add new members to the govdao +// +// Status: execution successful +// +// Voting stats: YAY 10 (0%), NAY 0 (0%), ABSTAIN 0 (0%), HAVEN'T VOTED 30 (0%) +// +// Threshold met: false +// +// +// -- +// - [Proposal #0](/r/gov/dao/v2:0) - (**execution successful**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm) +// +// -- +// 4 From 06fe4fe33428b4cda3f91b1bcd03175165f9ae6b Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Sat, 20 Jul 2024 15:14:01 +0200 Subject: [PATCH 58/80] Move out memberstore --- examples/gno.land/p/demo/membstore/gno.mod | 1 - .../p/{gov/dao => demo/membstore}/members.gno | 2 +- .../gno.land/p/demo/membstore/membstore.gno | 25 ++++----- .../p/demo/membstore/membstore_test.gno | 9 ++- examples/gno.land/p/demo/simpledao/dao.gno | 5 +- .../gno.land/p/demo/simpledao/dao_test.gno | 55 ++++++++++--------- examples/gno.land/p/demo/simpledao/gno.mod | 1 + .../gno.land/p/demo/simpledao/mock_test.gno | 20 +++---- examples/gno.land/p/demo/simpledao/vote.gno | 3 +- examples/gno.land/r/gov/dao/v2/dao.gno | 8 +-- examples/gno.land/r/gov/dao/v2/poc.gno | 9 +-- .../gno.land/r/gov/dao/v2/prop3_filetest.gno | 5 +- 12 files changed, 73 insertions(+), 70 deletions(-) rename examples/gno.land/p/{gov/dao => demo/membstore}/members.gno (98%) diff --git a/examples/gno.land/p/demo/membstore/gno.mod b/examples/gno.land/p/demo/membstore/gno.mod index a85c1f480ca..da22a8dcae4 100644 --- a/examples/gno.land/p/demo/membstore/gno.mod +++ b/examples/gno.land/p/demo/membstore/gno.mod @@ -6,5 +6,4 @@ require ( 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/dao v0.0.0-latest ) diff --git a/examples/gno.land/p/gov/dao/members.gno b/examples/gno.land/p/demo/membstore/members.gno similarity index 98% rename from examples/gno.land/p/gov/dao/members.gno rename to examples/gno.land/p/demo/membstore/members.gno index 871f2feb998..0bbaaaa8b04 100644 --- a/examples/gno.land/p/gov/dao/members.gno +++ b/examples/gno.land/p/demo/membstore/members.gno @@ -1,4 +1,4 @@ -package dao +package membstore import ( "std" diff --git a/examples/gno.land/p/demo/membstore/membstore.gno b/examples/gno.land/p/demo/membstore/membstore.gno index 14ad37913c5..9cd5c02b191 100644 --- a/examples/gno.land/p/demo/membstore/membstore.gno +++ b/examples/gno.land/p/demo/membstore/membstore.gno @@ -6,7 +6,6 @@ import ( "std" "gno.land/p/demo/avl" - "gno.land/p/gov/dao" ) var ( @@ -24,7 +23,7 @@ type Option func(*MembStore) // WithInitialMembers initializes the member store // with an initial member list -func WithInitialMembers(members []dao.Member) Option { +func WithInitialMembers(members []Member) Option { return func(store *MembStore) { for _, m := range members { store.members.Set(m.Address.String(), m) @@ -44,11 +43,11 @@ func WithDAOPkgPath(daoPkgPath string) Option { // MembStore implements the dao.MembStore abstraction type MembStore struct { daoPkgPath string // active dao pkg path, if any - members *avl.Tree // std.Address -> dao.Member + members *avl.Tree // std.Address -> Member totalVotingPower uint64 // cached value for quick lookups } -// NewMembStore creates a new simpledao member store +// NewMembStore creates a new member store func NewMembStore(opts ...Option) *MembStore { m := &MembStore{ members: avl.NewTree(), // empty set @@ -64,7 +63,7 @@ func NewMembStore(opts ...Option) *MembStore { return m } -func (m *MembStore) AddMember(member dao.Member) error { +func (m *MembStore) AddMember(member Member) error { if !m.isCallerGOVDAO() { return ErrNotGovDAO } @@ -83,7 +82,7 @@ func (m *MembStore) AddMember(member dao.Member) error { return nil } -func (m *MembStore) UpdateMember(address std.Address, member dao.Member) error { +func (m *MembStore) UpdateMember(address std.Address, member Member) error { if !m.isCallerGOVDAO() { return ErrNotGovDAO } @@ -133,19 +132,19 @@ func (m *MembStore) IsMember(address std.Address) bool { return exists } -func (m *MembStore) Member(address std.Address) (dao.Member, error) { +func (m *MembStore) Member(address std.Address) (Member, error) { member, exists := m.members.Get(address.String()) if !exists { - return dao.Member{}, ErrMissingMember + return Member{}, ErrMissingMember } - return member.(dao.Member), nil + return member.(Member), nil } -func (m *MembStore) Members(offset, count uint64) []dao.Member { +func (m *MembStore) Members(offset, count uint64) []Member { // Calculate the left and right bounds if count < 1 || offset >= uint64(m.members.Size()) { - return []dao.Member{} + return []Member{} } // Limit the maximum number of returned members @@ -154,12 +153,12 @@ func (m *MembStore) Members(offset, count uint64) []dao.Member { } // Gather the members - members := make([]dao.Member, 0) + members := make([]Member, 0) m.members.IterateByOffset( int(offset), int(count), func(_ string, val interface{}) bool { - member := val.(dao.Member) + member := val.(Member) // Save the member members = append(members, member) diff --git a/examples/gno.land/p/demo/membstore/membstore_test.gno b/examples/gno.land/p/demo/membstore/membstore_test.gno index ea3a2e359f6..2181adde077 100644 --- a/examples/gno.land/p/demo/membstore/membstore_test.gno +++ b/examples/gno.land/p/demo/membstore/membstore_test.gno @@ -9,17 +9,16 @@ import ( "gno.land/p/demo/uassert" "gno.land/p/demo/ufmt" "gno.land/p/demo/urequire" - "gno.land/p/gov/dao" ) // generateMembers generates dummy govdao members -func generateMembers(t *testing.T, count int) []dao.Member { +func generateMembers(t *testing.T, count int) []Member { t.Helper() - members := make([]dao.Member, 0, count) + members := make([]Member, 0, count) for i := 0; i < count; i++ { - members = append(members, dao.Member{ + members = append(members, Member{ Address: testutils.TestAddress(ufmt.Sprintf("member %d", i)), VotingPower: 10, }) @@ -77,7 +76,7 @@ func TestMembStore_GetMembers(t *testing.T) { members = generateMembers(t, numMembers) m = NewMembStore(WithInitialMembers(members)) - verifyMembersPresent = func(members, fetchedMembers []dao.Member) { + verifyMembersPresent = func(members, fetchedMembers []Member) { for _, fetchedMember := range fetchedMembers { for _, member := range members { if member.Address != fetchedMember.Address { diff --git a/examples/gno.land/p/demo/simpledao/dao.gno b/examples/gno.land/p/demo/simpledao/dao.gno index 20e5c3a7897..a77ebd26c26 100644 --- a/examples/gno.land/p/demo/simpledao/dao.gno +++ b/examples/gno.land/p/demo/simpledao/dao.gno @@ -5,6 +5,7 @@ import ( "std" "gno.land/p/demo/avl" + "gno.land/p/demo/membstore" "gno.land/p/demo/ufmt" "gno.land/p/gov/dao" ) @@ -30,11 +31,11 @@ var ( // SimpleDAO is a simple DAO implementation type SimpleDAO struct { proposals *avl.Tree // seqid.ID -> proposal - membStore dao.MemberStore + membStore membstore.MemberStore } // New creates a new instance of the simpledao DAO -func New(membStore dao.MemberStore) *SimpleDAO { +func New(membStore membstore.MemberStore) *SimpleDAO { return &SimpleDAO{ proposals: avl.NewTree(), membStore: membStore, diff --git a/examples/gno.land/p/demo/simpledao/dao_test.gno b/examples/gno.land/p/demo/simpledao/dao_test.gno index 8d493d99296..685107b04e4 100644 --- a/examples/gno.land/p/demo/simpledao/dao_test.gno +++ b/examples/gno.land/p/demo/simpledao/dao_test.gno @@ -5,6 +5,7 @@ import ( "std" "testing" + "gno.land/p/demo/membstore" "gno.land/p/demo/testutils" "gno.land/p/demo/uassert" "gno.land/p/demo/ufmt" @@ -13,13 +14,13 @@ import ( ) // generateMembers generates dummy govdao members -func generateMembers(t *testing.T, count int) []dao.Member { +func generateMembers(t *testing.T, count int) []membstore.Member { t.Helper() - members := make([]dao.Member, 0, count) + members := make([]membstore.Member, 0, count) for i := 0; i < count; i++ { - members = append(members, dao.Member{ + members = append(members, membstore.Member{ Address: testutils.TestAddress(ufmt.Sprintf("member %d", i)), VotingPower: 10, }) @@ -156,8 +157,8 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { fetchErr = errors.New("fetch error") ms = &mockMemberStore{ - memberFn: func(_ std.Address) (dao.Member, error) { - return dao.Member{ + memberFn: func(_ std.Address) (membstore.Member, error) { + return membstore.Member{ Address: voter, }, fetchErr }, @@ -181,12 +182,12 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { var ( voter = testutils.TestAddress("voter") ms = &mockMemberStore{ - memberFn: func(a std.Address) (dao.Member, error) { + memberFn: func(a std.Address) (membstore.Member, error) { if a != voter { - return dao.Member{}, errors.New("not found") + return membstore.Member{}, errors.New("not found") } - return dao.Member{ + return membstore.Member{ Address: voter, }, nil }, @@ -212,12 +213,12 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { voter = testutils.TestAddress("voter") ms = &mockMemberStore{ - memberFn: func(a std.Address) (dao.Member, error) { + memberFn: func(a std.Address) (membstore.Member, error) { if a != voter { - return dao.Member{}, errors.New("not found") + return membstore.Member{}, errors.New("not found") } - return dao.Member{ + return membstore.Member{ Address: voter, }, nil }, @@ -250,12 +251,12 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { voter = testutils.TestAddress("voter") ms = &mockMemberStore{ - memberFn: func(a std.Address) (dao.Member, error) { + memberFn: func(a std.Address) (membstore.Member, error) { if a != voter { - return dao.Member{}, errors.New("not found") + return membstore.Member{}, errors.New("not found") } - return dao.Member{ + return membstore.Member{ Address: voter, }, nil }, @@ -293,15 +294,15 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { var ( voter = testutils.TestAddress("voter") - member = dao.Member{ + member = membstore.Member{ Address: voter, VotingPower: 10, } ms = &mockMemberStore{ - memberFn: func(a std.Address) (dao.Member, error) { + memberFn: func(a std.Address) (membstore.Member, error) { if a != voter { - return dao.Member{}, errors.New("not found") + return membstore.Member{}, errors.New("not found") } return member, nil @@ -340,14 +341,14 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { members = generateMembers(t, 50) ms = &mockMemberStore{ - memberFn: func(address std.Address) (dao.Member, error) { + memberFn: func(address std.Address) (membstore.Member, error) { for _, m := range members { if m.Address == address { return m, nil } } - return dao.Member{}, errors.New("not found") + return membstore.Member{}, errors.New("not found") }, totalPowerFn: func() uint64 { @@ -395,14 +396,14 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { members = generateMembers(t, 50) ms = &mockMemberStore{ - memberFn: func(address std.Address) (dao.Member, error) { + memberFn: func(address std.Address) (membstore.Member, error) { for _, m := range members { if m.Address == address { return m, nil } } - return dao.Member{}, errors.New("member not found") + return membstore.Member{}, errors.New("member not found") }, totalPowerFn: func() uint64 { @@ -450,14 +451,14 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { members = generateMembers(t, 50) ms = &mockMemberStore{ - memberFn: func(address std.Address) (dao.Member, error) { + memberFn: func(address std.Address) (membstore.Member, error) { for _, m := range members { if m.Address == address { return m, nil } } - return dao.Member{}, errors.New("member not found") + return membstore.Member{}, errors.New("member not found") }, totalPowerFn: func() uint64 { @@ -505,14 +506,14 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { members = generateMembers(t, 50) ms = &mockMemberStore{ - memberFn: func(address std.Address) (dao.Member, error) { + memberFn: func(address std.Address) (membstore.Member, error) { for _, m := range members { if m.Address == address { return m, nil } } - return dao.Member{}, errors.New("member not found") + return membstore.Member{}, errors.New("member not found") }, totalPowerFn: func() uint64 { @@ -572,14 +573,14 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { members = generateMembers(t, 50) ms = &mockMemberStore{ - memberFn: func(address std.Address) (dao.Member, error) { + memberFn: func(address std.Address) (membstore.Member, error) { for _, m := range members { if m.Address == address { return m, nil } } - return dao.Member{}, errors.New("member not found") + return membstore.Member{}, errors.New("member not found") }, totalPowerFn: func() uint64 { diff --git a/examples/gno.land/p/demo/simpledao/gno.mod b/examples/gno.land/p/demo/simpledao/gno.mod index a208a173b27..ace427bf817 100644 --- a/examples/gno.land/p/demo/simpledao/gno.mod +++ b/examples/gno.land/p/demo/simpledao/gno.mod @@ -2,6 +2,7 @@ module gno.land/p/demo/simpledao require ( gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/membstore v0.0.0-latest gno.land/p/demo/seqid v0.0.0-latest gno.land/p/demo/testutils v0.0.0-latest gno.land/p/demo/uassert v0.0.0-latest diff --git a/examples/gno.land/p/demo/simpledao/mock_test.gno b/examples/gno.land/p/demo/simpledao/mock_test.gno index 066a8ea4a2c..f335c33daf1 100644 --- a/examples/gno.land/p/demo/simpledao/mock_test.gno +++ b/examples/gno.land/p/demo/simpledao/mock_test.gno @@ -3,7 +3,7 @@ package simpledao import ( "std" - "gno.land/p/gov/dao" + "gno.land/p/demo/membstore" ) type ( @@ -33,13 +33,13 @@ func (m *mockExecutor) IsExpired() bool { } type ( - membersDelegate func(uint64, uint64) []dao.Member + membersDelegate func(uint64, uint64) []membstore.Member sizeDelegate func() int isMemberDelegate func(std.Address) bool totalPowerDelegate func() uint64 - memberDelegate func(std.Address) (dao.Member, error) - addMemberDelegate func(dao.Member) error - updateMemberDelegate func(std.Address, dao.Member) error + memberDelegate func(std.Address) (membstore.Member, error) + addMemberDelegate func(membstore.Member) error + updateMemberDelegate func(std.Address, membstore.Member) error ) type mockMemberStore struct { @@ -52,7 +52,7 @@ type mockMemberStore struct { updateMemberFn updateMemberDelegate } -func (m *mockMemberStore) Members(offset, count uint64) []dao.Member { +func (m *mockMemberStore) Members(offset, count uint64) []membstore.Member { if m.membersFn != nil { return m.membersFn(offset, count) } @@ -84,15 +84,15 @@ func (m *mockMemberStore) TotalPower() uint64 { return 0 } -func (m *mockMemberStore) Member(address std.Address) (dao.Member, error) { +func (m *mockMemberStore) Member(address std.Address) (membstore.Member, error) { if m.memberFn != nil { return m.memberFn(address) } - return dao.Member{}, nil + return membstore.Member{}, nil } -func (m *mockMemberStore) AddMember(member dao.Member) error { +func (m *mockMemberStore) AddMember(member membstore.Member) error { if m.addMemberFn != nil { return m.addMemberFn(member) } @@ -100,7 +100,7 @@ func (m *mockMemberStore) AddMember(member dao.Member) error { return nil } -func (m *mockMemberStore) UpdateMember(address std.Address, member dao.Member) error { +func (m *mockMemberStore) UpdateMember(address std.Address, member membstore.Member) error { if m.updateMemberFn != nil { return m.updateMemberFn(address, member) } diff --git a/examples/gno.land/p/demo/simpledao/vote.gno b/examples/gno.land/p/demo/simpledao/vote.gno index ff139333460..577a8a1604d 100644 --- a/examples/gno.land/p/demo/simpledao/vote.gno +++ b/examples/gno.land/p/demo/simpledao/vote.gno @@ -4,6 +4,7 @@ import ( "errors" "gno.land/p/demo/avl" + "gno.land/p/demo/membstore" "gno.land/p/gov/dao" ) @@ -28,7 +29,7 @@ func newVotes() *votes { } // castVote casts a single vote in the name of the given member -func (v *votes) castVote(member dao.Member, option dao.VoteOption) error { +func (v *votes) castVote(member membstore.Member, option dao.VoteOption) error { // Check if the member voted already address := member.Address.String() diff --git a/examples/gno.land/r/gov/dao/v2/dao.gno b/examples/gno.land/r/gov/dao/v2/dao.gno index 06c422385ea..38df270f68e 100644 --- a/examples/gno.land/r/gov/dao/v2/dao.gno +++ b/examples/gno.land/r/gov/dao/v2/dao.gno @@ -11,14 +11,14 @@ import ( ) var ( - d dao.DAO // the current active DAO implementation - members dao.MemberStore // the member store + d dao.DAO // the current active DAO implementation + members membstore.MemberStore // the member store ) func init() { var ( // Example initial member set (just test addresses) - set = []dao.Member{ + set = []membstore.Member{ { Address: std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"), VotingPower: 10, @@ -64,7 +64,7 @@ func GetPropStore() dao.PropStore { } // GetMembStore returns the active member store -func GetMembStore() dao.MemberStore { +func GetMembStore() membstore.MemberStore { return members } diff --git a/examples/gno.land/r/gov/dao/v2/poc.gno b/examples/gno.land/r/gov/dao/v2/poc.gno index fe286e52786..6f32adefb75 100644 --- a/examples/gno.land/r/gov/dao/v2/poc.gno +++ b/examples/gno.land/r/gov/dao/v2/poc.gno @@ -4,6 +4,7 @@ import ( "errors" "std" + "gno.land/p/demo/membstore" "gno.land/p/gov/dao" "gno.land/p/gov/executor" ) @@ -23,7 +24,7 @@ func NewGovDAOExecutor(cb func() error) dao.Executor { } // NewMemberPropExecutor returns the GOVDAO member change executor -func NewMemberPropExecutor(changesFn func() []dao.Member) dao.Executor { +func NewMemberPropExecutor(changesFn func() []membstore.Member) dao.Executor { if changesFn == nil { panic(errNoChangesProposed) } @@ -41,7 +42,7 @@ func NewMemberPropExecutor(changesFn func() []dao.Member) dao.Executor { errs.Add(err) case member.VotingPower == 0: // Remove request - err := members.UpdateMember(member.Address, dao.Member{ + err := members.UpdateMember(member.Address, membstore.Member{ Address: member.Address, VotingPower: 0, // 0 indicated removal }) @@ -80,7 +81,7 @@ func NewDAOImplExecutor(changeFn func() dao.DAO) dao.Executor { return NewGovDAOExecutor(callback) } -func NewMembStoreImplExecutor(changeFn func() dao.MemberStore) dao.Executor { +func NewMembStoreImplExecutor(changeFn func() membstore.MemberStore) dao.Executor { if changeFn == nil { panic(errNoChangesProposed) } @@ -104,7 +105,7 @@ func setDAOImpl(impl dao.DAO) { } // setMembStoreImpl sets a new dao.MembStore implementation -func setMembStoreImpl(impl dao.MemberStore) { +func setMembStoreImpl(impl membstore.MemberStore) { if impl == nil { panic("invalid member store") } diff --git a/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno index 928ed9b83f8..18484648447 100644 --- a/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno +++ b/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno @@ -3,13 +3,14 @@ package main import ( "std" + "gno.land/p/demo/membstore" "gno.land/p/gov/dao" govdao "gno.land/r/gov/dao/v2" ) func init() { - memberFn := func() []dao.Member { - return []dao.Member{ + memberFn := func() []membstore.Member { + return []membstore.Member{ { Address: std.Address("g123"), VotingPower: 10, From dba0379fc57b3bd6372b7af94c739612fa4c817c Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Sat, 20 Jul 2024 15:21:40 +0200 Subject: [PATCH 59/80] Move p/gov/dao -> p/demo/dao --- examples/gno.land/p/{gov => demo}/dao/dao.gno | 0 examples/gno.land/p/{gov => demo}/dao/executor.gno | 0 examples/gno.land/p/demo/dao/gno.mod | 1 + examples/gno.land/p/{gov => demo}/dao/proposals.gno | 0 examples/gno.land/p/{gov => demo}/dao/vote.gno | 0 examples/gno.land/p/demo/simpledao/dao.gno | 2 +- examples/gno.land/p/demo/simpledao/dao_test.gno | 2 +- examples/gno.land/p/demo/simpledao/gno.mod | 2 +- examples/gno.land/p/demo/simpledao/propstore.gno | 2 +- examples/gno.land/p/demo/simpledao/propstore_test.gno | 2 +- examples/gno.land/p/demo/simpledao/vote.gno | 2 +- examples/gno.land/p/gov/dao/gno.mod | 1 - examples/gno.land/r/gnoland/blog/admin.gno | 2 +- examples/gno.land/r/gnoland/blog/gno.mod | 2 +- examples/gno.land/r/gnoland/valopers/v2/gno.mod | 2 +- examples/gno.land/r/gnoland/valopers/v2/valopers.gno | 2 +- examples/gno.land/r/gov/dao/v2/dao.gno | 2 +- examples/gno.land/r/gov/dao/v2/gno.mod | 2 +- examples/gno.land/r/gov/dao/v2/poc.gno | 2 +- examples/gno.land/r/gov/dao/v2/prop1_filetest.gno | 2 +- examples/gno.land/r/gov/dao/v2/prop2_filetest.gno | 2 +- examples/gno.land/r/gov/dao/v2/prop3_filetest.gno | 2 +- examples/gno.land/r/sys/validators/v2/gno.mod | 2 +- examples/gno.land/r/sys/validators/v2/poc.gno | 2 +- examples/gno.land/r/sys/vars/gno.mod | 2 +- examples/gno.land/r/sys/vars/prop.gno | 2 +- examples/gno.land/r/sys/vars/prop_filetest.gno | 2 +- 27 files changed, 22 insertions(+), 22 deletions(-) rename examples/gno.land/p/{gov => demo}/dao/dao.gno (100%) rename examples/gno.land/p/{gov => demo}/dao/executor.gno (100%) create mode 100644 examples/gno.land/p/demo/dao/gno.mod rename examples/gno.land/p/{gov => demo}/dao/proposals.gno (100%) rename examples/gno.land/p/{gov => demo}/dao/vote.gno (100%) delete mode 100644 examples/gno.land/p/gov/dao/gno.mod diff --git a/examples/gno.land/p/gov/dao/dao.gno b/examples/gno.land/p/demo/dao/dao.gno similarity index 100% rename from examples/gno.land/p/gov/dao/dao.gno rename to examples/gno.land/p/demo/dao/dao.gno diff --git a/examples/gno.land/p/gov/dao/executor.gno b/examples/gno.land/p/demo/dao/executor.gno similarity index 100% rename from examples/gno.land/p/gov/dao/executor.gno rename to examples/gno.land/p/demo/dao/executor.gno diff --git a/examples/gno.land/p/demo/dao/gno.mod b/examples/gno.land/p/demo/dao/gno.mod new file mode 100644 index 00000000000..fbb23299116 --- /dev/null +++ b/examples/gno.land/p/demo/dao/gno.mod @@ -0,0 +1 @@ +module gno.land/p/demo/dao diff --git a/examples/gno.land/p/gov/dao/proposals.gno b/examples/gno.land/p/demo/dao/proposals.gno similarity index 100% rename from examples/gno.land/p/gov/dao/proposals.gno rename to examples/gno.land/p/demo/dao/proposals.gno diff --git a/examples/gno.land/p/gov/dao/vote.gno b/examples/gno.land/p/demo/dao/vote.gno similarity index 100% rename from examples/gno.land/p/gov/dao/vote.gno rename to examples/gno.land/p/demo/dao/vote.gno diff --git a/examples/gno.land/p/demo/simpledao/dao.gno b/examples/gno.land/p/demo/simpledao/dao.gno index a77ebd26c26..e929cf7c617 100644 --- a/examples/gno.land/p/demo/simpledao/dao.gno +++ b/examples/gno.land/p/demo/simpledao/dao.gno @@ -5,9 +5,9 @@ import ( "std" "gno.land/p/demo/avl" + "gno.land/p/demo/dao" "gno.land/p/demo/membstore" "gno.land/p/demo/ufmt" - "gno.land/p/gov/dao" ) var ( diff --git a/examples/gno.land/p/demo/simpledao/dao_test.gno b/examples/gno.land/p/demo/simpledao/dao_test.gno index 685107b04e4..11578709fff 100644 --- a/examples/gno.land/p/demo/simpledao/dao_test.gno +++ b/examples/gno.land/p/demo/simpledao/dao_test.gno @@ -5,12 +5,12 @@ import ( "std" "testing" + "gno.land/p/demo/dao" "gno.land/p/demo/membstore" "gno.land/p/demo/testutils" "gno.land/p/demo/uassert" "gno.land/p/demo/ufmt" "gno.land/p/demo/urequire" - "gno.land/p/gov/dao" ) // generateMembers generates dummy govdao members diff --git a/examples/gno.land/p/demo/simpledao/gno.mod b/examples/gno.land/p/demo/simpledao/gno.mod index ace427bf817..7ae450b335b 100644 --- a/examples/gno.land/p/demo/simpledao/gno.mod +++ b/examples/gno.land/p/demo/simpledao/gno.mod @@ -8,5 +8,5 @@ require ( 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/dao v0.0.0-latest + gno.land/p/demo/dao v0.0.0-latest ) diff --git a/examples/gno.land/p/demo/simpledao/propstore.gno b/examples/gno.land/p/demo/simpledao/propstore.gno index 79310445d7b..71c71c11740 100644 --- a/examples/gno.land/p/demo/simpledao/propstore.gno +++ b/examples/gno.land/p/demo/simpledao/propstore.gno @@ -4,9 +4,9 @@ import ( "errors" "std" + "gno.land/p/demo/dao" "gno.land/p/demo/seqid" "gno.land/p/demo/ufmt" - "gno.land/p/gov/dao" ) var ( diff --git a/examples/gno.land/p/demo/simpledao/propstore_test.gno b/examples/gno.land/p/demo/simpledao/propstore_test.gno index bb49bf4e1a0..eaf5150cfeb 100644 --- a/examples/gno.land/p/demo/simpledao/propstore_test.gno +++ b/examples/gno.land/p/demo/simpledao/propstore_test.gno @@ -3,11 +3,11 @@ package simpledao import ( "testing" + "gno.land/p/demo/dao" "gno.land/p/demo/testutils" "gno.land/p/demo/uassert" "gno.land/p/demo/ufmt" "gno.land/p/demo/urequire" - "gno.land/p/gov/dao" ) // generateProposals generates dummy proposals diff --git a/examples/gno.land/p/demo/simpledao/vote.gno b/examples/gno.land/p/demo/simpledao/vote.gno index 577a8a1604d..ecd616d978e 100644 --- a/examples/gno.land/p/demo/simpledao/vote.gno +++ b/examples/gno.land/p/demo/simpledao/vote.gno @@ -4,8 +4,8 @@ import ( "errors" "gno.land/p/demo/avl" + "gno.land/p/demo/dao" "gno.land/p/demo/membstore" - "gno.land/p/gov/dao" ) var ErrAlreadyVoted = errors.New("vote already cast") diff --git a/examples/gno.land/p/gov/dao/gno.mod b/examples/gno.land/p/gov/dao/gno.mod deleted file mode 100644 index 66a285c4465..00000000000 --- a/examples/gno.land/p/gov/dao/gno.mod +++ /dev/null @@ -1 +0,0 @@ -module gno.land/p/gov/dao diff --git a/examples/gno.land/r/gnoland/blog/admin.gno b/examples/gno.land/r/gnoland/blog/admin.gno index 9ca705a8b94..3cd069effda 100644 --- a/examples/gno.land/r/gnoland/blog/admin.gno +++ b/examples/gno.land/r/gnoland/blog/admin.gno @@ -5,7 +5,7 @@ import ( "strings" "gno.land/p/demo/avl" - "gno.land/p/gov/dao" + "gno.land/p/demo/dao" govdao "gno.land/r/gov/dao/v2" ) diff --git a/examples/gno.land/r/gnoland/blog/gno.mod b/examples/gno.land/r/gnoland/blog/gno.mod index 7bb6f3538b7..be52f25b7f3 100644 --- a/examples/gno.land/r/gnoland/blog/gno.mod +++ b/examples/gno.land/r/gnoland/blog/gno.mod @@ -3,6 +3,6 @@ module gno.land/r/gnoland/blog require ( gno.land/p/demo/avl v0.0.0-latest gno.land/p/demo/blog v0.0.0-latest - gno.land/p/gov/dao v0.0.0-latest + gno.land/p/demo/dao v0.0.0-latest gno.land/r/gov/dao/v2 v0.0.0-latest ) diff --git a/examples/gno.land/r/gnoland/valopers/v2/gno.mod b/examples/gno.land/r/gnoland/valopers/v2/gno.mod index 03ba2638dfe..3f3a4e1d163 100644 --- a/examples/gno.land/r/gnoland/valopers/v2/gno.mod +++ b/examples/gno.land/r/gnoland/valopers/v2/gno.mod @@ -5,7 +5,7 @@ require ( 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/gov/dao v0.0.0-latest + gno.land/p/demo/dao v0.0.0-latest gno.land/p/sys/validators v0.0.0-latest gno.land/r/gov/dao/v2 v0.0.0-latest gno.land/r/sys/validators/v2 v0.0.0-latest diff --git a/examples/gno.land/r/gnoland/valopers/v2/valopers.gno b/examples/gno.land/r/gnoland/valopers/v2/valopers.gno index 28c25344c1d..00b22912d0a 100644 --- a/examples/gno.land/r/gnoland/valopers/v2/valopers.gno +++ b/examples/gno.land/r/gnoland/valopers/v2/valopers.gno @@ -6,8 +6,8 @@ import ( "std" "gno.land/p/demo/avl" + "gno.land/p/demo/dao" "gno.land/p/demo/ufmt" - "gno.land/p/gov/dao" pVals "gno.land/p/sys/validators" govdao "gno.land/r/gov/dao/v2" validators "gno.land/r/sys/validators/v2" diff --git a/examples/gno.land/r/gov/dao/v2/dao.gno b/examples/gno.land/r/gov/dao/v2/dao.gno index 38df270f68e..ff4d9493020 100644 --- a/examples/gno.land/r/gov/dao/v2/dao.gno +++ b/examples/gno.land/r/gov/dao/v2/dao.gno @@ -4,10 +4,10 @@ import ( "std" "strconv" + "gno.land/p/demo/dao" "gno.land/p/demo/membstore" "gno.land/p/demo/simpledao" "gno.land/p/demo/ufmt" - "gno.land/p/gov/dao" ) var ( diff --git a/examples/gno.land/r/gov/dao/v2/gno.mod b/examples/gno.land/r/gov/dao/v2/gno.mod index 28a5081502d..8ebb7580ddb 100644 --- a/examples/gno.land/r/gov/dao/v2/gno.mod +++ b/examples/gno.land/r/gov/dao/v2/gno.mod @@ -4,6 +4,6 @@ require ( gno.land/p/demo/membstore v0.0.0-latest gno.land/p/demo/simpledao v0.0.0-latest gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/gov/dao v0.0.0-latest + gno.land/p/demo/dao v0.0.0-latest gno.land/p/gov/executor v0.0.0-latest ) diff --git a/examples/gno.land/r/gov/dao/v2/poc.gno b/examples/gno.land/r/gov/dao/v2/poc.gno index 6f32adefb75..568781b9b0c 100644 --- a/examples/gno.land/r/gov/dao/v2/poc.gno +++ b/examples/gno.land/r/gov/dao/v2/poc.gno @@ -4,8 +4,8 @@ import ( "errors" "std" + "gno.land/p/demo/dao" "gno.land/p/demo/membstore" - "gno.land/p/gov/dao" "gno.land/p/gov/executor" ) diff --git a/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno index f2b51cd3de8..69e55ef1ab6 100644 --- a/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno +++ b/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno @@ -10,7 +10,7 @@ package main import ( "std" - "gno.land/p/gov/dao" + "gno.land/p/demo/dao" pVals "gno.land/p/sys/validators" govdao "gno.land/r/gov/dao/v2" validators "gno.land/r/sys/validators/v2" diff --git a/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno index 4998bcf5d81..32ddc11b67c 100644 --- a/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno +++ b/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno @@ -3,7 +3,7 @@ package main import ( "time" - "gno.land/p/gov/dao" + "gno.land/p/demo/dao" gnoblog "gno.land/r/gnoland/blog" govdao "gno.land/r/gov/dao/v2" ) diff --git a/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno index 18484648447..3711ce304e0 100644 --- a/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno +++ b/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno @@ -3,8 +3,8 @@ package main import ( "std" + "gno.land/p/demo/dao" "gno.land/p/demo/membstore" - "gno.land/p/gov/dao" govdao "gno.land/r/gov/dao/v2" ) diff --git a/examples/gno.land/r/sys/validators/v2/gno.mod b/examples/gno.land/r/sys/validators/v2/gno.mod index fe3b4928c26..3dd5e58203d 100644 --- a/examples/gno.land/r/sys/validators/v2/gno.mod +++ b/examples/gno.land/r/sys/validators/v2/gno.mod @@ -6,7 +6,7 @@ require ( 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/gov/dao v0.0.0-latest + gno.land/p/demo/dao v0.0.0-latest gno.land/p/nt/poa v0.0.0-latest gno.land/p/sys/validators v0.0.0-latest gno.land/r/gov/dao/v2 v0.0.0-latest diff --git a/examples/gno.land/r/sys/validators/v2/poc.gno b/examples/gno.land/r/sys/validators/v2/poc.gno index b0836dfff9e..db91525fe0c 100644 --- a/examples/gno.land/r/sys/validators/v2/poc.gno +++ b/examples/gno.land/r/sys/validators/v2/poc.gno @@ -3,7 +3,7 @@ package validators import ( "std" - "gno.land/p/gov/dao" + "gno.land/p/demo/dao" "gno.land/p/sys/validators" govdao "gno.land/r/gov/dao/v2" ) diff --git a/examples/gno.land/r/sys/vars/gno.mod b/examples/gno.land/r/sys/vars/gno.mod index b92f8990b23..d08b707b153 100644 --- a/examples/gno.land/r/sys/vars/gno.mod +++ b/examples/gno.land/r/sys/vars/gno.mod @@ -5,6 +5,6 @@ require ( 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/dao v0.0.0-latest + gno.land/p/demo/dao v0.0.0-latest gno.land/r/gov/dao/v2 v0.0.0-latest ) diff --git a/examples/gno.land/r/sys/vars/prop.gno b/examples/gno.land/r/sys/vars/prop.gno index 216b0fe4b05..802eb42f0b0 100644 --- a/examples/gno.land/r/sys/vars/prop.gno +++ b/examples/gno.land/r/sys/vars/prop.gno @@ -3,7 +3,7 @@ package vars import ( "std" - "gno.land/p/gov/dao" + "gno.land/p/demo/dao" govdao "gno.land/r/gov/dao/v2" ) diff --git a/examples/gno.land/r/sys/vars/prop_filetest.gno b/examples/gno.land/r/sys/vars/prop_filetest.gno index 0132e0a4404..560d80385cf 100644 --- a/examples/gno.land/r/sys/vars/prop_filetest.gno +++ b/examples/gno.land/r/sys/vars/prop_filetest.gno @@ -1,7 +1,7 @@ package main import ( - "gno.land/p/gov/dao" + "gno.land/p/demo/dao" govdao "gno.land/r/gov/dao/v2" "gno.land/r/sys/vars" ) From 873ba9b1ecd9638fc18337578720e76fb379ae09 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Sun, 21 Jul 2024 18:42:16 +0200 Subject: [PATCH 60/80] Drop voting fetchers from the propstore --- examples/gno.land/p/demo/dao/dao.gno | 13 ++- examples/gno.land/p/demo/dao/proposals.gno | 6 - examples/gno.land/p/demo/dao/vote.gno | 18 +++ examples/gno.land/p/demo/simpledao/dao.gno | 59 ++++++++-- .../gno.land/p/demo/simpledao/dao_test.gno | 24 ++-- examples/gno.land/p/demo/simpledao/gno.mod | 2 +- .../gno.land/p/demo/simpledao/propstore.gno | 60 +--------- .../p/demo/simpledao/propstore_test.gno | 110 ++++++------------ .../simpledao/{vote.gno => votestore.gno} | 22 ++-- .../gno.land/r/gnoland/valopers/v2/gno.mod | 2 +- examples/gno.land/r/gov/dao/v2/dao.gno | 2 +- examples/gno.land/r/gov/dao/v2/gno.mod | 2 +- examples/gno.land/r/gov/dao/v2/poc.gno | 23 ---- examples/gno.land/r/sys/validators/v2/gno.mod | 2 +- examples/gno.land/r/sys/vars/gno.mod | 2 +- 15 files changed, 148 insertions(+), 199 deletions(-) rename examples/gno.land/p/demo/simpledao/{vote.gno => votestore.gno} (64%) diff --git a/examples/gno.land/p/demo/dao/dao.gno b/examples/gno.land/p/demo/dao/dao.gno index 1da6ae30b0c..66d6720033c 100644 --- a/examples/gno.land/p/demo/dao/dao.gno +++ b/examples/gno.land/p/demo/dao/dao.gno @@ -1,5 +1,15 @@ 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" + ProposalEventExecutionKey = "exec-status" +) + // ProposalRequest is a single govdao proposal request // that contains the necessary information to // log and generate a valid proposal @@ -17,9 +27,6 @@ type DAO interface { // Returns the generated proposal ID Propose(request ProposalRequest) (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 } diff --git a/examples/gno.land/p/demo/dao/proposals.gno b/examples/gno.land/p/demo/dao/proposals.gno index a9daa146b76..6a1b273cb9b 100644 --- a/examples/gno.land/p/demo/dao/proposals.gno +++ b/examples/gno.land/p/demo/dao/proposals.gno @@ -46,15 +46,9 @@ type Proposal interface { // Executor returns the proposal executor Executor() Executor - // Votes returns the votes of the proposal - Votes(offset, count uint64) []Vote - // VotingStats returns the voting stats of the proposal VotingStats() VotingStats - // VoteByMember returns the proposal vote by the member, if any - VoteByMember(address std.Address) (Vote, error) - // Render renders the proposal in a readable format Render() string } diff --git a/examples/gno.land/p/demo/dao/vote.gno b/examples/gno.land/p/demo/dao/vote.gno index d8825ca9bcb..70b23c943cc 100644 --- a/examples/gno.land/p/demo/dao/vote.gno +++ b/examples/gno.land/p/demo/dao/vote.gno @@ -4,6 +4,14 @@ import ( "std" ) +const ( + VoteAddedEvent = "VoteAdded" // emitted when a vote was cast for a proposal + + VoteAddedIDKey = "proposal-id" + VoteAddedAuthorKey = "author" + VoteAddedOptionKey = "option" +) + type VoteOption string const ( @@ -31,22 +39,32 @@ type VotingStats struct { TotalVotingPower uint64 } +// YayPercent returns the percentage (0-100) of the yay votes +// in relation to the total voting power func (v VotingStats) YayPercent() uint64 { return (v.YayVotes / v.TotalVotingPower) * 100 } +// NayPercent returns the percentage (0-100) of the nay votes +// in relation to the total voting power func (v VotingStats) NayPercent() uint64 { return (v.NayVotes / v.TotalVotingPower) * 100 } +// AbstainPercent returns the percentage (0-100) of the abstain votes +// in relation to the total voting power func (v VotingStats) AbstainPercent() uint64 { return (v.AbstainVotes / v.TotalVotingPower) * 100 } +// MissingVotes returns the summed voting power that has not +// participated in proposal voting yet func (v VotingStats) 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 VotingStats) MissingVotesPercent() uint64 { return (v.MissingVotes() / v.TotalVotingPower) * 100 } diff --git a/examples/gno.land/p/demo/simpledao/dao.gno b/examples/gno.land/p/demo/simpledao/dao.gno index e929cf7c617..4bb40d860b0 100644 --- a/examples/gno.land/p/demo/simpledao/dao.gno +++ b/examples/gno.land/p/demo/simpledao/dao.gno @@ -65,7 +65,7 @@ func (s *SimpleDAO) Propose(request dao.ProposalRequest) (uint64, error) { description: request.Description, executor: request.Executor, status: dao.Active, - votes: newVotes(), + tally: newTally(), getTotalVotingPowerFn: s.membStore.TotalPower, } @@ -80,7 +80,9 @@ func (s *SimpleDAO) Propose(request dao.ProposalRequest) (uint64, error) { func (s *SimpleDAO) VoteOnProposal(id uint64, option dao.VoteOption) error { // Verify the GOVDAO member - member, err := s.membStore.Member(getDAOCaller()) + caller := getDAOCaller() + + member, err := s.membStore.Member(caller) if err != nil { return ufmt.Errorf("unable to get govdao member, %s", err.Error()) } @@ -112,30 +114,56 @@ func (s *SimpleDAO) VoteOnProposal(id uint64, option dao.VoteOption) error { } // Cast the vote - if err = prop.votes.castVote(member, option); err != nil { + if err = prop.tally.castVote(member, option); err != nil { return ufmt.Errorf("unable to vote on proposal %d, %s", id, err.Error()) } + // Emit the vote cast event + std.Emit( + dao.VoteAddedEvent, + dao.VoteAddedIDKey, ufmt.Sprintf("%d", id), + dao.VoteAddedAuthorKey, caller.String(), + dao.VoteAddedOptionKey, option.String(), + ) + // Check the votes to see if quorum is reached var ( totalPower = s.membStore.TotalPower() majorityPower = (2 * totalPower) / 3 ) + acceptProposal := func() { + prop.status = dao.Accepted + + std.Emit( + dao.ProposalAcceptedEvent, + dao.ProposalEventIDKey, ufmt.Sprintf("%d", id), + ) + } + + declineProposal := func() { + prop.status = dao.NotAccepted + + std.Emit( + dao.ProposalNotAcceptedEvent, + dao.ProposalEventIDKey, ufmt.Sprintf("%d", id), + ) + } + switch { - case prop.votes.yays > majorityPower: + case prop.tally.yays > majorityPower: // 2/3+ voted YES - prop.status = dao.Accepted - case prop.votes.nays > majorityPower: + acceptProposal() + case prop.tally.nays > majorityPower: // 2/3+ voted NO - prop.status = dao.NotAccepted - case prop.votes.abstains > majorityPower: + declineProposal() + case prop.tally.abstains > majorityPower: // 2/3+ voted ABSTAIN - prop.status = dao.NotAccepted - case prop.votes.yays+prop.votes.nays+prop.votes.abstains >= totalPower: + declineProposal() + case prop.tally.yays+prop.tally.nays+prop.tally.abstains >= totalPower: // Everyone voted, but it's undecided, // hence the proposal can't go through - prop.status = dao.NotAccepted + declineProposal() default: // Quorum not reached } @@ -181,6 +209,15 @@ func (s *SimpleDAO) ExecuteProposal(id uint64) error { return ErrExecutorExpired } + // Emit an event when the execution finishes + defer func() { + std.Emit( + dao.ProposalExecutedEvent, + dao.ProposalEventIDKey, ufmt.Sprintf("%d", id), + dao.ProposalEventExecutionKey, prop.status.String(), + ) + }() + // Attempt to execute the proposal if err = prop.executor.Execute(); err != nil { prop.status = dao.ExecutionFailed diff --git a/examples/gno.land/p/demo/simpledao/dao_test.gno b/examples/gno.land/p/demo/simpledao/dao_test.gno index 11578709fff..189cf4068bd 100644 --- a/examples/gno.land/p/demo/simpledao/dao_test.gno +++ b/examples/gno.land/p/demo/simpledao/dao_test.gno @@ -122,7 +122,7 @@ func TestSimpleDAO_Propose(t *testing.T) { s = New(ms) ) - // Set the sent coins to enough + // Set the sent coins to be enough // to cover the fee std.TestSetOrigSend(sentCoins, std.Coins{}) std.TestSetOrigCaller(proposer) @@ -142,7 +142,13 @@ func TestSimpleDAO_Propose(t *testing.T) { uassert.Equal(t, proposer.String(), prop.Author().String()) uassert.Equal(t, description, prop.Description()) uassert.Equal(t, dao.Active.String(), prop.Status().String()) - uassert.Equal(t, 0, len(prop.Votes(0, maxRequestVotes))) + + stats := prop.VotingStats() + + uassert.Equal(t, uint64(0), stats.YayVotes) + uassert.Equal(t, uint64(0), stats.NayVotes) + uassert.Equal(t, uint64(0), stats.AbstainVotes) + uassert.Equal(t, uint64(0), stats.TotalVotingPower) }) } @@ -313,14 +319,14 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { prop = &proposal{ status: dao.Active, executor: &mockExecutor{}, - votes: newVotes(), + tally: newTally(), } ) std.TestSetOrigCaller(voter) // Cast the initial vote - urequire.NoError(t, prop.votes.castVote(member, dao.YesVote)) + urequire.NoError(t, prop.tally.castVote(member, dao.YesVote)) // Add an initial proposal id, err := s.addProposal(prop) @@ -366,7 +372,7 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { prop = &proposal{ status: dao.Active, executor: &mockExecutor{}, - votes: newVotes(), + tally: newTally(), } ) @@ -421,7 +427,7 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { prop = &proposal{ status: dao.Active, executor: &mockExecutor{}, - votes: newVotes(), + tally: newTally(), } ) @@ -476,7 +482,7 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { prop = &proposal{ status: dao.Active, executor: &mockExecutor{}, - votes: newVotes(), + tally: newTally(), } ) @@ -531,7 +537,7 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { prop = &proposal{ status: dao.Active, executor: &mockExecutor{}, - votes: newVotes(), + tally: newTally(), } ) @@ -598,7 +604,7 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { prop = &proposal{ status: dao.Active, executor: &mockExecutor{}, - votes: newVotes(), + tally: newTally(), } ) diff --git a/examples/gno.land/p/demo/simpledao/gno.mod b/examples/gno.land/p/demo/simpledao/gno.mod index 7ae450b335b..f6f14f379ec 100644 --- a/examples/gno.land/p/demo/simpledao/gno.mod +++ b/examples/gno.land/p/demo/simpledao/gno.mod @@ -2,11 +2,11 @@ module gno.land/p/demo/simpledao require ( gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/dao v0.0.0-latest gno.land/p/demo/membstore v0.0.0-latest gno.land/p/demo/seqid 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/demo/dao v0.0.0-latest ) diff --git a/examples/gno.land/p/demo/simpledao/propstore.gno b/examples/gno.land/p/demo/simpledao/propstore.gno index 71c71c11740..2cadcd4e077 100644 --- a/examples/gno.land/p/demo/simpledao/propstore.gno +++ b/examples/gno.land/p/demo/simpledao/propstore.gno @@ -9,10 +9,7 @@ import ( "gno.land/p/demo/ufmt" ) -var ( - ErrMissingProposal = errors.New("proposal is missing") - ErrMissingVote = errors.New("member has not voted") -) +var ErrMissingProposal = errors.New("proposal is missing") const ( // maxRequestProposals is the maximum number of @@ -32,8 +29,8 @@ type proposal struct { executor dao.Executor // executor for the proposal status dao.ProposalStatus // status of the proposal - votes *votes // voting mechanism - getTotalVotingPowerFn func() uint64 // total voting power of the voting body + tally *tally // voting tally + getTotalVotingPowerFn func() uint64 // callback for the total voting power } func (p *proposal) Author() std.Address { @@ -52,63 +49,18 @@ func (p *proposal) Executor() dao.Executor { return p.executor } -func (p *proposal) Votes(offset, count uint64) []dao.Vote { - // Calculate the left and right bounds - if count < 1 || offset >= uint64(p.votes.voters.Size()) { - return []dao.Vote{} - } - - // Limit the maximum number of returned votes - if count > maxRequestVotes { - count = maxRequestVotes - } - - votes := make([]dao.Vote, 0, p.votes.voters.Size()) - p.votes.voters.IterateByOffset( - int(offset), - int(count), - func(key string, val interface{}) bool { - option := val.(dao.VoteOption) - - vote := dao.Vote{ - Address: std.Address(key), - Option: option, - } - - votes = append(votes, vote) - - return false - }) - - return votes -} - func (p *proposal) VotingStats() dao.VotingStats { // Get the total voting power of the body totalPower := p.getTotalVotingPowerFn() return dao.VotingStats{ - YayVotes: p.votes.yays, - NayVotes: p.votes.nays, - AbstainVotes: p.votes.abstains, + YayVotes: p.tally.yays, + NayVotes: p.tally.nays, + AbstainVotes: p.tally.abstains, TotalVotingPower: totalPower, } } -func (p *proposal) VoteByMember(address std.Address) (dao.Vote, error) { - optionRaw, exists := p.votes.voters.Get(address.String()) - if !exists { - return dao.Vote{}, ErrMissingVote - } - - vote := dao.Vote{ - Address: address, - Option: optionRaw.(dao.VoteOption), - } - - return vote, nil -} - func (p *proposal) Render() string { // Fetch the voting stats stats := p.VotingStats() diff --git a/examples/gno.land/p/demo/simpledao/propstore_test.gno b/examples/gno.land/p/demo/simpledao/propstore_test.gno index eaf5150cfeb..c3c3864ad14 100644 --- a/examples/gno.land/p/demo/simpledao/propstore_test.gno +++ b/examples/gno.land/p/demo/simpledao/propstore_test.gno @@ -24,8 +24,11 @@ func generateProposals(t *testing.T, count int) []*proposal { author: members[i].Address, description: ufmt.Sprintf("proposal %d", i), status: dao.Active, - votes: newVotes(), - executor: nil, + tally: newTally(), + getTotalVotingPowerFn: func() uint64 { + return 0 + }, + executor: nil, } proposals = append(proposals, proposal) @@ -55,26 +58,13 @@ func equalProposals(t *testing.T, p1, p2 dao.Proposal) { p2.Status().String(), ) - uassert.Equal( - t, - len(p1.Votes(0, maxRequestVotes)), - len(p2.Votes(0, maxRequestVotes)), - ) - - p1Votes := p1.Votes(0, maxRequestVotes) - for index, v2 := range p2.Votes(0, maxRequestVotes) { - uassert.Equal( - t, - p1Votes[index].Address.String(), - v2.Address.String(), - ) + p1Stats := p1.VotingStats() + p2Stats := p2.VotingStats() - uassert.Equal( - t, - p1Votes[index].Option.String(), - v2.Option.String(), - ) - } + uassert.Equal(t, p1Stats.YayVotes, p2Stats.YayVotes) + uassert.Equal(t, p1Stats.NayVotes, p2Stats.NayVotes) + uassert.Equal(t, p1Stats.AbstainVotes, p2Stats.AbstainVotes) + uassert.Equal(t, p1Stats.TotalVotingPower, p2Stats.TotalVotingPower) } func TestProposal_Data(t *testing.T) { @@ -140,77 +130,45 @@ func TestProposal_Data(t *testing.T) { t.Parallel() p := &proposal{ - votes: newVotes(), + tally: newTally(), + getTotalVotingPowerFn: func() uint64 { + return 0 + }, } - uassert.Equal(t, 0, len(p.Votes(0, maxRequestVotes))) + stats := p.VotingStats() + + uassert.Equal(t, uint64(0), stats.YayVotes) + uassert.Equal(t, uint64(0), stats.NayVotes) + uassert.Equal(t, uint64(0), stats.AbstainVotes) + uassert.Equal(t, uint64(0), stats.TotalVotingPower) }) t.Run("existing votes", func(t *testing.T) { t.Parallel() var ( - members = generateMembers(t, 50) - p = &proposal{ - votes: newVotes(), - } - ) - - for _, m := range members { - urequire.NoError(t, p.votes.castVote(m, dao.YesVote)) - } - - votes := p.Votes(0, maxRequestVotes) - - urequire.Equal(t, len(members), len(votes)) - - for _, v := range votes { - for _, m := range members { - if m.Address != v.Address { - continue - } - - uassert.Equal(t, dao.YesVote.String(), v.Option.String()) - } - } - }) - - t.Run("vote by member", func(t *testing.T) { - t.Parallel() + members = generateMembers(t, 50) + totalPower = uint64(len(members)) * 10 - var ( - members = generateMembers(t, 50) - p = &proposal{ - votes: newVotes(), + p = &proposal{ + tally: newTally(), + getTotalVotingPowerFn: func() uint64 { + return totalPower + }, } ) for _, m := range members { - urequire.NoError(t, p.votes.castVote(m, dao.YesVote)) - } - - votes := p.Votes(0, maxRequestVotes) - urequire.Equal(t, len(members), len(votes)) - - vote, err := p.VoteByMember(members[0].Address) - urequire.NoError(t, err) - - uassert.Equal(t, dao.YesVote.String(), vote.Option.String()) - uassert.Equal(t, members[0].Address.String(), vote.Address.String()) - }) - - t.Run("missing vote by member", func(t *testing.T) { - t.Parallel() - - p := &proposal{ - votes: newVotes(), + urequire.NoError(t, p.tally.castVote(m, dao.YesVote)) } - votes := p.Votes(0, maxRequestVotes) - urequire.Equal(t, 0, len(votes)) + stats := p.VotingStats() - _, err := p.VoteByMember(testutils.TestAddress("dummy")) - uassert.ErrorIs(t, err, ErrMissingVote) + uassert.Equal(t, totalPower, stats.YayVotes) + uassert.Equal(t, uint64(0), stats.NayVotes) + uassert.Equal(t, uint64(0), stats.AbstainVotes) + uassert.Equal(t, totalPower, stats.TotalVotingPower) }) } diff --git a/examples/gno.land/p/demo/simpledao/vote.gno b/examples/gno.land/p/demo/simpledao/votestore.gno similarity index 64% rename from examples/gno.land/p/demo/simpledao/vote.gno rename to examples/gno.land/p/demo/simpledao/votestore.gno index ecd616d978e..35a6564a1e3 100644 --- a/examples/gno.land/p/demo/simpledao/vote.gno +++ b/examples/gno.land/p/demo/simpledao/votestore.gno @@ -10,8 +10,8 @@ import ( var ErrAlreadyVoted = errors.New("vote already cast") -// votes is a simple weighted voting system -type votes struct { +// tally is a simple vote tally system +type tally struct { // tally cache to keep track of active // yes / no / abstain votes yays uint64 @@ -21,19 +21,19 @@ type votes struct { voters *avl.Tree // std.Address -> dao.VoteOption } -// newVotes creates a new weighted voting system instance -func newVotes() *votes { - return &votes{ +// newTally creates a new tally system instance +func newTally() *tally { + return &tally{ voters: avl.NewTree(), } } // castVote casts a single vote in the name of the given member -func (v *votes) castVote(member membstore.Member, option dao.VoteOption) error { +func (t *tally) castVote(member membstore.Member, option dao.VoteOption) error { // Check if the member voted already address := member.Address.String() - _, voted := v.voters.Get(address) + _, voted := t.voters.Get(address) if voted { return ErrAlreadyVoted } @@ -41,15 +41,15 @@ func (v *votes) castVote(member membstore.Member, option dao.VoteOption) error { // Update the tally switch option { case dao.YesVote: - v.yays += member.VotingPower + t.yays += member.VotingPower case dao.AbstainVote: - v.abstains += member.VotingPower + t.abstains += member.VotingPower default: - v.nays += member.VotingPower + t.nays += member.VotingPower } // Save the voting status - v.voters.Set(address, option) + t.voters.Set(address, option) return nil } diff --git a/examples/gno.land/r/gnoland/valopers/v2/gno.mod b/examples/gno.land/r/gnoland/valopers/v2/gno.mod index 3f3a4e1d163..b976359c476 100644 --- a/examples/gno.land/r/gnoland/valopers/v2/gno.mod +++ b/examples/gno.land/r/gnoland/valopers/v2/gno.mod @@ -2,10 +2,10 @@ module gno.land/r/gnoland/valopers/v2 require ( gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/dao 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/dao v0.0.0-latest gno.land/p/sys/validators v0.0.0-latest gno.land/r/gov/dao/v2 v0.0.0-latest gno.land/r/sys/validators/v2 v0.0.0-latest diff --git a/examples/gno.land/r/gov/dao/v2/dao.gno b/examples/gno.land/r/gov/dao/v2/dao.gno index ff4d9493020..c37eda80bff 100644 --- a/examples/gno.land/r/gov/dao/v2/dao.gno +++ b/examples/gno.land/r/gov/dao/v2/dao.gno @@ -11,7 +11,7 @@ import ( ) var ( - d dao.DAO // the current active DAO implementation + d *simpledao.SimpleDAO // the current active DAO implementation members membstore.MemberStore // the member store ) diff --git a/examples/gno.land/r/gov/dao/v2/gno.mod b/examples/gno.land/r/gov/dao/v2/gno.mod index 8ebb7580ddb..f8877c857a4 100644 --- a/examples/gno.land/r/gov/dao/v2/gno.mod +++ b/examples/gno.land/r/gov/dao/v2/gno.mod @@ -1,9 +1,9 @@ module gno.land/r/gov/dao/v2 require ( + gno.land/p/demo/dao v0.0.0-latest gno.land/p/demo/membstore v0.0.0-latest gno.land/p/demo/simpledao v0.0.0-latest gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/dao v0.0.0-latest gno.land/p/gov/executor v0.0.0-latest ) diff --git a/examples/gno.land/r/gov/dao/v2/poc.gno b/examples/gno.land/r/gov/dao/v2/poc.gno index 568781b9b0c..d4d3a629679 100644 --- a/examples/gno.land/r/gov/dao/v2/poc.gno +++ b/examples/gno.land/r/gov/dao/v2/poc.gno @@ -67,20 +67,6 @@ func NewMemberPropExecutor(changesFn func() []membstore.Member) dao.Executor { return NewGovDAOExecutor(callback) } -func NewDAOImplExecutor(changeFn func() dao.DAO) dao.Executor { - if changeFn == nil { - panic(errNoChangesProposed) - } - - callback := func() error { - setDAOImpl(changeFn()) - - return nil - } - - return NewGovDAOExecutor(callback) -} - func NewMembStoreImplExecutor(changeFn func() membstore.MemberStore) dao.Executor { if changeFn == nil { panic(errNoChangesProposed) @@ -95,15 +81,6 @@ func NewMembStoreImplExecutor(changeFn func() membstore.MemberStore) dao.Executo return NewGovDAOExecutor(callback) } -// setDAOImpl sets a new DAO implementation -func setDAOImpl(impl dao.DAO) { - if impl == nil { - panic("invalid member store") - } - - d = impl -} - // setMembStoreImpl sets a new dao.MembStore implementation func setMembStoreImpl(impl membstore.MemberStore) { if impl == nil { diff --git a/examples/gno.land/r/sys/validators/v2/gno.mod b/examples/gno.land/r/sys/validators/v2/gno.mod index 3dd5e58203d..3374666095b 100644 --- a/examples/gno.land/r/sys/validators/v2/gno.mod +++ b/examples/gno.land/r/sys/validators/v2/gno.mod @@ -2,11 +2,11 @@ module gno.land/r/sys/validators/v2 require ( gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/dao v0.0.0-latest gno.land/p/demo/seqid 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/dao v0.0.0-latest gno.land/p/nt/poa v0.0.0-latest gno.land/p/sys/validators v0.0.0-latest gno.land/r/gov/dao/v2 v0.0.0-latest diff --git a/examples/gno.land/r/sys/vars/gno.mod b/examples/gno.land/r/sys/vars/gno.mod index d08b707b153..7c6e06dc2d4 100644 --- a/examples/gno.land/r/sys/vars/gno.mod +++ b/examples/gno.land/r/sys/vars/gno.mod @@ -2,9 +2,9 @@ module gno.land/r/sys/vars require ( gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/dao 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/demo/dao v0.0.0-latest gno.land/r/gov/dao/v2 v0.0.0-latest ) From 6ceb72ad3021cfd6fc5843c0125d5fdc084fb643 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Sun, 21 Jul 2024 18:46:17 +0200 Subject: [PATCH 61/80] Rename VotingStats -> Stats --- examples/gno.land/p/demo/dao/proposals.gno | 4 ++-- examples/gno.land/p/demo/dao/vote.gno | 14 +++++++------- examples/gno.land/p/demo/simpledao/dao_test.gno | 2 +- examples/gno.land/p/demo/simpledao/propstore.gno | 6 +++--- .../gno.land/p/demo/simpledao/propstore_test.gno | 8 ++++---- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/examples/gno.land/p/demo/dao/proposals.gno b/examples/gno.land/p/demo/dao/proposals.gno index 6a1b273cb9b..cbc84d3377d 100644 --- a/examples/gno.land/p/demo/dao/proposals.gno +++ b/examples/gno.land/p/demo/dao/proposals.gno @@ -46,8 +46,8 @@ type Proposal interface { // Executor returns the proposal executor Executor() Executor - // VotingStats returns the voting stats of the proposal - VotingStats() VotingStats + // Stats returns the voting stats of the proposal + Stats() Stats // Render renders the proposal in a readable format Render() string diff --git a/examples/gno.land/p/demo/dao/vote.gno b/examples/gno.land/p/demo/dao/vote.gno index 70b23c943cc..75a0f8b27ea 100644 --- a/examples/gno.land/p/demo/dao/vote.gno +++ b/examples/gno.land/p/demo/dao/vote.gno @@ -30,8 +30,8 @@ type Vote struct { Option VoteOption // the voting option } -// VotingStats encompasses the proposal voting stats -type VotingStats struct { +// Stats encompasses the proposal voting stats +type Stats struct { YayVotes uint64 NayVotes uint64 AbstainVotes uint64 @@ -41,30 +41,30 @@ type VotingStats struct { // YayPercent returns the percentage (0-100) of the yay votes // in relation to the total voting power -func (v VotingStats) YayPercent() uint64 { +func (v Stats) YayPercent() uint64 { return (v.YayVotes / v.TotalVotingPower) * 100 } // NayPercent returns the percentage (0-100) of the nay votes // in relation to the total voting power -func (v VotingStats) NayPercent() uint64 { +func (v Stats) NayPercent() uint64 { return (v.NayVotes / v.TotalVotingPower) * 100 } // AbstainPercent returns the percentage (0-100) of the abstain votes // in relation to the total voting power -func (v VotingStats) AbstainPercent() uint64 { +func (v Stats) AbstainPercent() uint64 { return (v.AbstainVotes / v.TotalVotingPower) * 100 } // MissingVotes returns the summed voting power that has not // participated in proposal voting yet -func (v VotingStats) MissingVotes() uint64 { +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 VotingStats) MissingVotesPercent() uint64 { +func (v Stats) MissingVotesPercent() uint64 { return (v.MissingVotes() / v.TotalVotingPower) * 100 } diff --git a/examples/gno.land/p/demo/simpledao/dao_test.gno b/examples/gno.land/p/demo/simpledao/dao_test.gno index 189cf4068bd..81a0c3b9afe 100644 --- a/examples/gno.land/p/demo/simpledao/dao_test.gno +++ b/examples/gno.land/p/demo/simpledao/dao_test.gno @@ -143,7 +143,7 @@ func TestSimpleDAO_Propose(t *testing.T) { uassert.Equal(t, description, prop.Description()) uassert.Equal(t, dao.Active.String(), prop.Status().String()) - stats := prop.VotingStats() + stats := prop.Stats() uassert.Equal(t, uint64(0), stats.YayVotes) uassert.Equal(t, uint64(0), stats.NayVotes) diff --git a/examples/gno.land/p/demo/simpledao/propstore.gno b/examples/gno.land/p/demo/simpledao/propstore.gno index 2cadcd4e077..eefe23ecb4a 100644 --- a/examples/gno.land/p/demo/simpledao/propstore.gno +++ b/examples/gno.land/p/demo/simpledao/propstore.gno @@ -49,11 +49,11 @@ func (p *proposal) Executor() dao.Executor { return p.executor } -func (p *proposal) VotingStats() dao.VotingStats { +func (p *proposal) Stats() dao.Stats { // Get the total voting power of the body totalPower := p.getTotalVotingPowerFn() - return dao.VotingStats{ + return dao.Stats{ YayVotes: p.tally.yays, NayVotes: p.tally.nays, AbstainVotes: p.tally.abstains, @@ -63,7 +63,7 @@ func (p *proposal) VotingStats() dao.VotingStats { func (p *proposal) Render() string { // Fetch the voting stats - stats := p.VotingStats() + stats := p.Stats() output := "" output += ufmt.Sprintf("Author: %s", p.Author().String()) diff --git a/examples/gno.land/p/demo/simpledao/propstore_test.gno b/examples/gno.land/p/demo/simpledao/propstore_test.gno index c3c3864ad14..5aa6ba91a1e 100644 --- a/examples/gno.land/p/demo/simpledao/propstore_test.gno +++ b/examples/gno.land/p/demo/simpledao/propstore_test.gno @@ -58,8 +58,8 @@ func equalProposals(t *testing.T, p1, p2 dao.Proposal) { p2.Status().String(), ) - p1Stats := p1.VotingStats() - p2Stats := p2.VotingStats() + p1Stats := p1.Stats() + p2Stats := p2.Stats() uassert.Equal(t, p1Stats.YayVotes, p2Stats.YayVotes) uassert.Equal(t, p1Stats.NayVotes, p2Stats.NayVotes) @@ -136,7 +136,7 @@ func TestProposal_Data(t *testing.T) { }, } - stats := p.VotingStats() + stats := p.Stats() uassert.Equal(t, uint64(0), stats.YayVotes) uassert.Equal(t, uint64(0), stats.NayVotes) @@ -163,7 +163,7 @@ func TestProposal_Data(t *testing.T) { urequire.NoError(t, p.tally.castVote(m, dao.YesVote)) } - stats := p.VotingStats() + stats := p.Stats() uassert.Equal(t, totalPower, stats.YayVotes) uassert.Equal(t, uint64(0), stats.NayVotes) From 8198c7fb95b60298e17330b32c2679b3a1851e45 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Sun, 21 Jul 2024 18:54:45 +0200 Subject: [PATCH 62/80] Add key name to r/sys/vars emit event --- examples/gno.land/r/sys/vars/prop.gno | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/examples/gno.land/r/sys/vars/prop.gno b/examples/gno.land/r/sys/vars/prop.gno index 802eb42f0b0..e33fa7e9a06 100644 --- a/examples/gno.land/r/sys/vars/prop.gno +++ b/examples/gno.land/r/sys/vars/prop.gno @@ -11,6 +11,8 @@ const ( KeyAddedEvent = "KeyAdded" // emitted when a key was added to the set KeyRemovedEvent = "KeyRemoved" // emitted when a key was removed from the set KeyUpdatedEvent = "KeyUpdated" // emitted when a key value was updated in the set + + KeyEventNameKey = "name" ) type KV struct { @@ -28,7 +30,10 @@ func NewVarPropExecutor(changesFn func() []KV) dao.Executor { vars.Remove(kv.Key) // Emit the key set change - std.Emit(KeyRemovedEvent) + std.Emit( + KeyRemovedEvent, + KeyEventNameKey, kv.Key, + ) continue } @@ -39,7 +44,10 @@ func NewVarPropExecutor(changesFn func() []KV) dao.Executor { vars.Set(kv.Key, kv.Value) // Emit the key set change - std.Emit(KeyUpdatedEvent) + std.Emit( + KeyUpdatedEvent, + KeyEventNameKey, kv.Key, + ) continue } @@ -48,7 +56,10 @@ func NewVarPropExecutor(changesFn func() []KV) dao.Executor { vars.Set(kv.Key, kv.Value) // Emit the key set change - std.Emit(KeyAddedEvent) + std.Emit( + KeyAddedEvent, + KeyEventNameKey, kv.Key, + ) } return nil From 676b2021578446fc945de1b506aa2793e5595078 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20=C5=BDivkovi=C4=87?= Date: Thu, 15 Aug 2024 10:45:41 +0200 Subject: [PATCH 63/80] Make the executor not panic due to wrong caller --- examples/gno.land/p/gov/executor/callback.gno | 16 +++------- examples/gno.land/p/gov/executor/context.gno | 8 +++-- examples/gno.land/p/gov/executor/gno.mod | 1 + .../gno.land/p/gov/executor/proposal_test.gno | 32 ++++--------------- 4 files changed, 18 insertions(+), 39 deletions(-) diff --git a/examples/gno.land/p/gov/executor/callback.gno b/examples/gno.land/p/gov/executor/callback.gno index 1e4417c0623..509bd4ebeb5 100644 --- a/examples/gno.land/p/gov/executor/callback.gno +++ b/examples/gno.land/p/gov/executor/callback.gno @@ -25,8 +25,11 @@ type CallbackExecutor struct { // Execute runs the executor's callback function. func (exec *CallbackExecutor) Execute() error { - // Verify the executor is r/gov/dao - assertCaller(exec.daoPkgPath) + // Verify the caller is an adequate Realm + caller := std.CurrentRealm().PkgPath() + if caller != exec.daoPkgPath { + return errInvalidCaller + } if exec.callback != nil { return exec.callback() @@ -40,12 +43,3 @@ func (exec *CallbackExecutor) Execute() error { func (exec *CallbackExecutor) IsExpired() bool { return false } - -// assertCaller asserts that the caller Realm matches the path -func assertCaller(path string) { - caller := std.CurrentRealm().PkgPath() - - if caller != path { - panic(errInvalidCaller) - } -} diff --git a/examples/gno.land/p/gov/executor/context.gno b/examples/gno.land/p/gov/executor/context.gno index 0740fb76a6a..d892444581e 100644 --- a/examples/gno.land/p/gov/executor/context.gno +++ b/examples/gno.land/p/gov/executor/context.gno @@ -2,6 +2,7 @@ package executor import ( "errors" + "std" "gno.land/p/demo/context" ) @@ -35,8 +36,11 @@ func NewCtxExecutor(callback func(ctx context.Context) error, path string) *CtxE // Execute runs the executor's callback function func (exec *CtxExecutor) Execute() error { - // Verify the executor is r/gov/dao - assertCaller(exec.daoPkgPath) + // Verify the caller is an adequate Realm + caller := std.CurrentRealm().PkgPath() + if caller != exec.daoPkgPath { + return errInvalidCaller + } // Create the context ctx := context.WithValue( diff --git a/examples/gno.land/p/gov/executor/gno.mod b/examples/gno.land/p/gov/executor/gno.mod index 3ae47397665..99f2ab3610b 100644 --- a/examples/gno.land/p/gov/executor/gno.mod +++ b/examples/gno.land/p/gov/executor/gno.mod @@ -3,4 +3,5 @@ module gno.land/p/gov/executor require ( gno.land/p/demo/context v0.0.0-latest gno.land/p/demo/uassert v0.0.0-latest + gno.land/p/demo/urequire v0.0.0-latest ) diff --git a/examples/gno.land/p/gov/executor/proposal_test.gno b/examples/gno.land/p/gov/executor/proposal_test.gno index 58a918cfaf8..3a70fc40596 100644 --- a/examples/gno.land/p/gov/executor/proposal_test.gno +++ b/examples/gno.land/p/gov/executor/proposal_test.gno @@ -7,6 +7,7 @@ import ( "gno.land/p/demo/context" "gno.land/p/demo/uassert" + "gno.land/p/demo/urequire" ) func TestExecutor_Callback(t *testing.T) { @@ -29,10 +30,7 @@ func TestExecutor_Callback(t *testing.T) { e := NewCallbackExecutor(cb, "gno.land/r/gov/dao") // Execute as not the /r/gov/dao caller - uassert.PanicsWithMessage(t, errInvalidCaller.Error(), func() { - _ = e.Execute() - }) - + uassert.ErrorIs(t, e.Execute(), errInvalidCaller) uassert.False(t, called, "expected proposal to not execute") }) @@ -57,12 +55,7 @@ func TestExecutor_Callback(t *testing.T) { r := std.NewCodeRealm(daoPkgPath) std.TestSetRealm(r) - uassert.NotPanics(t, func() { - err := e.Execute() - - uassert.NoError(t, err) - }) - + uassert.NoError(t, e.Execute()) uassert.True(t, called, "expected proposal to execute") }) @@ -88,12 +81,7 @@ func TestExecutor_Callback(t *testing.T) { r := std.NewCodeRealm(daoPkgPath) std.TestSetRealm(r) - uassert.NotPanics(t, func() { - err := e.Execute() - - uassert.ErrorIs(t, err, expectedErr) - }) - + uassert.ErrorIs(t, e.Execute(), expectedErr) uassert.True(t, called, "expected proposal to execute") }) } @@ -122,10 +110,7 @@ func TestExecutor_Context(t *testing.T) { e := NewCtxExecutor(cb, "gno.land/r/gov/dao") // Execute as not the /r/gov/dao caller - uassert.PanicsWithMessage(t, errInvalidCaller.Error(), func() { - _ = e.Execute() - }) - + uassert.ErrorIs(t, e.Execute(), errInvalidCaller) uassert.False(t, called, "expected proposal to not execute") }) @@ -154,12 +139,7 @@ func TestExecutor_Context(t *testing.T) { r := std.NewCodeRealm(daoPkgPath) std.TestSetRealm(r) - uassert.NotPanics(t, func() { - err := e.Execute() - - uassert.NoError(t, err) - }) - + urequire.NoError(t, e.Execute()) uassert.True(t, called, "expected proposal to execute") }) From 852291cb342d7eacdce178ae2f24084767a77126 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20=C5=BDivkovi=C4=87?= Date: Thu, 15 Aug 2024 10:52:11 +0200 Subject: [PATCH 64/80] isCallerGOVDAO -> isCallerDAORealm --- examples/gno.land/p/demo/membstore/membstore.gno | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/gno.land/p/demo/membstore/membstore.gno b/examples/gno.land/p/demo/membstore/membstore.gno index 9cd5c02b191..04d2fbd6d1a 100644 --- a/examples/gno.land/p/demo/membstore/membstore.gno +++ b/examples/gno.land/p/demo/membstore/membstore.gno @@ -64,7 +64,7 @@ func NewMembStore(opts ...Option) *MembStore { } func (m *MembStore) AddMember(member Member) error { - if !m.isCallerGOVDAO() { + if !m.isCallerDAORealm() { return ErrNotGovDAO } @@ -83,7 +83,7 @@ func (m *MembStore) AddMember(member Member) error { } func (m *MembStore) UpdateMember(address std.Address, member Member) error { - if !m.isCallerGOVDAO() { + if !m.isCallerDAORealm() { return ErrNotGovDAO } @@ -179,12 +179,12 @@ func (m *MembStore) TotalPower() uint64 { return m.totalVotingPower } -// isCallerGOVDAO returns a flag indicating if the -// current caller context is the active GOVDAO. -// We need to include a govdao guard, even if the +// isCallerDAORealm returns a flag indicating if the +// current caller context is the active DAO Realm. +// We need to include a dao guard, even if the // executor guarantees it, because // the API of the member store is public and callable // by anyone who has a reference to the member store instance. -func (m *MembStore) isCallerGOVDAO() bool { +func (m *MembStore) isCallerDAORealm() bool { return m.daoPkgPath == "" || std.CurrentRealm().PkgPath() == m.daoPkgPath } From ae650d2375181017d3f65762588d349886cfa47e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20=C5=BDivkovi=C4=87?= Date: Thu, 15 Aug 2024 11:06:25 +0200 Subject: [PATCH 65/80] Resolve nitpick --- examples/gno.land/p/demo/dao/vote.gno | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/gno.land/p/demo/dao/vote.gno b/examples/gno.land/p/demo/dao/vote.gno index 75a0f8b27ea..58d74ad73ed 100644 --- a/examples/gno.land/p/demo/dao/vote.gno +++ b/examples/gno.land/p/demo/dao/vote.gno @@ -42,19 +42,19 @@ type Stats struct { // 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 / v.TotalVotingPower) * 100 + 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 / v.TotalVotingPower) * 100 + 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 / v.TotalVotingPower) * 100 + return v.AbstainVotes * 100 / v.TotalVotingPower } // MissingVotes returns the summed voting power that has not @@ -66,5 +66,5 @@ func (v Stats) MissingVotes() uint64 { // 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() / v.TotalVotingPower) * 100 + return v.MissingVotes() * 100 / v.TotalVotingPower } From aa4f9efd9001c3c67ecadd6c5db160a89e955da6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20=C5=BDivkovi=C4=87?= Date: Thu, 15 Aug 2024 13:19:26 +0200 Subject: [PATCH 66/80] Fix faulty test assert --- examples/gno.land/r/gov/dao/v2/prop3_filetest.gno | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno index 3711ce304e0..5aa9947c74b 100644 --- a/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno +++ b/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno @@ -108,7 +108,7 @@ func main() { // // Status: execution successful // -// Voting stats: YAY 10 (0%), NAY 0 (0%), ABSTAIN 0 (0%), HAVEN'T VOTED 30 (0%) +// Voting stats: YAY 10 (25%), NAY 0 (0%), ABSTAIN 0 (0%), HAVEN'T VOTED 30 (75%) // // Threshold met: false // From 30b80f7472aa32f200890178e02a8fd49125776f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20=C5=BDivkovi=C4=87?= Date: Mon, 7 Oct 2024 11:24:28 +0200 Subject: [PATCH 67/80] Move r/sys/vars outside the PR --- examples/gno.land/r/sys/vars/gno.mod | 10 -- examples/gno.land/r/sys/vars/prop.gno | 69 ---------- .../gno.land/r/sys/vars/prop_filetest.gno | 107 ---------------- examples/gno.land/r/sys/vars/vars.gno | 92 -------------- examples/gno.land/r/sys/vars/vars_test.gno | 120 ------------------ 5 files changed, 398 deletions(-) delete mode 100644 examples/gno.land/r/sys/vars/gno.mod delete mode 100644 examples/gno.land/r/sys/vars/prop.gno delete mode 100644 examples/gno.land/r/sys/vars/prop_filetest.gno delete mode 100644 examples/gno.land/r/sys/vars/vars.gno delete mode 100644 examples/gno.land/r/sys/vars/vars_test.gno diff --git a/examples/gno.land/r/sys/vars/gno.mod b/examples/gno.land/r/sys/vars/gno.mod deleted file mode 100644 index 7c6e06dc2d4..00000000000 --- a/examples/gno.land/r/sys/vars/gno.mod +++ /dev/null @@ -1,10 +0,0 @@ -module gno.land/r/sys/vars - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/dao 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/r/gov/dao/v2 v0.0.0-latest -) diff --git a/examples/gno.land/r/sys/vars/prop.gno b/examples/gno.land/r/sys/vars/prop.gno deleted file mode 100644 index e33fa7e9a06..00000000000 --- a/examples/gno.land/r/sys/vars/prop.gno +++ /dev/null @@ -1,69 +0,0 @@ -package vars - -import ( - "std" - - "gno.land/p/demo/dao" - govdao "gno.land/r/gov/dao/v2" -) - -const ( - KeyAddedEvent = "KeyAdded" // emitted when a key was added to the set - KeyRemovedEvent = "KeyRemoved" // emitted when a key was removed from the set - KeyUpdatedEvent = "KeyUpdated" // emitted when a key value was updated in the set - - KeyEventNameKey = "name" -) - -type KV struct { - Key string - Value interface{} -} - -// NewVarPropExecutor creates a new proposal executor for -// changing r/sys/vars values through governance proposals -func NewVarPropExecutor(changesFn func() []KV) dao.Executor { - cb := func() error { - for _, kv := range changesFn() { - if kv.Value == nil { - // Removal request - vars.Remove(kv.Key) - - // Emit the key set change - std.Emit( - KeyRemovedEvent, - KeyEventNameKey, kv.Key, - ) - - continue - } - - // Check if it's an update request - if vars.Has(kv.Key) { - // Update the value - vars.Set(kv.Key, kv.Value) - - // Emit the key set change - std.Emit( - KeyUpdatedEvent, - KeyEventNameKey, kv.Key, - ) - - continue - } - - // Set the new value - vars.Set(kv.Key, kv.Value) - - // Emit the key set change - std.Emit( - KeyAddedEvent, - KeyEventNameKey, kv.Key, - ) - } - - return nil - } - - return govdao.NewGovDAOExecutor(cb) -} diff --git a/examples/gno.land/r/sys/vars/prop_filetest.gno b/examples/gno.land/r/sys/vars/prop_filetest.gno deleted file mode 100644 index 560d80385cf..00000000000 --- a/examples/gno.land/r/sys/vars/prop_filetest.gno +++ /dev/null @@ -1,107 +0,0 @@ -package main - -import ( - "gno.land/p/demo/dao" - govdao "gno.land/r/gov/dao/v2" - "gno.land/r/sys/vars" -) - -func init() { - changesFn := func() []vars.KV { - return []vars.KV{ - { - Key: "key 1", - Value: "value 1", - }, - { - Key: "key 2", - Value: "value 2", - }, - } - } - - // Create the executor - executor := vars.NewVarPropExecutor(changesFn) - - // Create the proposal - description := "Example value setting" - - prop := dao.ProposalRequest{ - Description: description, - Executor: executor, - } - - govdao.Propose(prop) -} - -func main() { - println("--") - println(govdao.Render("")) - println("--") - println(govdao.Render("0")) - println("--") - govdao.VoteOnProposal(0, dao.YesVote) - println("--") - println(govdao.Render("0")) - println("--") - govdao.ExecuteProposal(0) - println("--") - println(govdao.Render("0")) - println("--") - println(vars.GetStringValue("key 1")) - println("--") - println(vars.GetStringValue("key 2")) -} - -// Output: -// -- -// - [Proposal #0](/r/gov/dao/v2:0) - (**active**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm) -// -// -- -// # Prop #0 -// -// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm -// -// Example value setting -// -// Status: active -// -// Voting stats: YAY 0 (0%), NAY 0 (0%), ABSTAIN 0 (0%), HAVEN'T VOTED 10 (100%) -// -// Threshold met: false -// -// -// -- -// -- -// # Prop #0 -// -// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm -// -// Example value setting -// -// Status: accepted -// -// Voting stats: YAY 10 (100%), NAY 0 (0%), ABSTAIN 0 (0%), HAVEN'T VOTED 0 (0%) -// -// Threshold met: true -// -// -// -- -// -- -// # Prop #0 -// -// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm -// -// Example value setting -// -// Status: execution successful -// -// Voting stats: YAY 10 (100%), NAY 0 (0%), ABSTAIN 0 (0%), HAVEN'T VOTED 0 (0%) -// -// Threshold met: true -// -// -// -- -// value 1 -// -- -// value 2 diff --git a/examples/gno.land/r/sys/vars/vars.gno b/examples/gno.land/r/sys/vars/vars.gno deleted file mode 100644 index 4d85ad0cf29..00000000000 --- a/examples/gno.land/r/sys/vars/vars.gno +++ /dev/null @@ -1,92 +0,0 @@ -package vars - -import ( - "errors" - - "gno.land/p/demo/avl" -) - -// maxRequestKeys is the maximum number of keys -// that can be returned on request -const maxRequestKeys = 50 - -var ( - ErrMissingValue = errors.New("missing value") - ErrInvalidValueType = errors.New("invalid value type") - ErrTooManyValues = errors.New("too many values requested") -) - -// vars is a simple KV store -var vars *avl.Tree - -func init() { - // The initial variable set is empty - vars = avl.NewTree() -} - -// GetValue fetches the given value from the store, if any -func GetValue(key string) interface{} { - val, exists := vars.Get(key) - if !exists { - panic(ErrMissingValue) - } - - return val -} - -// GetStringValue returns the string value associated -// with the given key, if any -func GetStringValue(key string) string { - // Get the value - val := GetValue(key) - - // Make sure it's actually a string - valStr, ok := val.(string) - if !ok { - panic(ErrInvalidValueType) - } - - return valStr -} - -// GetStringValues fetches a list of values -func GetStringValues(keys ...string) []string { - if len(keys) > maxRequestKeys { - panic(ErrTooManyValues) - } - - vals := make([]string, 0, len(keys)) - for _, key := range keys { - vals = append(vals, GetStringValue(key)) - } - - return vals -} - -// GetKeys returns the paginated key values -func GetKeys(offset, count uint64) []string { - // Calculate the left and right bounds - if count < 1 || offset >= uint64(vars.Size()) { - return []string{} - } - - // Limit the maximum number of returned keys - if count > maxRequestKeys { - count = maxRequestKeys - } - - // Gather the members - keys := make([]string, 0) - vars.IterateByOffset( - int(offset), - int(count), - func(key string, _ interface{}) bool { - // Save the key - keys = append(keys, key) - - return false - }, - ) - - return keys -} diff --git a/examples/gno.land/r/sys/vars/vars_test.gno b/examples/gno.land/r/sys/vars/vars_test.gno deleted file mode 100644 index 76d02be4cc1..00000000000 --- a/examples/gno.land/r/sys/vars/vars_test.gno +++ /dev/null @@ -1,120 +0,0 @@ -package vars - -import ( - "sort" - "testing" - - "gno.land/p/demo/avl" - "gno.land/p/demo/uassert" - "gno.land/p/demo/ufmt" - "gno.land/p/demo/urequire" -) - -func TestVars_GetValue(t *testing.T) { - t.Parallel() - - t.Run("missing value", func(t *testing.T) { - t.Parallel() - - uassert.PanicsWithMessage(t, ErrMissingValue.Error(), func() { - GetValue("random key") - }) - }) - - t.Run("existing value", func(t *testing.T) { - t.Parallel() - - vars = avl.NewTree() - - var ( - key = "new key" - value = "new value" - ) - - vars.Set(key, value) - - uassert.NotPanics(t, func() { - valRaw := GetValue(key) - - val, ok := valRaw.(string) - urequire.True(t, ok) - - uassert.Equal(t, value, val) - }) - }) -} - -func TestVars_GetStringValue(t *testing.T) { - t.Parallel() - - t.Run("missing value", func(t *testing.T) { - t.Parallel() - - uassert.PanicsWithMessage(t, ErrMissingValue.Error(), func() { - GetStringValue("random key") - }) - }) - - t.Run("existing string value", func(t *testing.T) { - t.Parallel() - - vars = avl.NewTree() - - var ( - key = "new string key" - value = "new value" - ) - - vars.Set(key, value) - - uassert.NotPanics(t, func() { - val := GetStringValue(key) - - uassert.Equal(t, value, val) - }) - }) -} - -func TestVars_GetKeys(t *testing.T) { - t.Parallel() - - t.Run("no keys", func(t *testing.T) { - t.Parallel() - - vars = avl.NewTree() - - keys := GetKeys(0, maxRequestKeys) - - // Make sure there are 0 keys - urequire.Equal(t, 0, len(keys)) - }) - - t.Run("existing keys", func(t *testing.T) { - t.Parallel() - - vars = avl.NewTree() - - savedKeys := make([]string, 0, maxRequestKeys) - for i := 0; i < maxRequestKeys; i++ { - kv := KV{ - Key: ufmt.Sprintf("key-%d", i), - Value: ufmt.Sprintf("value-%d", i), - } - - savedKeys = append(savedKeys, kv.Key) - - vars.Set(kv.Key, kv.Value) - } - - // Fetch the keys - keys := GetKeys(0, maxRequestKeys) - urequire.Equal(t, maxRequestKeys, len(keys)) - - sort.Strings(keys) - sort.Strings(savedKeys) - - for index, key := range keys { - uassert.Equal(t, savedKeys[index], key) - } - }) -} From 7addbd5c47e7e233d567033aec1d48c384c6d5c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20=C5=BDivkovi=C4=87?= Date: Mon, 7 Oct 2024 11:36:07 +0200 Subject: [PATCH 68/80] Move event emissions to the dao package --- examples/gno.land/p/demo/dao/events.gno | 46 ++++++++++++++++++++++ examples/gno.land/p/demo/simpledao/dao.gno | 25 ++---------- 2 files changed, 50 insertions(+), 21 deletions(-) create mode 100644 examples/gno.land/p/demo/dao/events.gno diff --git a/examples/gno.land/p/demo/dao/events.gno b/examples/gno.land/p/demo/dao/events.gno new file mode 100644 index 00000000000..21d7ddfcbe5 --- /dev/null +++ b/examples/gno.land/p/demo/dao/events.gno @@ -0,0 +1,46 @@ +package dao + +import ( + "std" + + "gno.land/p/demo/ufmt" +) + +// 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(), + ) +} diff --git a/examples/gno.land/p/demo/simpledao/dao.gno b/examples/gno.land/p/demo/simpledao/dao.gno index 4bb40d860b0..36634a17cfc 100644 --- a/examples/gno.land/p/demo/simpledao/dao.gno +++ b/examples/gno.land/p/demo/simpledao/dao.gno @@ -119,12 +119,7 @@ func (s *SimpleDAO) VoteOnProposal(id uint64, option dao.VoteOption) error { } // Emit the vote cast event - std.Emit( - dao.VoteAddedEvent, - dao.VoteAddedIDKey, ufmt.Sprintf("%d", id), - dao.VoteAddedAuthorKey, caller.String(), - dao.VoteAddedOptionKey, option.String(), - ) + dao.EmitVoteAdded(id, caller, option) // Check the votes to see if quorum is reached var ( @@ -135,19 +130,13 @@ func (s *SimpleDAO) VoteOnProposal(id uint64, option dao.VoteOption) error { acceptProposal := func() { prop.status = dao.Accepted - std.Emit( - dao.ProposalAcceptedEvent, - dao.ProposalEventIDKey, ufmt.Sprintf("%d", id), - ) + dao.EmitProposalAccepted(id) } declineProposal := func() { prop.status = dao.NotAccepted - std.Emit( - dao.ProposalNotAcceptedEvent, - dao.ProposalEventIDKey, ufmt.Sprintf("%d", id), - ) + dao.EmitProposalNotAccepted(id) } switch { @@ -210,13 +199,7 @@ func (s *SimpleDAO) ExecuteProposal(id uint64) error { } // Emit an event when the execution finishes - defer func() { - std.Emit( - dao.ProposalExecutedEvent, - dao.ProposalEventIDKey, ufmt.Sprintf("%d", id), - dao.ProposalEventExecutionKey, prop.status.String(), - ) - }() + defer dao.EmitProposalExecuted(id, prop.status) // Attempt to execute the proposal if err = prop.executor.Execute(); err != nil { From 2f2632b79502f45da031de52e1a5e8fa6888c8e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20=C5=BDivkovi=C4=87?= Date: Mon, 7 Oct 2024 11:43:01 +0200 Subject: [PATCH 69/80] Add p/demo/dao doc and new event --- examples/gno.land/p/demo/dao/dao.gno | 1 + examples/gno.land/p/demo/dao/doc.gno | 5 +++++ examples/gno.land/p/demo/dao/events.gno | 10 ++++++++++ examples/gno.land/p/demo/simpledao/dao.gno | 3 +++ 4 files changed, 19 insertions(+) create mode 100644 examples/gno.land/p/demo/dao/doc.gno diff --git a/examples/gno.land/p/demo/dao/dao.gno b/examples/gno.land/p/demo/dao/dao.gno index 66d6720033c..f8ea433192f 100644 --- a/examples/gno.land/p/demo/dao/dao.gno +++ b/examples/gno.land/p/demo/dao/dao.gno @@ -7,6 +7,7 @@ const ( ProposalExecutedEvent = "ProposalExecuted" // emitted when a proposal has been executed ProposalEventIDKey = "proposal-id" + ProposalEventAuthorKey = "proposal-author" ProposalEventExecutionKey = "exec-status" ) diff --git a/examples/gno.land/p/demo/dao/doc.gno b/examples/gno.land/p/demo/dao/doc.gno new file mode 100644 index 00000000000..3fb28204013 --- /dev/null +++ b/examples/gno.land/p/demo/dao/doc.gno @@ -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 diff --git a/examples/gno.land/p/demo/dao/events.gno b/examples/gno.land/p/demo/dao/events.gno index 21d7ddfcbe5..97bc794e6f3 100644 --- a/examples/gno.land/p/demo/dao/events.gno +++ b/examples/gno.land/p/demo/dao/events.gno @@ -6,6 +6,16 @@ import ( "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) { diff --git a/examples/gno.land/p/demo/simpledao/dao.gno b/examples/gno.land/p/demo/simpledao/dao.gno index 36634a17cfc..ab7e5bf76c4 100644 --- a/examples/gno.land/p/demo/simpledao/dao.gno +++ b/examples/gno.land/p/demo/simpledao/dao.gno @@ -75,6 +75,9 @@ func (s *SimpleDAO) Propose(request dao.ProposalRequest) (uint64, error) { return 0, ufmt.Errorf("unable to add proposal, %s", err.Error()) } + // Emit the proposal added event + dao.EmitProposalAdded(id, caller) + return id, nil } From 671872a9a3c629da0ea532768b96db34f46fa743 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20=C5=BDivkovi=C4=87?= Date: Mon, 7 Oct 2024 11:47:25 +0200 Subject: [PATCH 70/80] Add duplicate check for WithInitialMembers --- examples/gno.land/p/demo/membstore/membstore.gno | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/examples/gno.land/p/demo/membstore/membstore.gno b/examples/gno.land/p/demo/membstore/membstore.gno index 04d2fbd6d1a..f5fa1bca7d5 100644 --- a/examples/gno.land/p/demo/membstore/membstore.gno +++ b/examples/gno.land/p/demo/membstore/membstore.gno @@ -2,10 +2,10 @@ package membstore import ( "errors" - "std" "gno.land/p/demo/avl" + "gno.land/p/demo/ufmt" ) var ( @@ -26,7 +26,14 @@ type Option func(*MembStore) func WithInitialMembers(members []Member) Option { return func(store *MembStore) { for _, m := range members { - store.members.Set(m.Address.String(), m) + memberAddr := m.Address.String() + + // Check if the member already exists + if store.members.Has(memberAddr) { + panic(ufmt.Errorf("%s, %s", memberAddr, ErrAlreadyMember)) + } + + store.members.Set(memberAddr, m) store.totalVotingPower += m.VotingPower } } From 3f196b2bd8d65b172ea46c85b9b9210d74f4599e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20=C5=BDivkovi=C4=87?= Date: Mon, 7 Oct 2024 11:50:14 +0200 Subject: [PATCH 71/80] Change valoper proposal comment --- examples/gno.land/r/gnoland/valopers/v2/valopers.gno | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/gno.land/r/gnoland/valopers/v2/valopers.gno b/examples/gno.land/r/gnoland/valopers/v2/valopers.gno index 00b22912d0a..88594b5821a 100644 --- a/examples/gno.land/r/gnoland/valopers/v2/valopers.gno +++ b/examples/gno.land/r/gnoland/valopers/v2/valopers.gno @@ -172,7 +172,7 @@ func GovDAOProposal(address std.Address) { // Craft the proposal description description := ufmt.Sprintf( - "Proposal to add valoper %s (Address: %s; PubKey: %s) to the valset", + "Add valoper %s (Address: %s; PubKey: %s) to the valset", valoper.Name, valoper.Address.String(), valoper.PubKey, From c4b3970ce71b7b45e75a28c74849b9f443042814 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20=C5=BDivkovi=C4=87?= Date: Mon, 7 Oct 2024 11:51:18 +0200 Subject: [PATCH 72/80] Drop unused constant from propstore --- examples/gno.land/p/demo/simpledao/propstore.gno | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/examples/gno.land/p/demo/simpledao/propstore.gno b/examples/gno.land/p/demo/simpledao/propstore.gno index eefe23ecb4a..c3f2bab39d3 100644 --- a/examples/gno.land/p/demo/simpledao/propstore.gno +++ b/examples/gno.land/p/demo/simpledao/propstore.gno @@ -11,15 +11,9 @@ import ( var ErrMissingProposal = errors.New("proposal is missing") -const ( - // maxRequestProposals is the maximum number of - // paginated proposals that can be requested - maxRequestProposals = 10 - - // maxRequestVotes is the maximum number of - // paginated votes that can be requested - maxRequestVotes = 50 -) +// maxRequestProposals is the maximum number of +// paginated proposals that can be requested +const maxRequestProposals = 10 // proposal is the internal simpledao proposal implementation type proposal struct { From 38e2eaec970d8c4422699538530e6078cd0b1d1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20=C5=BDivkovi=C4=87?= Date: Mon, 7 Oct 2024 11:52:35 +0200 Subject: [PATCH 73/80] Remove unused Vote struct from vote.gno --- examples/gno.land/p/demo/dao/vote.gno | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/examples/gno.land/p/demo/dao/vote.gno b/examples/gno.land/p/demo/dao/vote.gno index 58d74ad73ed..cb6767c0f6a 100644 --- a/examples/gno.land/p/demo/dao/vote.gno +++ b/examples/gno.land/p/demo/dao/vote.gno @@ -1,9 +1,5 @@ package dao -import ( - "std" -) - const ( VoteAddedEvent = "VoteAdded" // emitted when a vote was cast for a proposal @@ -24,12 +20,6 @@ func (v VoteOption) String() string { return string(v) } -// Vote encompasses a single GOVDAO vote -type Vote struct { - Address std.Address // the address of the voter - Option VoteOption // the voting option -} - // Stats encompasses the proposal voting stats type Stats struct { YayVotes uint64 From 82745d961c7f235cdd36a7819305ce39738b89a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20=C5=BDivkovi=C4=87?= Date: Mon, 7 Oct 2024 11:56:35 +0200 Subject: [PATCH 74/80] Tidy mods --- examples/gno.land/p/demo/dao/gno.mod | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/gno.land/p/demo/dao/gno.mod b/examples/gno.land/p/demo/dao/gno.mod index fbb23299116..ecbab2f7692 100644 --- a/examples/gno.land/p/demo/dao/gno.mod +++ b/examples/gno.land/p/demo/dao/gno.mod @@ -1 +1,3 @@ module gno.land/p/demo/dao + +require gno.land/p/demo/ufmt v0.0.0-latest From 15404f519e5eeee065e034939a063d0948e555e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20=C5=BDivkovi=C4=87?= Date: Mon, 7 Oct 2024 12:01:35 +0200 Subject: [PATCH 75/80] Move combinederr to a specific package --- .../p/{gov/executor/errors.gno => combinederr/combinederr.gno} | 2 +- examples/gno.land/p/combinederr/gno.mod | 1 + examples/gno.land/r/gov/dao/v2/gno.mod | 1 + examples/gno.land/r/gov/dao/v2/poc.gno | 3 ++- 4 files changed, 5 insertions(+), 2 deletions(-) rename examples/gno.land/p/{gov/executor/errors.gno => combinederr/combinederr.gno} (97%) create mode 100644 examples/gno.land/p/combinederr/gno.mod diff --git a/examples/gno.land/p/gov/executor/errors.gno b/examples/gno.land/p/combinederr/combinederr.gno similarity index 97% rename from examples/gno.land/p/gov/executor/errors.gno rename to examples/gno.land/p/combinederr/combinederr.gno index 5a26384a43f..f446c7846bd 100644 --- a/examples/gno.land/p/gov/executor/errors.gno +++ b/examples/gno.land/p/combinederr/combinederr.gno @@ -1,4 +1,4 @@ -package executor +package combinederr import "strings" diff --git a/examples/gno.land/p/combinederr/gno.mod b/examples/gno.land/p/combinederr/gno.mod new file mode 100644 index 00000000000..87dafad7d89 --- /dev/null +++ b/examples/gno.land/p/combinederr/gno.mod @@ -0,0 +1 @@ +module gno.land/p/combinederr diff --git a/examples/gno.land/r/gov/dao/v2/gno.mod b/examples/gno.land/r/gov/dao/v2/gno.mod index f8877c857a4..933b4661634 100644 --- a/examples/gno.land/r/gov/dao/v2/gno.mod +++ b/examples/gno.land/r/gov/dao/v2/gno.mod @@ -1,6 +1,7 @@ module gno.land/r/gov/dao/v2 require ( + gno.land/p/combinederr v0.0.0-latest gno.land/p/demo/dao v0.0.0-latest gno.land/p/demo/membstore v0.0.0-latest gno.land/p/demo/simpledao v0.0.0-latest diff --git a/examples/gno.land/r/gov/dao/v2/poc.gno b/examples/gno.land/r/gov/dao/v2/poc.gno index d4d3a629679..7d4003b971d 100644 --- a/examples/gno.land/r/gov/dao/v2/poc.gno +++ b/examples/gno.land/r/gov/dao/v2/poc.gno @@ -4,6 +4,7 @@ import ( "errors" "std" + "gno.land/p/combinederr" "gno.land/p/demo/dao" "gno.land/p/demo/membstore" "gno.land/p/gov/executor" @@ -30,7 +31,7 @@ func NewMemberPropExecutor(changesFn func() []membstore.Member) dao.Executor { } callback := func() error { - errs := &executor.CombinedError{} + errs := &combinederr.CombinedError{} cbMembers := changesFn() for _, member := range cbMembers { From 2f19233737c95e18a9a7c9033c251b88e49293b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20=C5=BDivkovi=C4=87?= Date: Mon, 7 Oct 2024 12:49:37 +0200 Subject: [PATCH 76/80] Remove IsExpired from the executor --- examples/gno.land/p/demo/dao/executor.gno | 4 - examples/gno.land/p/demo/dao/proposals.gno | 3 + examples/gno.land/p/demo/simpledao/dao.gno | 14 +-- .../gno.land/p/demo/simpledao/dao_test.gno | 85 ------------------- .../gno.land/p/demo/simpledao/mock_test.gno | 16 +--- .../gno.land/p/demo/simpledao/propstore.gno | 4 + examples/gno.land/p/gov/executor/callback.gno | 6 -- examples/gno.land/p/gov/executor/context.gno | 6 -- 8 files changed, 16 insertions(+), 122 deletions(-) diff --git a/examples/gno.land/p/demo/dao/executor.gno b/examples/gno.land/p/demo/dao/executor.gno index 410d3e5b288..9291c2c53c5 100644 --- a/examples/gno.land/p/demo/dao/executor.gno +++ b/examples/gno.land/p/demo/dao/executor.gno @@ -6,8 +6,4 @@ type Executor interface { // Execute executes the given proposal, and returns any error encountered // during the execution Execute() error - - // IsExpired returns a flag indicating - // if the executor is expired - IsExpired() bool } diff --git a/examples/gno.land/p/demo/dao/proposals.gno b/examples/gno.land/p/demo/dao/proposals.gno index cbc84d3377d..e5929901c35 100644 --- a/examples/gno.land/p/demo/dao/proposals.gno +++ b/examples/gno.land/p/demo/dao/proposals.gno @@ -49,6 +49,9 @@ type Proposal interface { // 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 } diff --git a/examples/gno.land/p/demo/simpledao/dao.gno b/examples/gno.land/p/demo/simpledao/dao.gno index ab7e5bf76c4..56c029de39c 100644 --- a/examples/gno.land/p/demo/simpledao/dao.gno +++ b/examples/gno.land/p/demo/simpledao/dao.gno @@ -17,7 +17,7 @@ var ( ErrProposalExecuted = errors.New("proposal already executed") ErrProposalInactive = errors.New("proposal is inactive") ErrProposalNotAccepted = errors.New("proposal is not accepted") - ErrExecutorExpired = errors.New("executor is expired") + ErrProposalExpired = errors.New("proposal is expired") ) var ( @@ -111,9 +111,9 @@ func (s *SimpleDAO) VoteOnProposal(id uint64, option dao.VoteOption) error { return ErrProposalInactive } - // Check if the executor is expired - if prop.executor.IsExpired() { - return ErrExecutorExpired + // Check if the proposal is expired + if prop.IsExpired() { + return ErrProposalExpired } // Cast the vote @@ -196,9 +196,9 @@ func (s *SimpleDAO) ExecuteProposal(id uint64) error { return ErrProposalNotAccepted } - // Check if the executor is expired - if prop.executor.IsExpired() { - return ErrExecutorExpired + // Check if the proposal is expired + if prop.IsExpired() { + return ErrProposalExpired } // Emit an event when the execution finishes diff --git a/examples/gno.land/p/demo/simpledao/dao_test.gno b/examples/gno.land/p/demo/simpledao/dao_test.gno index 81a0c3b9afe..1bcc2038a23 100644 --- a/examples/gno.land/p/demo/simpledao/dao_test.gno +++ b/examples/gno.land/p/demo/simpledao/dao_test.gno @@ -250,51 +250,6 @@ func TestSimpleDAO_VoteOnProposal(t *testing.T) { ) }) - t.Run("proposal expired", func(t *testing.T) { - t.Parallel() - - var ( - voter = testutils.TestAddress("voter") - - ms = &mockMemberStore{ - memberFn: func(a std.Address) (membstore.Member, error) { - if a != voter { - return membstore.Member{}, errors.New("not found") - } - - return membstore.Member{ - Address: voter, - }, nil - }, - } - s = New(ms) - - mockExecutor = &mockExecutor{ - isExpiredFn: func() bool { - return true - }, - } - - prop = &proposal{ - status: dao.Active, - executor: mockExecutor, - } - ) - - std.TestSetOrigCaller(voter) - - // Add an initial proposal - id, err := s.addProposal(prop) - urequire.NoError(t, err) - - // Attempt to vote on the proposal - uassert.ErrorIs( - t, - s.VoteOnProposal(id, dao.YesVote), - ErrExecutorExpired, - ) - }) - t.Run("double vote on proposal", func(t *testing.T) { t.Parallel() @@ -790,46 +745,6 @@ func TestSimpleDAO_ExecuteProposal(t *testing.T) { } }) - t.Run("proposal expired", func(t *testing.T) { - t.Parallel() - - var ( - voter = testutils.TestAddress("voter") - - ms = &mockMemberStore{ - isMemberFn: func(_ std.Address) bool { - return true - }, - } - - s = New(ms) - - mockExecutor = &mockExecutor{ - isExpiredFn: func() bool { - return true - }, - } - - prop = &proposal{ - status: dao.Accepted, - executor: mockExecutor, - } - ) - - std.TestSetOrigCaller(voter) - - // Add an initial proposal - id, err := s.addProposal(prop) - urequire.NoError(t, err) - - // Attempt to vote on the proposal - uassert.ErrorIs( - t, - s.ExecuteProposal(id), - ErrExecutorExpired, - ) - }) - t.Run("execution error", func(t *testing.T) { t.Parallel() diff --git a/examples/gno.land/p/demo/simpledao/mock_test.gno b/examples/gno.land/p/demo/simpledao/mock_test.gno index f335c33daf1..0cf12ccff01 100644 --- a/examples/gno.land/p/demo/simpledao/mock_test.gno +++ b/examples/gno.land/p/demo/simpledao/mock_test.gno @@ -6,14 +6,10 @@ import ( "gno.land/p/demo/membstore" ) -type ( - executeDelegate func() error - isExpiredDelegate func() bool -) +type executeDelegate func() error type mockExecutor struct { - executeFn executeDelegate - isExpiredFn isExpiredDelegate + executeFn executeDelegate } func (m *mockExecutor) Execute() error { @@ -24,14 +20,6 @@ func (m *mockExecutor) Execute() error { return nil } -func (m *mockExecutor) IsExpired() bool { - if m.isExpiredFn != nil { - return m.isExpiredFn() - } - - return false -} - type ( membersDelegate func(uint64, uint64) []membstore.Member sizeDelegate func() int diff --git a/examples/gno.land/p/demo/simpledao/propstore.gno b/examples/gno.land/p/demo/simpledao/propstore.gno index c3f2bab39d3..972297ff0ce 100644 --- a/examples/gno.land/p/demo/simpledao/propstore.gno +++ b/examples/gno.land/p/demo/simpledao/propstore.gno @@ -55,6 +55,10 @@ func (p *proposal) Stats() dao.Stats { } } +func (p *proposal) IsExpired() bool { + return false // this proposal never expires +} + func (p *proposal) Render() string { // Fetch the voting stats stats := p.Stats() diff --git a/examples/gno.land/p/gov/executor/callback.gno b/examples/gno.land/p/gov/executor/callback.gno index 509bd4ebeb5..5d46a97cd69 100644 --- a/examples/gno.land/p/gov/executor/callback.gno +++ b/examples/gno.land/p/gov/executor/callback.gno @@ -37,9 +37,3 @@ func (exec *CallbackExecutor) Execute() error { return nil } - -// IsExpired returns a flag indicating if the executor is expired. -// The CallbackExecutor never expires -func (exec *CallbackExecutor) IsExpired() bool { - return false -} diff --git a/examples/gno.land/p/gov/executor/context.gno b/examples/gno.land/p/gov/executor/context.gno index d892444581e..158e3b1e0be 100644 --- a/examples/gno.land/p/gov/executor/context.gno +++ b/examples/gno.land/p/gov/executor/context.gno @@ -52,12 +52,6 @@ func (exec *CtxExecutor) Execute() error { return exec.callbackCtx(ctx) } -// IsExpired returns a flag indicating if the executor is expired. -// The CtxExecutor never expires -func (exec *CtxExecutor) IsExpired() bool { - return false -} - // IsApprovedByGovdaoContext asserts that the govdao approved the context func IsApprovedByGovdaoContext(ctx context.Context) bool { v := ctx.Value(statusContextKey) From 9cbc56a1a697d2d8c36d9ccde4ef633da83960d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20=C5=BDivkovi=C4=87?= Date: Mon, 7 Oct 2024 12:50:45 +0200 Subject: [PATCH 77/80] Remove unused check --- examples/gno.land/p/demo/simpledao/dao.gno | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/examples/gno.land/p/demo/simpledao/dao.gno b/examples/gno.land/p/demo/simpledao/dao.gno index 56c029de39c..ed84d327fbb 100644 --- a/examples/gno.land/p/demo/simpledao/dao.gno +++ b/examples/gno.land/p/demo/simpledao/dao.gno @@ -111,11 +111,6 @@ func (s *SimpleDAO) VoteOnProposal(id uint64, option dao.VoteOption) error { return ErrProposalInactive } - // Check if the proposal is expired - if prop.IsExpired() { - return ErrProposalExpired - } - // Cast the vote if err = prop.tally.castVote(member, option); err != nil { return ufmt.Errorf("unable to vote on proposal %d, %s", id, err.Error()) @@ -196,11 +191,6 @@ func (s *SimpleDAO) ExecuteProposal(id uint64) error { return ErrProposalNotAccepted } - // Check if the proposal is expired - if prop.IsExpired() { - return ErrProposalExpired - } - // Emit an event when the execution finishes defer dao.EmitProposalExecuted(id, prop.status) From 3858a3c14ae17dd904d0c672bcac01c52b8476ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20=C5=BDivkovi=C4=87?= Date: Mon, 7 Oct 2024 12:57:17 +0200 Subject: [PATCH 78/80] Update gno.land/pkg/gnoland/vals.go Co-authored-by: Manfred Touron <94029+moul@users.noreply.github.com> --- gno.land/pkg/gnoland/vals.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gno.land/pkg/gnoland/vals.go b/gno.land/pkg/gnoland/vals.go index da92171bd08..339ebd9dcad 100644 --- a/gno.land/pkg/gnoland/vals.go +++ b/gno.land/pkg/gnoland/vals.go @@ -9,7 +9,7 @@ import ( ) const ( - valRealm = "gno.land/r/sys/validators/v2" + valRealm = "gno.land/r/sys/validators/v2" // XXX: make it configurable from GovDAO valChangesFn = "GetChanges" validatorAddedEvent = "ValidatorAdded" From ee989ff12115156b2bed326766a510110abe3260 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20=C5=BDivkovi=C4=87?= Date: Mon, 7 Oct 2024 14:04:25 +0200 Subject: [PATCH 79/80] Add note about votes in the DAO --- examples/gno.land/p/demo/dao/vote.gno | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/examples/gno.land/p/demo/dao/vote.gno b/examples/gno.land/p/demo/dao/vote.gno index cb6767c0f6a..1e99834360a 100644 --- a/examples/gno.land/p/demo/dao/vote.gno +++ b/examples/gno.land/p/demo/dao/vote.gno @@ -1,5 +1,13 @@ 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 From d98d78051c2c91044d0c3d87e83f9b08e5441d68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20=C5=BDivkovi=C4=87?= Date: Wed, 9 Oct 2024 13:58:09 +0200 Subject: [PATCH 80/80] Remove /r/gov/dao/v2 dependency --- examples/gno.land/r/gnoland/blog/admin.gno | 4 +- examples/gno.land/r/gnoland/blog/gno.mod | 2 +- .../gno.land/r/gnoland/valopers/v2/gno.mod | 2 +- .../r/gnoland/valopers/v2/valopers.gno | 4 +- examples/gno.land/r/gov/dao/bridge/bridge.gno | 39 +++++++++++ .../gno.land/r/gov/dao/bridge/bridge_test.gno | 64 +++++++++++++++++ examples/gno.land/r/gov/dao/bridge/doc.gno | 4 ++ examples/gno.land/r/gov/dao/bridge/gno.mod | 11 +++ .../gno.land/r/gov/dao/bridge/mock_test.gno | 68 +++++++++++++++++++ examples/gno.land/r/gov/dao/bridge/types.gno | 17 +++++ examples/gno.land/r/gov/dao/bridge/v2.gno | 42 ++++++++++++ examples/gno.land/r/sys/validators/v2/gno.mod | 2 +- examples/gno.land/r/sys/validators/v2/poc.gno | 4 +- 13 files changed, 254 insertions(+), 9 deletions(-) create mode 100644 examples/gno.land/r/gov/dao/bridge/bridge.gno create mode 100644 examples/gno.land/r/gov/dao/bridge/bridge_test.gno create mode 100644 examples/gno.land/r/gov/dao/bridge/doc.gno create mode 100644 examples/gno.land/r/gov/dao/bridge/gno.mod create mode 100644 examples/gno.land/r/gov/dao/bridge/mock_test.gno create mode 100644 examples/gno.land/r/gov/dao/bridge/types.gno create mode 100644 examples/gno.land/r/gov/dao/bridge/v2.gno diff --git a/examples/gno.land/r/gnoland/blog/admin.gno b/examples/gno.land/r/gnoland/blog/admin.gno index 3cd069effda..9c94a265fca 100644 --- a/examples/gno.land/r/gnoland/blog/admin.gno +++ b/examples/gno.land/r/gnoland/blog/admin.gno @@ -6,7 +6,7 @@ import ( "gno.land/p/demo/avl" "gno.land/p/demo/dao" - govdao "gno.land/r/gov/dao/v2" + "gno.land/r/gov/dao/bridge" ) var ( @@ -48,7 +48,7 @@ func NewPostExecutor(slug, title, body, publicationDate, authors, tags string) d return nil } - return govdao.NewGovDAOExecutor(callback) + return bridge.GovDAO().NewGovDAOExecutor(callback) } func ModAddPost(slug, title, body, publicationDate, authors, tags string) { diff --git a/examples/gno.land/r/gnoland/blog/gno.mod b/examples/gno.land/r/gnoland/blog/gno.mod index be52f25b7f3..8a4c5851b4c 100644 --- a/examples/gno.land/r/gnoland/blog/gno.mod +++ b/examples/gno.land/r/gnoland/blog/gno.mod @@ -4,5 +4,5 @@ require ( gno.land/p/demo/avl v0.0.0-latest gno.land/p/demo/blog v0.0.0-latest gno.land/p/demo/dao v0.0.0-latest - gno.land/r/gov/dao/v2 v0.0.0-latest + gno.land/r/gov/dao/bridge v0.0.0-latest ) diff --git a/examples/gno.land/r/gnoland/valopers/v2/gno.mod b/examples/gno.land/r/gnoland/valopers/v2/gno.mod index b976359c476..099a8406db4 100644 --- a/examples/gno.land/r/gnoland/valopers/v2/gno.mod +++ b/examples/gno.land/r/gnoland/valopers/v2/gno.mod @@ -7,6 +7,6 @@ require ( gno.land/p/demo/uassert v0.0.0-latest gno.land/p/demo/ufmt v0.0.0-latest gno.land/p/sys/validators v0.0.0-latest - gno.land/r/gov/dao/v2 v0.0.0-latest + gno.land/r/gov/dao/bridge v0.0.0-latest gno.land/r/sys/validators/v2 v0.0.0-latest ) diff --git a/examples/gno.land/r/gnoland/valopers/v2/valopers.gno b/examples/gno.land/r/gnoland/valopers/v2/valopers.gno index 88594b5821a..d88ea4b872f 100644 --- a/examples/gno.land/r/gnoland/valopers/v2/valopers.gno +++ b/examples/gno.land/r/gnoland/valopers/v2/valopers.gno @@ -9,7 +9,7 @@ import ( "gno.land/p/demo/dao" "gno.land/p/demo/ufmt" pVals "gno.land/p/sys/validators" - govdao "gno.land/r/gov/dao/v2" + "gno.land/r/gov/dao/bridge" validators "gno.land/r/sys/validators/v2" ) @@ -184,5 +184,5 @@ func GovDAOProposal(address std.Address) { } // Create the govdao proposal - govdao.Propose(prop) + bridge.GovDAO().Propose(prop) } diff --git a/examples/gno.land/r/gov/dao/bridge/bridge.gno b/examples/gno.land/r/gov/dao/bridge/bridge.gno new file mode 100644 index 00000000000..ba47978f33f --- /dev/null +++ b/examples/gno.land/r/gov/dao/bridge/bridge.gno @@ -0,0 +1,39 @@ +package bridge + +import ( + "std" + + "gno.land/p/demo/ownable" +) + +const initialOwner = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") // @moul + +var b *Bridge + +// Bridge is the active GovDAO +// implementation bridge +type Bridge struct { + *ownable.Ownable + + dao DAO +} + +// init constructs the initial GovDAO implementation +func init() { + b = &Bridge{ + Ownable: ownable.NewWithAddress(initialOwner), + dao: &govdaoV2{}, + } +} + +// SetDAO sets the currently active GovDAO implementation +func SetDAO(dao DAO) { + b.AssertCallerIsOwner() + + b.dao = dao +} + +// GovDAO returns the current GovDAO implementation +func GovDAO() DAO { + return b.dao +} diff --git a/examples/gno.land/r/gov/dao/bridge/bridge_test.gno b/examples/gno.land/r/gov/dao/bridge/bridge_test.gno new file mode 100644 index 00000000000..38b5d4be257 --- /dev/null +++ b/examples/gno.land/r/gov/dao/bridge/bridge_test.gno @@ -0,0 +1,64 @@ +package bridge + +import ( + "testing" + + "std" + + "gno.land/p/demo/dao" + "gno.land/p/demo/ownable" + "gno.land/p/demo/testutils" + "gno.land/p/demo/uassert" + "gno.land/p/demo/urequire" +) + +func TestBridge_DAO(t *testing.T) { + var ( + proposalID = uint64(10) + mockDAO = &mockDAO{ + proposeFn: func(_ dao.ProposalRequest) uint64 { + return proposalID + }, + } + ) + + b.dao = mockDAO + + uassert.Equal(t, proposalID, GovDAO().Propose(dao.ProposalRequest{})) +} + +func TestBridge_SetDAO(t *testing.T) { + t.Run("invalid owner", func(t *testing.T) { + // Attempt to set a new DAO implementation + uassert.PanicsWithMessage(t, ownable.ErrUnauthorized.Error(), func() { + SetDAO(&mockDAO{}) + }) + }) + + t.Run("valid owner", func(t *testing.T) { + var ( + addr = testutils.TestAddress("owner") + + proposalID = uint64(10) + mockDAO = &mockDAO{ + proposeFn: func(_ dao.ProposalRequest) uint64 { + return proposalID + }, + } + ) + + std.TestSetOrigCaller(addr) + + b.Ownable = ownable.NewWithAddress(addr) + + urequire.NotPanics(t, func() { + SetDAO(mockDAO) + }) + + uassert.Equal( + t, + mockDAO.Propose(dao.ProposalRequest{}), + GovDAO().Propose(dao.ProposalRequest{}), + ) + }) +} diff --git a/examples/gno.land/r/gov/dao/bridge/doc.gno b/examples/gno.land/r/gov/dao/bridge/doc.gno new file mode 100644 index 00000000000..f812b3c0787 --- /dev/null +++ b/examples/gno.land/r/gov/dao/bridge/doc.gno @@ -0,0 +1,4 @@ +// Package bridge represents a GovDAO implementation wrapper, used by other Realms and Packages to +// always fetch the most active GovDAO implementation, instead of directly referencing it, and having to +// update it each time the GovDAO implementation changes +package bridge diff --git a/examples/gno.land/r/gov/dao/bridge/gno.mod b/examples/gno.land/r/gov/dao/bridge/gno.mod new file mode 100644 index 00000000000..3382557573a --- /dev/null +++ b/examples/gno.land/r/gov/dao/bridge/gno.mod @@ -0,0 +1,11 @@ +module gno.land/r/gov/dao/bridge + +require ( + gno.land/p/demo/dao v0.0.0-latest + gno.land/p/demo/membstore v0.0.0-latest + gno.land/p/demo/ownable 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/urequire v0.0.0-latest + gno.land/r/gov/dao/v2 v0.0.0-latest +) diff --git a/examples/gno.land/r/gov/dao/bridge/mock_test.gno b/examples/gno.land/r/gov/dao/bridge/mock_test.gno new file mode 100644 index 00000000000..05ac430b4c4 --- /dev/null +++ b/examples/gno.land/r/gov/dao/bridge/mock_test.gno @@ -0,0 +1,68 @@ +package bridge + +import ( + "gno.land/p/demo/dao" + "gno.land/p/demo/membstore" +) + +type ( + proposeDelegate func(dao.ProposalRequest) uint64 + voteOnProposalDelegate func(uint64, dao.VoteOption) + executeProposalDelegate func(uint64) + getPropStoreDelegate func() dao.PropStore + getMembStoreDelegate func() membstore.MemberStore + newGovDAOExecutorDelegate func(func() error) dao.Executor +) + +type mockDAO struct { + proposeFn proposeDelegate + voteOnProposalFn voteOnProposalDelegate + executeProposalFn executeProposalDelegate + getPropStoreFn getPropStoreDelegate + getMembStoreFn getMembStoreDelegate + newGovDAOExecutorFn newGovDAOExecutorDelegate +} + +func (m *mockDAO) Propose(request dao.ProposalRequest) uint64 { + if m.proposeFn != nil { + return m.proposeFn(request) + } + + return 0 +} + +func (m *mockDAO) VoteOnProposal(id uint64, option dao.VoteOption) { + if m.voteOnProposalFn != nil { + m.voteOnProposalFn(id, option) + } +} + +func (m *mockDAO) ExecuteProposal(id uint64) { + if m.executeProposalFn != nil { + m.executeProposalFn(id) + } +} + +func (m *mockDAO) GetPropStore() dao.PropStore { + if m.getPropStoreFn != nil { + return m.getPropStoreFn() + } + + return nil +} + +func (m *mockDAO) GetMembStore() membstore.MemberStore { + if m.getMembStoreFn != nil { + return m.getMembStoreFn() + } + + return nil +} + +func (m *mockDAO) NewGovDAOExecutor(cb func() error) dao.Executor { + if m.newGovDAOExecutorFn != nil { + return m.newGovDAOExecutorFn(cb) + } + + return nil +} diff --git a/examples/gno.land/r/gov/dao/bridge/types.gno b/examples/gno.land/r/gov/dao/bridge/types.gno new file mode 100644 index 00000000000..27ea8fb62d4 --- /dev/null +++ b/examples/gno.land/r/gov/dao/bridge/types.gno @@ -0,0 +1,17 @@ +package bridge + +import ( + "gno.land/p/demo/dao" + "gno.land/p/demo/membstore" +) + +// DAO abstracts the commonly used DAO interface +type DAO interface { + Propose(dao.ProposalRequest) uint64 + VoteOnProposal(uint64, dao.VoteOption) + ExecuteProposal(uint64) + GetPropStore() dao.PropStore + GetMembStore() membstore.MemberStore + + NewGovDAOExecutor(func() error) dao.Executor +} diff --git a/examples/gno.land/r/gov/dao/bridge/v2.gno b/examples/gno.land/r/gov/dao/bridge/v2.gno new file mode 100644 index 00000000000..216419cf31d --- /dev/null +++ b/examples/gno.land/r/gov/dao/bridge/v2.gno @@ -0,0 +1,42 @@ +package bridge + +import ( + "gno.land/p/demo/dao" + "gno.land/p/demo/membstore" + govdao "gno.land/r/gov/dao/v2" +) + +// govdaoV2 is a wrapper for interacting with the /r/gov/dao/v2 Realm +type govdaoV2 struct{} + +func (g *govdaoV2) Propose(request dao.ProposalRequest) uint64 { + return govdao.Propose(request) +} + +func (g *govdaoV2) VoteOnProposal(id uint64, option dao.VoteOption) { + govdao.VoteOnProposal(id, option) +} + +func (g *govdaoV2) ExecuteProposal(id uint64) { + govdao.ExecuteProposal(id) +} + +func (g *govdaoV2) GetPropStore() dao.PropStore { + return govdao.GetPropStore() +} + +func (g *govdaoV2) GetMembStore() membstore.MemberStore { + return govdao.GetMembStore() +} + +func (g *govdaoV2) NewGovDAOExecutor(cb func() error) dao.Executor { + return govdao.NewGovDAOExecutor(cb) +} + +func (g *govdaoV2) NewMemberPropExecutor(cb func() []membstore.Member) dao.Executor { + return govdao.NewMemberPropExecutor(cb) +} + +func (g *govdaoV2) NewMembStoreImplExecutor(cb func() membstore.MemberStore) dao.Executor { + return govdao.NewMembStoreImplExecutor(cb) +} diff --git a/examples/gno.land/r/sys/validators/v2/gno.mod b/examples/gno.land/r/sys/validators/v2/gno.mod index 3374666095b..db94a208902 100644 --- a/examples/gno.land/r/sys/validators/v2/gno.mod +++ b/examples/gno.land/r/sys/validators/v2/gno.mod @@ -9,5 +9,5 @@ require ( gno.land/p/demo/ufmt v0.0.0-latest gno.land/p/nt/poa v0.0.0-latest gno.land/p/sys/validators v0.0.0-latest - gno.land/r/gov/dao/v2 v0.0.0-latest + gno.land/r/gov/dao/bridge v0.0.0-latest ) diff --git a/examples/gno.land/r/sys/validators/v2/poc.gno b/examples/gno.land/r/sys/validators/v2/poc.gno index db91525fe0c..760edc39d1e 100644 --- a/examples/gno.land/r/sys/validators/v2/poc.gno +++ b/examples/gno.land/r/sys/validators/v2/poc.gno @@ -5,7 +5,7 @@ import ( "gno.land/p/demo/dao" "gno.land/p/sys/validators" - govdao "gno.land/r/gov/dao/v2" + "gno.land/r/gov/dao/bridge" ) const errNoChangesProposed = "no set changes proposed" @@ -37,7 +37,7 @@ func NewPropExecutor(changesFn func() []validators.Validator) dao.Executor { return nil } - return govdao.NewGovDAOExecutor(callback) + return bridge.GovDAO().NewGovDAOExecutor(callback) } // IsValidator returns a flag indicating if the given bech32 address