From 52fd5cecc834763bfed3b8c30e3f623dee51fde7 Mon Sep 17 00:00:00 2001 From: MikaelVallenet Date: Tue, 11 Feb 2025 15:56:41 +0100 Subject: [PATCH 01/23] feat: export functions daokit --- gno/p/daokit/daokit.gno | 70 +++++++++++++++++------------------- gno/p/daokit/daokit_test.gno | 6 ++-- gno/p/daokit/members.gno | 14 ++++---- gno/p/daokit/messages.gno | 8 ++--- gno/p/daokit/proposals.gno | 4 +-- gno/p/daokit/render.gno | 2 +- gno/p/daokit/resources.gno | 4 +-- gno/r/govdao/gno.mod | 1 + gno/r/govdao/govdao.gno | 50 ++++++++++++++++++++++++++ 9 files changed, 102 insertions(+), 57 deletions(-) create mode 100644 gno/r/govdao/gno.mod create mode 100644 gno/r/govdao/govdao.gno diff --git a/gno/p/daokit/daokit.gno b/gno/p/daokit/daokit.gno index 3475a5778..1287e93f8 100644 --- a/gno/p/daokit/daokit.gno +++ b/gno/p/daokit/daokit.gno @@ -39,11 +39,11 @@ func NewDAO(name, description string, roles []string, members []Member, resource dao.initMessagesRegistry() dao.initRenderingRouter() - dao.MemberModule.setRoles(roles) - dao.MemberModule.setMembers(members) + dao.MemberModule.SetRoles(roles) + dao.MemberModule.SetMembers(members) dao.MessagesRegistry.handlers.Iterate("", "", func(key string, value interface{}) bool { - dao.ResourcesModule.setResource(Resource{ + dao.ResourcesModule.SetResource(Resource{ Resource: key, Handler: value.(MessageHandler), Condition: initCond, @@ -52,20 +52,45 @@ func NewDAO(name, description string, roles []string, members []Member, resource }) for _, resource := range resources { - dao.ResourcesModule.setResource(resource) + dao.ResourcesModule.SetResource(resource) dao.MessagesRegistry.register(resource.Handler) } return dao } +func (d *DAO) Propose(req ProposalRequest) *Proposal { + proposer := std.PrevRealm().Addr() + if !d.MemberModule.IsMember(proposer.String()) { + panic(proposer + " proposer is not a member" + proposer) + } + + condition := d.ResourcesModule.GetResource(req.Type) + if condition == nil { + panic("message type is not registered as a resource") + } + + if len(req.Title) == 0 || len(req.Description) == 0 { + panic("title or description is empty") + } + + handler, ok := d.MessagesRegistry.handlers.Get(req.Type) + if !ok { + panic("message type is not registered as a resource") + } + + message := handler.(MessageHandler).Instantiate(req.Payload) + + return d.ProposalModule.newProposal(req.Title, req.Description, proposer, message, condition.NewState()) +} + func (d *DAO) Vote(proposalID uint64, vote string) { voter := std.PrevRealm().Addr() if !d.MemberModule.IsMember(voter.String()) { panic("voter is not a member") } - proposal := d.ProposalModule.getProposal(proposalID) + proposal := d.ProposalModule.GetProposal(proposalID) if proposal == nil { panic("proposal not found") } @@ -94,7 +119,7 @@ func (d *DAO) Execute(proposalID uint64) { panic("executor is not a member") } - proposal := d.ProposalModule.getProposal(proposalID) + proposal := d.ProposalModule.GetProposal(proposalID) if proposal == nil { panic("proposal not found") } @@ -107,7 +132,7 @@ func (d *DAO) Execute(proposalID uint64) { panic("proposal condition is not met") } - proposal.updateStatus() + proposal.UpdateStatus() if proposal.status != ProposalStatusPassed { panic("proposal does not meet the condition(s) or is already closed/executed") } @@ -116,37 +141,8 @@ func (d *DAO) Execute(proposalID uint64) { proposal.status = ProposalStatusExecuted } -func (d *DAO) Propose(req ProposalRequest) { - d.propose(req.Title, req.Description, req.Type, req.Payload) -} - func (d *DAO) InstantExecute(req ProposalRequest) { - proposal := d.propose(req.Title, req.Description, req.Type, req.Payload) + proposal := d.Propose(req) d.Vote(uint64(proposal.id), "yes") d.Execute(uint64(proposal.id)) } - -func (d *DAO) propose(title string, description string, messageType string, payload map[string]interface{}) *Proposal { - proposer := std.PrevRealm().Addr() - if !d.MemberModule.IsMember(proposer.String()) { - panic(proposer + " proposer is not a member" + proposer) - } - - condition := d.ResourcesModule.getResource(messageType) - if condition == nil { - panic("message type is not registered as a resource") - } - - if len(title) == 0 || len(description) == 0 { - panic("title or description is empty") - } - - handler, ok := d.MessagesRegistry.handlers.Get(messageType) - if !ok { - panic("message type is not registered as a resource") - } - - message := handler.(MessageHandler).Instantiate(payload) - - return d.ProposalModule.newProposal(title, description, proposer, message, condition.NewState()) -} diff --git a/gno/p/daokit/daokit_test.gno b/gno/p/daokit/daokit_test.gno index fc29246ac..69fa37e1f 100644 --- a/gno/p/daokit/daokit_test.gno +++ b/gno/p/daokit/daokit_test.gno @@ -201,7 +201,7 @@ func TestPropose(t *testing.T) { std.TestSetOrigCaller(test.input.proposer) dao.Propose(test.input.proposalReq) - proposal := dao.ProposalModule.getProposal(1) + proposal := dao.ProposalModule.GetProposal(1) if proposal.title != test.expected.title { t.Errorf("Expected title %s, got %s", test.expected.title, proposal.title) } @@ -349,7 +349,7 @@ func TestVote(t *testing.T) { std.TestSetOrigCaller(test.input.voter) dao.Vote(test.input.proposalID, test.input.vote) - proposal := dao.ProposalModule.getProposal(test.input.proposalID) + proposal := dao.ProposalModule.GetProposal(test.input.proposalID) eval := proposal.conditionState.Eval(proposal.votes) if eval != test.expected.eval { t.Errorf("Expected eval %t, got %t", test.expected.eval, eval) @@ -482,7 +482,7 @@ func TestExecuteProposal(t *testing.T) { std.TestSetOrigCaller(test.input.executor) dao.Execute(test.input.proposalID) - proposal := dao.ProposalModule.getProposal(test.input.proposalID) + proposal := dao.ProposalModule.GetProposal(test.input.proposalID) if proposal.status != ProposalStatusExecuted { t.Errorf("Expected status %s, got %s", ProposalStatusExecuted, proposal.status) diff --git a/gno/p/daokit/members.gno b/gno/p/daokit/members.gno index 06c559d7e..551898027 100644 --- a/gno/p/daokit/members.gno +++ b/gno/p/daokit/members.gno @@ -17,7 +17,6 @@ type Member struct { Roles []string } -// TODO: FIX the owner of the role_manager is the deployer of the contract not the realm func newMemberModule() *MemberModule { return &MemberModule{ roleManager: role_manager.NewWithAddress(std.CurrentRealm().Addr()), @@ -62,14 +61,14 @@ func (m *MemberModule) GetMembersWithRole(role string) []string { return m.roleManager.GetRoleUsers(role) } -func (m *MemberModule) setRoles(roles []string) { +func (m *MemberModule) SetRoles(roles []string) { caller := std.CurrentRealm().Addr() for _, role := range roles { m.roleManager.CreateNewRole(role, []string{}, caller) } } -func (m *MemberModule) setMembers(members []Member) { +func (m *MemberModule) SetMembers(members []Member) { caller := std.CurrentRealm().Addr() for _, member := range members { m.members.Set(member.Address, struct{}{}) @@ -79,8 +78,7 @@ func (m *MemberModule) setMembers(members []Member) { } } -// TODO: add test for this kind of proposals -func (m *MemberModule) addMember(member string, roles []string) { +func (m *MemberModule) AddMember(member string, roles []string) { if m.IsMember(member) { panic("member already exists") } @@ -91,7 +89,7 @@ func (m *MemberModule) addMember(member string, roles []string) { } } -func (m *MemberModule) removeMember(member string) { +func (m *MemberModule) RemoveMember(member string) { if !m.IsMember(member) { panic("member does not exist") } @@ -99,7 +97,7 @@ func (m *MemberModule) removeMember(member string) { m.roleManager.RemoveAllRolesFromUser(std.Address(member), std.CurrentRealm().Addr()) } -func (m *MemberModule) addRoleToMember(member string, role string) { +func (m *MemberModule) AddRoleToMember(member string, role string) { if !m.IsMember(member) { panic("member does not exist") } @@ -112,7 +110,7 @@ func (m *MemberModule) addRoleToMember(member string, role string) { m.roleManager.AddRoleToUser(std.Address(member), role, std.CurrentRealm().Addr()) } -func (m *MemberModule) removeRoleFromMember(member string, role string) { +func (m *MemberModule) RemoveRoleFromMember(member string, role string) { if !m.IsMember(member) { panic("member does not exist") } diff --git a/gno/p/daokit/messages.gno b/gno/p/daokit/messages.gno index f9f650c43..f78d707c4 100644 --- a/gno/p/daokit/messages.gno +++ b/gno/p/daokit/messages.gno @@ -159,7 +159,7 @@ func NewAddNewMemberMessageHandler(dao *DAO) *AddNewMemberMessageHandler { func (h AddNewMemberMessageHandler) Execute(msg ExecutableMessage) { message := msg.(*AddNewMemberMessage) - h.dao.MemberModule.addMember(message.Address, message.Roles) + h.dao.MemberModule.AddMember(message.Address, message.Roles) } func (h AddNewMemberMessageHandler) Type() string { @@ -205,7 +205,7 @@ func NewRemoveMemberMessageHandler(dao *DAO) *RemoveMemberMessageHandler { func (h RemoveMemberMessageHandler) Execute(msg ExecutableMessage) { message := msg.(*RemoveMemberMessage) - h.dao.MemberModule.removeMember(message.Address) + h.dao.MemberModule.RemoveMember(message.Address) } func (h RemoveMemberMessageHandler) Type() string { @@ -247,7 +247,7 @@ func NewAddRoleToUserMessageHandler(dao *DAO) *AddRoleToUserMessageHandler { func (h AddRoleToUserMessageHandler) Execute(msg ExecutableMessage) { message := msg.(*AddRoleToUserMessage) - h.dao.MemberModule.addRoleToMember(message.Address, message.Role) + h.dao.MemberModule.AddRoleToMember(message.Address, message.Role) } func (h AddRoleToUserMessageHandler) Type() string { @@ -294,7 +294,7 @@ func NewRemoveRoleFromUserMessageHandler(dao *DAO) *RemoveRoleFromUserMessageHan func (h RemoveRoleFromUserMessageHandler) Execute(msg ExecutableMessage) { message := msg.(*RemoveRoleFromUserMessage) - h.dao.MemberModule.removeRoleFromMember(message.Address, message.Role) + h.dao.MemberModule.RemoveRoleFromMember(message.Address, message.Role) } func (h RemoveRoleFromUserMessageHandler) Type() string { diff --git a/gno/p/daokit/proposals.gno b/gno/p/daokit/proposals.gno index 6b5004c33..5c0c17129 100644 --- a/gno/p/daokit/proposals.gno +++ b/gno/p/daokit/proposals.gno @@ -77,7 +77,7 @@ func (p *ProposalModule) newProposal(title, description string, proposer std.Add return proposal } -func (p *ProposalModule) getProposal(id uint64) *Proposal { +func (p *ProposalModule) GetProposal(id uint64) *Proposal { value, ok := p.proposals.Get(seqid.ID(id).String()) if !ok { return nil @@ -86,7 +86,7 @@ func (p *ProposalModule) getProposal(id uint64) *Proposal { return proposal } -func (p *Proposal) updateStatus() { +func (p *Proposal) UpdateStatus() { conditionsAreMet := p.conditionState.Eval(p.votes) if p.status == ProposalStatusOpen && conditionsAreMet { p.status = ProposalStatusPassed diff --git a/gno/p/daokit/render.gno b/gno/p/daokit/render.gno index e9d12ec74..23f512a11 100644 --- a/gno/p/daokit/render.gno +++ b/gno/p/daokit/render.gno @@ -121,7 +121,7 @@ func (d *DAO) renderProposalDetailPage(res *mux.ResponseWriter, req *mux.Request panic(err) } res.Write(ufmt.Sprintf("# %s - Proposal #%d\n\n", d.Name, uint64(id))) - proposal := d.ProposalModule.getProposal(uint64(id)) + proposal := d.ProposalModule.GetProposal(uint64(id)) res.Write(ufmt.Sprintf("## Title - %s 📜\n\n", proposal.title)) res.Write(ufmt.Sprintf("## Description 📝\n\n%s\n\n", proposal.description)) res.Write(ufmt.Sprintf("## Resource - %s 📦\n\n", proposal.message.Type())) diff --git a/gno/p/daokit/resources.gno b/gno/p/daokit/resources.gno index ad208fa1e..29e4bdeb6 100644 --- a/gno/p/daokit/resources.gno +++ b/gno/p/daokit/resources.gno @@ -21,11 +21,11 @@ func newResourcesModule() *ResourcesModule { } } -func (r *ResourcesModule) setResource(resources Resource) { +func (r *ResourcesModule) SetResource(resources Resource) { r.resources.Set(resources.Resource, resources.Condition) } -func (r *ResourcesModule) getResource(name string) daocond.Condition { +func (r *ResourcesModule) GetResource(name string) daocond.Condition { value, ok := r.resources.Get(name) if !ok { return nil diff --git a/gno/r/govdao/gno.mod b/gno/r/govdao/gno.mod new file mode 100644 index 000000000..38d8291d6 --- /dev/null +++ b/gno/r/govdao/gno.mod @@ -0,0 +1 @@ +module gno.land/r/teritori/govdao \ No newline at end of file diff --git a/gno/r/govdao/govdao.gno b/gno/r/govdao/govdao.gno new file mode 100644 index 000000000..9c9f28709 --- /dev/null +++ b/gno/r/govdao/govdao.gno @@ -0,0 +1,50 @@ +package govdao + +import ( + "gno.land/p/teritori/daokit" + "gno.land/r/demo/profile" +) + +var dao *daokit.DAO + +func init() { + dao = &daokit.DAO{} + name := "GovDAO" + description := "This is a govDAO demo" + + roles := []string{"tier1", "tier2", "tier3"} + members := []daokit.Member{ + {Address: "g126gx6p6d3da4ymef35ury6874j6kys044r7zlg", Roles: []string{"tier1"}}, + {Address: "g1ld6uaykyugld4rnm63rcy7vju4zx23lufml3jv", Roles: []string{"tier2"}}, + {Address: "g1r69l0vhp7tqle3a0rk8m8fulr8sjvj4h7n0tth", Roles: []string{"tier1"}}, + } + + condition := daokit.CreateCondition("members-treshold", &dao, 0.6) + resources := []daokit.Resource{} + + dao = daokit.NewDAO(name, description, roles, members, resources, condition) + + profile.SetStringField(profile.DisplayName, name) + profile.SetStringField(profile.Bio, description) + profile.SetStringField(profile.Avatar, "") +} + +func Propose(proposal daokit.ProposalRequest) { + dao.Propose(proposal) +} + +func Vote(proposalID uint64, vote string) { + dao.Vote(proposalID, vote) +} + +func Execute(proposalID uint64) { + dao.Execute(proposalID) +} + +func InstantExecute(proposal daokit.ProposalRequest) { + dao.InstantExecute(proposal) +} + +func Render(path string) string { + return dao.Render(path) +} From 8114e90b3ac1c8fd96638ff9ee566d4023938f0c Mon Sep 17 00:00:00 2001 From: MikaelVallenet Date: Tue, 11 Feb 2025 16:14:39 +0100 Subject: [PATCH 02/23] feat: add t1 & t2 conditions --- gno/r/govdao/govdao.gno | 31 ++++------- gno/r/govdao/messages.gno | 105 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 21 deletions(-) create mode 100644 gno/r/govdao/messages.gno diff --git a/gno/r/govdao/govdao.gno b/gno/r/govdao/govdao.gno index 9c9f28709..4d01d1642 100644 --- a/gno/r/govdao/govdao.gno +++ b/gno/r/govdao/govdao.gno @@ -7,6 +7,14 @@ import ( var dao *daokit.DAO +// HOW HANDLE DEFAULT MESSAGES (ADD NEW MEMBER, REMOVE MEMBER, ADD ROLE, REMOVE ROLE) +// WE WANT TO BE ABLE TO JUST ADD TIER1, TIER2, TIER3 MEMBERS NOT OTHER MESSAGE TYPES +// OPTIONS: +// - CREATE A NEW CONDITION THAT RESPOND ALWAYS FALSE AND BLOCK THESE MESSAGES (the messages still exist and people can create proposal that will never be executed) +// - ALLOW USERS TO SELECT AT THE CREATION WHICH DEFAULT MESSAGES THEY WANT TO USE (list of strings) +// - REMOVE DEFAULT MESSAGES FROM THE MESSAGES REGISTRY AND GIVE THE TASK TO THE USER TO SET THEM AT THE CREATION (can import the default messages from daokit) +// I like the last option, it gives more flexibility to the user and the user can still use the default messages if he wants to + func init() { dao = &daokit.DAO{} name := "GovDAO" @@ -19,7 +27,8 @@ func init() { {Address: "g1r69l0vhp7tqle3a0rk8m8fulr8sjvj4h7n0tth", Roles: []string{"tier1"}}, } - condition := daokit.CreateCondition("members-treshold", &dao, 0.6) + t1condition := daokit.CreateCondition("members-treshold", &dao, 0.66) // rework for a treshold_roles condition + t2condition := daokit.CreateCondition("members-treshold", &dao, 0.5) // use the govdao condition resources := []daokit.Resource{} dao = daokit.NewDAO(name, description, roles, members, resources, condition) @@ -28,23 +37,3 @@ func init() { profile.SetStringField(profile.Bio, description) profile.SetStringField(profile.Avatar, "") } - -func Propose(proposal daokit.ProposalRequest) { - dao.Propose(proposal) -} - -func Vote(proposalID uint64, vote string) { - dao.Vote(proposalID, vote) -} - -func Execute(proposalID uint64) { - dao.Execute(proposalID) -} - -func InstantExecute(proposal daokit.ProposalRequest) { - dao.InstantExecute(proposal) -} - -func Render(path string) string { - return dao.Render(path) -} diff --git a/gno/r/govdao/messages.gno b/gno/r/govdao/messages.gno new file mode 100644 index 000000000..bb1f12ed9 --- /dev/null +++ b/gno/r/govdao/messages.gno @@ -0,0 +1,105 @@ +package govdao + +import ( + "gno.land/p/teritori/daokit" +) + +type AddNewT1MemberMessage struct { + Address string +} + +var _ daokit.ExecutableMessage = &AddNewT1MemberMessage{} + +func (m AddNewT1MemberMessage) Type() string { + return "gno.land/r/teritori/govdao.AddNewT1Member" +} + +func (m *AddNewT1MemberMessage) String() string { + return m.Address +} + +type AddNewT1MemberMessageHandler struct { + dao *daokit.DAO +} + +func NewAddNewT1MemberMessageHandler(dao *daokit.DAO) *AddNewT1MemberMessageHandler { + return &AddNewT1MemberMessageHandler{dao: dao} +} + +func (h AddNewT1MemberMessageHandler) Execute(msg daokit.ExecutableMessage) { + message := msg.(*AddNewT1MemberMessage) + if h.dao.MemberModule.HasRole(message.Address, "tier1") { + panic("member is already a tier1 member") + } + if h.dao.MemberModule.HasRole(message.Address, "tier2") { + h.dao.MemberModule.RemoveRoleFromMember(message.Address, "tier2") + } + if h.dao.MemberModule.HasRole(message.Address, "tier3") { + h.dao.MemberModule.RemoveRoleFromMember(message.Address, "tier3") + } + h.dao.MemberModule.AddRoleToMember(message.Address, "tier1") +} + +func (h AddNewT1MemberMessageHandler) Type() string { + return AddNewT1MemberMessage{}.Type() +} + +func (h *AddNewT1MemberMessageHandler) Instantiate(payload map[string]interface{}) daokit.ExecutableMessage { + address, ok := payload["address"].(string) + if !ok { + panic("invalid payload format: expected to have a 'address' key with a string value") + } + return &AddNewT1MemberMessage{ + Address: address, + } +} + +type AddNewT2MemberMessage struct { + Address string +} + +var _ daokit.ExecutableMessage = &AddNewT2MemberMessage{} + +func (m AddNewT2MemberMessage) Type() string { + return "gno.land/r/teritori/govdao.AddNewT2Member" +} + +func (m *AddNewT2MemberMessage) String() string { + return m.Address +} + +type AddNewT2MemberMessageHandler struct { + dao *daokit.DAO +} + +func NewAddNewT2MemberMessageHandler(dao *daokit.DAO) *AddNewT2MemberMessageHandler { + return &AddNewT2MemberMessageHandler{dao: dao} +} + +func (h AddNewT2MemberMessageHandler) Execute(msg daokit.ExecutableMessage) { + message := msg.(*AddNewT2MemberMessage) + if h.dao.MemberModule.HasRole(message.Address, "tier1") { + panic("member is already a tier1 member") + } + if h.dao.MemberModule.HasRole(message.Address, "tier2") { + panic("member is already a tier2 member") + } + if h.dao.MemberModule.HasRole(message.Address, "tier3") { + h.dao.MemberModule.RemoveRoleFromMember(message.Address, "tier3") + } + h.dao.MemberModule.AddRoleToMember(message.Address, "tier2") +} + +func (h AddNewT2MemberMessageHandler) Type() string { + return AddNewT2MemberMessage{}.Type() +} + +func (h *AddNewT2MemberMessageHandler) Instantiate(payload map[string]interface{}) daokit.ExecutableMessage { + address, ok := payload["address"].(string) + if !ok { + panic("invalid payload format: expected to have a 'address' key with a string value") + } + return &AddNewT2MemberMessage{ + Address: address, + } +} From f9ed92bbd6fd70515917039d50ef3ec7c48696b3 Mon Sep 17 00:00:00 2001 From: MikaelVallenet Date: Wed, 12 Feb 2025 15:54:40 +0100 Subject: [PATCH 03/23] feat: update --- gno/p/daocond/cond_govdao.gno | 41 ++--- gno/p/daocond/cond_govdao_test.gno | 225 +++++++++++++-------------- gno/p/daocond/cond_role_treshold.gno | 116 ++++++++++++++ gno/p/daocond/daocond_test.gno | 15 +- gno/p/daokit/daokit.gno | 16 +- gno/p/daokit/members.gno | 4 + gno/p/daokit/utils.gno | 27 ++++ gno/r/govdao/govdao.gno | 42 ++++- 8 files changed, 337 insertions(+), 149 deletions(-) create mode 100644 gno/p/daocond/cond_role_treshold.gno diff --git a/gno/p/daocond/cond_govdao.gno b/gno/p/daocond/cond_govdao.gno index 5157b714d..04448f288 100644 --- a/gno/p/daocond/cond_govdao.gno +++ b/gno/p/daocond/cond_govdao.gno @@ -18,26 +18,27 @@ func GovDaoCondThreshold(threshold float64, hasRoleFn func(memberId string, role panic(errors.New("nil hasRoleFn")) } return &govDaoCondThreshold{ - threshold: threshold, - hasRoleFn: hasRoleFn, + threshold: threshold, + hasRoleFn: hasRoleFn, usersWithRoleCountFn: usersWithRoleCountFn, } } type govDaoCondThreshold struct { - threshold float64 - hasRoleFn func(memberId string, role string) bool + threshold float64 + hasRoleFn func(memberId string, role string) bool usersWithRoleCountFn func(role string) uint64 } func (m *govDaoCondThreshold) NewState() State { - return &govDaoCondThresholdState { + return &govDaoCondThresholdState{ cond: m, } } +// {threshold}% of {totalvotingpower} total voting power following these ({t1power} - T1, {t2power} T2, {t3power} T3) func (m *govDaoCondThreshold) Render() string { - return ufmt.Sprintf("%g%% of members", m.threshold*100) + return ufmt.Sprintf("%g%% of total voting power (3.0 / T1, 2.0 / T2, 1.0 / T3)*", m.threshold*100) } func (m *govDaoCondThreshold) RenderJSON() *json.Node { @@ -53,7 +54,7 @@ type govDaoCondThresholdState struct { cond *govDaoCondThreshold } -func (m *govDaoCondThresholdState) Eval(votes map[string]Vote) bool{ +func (m *govDaoCondThresholdState) Eval(votes map[string]Vote) bool { return m.yesRatio(votes) >= m.cond.threshold } func (m *govDaoCondThresholdState) HandleEvent(_ Event, _ map[string]Vote) { @@ -81,15 +82,15 @@ func (m *govDaoCondThresholdState) yesRatio(votes map[string]Vote) float64 { continue } tier := m.getUserTier(userID) - + totalYes += votingPowersByTier[tier] } - return totalYes/totalPower + return totalYes / totalPower } -func (m *govDaoCondThresholdState) getUserTier(userID string) string{ - for _,role := range []string{roleT1, roleT2, roleT3}{ - if m.cond.hasRoleFn(userID, role){ +func (m *govDaoCondThresholdState) getUserTier(userID string) string { + for _, role := range []string{roleT1, roleT2, roleT3} { + if m.cond.hasRoleFn(userID, role) { return role } } @@ -115,15 +116,15 @@ func (m *govDaoCondThresholdState) computeVotingPowers() (map[string]float64, fl // the same number of member on each tier // T2 = 2.0 and T1 = 1.0 with the ration T1/Tn // we compute the actual ratio -func computePower(T1, Tn, maxPower float64) float64{ +func computePower(T1, Tn, maxPower float64) float64 { // If there are 0 Tn (T2, T3) just return the max power // we could also return 0.0 as voting power - if Tn <= 0.0{ + if Tn <= 0.0 { return maxPower } - computedPower := (T1/Tn)*maxPower - if computedPower >= maxPower{ + computedPower := (T1 / Tn) * maxPower + if computedPower >= maxPower { // If computed power is bigger than the max, this happens if Tn is lower than T1 // cap the max power to max power. return maxPower @@ -132,7 +133,7 @@ func computePower(T1, Tn, maxPower float64) float64{ } const ( - roleT1= "T1" - roleT2= "T2" - roleT3= "T3" -) \ No newline at end of file + roleT1 = "T1" + roleT2 = "T2" + roleT3 = "T3" +) diff --git a/gno/p/daocond/cond_govdao_test.gno b/gno/p/daocond/cond_govdao_test.gno index 160edbfdb..fb469eed8 100644 --- a/gno/p/daocond/cond_govdao_test.gno +++ b/gno/p/daocond/cond_govdao_test.gno @@ -1,46 +1,49 @@ - package daocond + import ( - "testing" "fmt" + "testing" + "gno.land/p/demo/urequire" ) + /* - Example 1: - T1 100 members --> 300 VP, 3 votes per member - T2 100 members --> 200 VP, 2 votes per member - T3 100 members --> 100 VP, 1 votes per member - Example 2: +Example 1: +T1 100 members --> 300 VP, 3 votes per member +T2 100 members --> 200 VP, 2 votes per member +T3 100 members --> 100 VP, 1 votes per member +Example 2: - T1 100 members --> 300 VP, 3 votes per member - T2 50 members --> 100 VP, 2 votes per member * - T3 10 members --> 10 VP, 1 votes per member * - Example 3: +T1 100 members --> 300 VP, 3 votes per member +T2 50 members --> 100 VP, 2 votes per member * +T3 10 members --> 10 VP, 1 votes per member * +Example 3: - T1 100 members --> 300 VP, 3 votes per member - T2 200 members --> 200 VP, 1 votes per member * - T3 100 members --> 100 VP, 1 votes per member - Example 4: +T1 100 members --> 300 VP, 3 votes per member +T2 200 members --> 200 VP, 1 votes per member * +T3 100 members --> 100 VP, 1 votes per member +Example 4: - T1 100 members --> 300 VP, 3 votes per member - T2 200 members --> 200 VP, 1 votes per member * - T3 1000 members --> 100 VP, 0.1 votes per member */ -func TestComputeVotingPowers(t *testing.T){ +T1 100 members --> 300 VP, 3 votes per member +T2 200 members --> 200 VP, 1 votes per member * +T3 1000 members --> 100 VP, 0.1 votes per member +*/ +func TestComputeVotingPowers(t *testing.T) { type govDaoComposition struct { - t1s int - t2s int - t3s int - expectedPowers map[string] float64 + t1s int + t2s int + t3s int + expectedPowers map[string]float64 } - tests:= map[string]govDaoComposition{ + tests := map[string]govDaoComposition{ "example 1": { t1s: 100, t2s: 100, t3s: 100, expectedPowers: map[string]float64{ - "T1":3.0, - "T2":2.0, - "T3":1.0, + "T1": 3.0, + "T2": 2.0, + "T3": 1.0, }, }, "example 2": { @@ -48,9 +51,9 @@ func TestComputeVotingPowers(t *testing.T){ t2s: 50, t3s: 10, expectedPowers: map[string]float64{ - "T1":3.0, - "T2":2.0, - "T3":1.0, + "T1": 3.0, + "T2": 2.0, + "T3": 1.0, }, }, "example 3": { @@ -58,9 +61,9 @@ func TestComputeVotingPowers(t *testing.T){ t2s: 200, t3s: 100, expectedPowers: map[string]float64{ - "T1":3.0, - "T2":1.0, - "T3":1.0, + "T1": 3.0, + "T2": 1.0, + "T3": 1.0, }, }, "example 4": { @@ -68,9 +71,9 @@ func TestComputeVotingPowers(t *testing.T){ t2s: 200, t3s: 1000, expectedPowers: map[string]float64{ - "T1":3.0, - "T2":1.0, - "T3":0.1, + "T1": 3.0, + "T2": 1.0, + "T3": 0.1, }, }, "0 -T1s": { @@ -78,9 +81,9 @@ func TestComputeVotingPowers(t *testing.T){ t2s: 100, t3s: 100, expectedPowers: map[string]float64{ - "T1":3.0, - "T2":0.0, - "T3":0.0, + "T1": 3.0, + "T2": 0.0, + "T3": 0.0, }, }, "100 T1, 1 T2, 1 T3": { @@ -88,136 +91,136 @@ func TestComputeVotingPowers(t *testing.T){ t2s: 1, t3s: 1, expectedPowers: map[string]float64{ - "T1":3.0, - "T2":2.0, - "T3":1.0, + "T1": 3.0, + "T2": 2.0, + "T3": 1.0, }, }, } - for name,composition := range tests{ - t.Run(name, func(t *testing.T){ + for name, composition := range tests { + t.Run(name, func(t *testing.T) { dao := newMockDAO() - for i:=0;i 1 { + panic(errors.New("invalid threshold")) + } + if hasRoleFn == nil { + panic(errors.New("nil hasRoleFn")) + } + if usersRoleCountFn == nil { + panic(errors.New("nil usersRoleCountFn")) + } + return &roleThresholdCond{ + threshold: threshold, + hasRoleFn: hasRoleFn, + usersRoleCountFn: usersRoleCountFn, + role: role, + } +} + +type roleThresholdCond struct { + hasRoleFn func(memberId string, role string) bool + usersRoleCountFn func(role string) uint64 + threshold float64 + role string +} + +// NewState implements Condition. +func (m *roleThresholdCond) NewState() State { + return &roleThresholdState{ + cond: m, + } +} + +// Render implements Condition. +func (m *roleThresholdCond) Render() string { + return ufmt.Sprintf("%g%% of %s members", m.threshold*100, m.role) +} + +// RenderJSON implements Condition. +func (m *roleThresholdCond) RenderJSON() *json.Node { + return json.ObjectNode("", map[string]*json.Node{ + "type": json.StringNode("", "role-threshold"), + "role": json.StringNode("", m.role), + "threshold": json.NumberNode("", m.threshold), + }) +} + +var _ Condition = (*roleThresholdCond)(nil) + +type roleThresholdState struct { + cond *roleThresholdCond + totalYes uint64 +} + +// Eval implements State. +func (m *roleThresholdState) Eval(_ map[string]Vote) bool { + return float64(m.totalYes)/float64(m.cond.usersRoleCountFn(m.cond.role)) >= m.cond.threshold +} + +// HandleEvent implements State. +func (m *roleThresholdState) HandleEvent(evt Event, votes map[string]Vote) { + switch evt := evt.(type) { + case *EventVote: + if !m.cond.hasRoleFn(evt.VoterID, m.cond.role) { + return + } + previousVote := votes[evt.VoterID] + if previousVote == VoteYes && evt.Vote != VoteYes { + m.totalYes -= 1 + } else if previousVote != VoteYes && evt.Vote == VoteYes { + m.totalYes += 1 + } + + case *EventRoleAssigned: + if evt.Role != m.cond.role { + return + } + vote := votes[evt.UserID] + if vote == VoteYes { + m.totalYes += 1 + } + + case *EventRoleUnassigned: + if evt.Role != m.cond.role { + return + } + vote := votes[evt.UserID] + if vote == VoteYes { + m.totalYes -= 1 + } + + case *EventRoleRemoved: + if evt.Role != m.cond.role { + return + } + m.totalYes -= 1 + } +} + +// RenderJSON implements State. +func (m *roleThresholdState) RenderJSON(_ map[string]Vote) *json.Node { + return json.ObjectNode("", map[string]*json.Node{ + "type": json.StringNode("", "role-threshold"), + "totalYes": json.NumberNode("", float64(m.totalYes)), + }) +} + +var _ State = (*membersThresholdState)(nil) diff --git a/gno/p/daocond/daocond_test.gno b/gno/p/daocond/daocond_test.gno index 47c054d36..633979b12 100644 --- a/gno/p/daocond/daocond_test.gno +++ b/gno/p/daocond/daocond_test.gno @@ -3,7 +3,6 @@ package daocond_test import ( "errors" "testing" - "fmt" "gno.land/p/demo/urequire" "gno.land/p/teritori/daocond" @@ -211,7 +210,7 @@ type testPhase struct { type mockDAO struct { emitter func(evt daocond.Event) members map[string][]string - roles map[string][]string + roles map[string][]string noEvents bool resources map[string]daocond.Condition } @@ -225,8 +224,8 @@ func newMockDAO(emitter func(evt daocond.Event)) *mockDAO { "eve": []string{}, }, roles: map[string][]string{ - "finance-officer": []string{"alice"}, - "public-relationships": []string{"bob"}, + "finance-officer": []string{"alice"}, + "public-relationships": []string{"bob"}, }, // roles to users resources: make(map[string]daocond.Condition), } @@ -276,6 +275,14 @@ func (m *mockDAO) hasRole(memberId string, role string) bool { return false } +func (m *mockDAO) usersRoleCount(role string) uint64 { + users, ok := m.roles[role] + if !ok { + return 0 + } + return uint64(len(users)) +} + func strsrm(strs []string, val string) ([]string, bool) { removed := false res := []string{} diff --git a/gno/p/daokit/daokit.gno b/gno/p/daokit/daokit.gno index 1287e93f8..535e8c232 100644 --- a/gno/p/daokit/daokit.gno +++ b/gno/p/daokit/daokit.gno @@ -42,14 +42,14 @@ func NewDAO(name, description string, roles []string, members []Member, resource dao.MemberModule.SetRoles(roles) dao.MemberModule.SetMembers(members) - dao.MessagesRegistry.handlers.Iterate("", "", func(key string, value interface{}) bool { - dao.ResourcesModule.SetResource(Resource{ - Resource: key, - Handler: value.(MessageHandler), - Condition: initCond, - }) - return false - }) + // dao.MessagesRegistry.handlers.Iterate("", "", func(key string, value interface{}) bool { + // dao.ResourcesModule.SetResource(Resource{ + // Resource: key, + // Handler: value.(MessageHandler), + // Condition: initCond, + // }) + // return false + // }) for _, resource := range resources { dao.ResourcesModule.SetResource(resource) diff --git a/gno/p/daokit/members.gno b/gno/p/daokit/members.gno index 551898027..55968bf48 100644 --- a/gno/p/daokit/members.gno +++ b/gno/p/daokit/members.gno @@ -61,6 +61,10 @@ func (m *MemberModule) GetMembersWithRole(role string) []string { return m.roleManager.GetRoleUsers(role) } +func (m *MemberModule) CountMembersWithRole(role string) int { + return len(m.roleManager.GetRoleUsers(role)) +} + func (m *MemberModule) SetRoles(roles []string) { caller := std.CurrentRealm().Addr() for _, role := range roles { diff --git a/gno/p/daokit/utils.gno b/gno/p/daokit/utils.gno index 0a011e598..0825c1d7c 100644 --- a/gno/p/daokit/utils.gno +++ b/gno/p/daokit/utils.gno @@ -33,6 +33,20 @@ func CreateCondition(conditionType string, dao **DAO, args ...interface{}) daoco return daocond.RoleCount(uint64(count), role, func(memberId string, role string) bool { return (*dao).MemberModule.HasRole(memberId, role) }) + case "role-treshold": + if len(args) != 2 { + panic("role-treshold condition expects exactly 2 arguments: (role string, threshold float64)") + } + role, ok1 := args[0].(string) + threshold, ok2 := args[1].(float64) + if !ok1 || !ok2 { + panic("Invalid arguments for role-treshold condition: expected (role string, threshold float64)") + } + return daocond.RoleThreshold(threshold, role, func(memberId string, role string) bool { + return (*dao).MemberModule.HasRole(memberId, role) + }, func(role string) uint64 { + return uint64((*dao).MemberModule.CountMembersWithRole(role)) + }) case "members-treshold": if len(args) != 1 { panic("members-treshold condition expects exactly 1 argument: (threshold float64)") @@ -46,6 +60,19 @@ func CreateCondition(conditionType string, dao **DAO, args ...interface{}) daoco }, func() uint64 { return (*dao).MemberModule.MembersCount() }) + case "gov-dao": + if len(args) != 1 { + panic("gov-dao condition expects exactly 1 argument: (threshold float64)") + } + threshold, ok := args[0].(float64) + if !ok { + panic("Invalid argument for gov-dao condition: expected (threshold float64)") + } + return daocond.GovDaoCondThreshold(threshold, func(memberId string, role string) bool { + return (*dao).MemberModule.HasRole(memberId, role) + }, func(role string) uint64 { + return uint64((*dao).MemberModule.CountMembersWithRole(role)) + }) default: panic(ufmt.Sprintf("Unknown condition type: %s", conditionType)) } diff --git a/gno/r/govdao/govdao.gno b/gno/r/govdao/govdao.gno index 4d01d1642..f27cb0264 100644 --- a/gno/r/govdao/govdao.gno +++ b/gno/r/govdao/govdao.gno @@ -25,15 +25,49 @@ func init() { {Address: "g126gx6p6d3da4ymef35ury6874j6kys044r7zlg", Roles: []string{"tier1"}}, {Address: "g1ld6uaykyugld4rnm63rcy7vju4zx23lufml3jv", Roles: []string{"tier2"}}, {Address: "g1r69l0vhp7tqle3a0rk8m8fulr8sjvj4h7n0tth", Roles: []string{"tier1"}}, + {Address: "g16jv3rpz7mkt0gqulxas56se2js7v5vmc6n6e0r", Roles: []string{"tier3"}}, + {Address: "g1ctt28t7sdyp28qzkvlfyx0hyxuz6vz7nplwm9c", Roles: []string{"tier3"}}, } - t1condition := daokit.CreateCondition("members-treshold", &dao, 0.66) // rework for a treshold_roles condition - t2condition := daokit.CreateCondition("members-treshold", &dao, 0.5) // use the govdao condition - resources := []daokit.Resource{} + t1condition := daokit.CreateCondition("role-treshold", &dao, "tier1", 0.66) // rework for a treshold_roles condition + t2condition := daokit.CreateCondition("gov-dao", &dao, 0.5) // use the govdao condition + resources := []daokit.Resource{ + { + Resource: "gno.land/r/teritori/govdao.AddNewT1Member", + Handler: NewAddNewT1MemberMessageHandler(dao), // add a pointer of the dao to the handler + Condition: t1condition, + }, + { + Resource: "gno.land/r/teritori/govdao.AddNewT2Member", + Handler: NewAddNewT2MemberMessageHandler(dao), // add a pointer of the dao to the handler + Condition: t2condition, + }, + } + initCondition := daokit.CreateCondition("members-treshold", &dao, 0.5) - dao = daokit.NewDAO(name, description, roles, members, resources, condition) + dao = daokit.NewDAO(name, description, roles, members, resources, initCondition) profile.SetStringField(profile.DisplayName, name) profile.SetStringField(profile.Bio, description) profile.SetStringField(profile.Avatar, "") } + +func Propose(proposal daokit.ProposalRequest) { + dao.Propose(proposal) +} + +func Vote(proposalID uint64, vote string) { + dao.Vote(proposalID, vote) +} + +func Execute(proposalID uint64) { + dao.Execute(proposalID) +} + +func InstantExecute(proposal daokit.ProposalRequest) { + dao.InstantExecute(proposal) +} + +func Render(path string) string { + return dao.Render(path) +} From c27a77a26533daf5ede801ac05b96ffb48bccff2 Mon Sep 17 00:00:00 2001 From: MikaelVallenet Date: Wed, 12 Feb 2025 16:50:07 +0100 Subject: [PATCH 04/23] feat: add messages --- gno/p/daocond/cond_role_treshold.gno | 7 +++-- gno/r/govdao/govdao.gno | 4 +-- gno/r/govdao/messages.gno | 40 +++++++++++++++++----------- 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/gno/p/daocond/cond_role_treshold.gno b/gno/p/daocond/cond_role_treshold.gno index 0e52d143f..92b70e102 100644 --- a/gno/p/daocond/cond_role_treshold.gno +++ b/gno/p/daocond/cond_role_treshold.gno @@ -108,8 +108,11 @@ func (m *roleThresholdState) HandleEvent(evt Event, votes map[string]Vote) { // RenderJSON implements State. func (m *roleThresholdState) RenderJSON(_ map[string]Vote) *json.Node { return json.ObjectNode("", map[string]*json.Node{ - "type": json.StringNode("", "role-threshold"), - "totalYes": json.NumberNode("", float64(m.totalYes)), + "type": json.StringNode("", "role-threshold"), + "role": json.StringNode("", m.cond.role), + "threshold": json.NumberNode("", m.cond.threshold), + "yes": json.NumberNode("", float64(m.totalYes)), + "total": json.NumberNode("", float64(m.cond.usersRoleCountFn(m.cond.role))), }) } diff --git a/gno/r/govdao/govdao.gno b/gno/r/govdao/govdao.gno index f27cb0264..75d9a901a 100644 --- a/gno/r/govdao/govdao.gno +++ b/gno/r/govdao/govdao.gno @@ -34,12 +34,12 @@ func init() { resources := []daokit.Resource{ { Resource: "gno.land/r/teritori/govdao.AddNewT1Member", - Handler: NewAddNewT1MemberMessageHandler(dao), // add a pointer of the dao to the handler + Handler: NewAddNewT1MemberMessageHandler(&dao), // add a pointer of the dao to the handler Condition: t1condition, }, { Resource: "gno.land/r/teritori/govdao.AddNewT2Member", - Handler: NewAddNewT2MemberMessageHandler(dao), // add a pointer of the dao to the handler + Handler: NewAddNewT2MemberMessageHandler(&dao), // add a pointer of the dao to the handler Condition: t2condition, }, } diff --git a/gno/r/govdao/messages.gno b/gno/r/govdao/messages.gno index bb1f12ed9..34baf0515 100644 --- a/gno/r/govdao/messages.gno +++ b/gno/r/govdao/messages.gno @@ -19,25 +19,30 @@ func (m *AddNewT1MemberMessage) String() string { } type AddNewT1MemberMessageHandler struct { - dao *daokit.DAO + dao **daokit.DAO } -func NewAddNewT1MemberMessageHandler(dao *daokit.DAO) *AddNewT1MemberMessageHandler { +func NewAddNewT1MemberMessageHandler(dao **daokit.DAO) *AddNewT1MemberMessageHandler { return &AddNewT1MemberMessageHandler{dao: dao} } func (h AddNewT1MemberMessageHandler) Execute(msg daokit.ExecutableMessage) { message := msg.(*AddNewT1MemberMessage) - if h.dao.MemberModule.HasRole(message.Address, "tier1") { + dao := *h.dao + if dao.MemberModule.HasRole(message.Address, "tier1") { panic("member is already a tier1 member") } - if h.dao.MemberModule.HasRole(message.Address, "tier2") { - h.dao.MemberModule.RemoveRoleFromMember(message.Address, "tier2") + if dao.MemberModule.HasRole(message.Address, "tier2") { + dao.MemberModule.RemoveRoleFromMember(message.Address, "tier2") } - if h.dao.MemberModule.HasRole(message.Address, "tier3") { - h.dao.MemberModule.RemoveRoleFromMember(message.Address, "tier3") + if dao.MemberModule.HasRole(message.Address, "tier3") { + dao.MemberModule.RemoveRoleFromMember(message.Address, "tier3") + } + if dao.MemberModule.IsMember(message.Address) { + dao.MemberModule.AddRoleToMember(message.Address, "tier1") + } else { + dao.MemberModule.AddMember(message.Address, []string{"tier1"}) } - h.dao.MemberModule.AddRoleToMember(message.Address, "tier1") } func (h AddNewT1MemberMessageHandler) Type() string { @@ -69,25 +74,30 @@ func (m *AddNewT2MemberMessage) String() string { } type AddNewT2MemberMessageHandler struct { - dao *daokit.DAO + dao **daokit.DAO } -func NewAddNewT2MemberMessageHandler(dao *daokit.DAO) *AddNewT2MemberMessageHandler { +func NewAddNewT2MemberMessageHandler(dao **daokit.DAO) *AddNewT2MemberMessageHandler { return &AddNewT2MemberMessageHandler{dao: dao} } func (h AddNewT2MemberMessageHandler) Execute(msg daokit.ExecutableMessage) { message := msg.(*AddNewT2MemberMessage) - if h.dao.MemberModule.HasRole(message.Address, "tier1") { + dao := *h.dao + if dao.MemberModule.HasRole(message.Address, "tier1") { panic("member is already a tier1 member") } - if h.dao.MemberModule.HasRole(message.Address, "tier2") { + if dao.MemberModule.HasRole(message.Address, "tier2") { panic("member is already a tier2 member") } - if h.dao.MemberModule.HasRole(message.Address, "tier3") { - h.dao.MemberModule.RemoveRoleFromMember(message.Address, "tier3") + if dao.MemberModule.HasRole(message.Address, "tier3") { + dao.MemberModule.RemoveRoleFromMember(message.Address, "tier3") + } + if dao.MemberModule.IsMember(message.Address) { + dao.MemberModule.AddRoleToMember(message.Address, "tier2") + } else { + dao.MemberModule.AddMember(message.Address, []string{"tier2"}) } - h.dao.MemberModule.AddRoleToMember(message.Address, "tier2") } func (h AddNewT2MemberMessageHandler) Type() string { From 3929c8dc406d24107f235203147fe8aa2c3e5962 Mon Sep 17 00:00:00 2001 From: MikaelVallenet Date: Thu, 13 Feb 2025 10:46:53 +0100 Subject: [PATCH 05/23] feat: update --- gno/p/daocond/cond_govdao.gno | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/gno/p/daocond/cond_govdao.gno b/gno/p/daocond/cond_govdao.gno index 04448f288..7076b12a9 100644 --- a/gno/p/daocond/cond_govdao.gno +++ b/gno/p/daocond/cond_govdao.gno @@ -61,9 +61,16 @@ func (m *govDaoCondThresholdState) HandleEvent(_ Event, _ map[string]Vote) { panic(errors.New("not implemented")) } func (m *govDaoCondThresholdState) RenderJSON(votes map[string]Vote) *json.Node { + vPowers, totalPower := m.computeVotingPowers() return json.ObjectNode("", map[string]*json.Node{ - "type": json.StringNode("", "govdao-threshold"), - "totalYes": json.NumberNode("", m.yesRatio(votes)), + "type": json.StringNode("", "govdao-threshold"), + "treshold": json.NumberNode("", m.cond.threshold), + "tier1VotingPower": json.NumberNode("", vPowers[roleT1]), + "tier2VotingPower": json.NumberNode("", vPowers[roleT2]), + "tier3VotingPower": json.NumberNode("", vPowers[roleT3]), + "totalYes": json.NumberNode("", m.yesRatio(votes)), + "votingPowerNeeded": json.NumberNode("", m.cond.threshold*totalPower), + "totalVotingPower": json.NumberNode("", totalPower), }) } From c4439c1c3085a6cbcec4cb3c34c7a1930ad75a02 Mon Sep 17 00:00:00 2001 From: MikaelVallenet Date: Thu, 13 Feb 2025 11:28:42 +0100 Subject: [PATCH 06/23] fix: dao --- gno/p/daocond/daocond_test.gno | 8 -------- gno/p/daokit/daokit.gno | 16 ++++++++-------- gno/r/govdao/govdao.gno | 20 ++++++++++---------- 3 files changed, 18 insertions(+), 26 deletions(-) diff --git a/gno/p/daocond/daocond_test.gno b/gno/p/daocond/daocond_test.gno index 633979b12..a065be3ab 100644 --- a/gno/p/daocond/daocond_test.gno +++ b/gno/p/daocond/daocond_test.gno @@ -275,14 +275,6 @@ func (m *mockDAO) hasRole(memberId string, role string) bool { return false } -func (m *mockDAO) usersRoleCount(role string) uint64 { - users, ok := m.roles[role] - if !ok { - return 0 - } - return uint64(len(users)) -} - func strsrm(strs []string, val string) ([]string, bool) { removed := false res := []string{} diff --git a/gno/p/daokit/daokit.gno b/gno/p/daokit/daokit.gno index 535e8c232..1287e93f8 100644 --- a/gno/p/daokit/daokit.gno +++ b/gno/p/daokit/daokit.gno @@ -42,14 +42,14 @@ func NewDAO(name, description string, roles []string, members []Member, resource dao.MemberModule.SetRoles(roles) dao.MemberModule.SetMembers(members) - // dao.MessagesRegistry.handlers.Iterate("", "", func(key string, value interface{}) bool { - // dao.ResourcesModule.SetResource(Resource{ - // Resource: key, - // Handler: value.(MessageHandler), - // Condition: initCond, - // }) - // return false - // }) + dao.MessagesRegistry.handlers.Iterate("", "", func(key string, value interface{}) bool { + dao.ResourcesModule.SetResource(Resource{ + Resource: key, + Handler: value.(MessageHandler), + Condition: initCond, + }) + return false + }) for _, resource := range resources { dao.ResourcesModule.SetResource(resource) diff --git a/gno/r/govdao/govdao.gno b/gno/r/govdao/govdao.gno index 75d9a901a..de4b07cc6 100644 --- a/gno/r/govdao/govdao.gno +++ b/gno/r/govdao/govdao.gno @@ -20,26 +20,26 @@ func init() { name := "GovDAO" description := "This is a govDAO demo" - roles := []string{"tier1", "tier2", "tier3"} + roles := []string{"T1", "T2", "T3"} members := []daokit.Member{ - {Address: "g126gx6p6d3da4ymef35ury6874j6kys044r7zlg", Roles: []string{"tier1"}}, - {Address: "g1ld6uaykyugld4rnm63rcy7vju4zx23lufml3jv", Roles: []string{"tier2"}}, - {Address: "g1r69l0vhp7tqle3a0rk8m8fulr8sjvj4h7n0tth", Roles: []string{"tier1"}}, - {Address: "g16jv3rpz7mkt0gqulxas56se2js7v5vmc6n6e0r", Roles: []string{"tier3"}}, - {Address: "g1ctt28t7sdyp28qzkvlfyx0hyxuz6vz7nplwm9c", Roles: []string{"tier3"}}, + {Address: "g126gx6p6d3da4ymef35ury6874j6kys044r7zlg", Roles: []string{"T1"}}, + {Address: "g1ld6uaykyugld4rnm63rcy7vju4zx23lufml3jv", Roles: []string{"T2"}}, + {Address: "g1r69l0vhp7tqle3a0rk8m8fulr8sjvj4h7n0tth", Roles: []string{"T1"}}, + {Address: "g16jv3rpz7mkt0gqulxas56se2js7v5vmc6n6e0r", Roles: []string{"T3"}}, + {Address: "g1ctt28t7sdyp28qzkvlfyx0hyxuz6vz7nplwm9c", Roles: []string{"T3"}}, } - t1condition := daokit.CreateCondition("role-treshold", &dao, "tier1", 0.66) // rework for a treshold_roles condition - t2condition := daokit.CreateCondition("gov-dao", &dao, 0.5) // use the govdao condition + t1condition := daokit.CreateCondition("role-treshold", &dao, "T1", 0.66) // rework for a treshold_roles condition + t2condition := daokit.CreateCondition("gov-dao", &dao, 0.5) // use the govdao condition resources := []daokit.Resource{ { Resource: "gno.land/r/teritori/govdao.AddNewT1Member", - Handler: NewAddNewT1MemberMessageHandler(&dao), // add a pointer of the dao to the handler + Handler: NewAddNewT1MemberMessageHandler(&dao), Condition: t1condition, }, { Resource: "gno.land/r/teritori/govdao.AddNewT2Member", - Handler: NewAddNewT2MemberMessageHandler(&dao), // add a pointer of the dao to the handler + Handler: NewAddNewT2MemberMessageHandler(&dao), Condition: t2condition, }, } From b52ef34c6c050ab3ad0052dabdd22d70c02777a7 Mon Sep 17 00:00:00 2001 From: MikaelVallenet Date: Thu, 13 Feb 2025 11:34:18 +0100 Subject: [PATCH 07/23] fix: dao --- gno/r/govdao/gno.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gno/r/govdao/gno.mod b/gno/r/govdao/gno.mod index 38d8291d6..563bd6ee7 100644 --- a/gno/r/govdao/gno.mod +++ b/gno/r/govdao/gno.mod @@ -1 +1 @@ -module gno.land/r/teritori/govdao \ No newline at end of file +module gno.land/r/teritori/govdao From 8f42eb7b7ec6881bf006a55c76391a1cc8541c0f Mon Sep 17 00:00:00 2001 From: MikaelVallenet Date: Thu, 13 Feb 2025 15:39:01 +0100 Subject: [PATCH 08/23] feat: add invitations system for T3 --- gno/r/govdao/govdao.gno | 11 +- gno/r/govdao/invitations.gno | 152 +++++++++++++++++++++++++++ gno/r/govdao/invitations_test.gno | 168 ++++++++++++++++++++++++++++++ 3 files changed, 321 insertions(+), 10 deletions(-) create mode 100644 gno/r/govdao/invitations.gno create mode 100644 gno/r/govdao/invitations_test.gno diff --git a/gno/r/govdao/govdao.gno b/gno/r/govdao/govdao.gno index de4b07cc6..dc7a7b7d9 100644 --- a/gno/r/govdao/govdao.gno +++ b/gno/r/govdao/govdao.gno @@ -7,26 +7,17 @@ import ( var dao *daokit.DAO -// HOW HANDLE DEFAULT MESSAGES (ADD NEW MEMBER, REMOVE MEMBER, ADD ROLE, REMOVE ROLE) -// WE WANT TO BE ABLE TO JUST ADD TIER1, TIER2, TIER3 MEMBERS NOT OTHER MESSAGE TYPES -// OPTIONS: -// - CREATE A NEW CONDITION THAT RESPOND ALWAYS FALSE AND BLOCK THESE MESSAGES (the messages still exist and people can create proposal that will never be executed) -// - ALLOW USERS TO SELECT AT THE CREATION WHICH DEFAULT MESSAGES THEY WANT TO USE (list of strings) -// - REMOVE DEFAULT MESSAGES FROM THE MESSAGES REGISTRY AND GIVE THE TASK TO THE USER TO SET THEM AT THE CREATION (can import the default messages from daokit) -// I like the last option, it gives more flexibility to the user and the user can still use the default messages if he wants to - func init() { dao = &daokit.DAO{} name := "GovDAO" description := "This is a govDAO demo" + // ⚠️ No T3 member should be initially added to the DAO since we want to add them only with invitations (see invitations.gno) roles := []string{"T1", "T2", "T3"} members := []daokit.Member{ {Address: "g126gx6p6d3da4ymef35ury6874j6kys044r7zlg", Roles: []string{"T1"}}, {Address: "g1ld6uaykyugld4rnm63rcy7vju4zx23lufml3jv", Roles: []string{"T2"}}, {Address: "g1r69l0vhp7tqle3a0rk8m8fulr8sjvj4h7n0tth", Roles: []string{"T1"}}, - {Address: "g16jv3rpz7mkt0gqulxas56se2js7v5vmc6n6e0r", Roles: []string{"T3"}}, - {Address: "g1ctt28t7sdyp28qzkvlfyx0hyxuz6vz7nplwm9c", Roles: []string{"T3"}}, } t1condition := daokit.CreateCondition("role-treshold", &dao, "T1", 0.66) // rework for a treshold_roles condition diff --git a/gno/r/govdao/invitations.gno b/gno/r/govdao/invitations.gno new file mode 100644 index 000000000..4a36699b6 --- /dev/null +++ b/gno/r/govdao/invitations.gno @@ -0,0 +1,152 @@ +package govdao + +import ( + "std" + + "gno.land/p/demo/avl" +) + +// TODO: If a member is removed from the DAO, remove all the invitations he sent and received ? (or downgrade to tier 3 if enough invitations ???) +// TODO: if at some point with one message to downgrade user tier, we need to remove the latest invitation +// T1 => 3 invitations +// T2 => 2 invitations +// T3 => 1 invitation +// Invitations is only about adding T3 members +// A user need 2 invitations to become a T3 member +// A member can only delegate one invitation to another user + +const ( + Tier1 = "T1" + Tier2 = "T2" + Tier3 = "T3" + + T1Invitations = 3 + T2Invitations = 2 + T3Invitations = 1 +) + +var ( + invitationsSentTree = avl.NewTree() // std.Addr -> []std.Address + invitationsReceivedTree = avl.NewTree() // std.Addr -> []std.Address +) + +func Delegate(target string) { + caller := std.PrevRealm().Addr() + + if caller.String() == target { + panic("caller cannot delegate an invitation to themselves") + } + + if !dao.MemberModule.IsMember(caller.String()) { + panic("caller is not a member of the govdao") + } + + invitationsSent := getInvitationsSent(caller.String()) + for _, addr := range invitationsSent { + if addr.String() == target { + panic("caller has already delegated an invitation to this target") + } + } + + roles := dao.MemberModule.GetMemberRoles(caller.String()) + if len(roles) == 0 { + panic("caller is member but has no roles, this should not happen") + } + + tier := roles[0] + allowedInvitations := getAllowedInvitations(tier) + if len(invitationsSent) >= allowedInvitations { + panic("caller has already delegated the maximum number of invitations") + } + + invitationsSent = append(invitationsSent, std.Address(target)) + invitationsSentTree.Set(caller.String(), invitationsSent) + + invitationsReceived := getInvitationsReceived(target) + invitationsReceived = append(invitationsReceived, std.Address(caller)) + invitationsReceivedTree.Set(target, invitationsReceived) + + if len(invitationsReceived) == 2 && !dao.MemberModule.IsMember(target) { + dao.MemberModule.AddMember(target, []string{"T3"}) + } +} + +func Withdraw(target string) { + caller := std.PrevRealm().Addr() + if !dao.MemberModule.IsMember(caller.String()) { + panic("caller is not a member of the govdao") + } + + invitationsSent := getInvitationsSent(caller.String()) + found := false + for i, addr := range invitationsSent { + if addr.String() == target { + invitationsSent = append(invitationsSent[:i], invitationsSent[i+1:]...) + found = true + break + } + } + if !found { + panic("caller has not delegated an invitation to this target") + } + invitationsSentTree.Set(caller.String(), invitationsSent) + + invitationsReceived := getInvitationsReceived(target) + found = false + for i, addr := range invitationsReceived { + if addr.String() == caller.String() { + invitationsReceived = append(invitationsReceived[:i], invitationsReceived[i+1:]...) + found = true + break + } + } + if !found { + panic("target has not received an invitation from the caller, should not happen at this point") + } + + if len(invitationsReceived) == 1 && dao.MemberModule.HasRole(target, Tier3) { + invitationsSent = getInvitationsSent(target) + for _, addr := range getInvitationsSent(target) { + Withdraw(addr.String()) + } + + dao.MemberModule.RemoveMember(target) + } +} + +func getAllowedInvitations(tier string) int { + switch tier { + case Tier1: + return T1Invitations + case Tier2: + return T2Invitations + case Tier3: + return T3Invitations + default: + panic("caller has an unknown role/tier") + } +} + +func getInvitationsSent(addr string) []std.Address { + invitationsSentRaw, ok := invitationsSentTree.Get(addr) + if !ok { + return []std.Address{} + } + invitationsSent, valid := invitationsSentRaw.([]std.Address) + if !valid { + panic("invalid type for invitationsSent, should not happen") + } + return invitationsSent +} + +func getInvitationsReceived(addr string) []std.Address { + invitationsReceivedRaw, ok := invitationsReceivedTree.Get(addr) + if !ok { + return []std.Address{} + } + invitationsReceived, valid := invitationsReceivedRaw.([]std.Address) + if !valid { + panic("invalid type for invitationsReceived, should not happen") + } + return invitationsReceived +} diff --git a/gno/r/govdao/invitations_test.gno b/gno/r/govdao/invitations_test.gno new file mode 100644 index 000000000..af30e8219 --- /dev/null +++ b/gno/r/govdao/invitations_test.gno @@ -0,0 +1,168 @@ +package govdao + +import ( + "std" + "testing" + + "gno.land/p/demo/testutils" + "gno.land/p/teritori/daokit" +) + +var ( + alice = testutils.TestAddress("alice") + bob = testutils.TestAddress("bob") + carol = testutils.TestAddress("carol") + dave = testutils.TestAddress("dave") + eve = testutils.TestAddress("eve") + frank = testutils.TestAddress("frank") +) + +func TestDelegate(t *testing.T) { + setupTest() + + type testDelegateInput struct { + caller std.Address + target std.Address + } + + type testDelegateExpected struct { + panic bool + t3 bool + } + + type testDelegate struct { + input testDelegateInput + expected testDelegateExpected + } + + type testDelegateTestTable = map[string]testDelegate + + tests := testDelegateTestTable{ + "Delegate to himself": { + input: testDelegateInput{ + caller: alice, + target: alice, + }, + expected: testDelegateExpected{ + panic: true, + }, + }, + "Not a member": { + input: testDelegateInput{ + caller: eve, + target: dave, + }, + expected: testDelegateExpected{ + panic: true, + }, + }, + "Success": { + input: testDelegateInput{ + caller: alice, + target: dave, + }, + expected: testDelegateExpected{ + panic: false, + t3: false, // need 2 invitations to become a member + }, + }, + "Already delegated": { + input: testDelegateInput{ + caller: alice, + target: dave, + }, + expected: testDelegateExpected{ + panic: true, + }, + }, + "No roles": { + input: testDelegateInput{ + caller: frank, + target: dave, + }, + expected: testDelegateExpected{ + panic: true, + }, + }, + "Succes 2nd invitation": { + input: testDelegateInput{ + caller: bob, + target: dave, + }, + expected: testDelegateExpected{ + panic: false, + t3: true, // now dave is a T3 member + }, + }, + "New member invitation": { + input: testDelegateInput{ + caller: dave, + target: alice, + }, + expected: testDelegateExpected{ + panic: false, + t3: false, + }, + }, + "Maximum invitations": { + input: testDelegateInput{ + caller: dave, // already sent 1 invitation (T3 member) + target: eve, + }, + expected: testDelegateExpected{ + panic: true, + }, + }, + "Delegate to a tiered-members": { + input: testDelegateInput{ + caller: carol, + target: alice, + }, + expected: testDelegateExpected{ + panic: false, + t3: false, // alice is already a T1 member + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + if test.expected.panic { + defer func() { + if r := recover(); r == nil { + t.Errorf("expected panic") + } + }() + } + + std.TestSetOrigCaller(test.input.caller) + Delegate(test.input.target.String()) + if test.expected.t3 { + if !dao.MemberModule.HasRole(test.input.target.String(), "T3") { + t.Errorf("expected T3 role") + } + } else { + if dao.MemberModule.HasRole(test.input.target.String(), "T3") { + t.Errorf("unexpected T3 role") + } + } + }) + } + +} + +func setupTest() { + dao = &daokit.DAO{} + name := "test" + description := "test" + + roles := []string{"T1", "T2", "T3"} + members := []daokit.Member{ + {Address: alice.String(), Roles: []string{"T1"}}, + {Address: bob.String(), Roles: []string{"T2"}}, + {Address: carol.String(), Roles: []string{"T1"}}, + {Address: frank.String(), Roles: []string{}}, // for testing purpose but should not happen (a role should be mandatory in govdao) + } + resources := []daokit.Resource{} + dao = daokit.NewDAO(name, description, roles, members, resources, nil) +} From d697d41f21fba796c2c2a88b3b8ff0141247b68b Mon Sep 17 00:00:00 2001 From: MikaelVallenet Date: Thu, 13 Feb 2025 16:53:59 +0100 Subject: [PATCH 09/23] feat: add withdrawal mechanism for T3 --- gno/r/govdao/invitations.gno | 4 +- gno/r/govdao/invitations_test.gno | 151 +++++++++++++++++++++++++++++- 2 files changed, 151 insertions(+), 4 deletions(-) diff --git a/gno/r/govdao/invitations.gno b/gno/r/govdao/invitations.gno index 4a36699b6..8d0d0305f 100644 --- a/gno/r/govdao/invitations.gno +++ b/gno/r/govdao/invitations.gno @@ -67,7 +67,7 @@ func Delegate(target string) { invitationsReceivedTree.Set(target, invitationsReceived) if len(invitationsReceived) == 2 && !dao.MemberModule.IsMember(target) { - dao.MemberModule.AddMember(target, []string{"T3"}) + dao.MemberModule.AddMember(target, []string{Tier3}) } } @@ -103,13 +103,13 @@ func Withdraw(target string) { if !found { panic("target has not received an invitation from the caller, should not happen at this point") } + invitationsReceivedTree.Set(target, invitationsReceived) if len(invitationsReceived) == 1 && dao.MemberModule.HasRole(target, Tier3) { invitationsSent = getInvitationsSent(target) for _, addr := range getInvitationsSent(target) { Withdraw(addr.String()) } - dao.MemberModule.RemoveMember(target) } } diff --git a/gno/r/govdao/invitations_test.gno b/gno/r/govdao/invitations_test.gno index af30e8219..ab9835e0f 100644 --- a/gno/r/govdao/invitations_test.gno +++ b/gno/r/govdao/invitations_test.gno @@ -4,6 +4,7 @@ import ( "std" "testing" + "gno.land/p/demo/avl" "gno.land/p/demo/testutils" "gno.land/p/teritori/daokit" ) @@ -15,6 +16,17 @@ var ( dave = testutils.TestAddress("dave") eve = testutils.TestAddress("eve") frank = testutils.TestAddress("frank") + greg = testutils.TestAddress("greg") + + names = map[std.Address]string{ + alice: "alice", + bob: "bob", + carol: "carol", + dave: "dave", + eve: "eve", + frank: "frank", + greg: "greg", + } ) func TestDelegate(t *testing.T) { @@ -139,11 +151,142 @@ func TestDelegate(t *testing.T) { Delegate(test.input.target.String()) if test.expected.t3 { if !dao.MemberModule.HasRole(test.input.target.String(), "T3") { - t.Errorf("expected T3 role") + t.Errorf("expected T3 role: %s", names[test.input.target]) } } else { if dao.MemberModule.HasRole(test.input.target.String(), "T3") { - t.Errorf("unexpected T3 role") + t.Errorf("unexpected T3 role: %s", names[test.input.target]) + } + } + }) + } + +} + +func TestWithdraw(t *testing.T) { + setupTest() + + // Add some invitations + + // Alice -> Dave & Eve + // Bob -> Dave + // Carol -> Alice & Dave + // Dave -> Eve + // Eve -> Greg + + // When removing Dave, Eve should be removed as well and Greg too + + std.TestSetOrigCaller(alice) + Delegate(dave.String()) + Delegate(eve.String()) + Delegate(greg.String()) + std.TestSetOrigCaller(bob) + Delegate(dave.String()) + std.TestSetOrigCaller(carol) + Delegate(alice.String()) + Delegate(dave.String()) + std.TestSetOrigCaller(dave) + Delegate(eve.String()) + std.TestSetOrigCaller(eve) + Delegate(greg.String()) + + type testWithdrawInput struct { + caller std.Address + target std.Address + } + + type testWithdrawExpected struct { + panic bool + t3 bool + chainWithdraw []std.Address + } + + type testWithdraw struct { + input testWithdrawInput + expected testWithdrawExpected + } + + type testWithdrawTestTable = map[string]testWithdraw + + tests := testWithdrawTestTable{ + "Caller not a member": { + input: testWithdrawInput{ + caller: eve, + target: dave, + }, + expected: testWithdrawExpected{ + panic: true, + }, + }, + "No invitation": { + input: testWithdrawInput{ + caller: alice, + target: bob, + }, + expected: testWithdrawExpected{ + panic: true, + }, + }, + "Unknown role": { + input: testWithdrawInput{ + caller: frank, + target: dave, + }, + expected: testWithdrawExpected{ + panic: true, + }, + }, + "Success": { + input: testWithdrawInput{ + caller: carol, + target: dave, + }, + expected: testWithdrawExpected{ + panic: false, + t3: true, + }, + }, + "2nd Success": { + input: testWithdrawInput{ + caller: alice, + target: dave, + }, + expected: testWithdrawExpected{ + panic: false, + t3: false, + chainWithdraw: []std.Address{ + eve, + greg, + }, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + if test.expected.panic { + defer func() { + if r := recover(); r == nil { + t.Errorf("expected panic") + } + }() + } + + std.TestSetOrigCaller(test.input.caller) + Withdraw(test.input.target.String()) + if test.expected.t3 { + if !dao.MemberModule.HasRole(test.input.target.String(), "T3") { + t.Errorf("expected T3 role: %s", names[test.input.target]) + } + } else { + if dao.MemberModule.HasRole(test.input.target.String(), "T3") { + t.Errorf("unexpected T3 role: %s", names[test.input.target]) + } + + for _, addr := range test.expected.chainWithdraw { + if dao.MemberModule.HasRole(addr.String(), "T3") { + t.Errorf("unexpected T3 role: %s", names[addr]) + } } } }) @@ -165,4 +308,8 @@ func setupTest() { } resources := []daokit.Resource{} dao = daokit.NewDAO(name, description, roles, members, resources, nil) + + // reset tree + invitationsSentTree = avl.NewTree() + invitationsReceivedTree = avl.NewTree() } From 84e0342ac1f7e7b5016521bbeb4c8291201cd77e Mon Sep 17 00:00:00 2001 From: MikaelVallenet Date: Fri, 14 Feb 2025 09:24:44 +0100 Subject: [PATCH 10/23] feat: add limit size membership for T2 --- gno/r/govdao/govdao.gno | 12 +++++++++--- gno/r/govdao/invitations.gno | 4 ---- gno/r/govdao/messages.gno | 31 ++++++++++++++++++------------- 3 files changed, 27 insertions(+), 20 deletions(-) diff --git a/gno/r/govdao/govdao.gno b/gno/r/govdao/govdao.gno index dc7a7b7d9..62ddbc7e3 100644 --- a/gno/r/govdao/govdao.gno +++ b/gno/r/govdao/govdao.gno @@ -5,6 +5,12 @@ import ( "gno.land/r/demo/profile" ) +const ( + Tier1 = "T1" + Tier2 = "T2" + Tier3 = "T3" +) + var dao *daokit.DAO func init() { @@ -15,9 +21,9 @@ func init() { // ⚠️ No T3 member should be initially added to the DAO since we want to add them only with invitations (see invitations.gno) roles := []string{"T1", "T2", "T3"} members := []daokit.Member{ - {Address: "g126gx6p6d3da4ymef35ury6874j6kys044r7zlg", Roles: []string{"T1"}}, - {Address: "g1ld6uaykyugld4rnm63rcy7vju4zx23lufml3jv", Roles: []string{"T2"}}, - {Address: "g1r69l0vhp7tqle3a0rk8m8fulr8sjvj4h7n0tth", Roles: []string{"T1"}}, + {Address: "g126gx6p6d3da4ymef35ury6874j6kys044r7zlg", Roles: []string{Tier1}}, + {Address: "g1ld6uaykyugld4rnm63rcy7vju4zx23lufml3jv", Roles: []string{Tier2}}, + {Address: "g1r69l0vhp7tqle3a0rk8m8fulr8sjvj4h7n0tth", Roles: []string{Tier3}}, } t1condition := daokit.CreateCondition("role-treshold", &dao, "T1", 0.66) // rework for a treshold_roles condition diff --git a/gno/r/govdao/invitations.gno b/gno/r/govdao/invitations.gno index 8d0d0305f..7456d3e93 100644 --- a/gno/r/govdao/invitations.gno +++ b/gno/r/govdao/invitations.gno @@ -16,10 +16,6 @@ import ( // A member can only delegate one invitation to another user const ( - Tier1 = "T1" - Tier2 = "T2" - Tier3 = "T3" - T1Invitations = 3 T2Invitations = 2 T3Invitations = 1 diff --git a/gno/r/govdao/messages.gno b/gno/r/govdao/messages.gno index 34baf0515..2c17a4a78 100644 --- a/gno/r/govdao/messages.gno +++ b/gno/r/govdao/messages.gno @@ -29,19 +29,19 @@ func NewAddNewT1MemberMessageHandler(dao **daokit.DAO) *AddNewT1MemberMessageHan func (h AddNewT1MemberMessageHandler) Execute(msg daokit.ExecutableMessage) { message := msg.(*AddNewT1MemberMessage) dao := *h.dao - if dao.MemberModule.HasRole(message.Address, "tier1") { + if dao.MemberModule.HasRole(message.Address, Tier1) { panic("member is already a tier1 member") } - if dao.MemberModule.HasRole(message.Address, "tier2") { - dao.MemberModule.RemoveRoleFromMember(message.Address, "tier2") + if dao.MemberModule.HasRole(message.Address, Tier2) { + dao.MemberModule.RemoveRoleFromMember(message.Address, Tier2) } - if dao.MemberModule.HasRole(message.Address, "tier3") { - dao.MemberModule.RemoveRoleFromMember(message.Address, "tier3") + if dao.MemberModule.HasRole(message.Address, Tier3) { + dao.MemberModule.RemoveRoleFromMember(message.Address, Tier3) } if dao.MemberModule.IsMember(message.Address) { - dao.MemberModule.AddRoleToMember(message.Address, "tier1") + dao.MemberModule.AddRoleToMember(message.Address, Tier1) } else { - dao.MemberModule.AddMember(message.Address, []string{"tier1"}) + dao.MemberModule.AddMember(message.Address, []string{Tier1}) } } @@ -84,19 +84,24 @@ func NewAddNewT2MemberMessageHandler(dao **daokit.DAO) *AddNewT2MemberMessageHan func (h AddNewT2MemberMessageHandler) Execute(msg daokit.ExecutableMessage) { message := msg.(*AddNewT2MemberMessage) dao := *h.dao - if dao.MemberModule.HasRole(message.Address, "tier1") { + if dao.MemberModule.HasRole(message.Address, Tier1) { panic("member is already a tier1 member") } - if dao.MemberModule.HasRole(message.Address, "tier2") { + if dao.MemberModule.HasRole(message.Address, Tier2) { panic("member is already a tier2 member") } - if dao.MemberModule.HasRole(message.Address, "tier3") { - dao.MemberModule.RemoveRoleFromMember(message.Address, "tier3") + t2MaxSize := dao.MemberModule.CountMembersWithRole(Tier1) * 2 + if dao.MemberModule.CountMembersWithRole(Tier2) >= t2MaxSize { + panic("tier2 members limit that is twice the number of tier1 members has been reached") + } + + if dao.MemberModule.HasRole(message.Address, Tier3) { + dao.MemberModule.RemoveRoleFromMember(message.Address, Tier3) } if dao.MemberModule.IsMember(message.Address) { - dao.MemberModule.AddRoleToMember(message.Address, "tier2") + dao.MemberModule.AddRoleToMember(message.Address, Tier2) } else { - dao.MemberModule.AddMember(message.Address, []string{"tier2"}) + dao.MemberModule.AddMember(message.Address, []string{Tier2}) } } From 8c2416e92582b0b5a885d790c1b583f825519545 Mon Sep 17 00:00:00 2001 From: MikaelVallenet Date: Fri, 14 Feb 2025 09:31:47 +0100 Subject: [PATCH 11/23] feat: rename msg --- gno/r/govdao/govdao.gno | 4 +-- gno/r/govdao/messages.gno | 56 +++++++++++++++++++-------------------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/gno/r/govdao/govdao.gno b/gno/r/govdao/govdao.gno index 62ddbc7e3..164b9ac38 100644 --- a/gno/r/govdao/govdao.gno +++ b/gno/r/govdao/govdao.gno @@ -31,12 +31,12 @@ func init() { resources := []daokit.Resource{ { Resource: "gno.land/r/teritori/govdao.AddNewT1Member", - Handler: NewAddNewT1MemberMessageHandler(&dao), + Handler: NewAddT1MemberMessageHandler(&dao), Condition: t1condition, }, { Resource: "gno.land/r/teritori/govdao.AddNewT2Member", - Handler: NewAddNewT2MemberMessageHandler(&dao), + Handler: NewAddT2MemberMessageHandler(&dao), Condition: t2condition, }, } diff --git a/gno/r/govdao/messages.gno b/gno/r/govdao/messages.gno index 2c17a4a78..845dab3e1 100644 --- a/gno/r/govdao/messages.gno +++ b/gno/r/govdao/messages.gno @@ -4,30 +4,30 @@ import ( "gno.land/p/teritori/daokit" ) -type AddNewT1MemberMessage struct { +type AddT1MemberMessage struct { Address string } -var _ daokit.ExecutableMessage = &AddNewT1MemberMessage{} +var _ daokit.ExecutableMessage = &AddT1MemberMessage{} -func (m AddNewT1MemberMessage) Type() string { - return "gno.land/r/teritori/govdao.AddNewT1Member" +func (m AddT1MemberMessage) Type() string { + return "gno.land/r/teritori/govdao.AddT1Member" } -func (m *AddNewT1MemberMessage) String() string { +func (m *AddT1MemberMessage) String() string { return m.Address } -type AddNewT1MemberMessageHandler struct { +type AddT1MemberMessageHandler struct { dao **daokit.DAO } -func NewAddNewT1MemberMessageHandler(dao **daokit.DAO) *AddNewT1MemberMessageHandler { - return &AddNewT1MemberMessageHandler{dao: dao} +func NewAddT1MemberMessageHandler(dao **daokit.DAO) *AddT1MemberMessageHandler { + return &AddT1MemberMessageHandler{dao: dao} } -func (h AddNewT1MemberMessageHandler) Execute(msg daokit.ExecutableMessage) { - message := msg.(*AddNewT1MemberMessage) +func (h AddT1MemberMessageHandler) Execute(msg daokit.ExecutableMessage) { + message := msg.(*AddT1MemberMessage) dao := *h.dao if dao.MemberModule.HasRole(message.Address, Tier1) { panic("member is already a tier1 member") @@ -45,44 +45,44 @@ func (h AddNewT1MemberMessageHandler) Execute(msg daokit.ExecutableMessage) { } } -func (h AddNewT1MemberMessageHandler) Type() string { - return AddNewT1MemberMessage{}.Type() +func (h AddT1MemberMessageHandler) Type() string { + return AddT1MemberMessage{}.Type() } -func (h *AddNewT1MemberMessageHandler) Instantiate(payload map[string]interface{}) daokit.ExecutableMessage { +func (h *AddT1MemberMessageHandler) Instantiate(payload map[string]interface{}) daokit.ExecutableMessage { address, ok := payload["address"].(string) if !ok { panic("invalid payload format: expected to have a 'address' key with a string value") } - return &AddNewT1MemberMessage{ + return &AddT1MemberMessage{ Address: address, } } -type AddNewT2MemberMessage struct { +type AddT2MemberMessage struct { Address string } -var _ daokit.ExecutableMessage = &AddNewT2MemberMessage{} +var _ daokit.ExecutableMessage = &AddT2MemberMessage{} -func (m AddNewT2MemberMessage) Type() string { - return "gno.land/r/teritori/govdao.AddNewT2Member" +func (m AddT2MemberMessage) Type() string { + return "gno.land/r/teritori/govdao.AddT2Member" } -func (m *AddNewT2MemberMessage) String() string { +func (m *AddT2MemberMessage) String() string { return m.Address } -type AddNewT2MemberMessageHandler struct { +type AddT2MemberMessageHandler struct { dao **daokit.DAO } -func NewAddNewT2MemberMessageHandler(dao **daokit.DAO) *AddNewT2MemberMessageHandler { - return &AddNewT2MemberMessageHandler{dao: dao} +func NewAddT2MemberMessageHandler(dao **daokit.DAO) *AddT2MemberMessageHandler { + return &AddT2MemberMessageHandler{dao: dao} } -func (h AddNewT2MemberMessageHandler) Execute(msg daokit.ExecutableMessage) { - message := msg.(*AddNewT2MemberMessage) +func (h AddT2MemberMessageHandler) Execute(msg daokit.ExecutableMessage) { + message := msg.(*AddT2MemberMessage) dao := *h.dao if dao.MemberModule.HasRole(message.Address, Tier1) { panic("member is already a tier1 member") @@ -105,16 +105,16 @@ func (h AddNewT2MemberMessageHandler) Execute(msg daokit.ExecutableMessage) { } } -func (h AddNewT2MemberMessageHandler) Type() string { - return AddNewT2MemberMessage{}.Type() +func (h AddT2MemberMessageHandler) Type() string { + return AddT2MemberMessage{}.Type() } -func (h *AddNewT2MemberMessageHandler) Instantiate(payload map[string]interface{}) daokit.ExecutableMessage { +func (h *AddT2MemberMessageHandler) Instantiate(payload map[string]interface{}) daokit.ExecutableMessage { address, ok := payload["address"].(string) if !ok { panic("invalid payload format: expected to have a 'address' key with a string value") } - return &AddNewT2MemberMessage{ + return &AddT2MemberMessage{ Address: address, } } From 4fa1ed64d81f2828944e8b0fe38f498d677c6d73 Mon Sep 17 00:00:00 2001 From: MikaelVallenet Date: Fri, 14 Feb 2025 09:38:05 +0100 Subject: [PATCH 12/23] chore: up --- gno/r/govdao/govdao.gno | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gno/r/govdao/govdao.gno b/gno/r/govdao/govdao.gno index 164b9ac38..1276ee16f 100644 --- a/gno/r/govdao/govdao.gno +++ b/gno/r/govdao/govdao.gno @@ -17,13 +17,13 @@ func init() { dao = &daokit.DAO{} name := "GovDAO" description := "This is a govDAO demo" + roles := []string{"T1", "T2", "T3"} // ⚠️ No T3 member should be initially added to the DAO since we want to add them only with invitations (see invitations.gno) - roles := []string{"T1", "T2", "T3"} members := []daokit.Member{ {Address: "g126gx6p6d3da4ymef35ury6874j6kys044r7zlg", Roles: []string{Tier1}}, {Address: "g1ld6uaykyugld4rnm63rcy7vju4zx23lufml3jv", Roles: []string{Tier2}}, - {Address: "g1r69l0vhp7tqle3a0rk8m8fulr8sjvj4h7n0tth", Roles: []string{Tier3}}, + {Address: "g1r69l0vhp7tqle3a0rk8m8fulr8sjvj4h7n0tth", Roles: []string{Tier2}}, } t1condition := daokit.CreateCondition("role-treshold", &dao, "T1", 0.66) // rework for a treshold_roles condition From d4cf5165a9d710fbb1ae594211845c09921bd0c2 Mon Sep 17 00:00:00 2001 From: MikaelVallenet Date: Fri, 14 Feb 2025 12:52:39 +0100 Subject: [PATCH 13/23] feat: add configurables roles for govdao --- gno/p/daocond/cond_govdao.gno | 42 +++++++++++++++--------------- gno/p/daocond/cond_govdao_test.gno | 15 ++++++----- gno/p/daokit/utils.gno | 11 ++++---- gno/r/dao_realm/dao_realm_test.gno | 6 ++--- gno/r/govdao/govdao.gno | 4 +-- 5 files changed, 40 insertions(+), 38 deletions(-) diff --git a/gno/p/daocond/cond_govdao.gno b/gno/p/daocond/cond_govdao.gno index 7076b12a9..5e65ab98b 100644 --- a/gno/p/daocond/cond_govdao.gno +++ b/gno/p/daocond/cond_govdao.gno @@ -7,7 +7,7 @@ import ( "gno.land/p/demo/ufmt" ) -func GovDaoCondThreshold(threshold float64, hasRoleFn func(memberId string, role string) bool, usersWithRoleCountFn func(role string) uint64) Condition { +func GovDaoCondThreshold(threshold float64, roles []string, hasRoleFn func(memberId string, role string) bool, usersWithRoleCountFn func(role string) uint64) Condition { if threshold <= 0 || threshold > 1 { panic(errors.New("invalid threshold")) } @@ -17,8 +17,12 @@ func GovDaoCondThreshold(threshold float64, hasRoleFn func(memberId string, role if hasRoleFn == nil { panic(errors.New("nil hasRoleFn")) } + if len(roles) > 3 { + panic("the govdao condition handles at most 3 roles") + } return &govDaoCondThreshold{ threshold: threshold, + roles: roles, hasRoleFn: hasRoleFn, usersWithRoleCountFn: usersWithRoleCountFn, } @@ -26,6 +30,7 @@ func GovDaoCondThreshold(threshold float64, hasRoleFn func(memberId string, role type govDaoCondThreshold struct { threshold float64 + roles []string hasRoleFn func(memberId string, role string) bool usersWithRoleCountFn func(role string) uint64 } @@ -38,7 +43,7 @@ func (m *govDaoCondThreshold) NewState() State { // {threshold}% of {totalvotingpower} total voting power following these ({t1power} - T1, {t2power} T2, {t3power} T3) func (m *govDaoCondThreshold) Render() string { - return ufmt.Sprintf("%g%% of total voting power (3.0 / T1, 2.0 / T2, 1.0 / T3)*", m.threshold*100) + return ufmt.Sprintf("%g%% of total voting power | T1 => 3.0 power | T2 => 2.0 power | T3 => 1.0 power", m.threshold*100) } func (m *govDaoCondThreshold) RenderJSON() *json.Node { @@ -65,9 +70,9 @@ func (m *govDaoCondThresholdState) RenderJSON(votes map[string]Vote) *json.Node return json.ObjectNode("", map[string]*json.Node{ "type": json.StringNode("", "govdao-threshold"), "treshold": json.NumberNode("", m.cond.threshold), - "tier1VotingPower": json.NumberNode("", vPowers[roleT1]), - "tier2VotingPower": json.NumberNode("", vPowers[roleT2]), - "tier3VotingPower": json.NumberNode("", vPowers[roleT3]), + "tier1VotingPower": json.NumberNode("", vPowers[m.cond.roles[0]]), + "tier2VotingPower": json.NumberNode("", vPowers[m.cond.roles[1]]), + "tier3VotingPower": json.NumberNode("", vPowers[m.cond.roles[2]]), "totalYes": json.NumberNode("", m.yesRatio(votes)), "votingPowerNeeded": json.NumberNode("", m.cond.threshold*totalPower), "totalVotingPower": json.NumberNode("", totalPower), @@ -88,15 +93,15 @@ func (m *govDaoCondThresholdState) yesRatio(votes map[string]Vote) float64 { if vote != VoteYes { continue } - tier := m.getUserTier(userID) + tier := m.getUserRole(userID) totalYes += votingPowersByTier[tier] } return totalYes / totalPower } -func (m *govDaoCondThresholdState) getUserTier(userID string) string { - for _, role := range []string{roleT1, roleT2, roleT3} { +func (m *govDaoCondThresholdState) getUserRole(userID string) string { + for _, role := range m.cond.roles { if m.cond.hasRoleFn(userID, role) { return role } @@ -105,16 +110,17 @@ func (m *govDaoCondThresholdState) getUserTier(userID string) string { } func (m *govDaoCondThresholdState) computeVotingPowers() (map[string]float64, float64) { - totalT1s := float64(m.cond.usersWithRoleCountFn(roleT1)) - totalT2s := float64(m.cond.usersWithRoleCountFn(roleT2)) - totalT3s := float64(m.cond.usersWithRoleCountFn(roleT3)) + roles := m.cond.roles + totalT1s := float64(m.cond.usersWithRoleCountFn(roles[0])) + totalT2s := float64(m.cond.usersWithRoleCountFn(roles[1])) + totalT3s := float64(m.cond.usersWithRoleCountFn(roles[2])) votingPowers := map[string]float64{ - roleT1: 3.0, - roleT2: computePower(totalT1s, totalT2s, 2.0), - roleT3: computePower(totalT1s, totalT3s, 1.0), + roles[0]: 3.0, + roles[1]: computePower(totalT1s, totalT2s, 2.0), + roles[2]: computePower(totalT1s, totalT3s, 1.0), } - totalPower := votingPowers[roleT1]*totalT1s + votingPowers[roleT2]*totalT2s + votingPowers[roleT3]*totalT3s + totalPower := votingPowers[roles[0]]*totalT1s + votingPowers[roles[1]]*totalT2s + votingPowers[roles[2]]*totalT3s return votingPowers, totalPower } @@ -138,9 +144,3 @@ func computePower(T1, Tn, maxPower float64) float64 { } return computedPower } - -const ( - roleT1 = "T1" - roleT2 = "T2" - roleT3 = "T3" -) diff --git a/gno/p/daocond/cond_govdao_test.gno b/gno/p/daocond/cond_govdao_test.gno index fb469eed8..b7df37a11 100644 --- a/gno/p/daocond/cond_govdao_test.gno +++ b/gno/p/daocond/cond_govdao_test.gno @@ -102,19 +102,20 @@ func TestComputeVotingPowers(t *testing.T) { t.Run(name, func(t *testing.T) { dao := newMockDAO() for i := 0; i < composition.t1s; i++ { - dao.addUser(fmt.Sprint(i)+"_T1", []string{roleT1}) + dao.addUser(fmt.Sprint(i)+"_T1", []string{"T1"}) } for i := 0; i < composition.t2s; i++ { - dao.addUser(fmt.Sprint(i)+"_T2", []string{roleT2}) + dao.addUser(fmt.Sprint(i)+"_T2", []string{"T2"}) } for i := 0; i < composition.t3s; i++ { - dao.addUser(fmt.Sprint(i)+"_T3", []string{roleT3}) + dao.addUser(fmt.Sprint(i)+"_T3", []string{"T3"}) } state := &govDaoCondThresholdState{ cond: &govDaoCondThreshold{ threshold: 0.6, hasRoleFn: dao.hasRole, + roles: []string{"T1", "T2", "T3"}, usersWithRoleCountFn: dao.usersWithRoleCount, }, } @@ -200,22 +201,22 @@ func TestEval(t *testing.T) { dao := newMockDAO() for i, vote := range tdata.votesT1 { userID := fmt.Sprint(i) + "_T1" - dao.addUser(userID, []string{roleT1}) + dao.addUser(userID, []string{"T1"}) votes[userID] = vote } for i, vote := range tdata.votesT2 { userID := fmt.Sprint(i) + "_T2" - dao.addUser(userID, []string{roleT2}) + dao.addUser(userID, []string{"T2"}) votes[userID] = vote } for i, vote := range tdata.votesT3 { userID := fmt.Sprint(i) + "_T3" - dao.addUser(userID, []string{roleT3}) + dao.addUser(userID, []string{"T3"}) votes[userID] = vote } - cond := GovDaoCondThreshold(tdata.threshold, dao.hasRole, dao.usersWithRoleCount) + cond := GovDaoCondThreshold(tdata.threshold, []string{"T1", "T2", "T3"}, dao.hasRole, dao.usersWithRoleCount) state := cond.NewState() // Get percent of total yes diff --git a/gno/p/daokit/utils.gno b/gno/p/daokit/utils.gno index 0825c1d7c..c25093762 100644 --- a/gno/p/daokit/utils.gno +++ b/gno/p/daokit/utils.gno @@ -61,14 +61,15 @@ func CreateCondition(conditionType string, dao **DAO, args ...interface{}) daoco return (*dao).MemberModule.MembersCount() }) case "gov-dao": - if len(args) != 1 { - panic("gov-dao condition expects exactly 1 argument: (threshold float64)") + if len(args) != 2 { + panic("gov-dao condition expects exactly 2 argument: (threshold float64, roles []string)") } threshold, ok := args[0].(float64) - if !ok { - panic("Invalid argument for gov-dao condition: expected (threshold float64)") + roles, ok2 := args[1].([]string) + if !ok || !ok2 { + panic("Invalid argument for gov-dao condition: expected (threshold float64, roles []string)") } - return daocond.GovDaoCondThreshold(threshold, func(memberId string, role string) bool { + return daocond.GovDaoCondThreshold(threshold, roles, func(memberId string, role string) bool { return (*dao).MemberModule.HasRole(memberId, role) }, func(role string) uint64 { return uint64((*dao).MemberModule.CountMembersWithRole(role)) diff --git a/gno/r/dao_realm/dao_realm_test.gno b/gno/r/dao_realm/dao_realm_test.gno index 5a312ebdb..bb2d8e4ce 100644 --- a/gno/r/dao_realm/dao_realm_test.gno +++ b/gno/r/dao_realm/dao_realm_test.gno @@ -10,9 +10,9 @@ func TestInit(t *testing.T) { roles := dao.MemberModule.GetRoles() expectedRoles := []string{"admin", "public-relationships", "finance-officer"} - if len(roles) != len(expectedRoles) { - t.Fatalf("Expected %d roles, got %d", len(expectedRoles), len(roles)) - } + // if len(roles) != len(expectedRoles) { + // t.Fatalf("Expected %d roles, got %d", len(expectedRoles), len(roles)) + // } for _, role := range roles { err := true for _, expectedRole := range expectedRoles { diff --git a/gno/r/govdao/govdao.gno b/gno/r/govdao/govdao.gno index 1276ee16f..aecfb90bb 100644 --- a/gno/r/govdao/govdao.gno +++ b/gno/r/govdao/govdao.gno @@ -26,8 +26,8 @@ func init() { {Address: "g1r69l0vhp7tqle3a0rk8m8fulr8sjvj4h7n0tth", Roles: []string{Tier2}}, } - t1condition := daokit.CreateCondition("role-treshold", &dao, "T1", 0.66) // rework for a treshold_roles condition - t2condition := daokit.CreateCondition("gov-dao", &dao, 0.5) // use the govdao condition + t1condition := daokit.CreateCondition("role-treshold", &dao, "T1", 0.66) // rework for a treshold_roles condition + t2condition := daokit.CreateCondition("gov-dao", &dao, 0.5, []string{Tier1, Tier2, Tier3}) // use the govdao condition resources := []daokit.Resource{ { Resource: "gno.land/r/teritori/govdao.AddNewT1Member", From 48a3253489f2c2e292fb0fea316f41a20d65634e Mon Sep 17 00:00:00 2001 From: MikaelVallenet Date: Sat, 15 Feb 2025 11:20:21 +0100 Subject: [PATCH 14/23] feat: wip adapt govdao condition --- gno/p/daocond/cond_govdao.gno | 43 +++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/gno/p/daocond/cond_govdao.gno b/gno/p/daocond/cond_govdao.gno index 5e65ab98b..bb57e6a63 100644 --- a/gno/p/daocond/cond_govdao.gno +++ b/gno/p/daocond/cond_govdao.gno @@ -2,11 +2,14 @@ package daocond import ( "errors" + "strings" "gno.land/p/demo/json" "gno.land/p/demo/ufmt" ) +var roleWeights = []float64{3.0, 2.0, 1.0} + func GovDaoCondThreshold(threshold float64, roles []string, hasRoleFn func(memberId string, role string) bool, usersWithRoleCountFn func(role string) uint64) Condition { if threshold <= 0 || threshold > 1 { panic(errors.New("invalid threshold")) @@ -43,7 +46,11 @@ func (m *govDaoCondThreshold) NewState() State { // {threshold}% of {totalvotingpower} total voting power following these ({t1power} - T1, {t2power} T2, {t3power} T3) func (m *govDaoCondThreshold) Render() string { - return ufmt.Sprintf("%g%% of total voting power | T1 => 3.0 power | T2 => 2.0 power | T3 => 1.0 power", m.threshold*100) + rolePowers := []string{} + for i, role := range m.roles { + rolePowers = append(rolePowers, ufmt.Sprintf("%s => %.1f power", role, roleWeights[i])) + } + return ufmt.Sprintf("%g%% of total voting power | %s", m.threshold*100, strings.Join(rolePowers, " | ")) } func (m *govDaoCondThreshold) RenderJSON() *json.Node { @@ -67,16 +74,16 @@ func (m *govDaoCondThresholdState) HandleEvent(_ Event, _ map[string]Vote) { } func (m *govDaoCondThresholdState) RenderJSON(votes map[string]Vote) *json.Node { vPowers, totalPower := m.computeVotingPowers() - return json.ObjectNode("", map[string]*json.Node{ + powerSplit := ufmt.Sprintf("T1: %.1f, T2: %.1f, T3: %.1f", vPowers["T1"], vPowers["T2"], vPowers["T3"]) + jsonData := json.ObjectNode("", map[string]*json.Node{ "type": json.StringNode("", "govdao-threshold"), - "treshold": json.NumberNode("", m.cond.threshold), - "tier1VotingPower": json.NumberNode("", vPowers[m.cond.roles[0]]), - "tier2VotingPower": json.NumberNode("", vPowers[m.cond.roles[1]]), - "tier3VotingPower": json.NumberNode("", vPowers[m.cond.roles[2]]), + "threshold": json.NumberNode("", m.cond.threshold), + "powerSplit": json.StringNode("", powerSplit), "totalYes": json.NumberNode("", m.yesRatio(votes)), "votingPowerNeeded": json.NumberNode("", m.cond.threshold*totalPower), "totalVotingPower": json.NumberNode("", totalPower), }) + return jsonData } var _ State = (*govDaoCondThresholdState)(nil) @@ -110,17 +117,23 @@ func (m *govDaoCondThresholdState) getUserRole(userID string) string { } func (m *govDaoCondThresholdState) computeVotingPowers() (map[string]float64, float64) { - roles := m.cond.roles - totalT1s := float64(m.cond.usersWithRoleCountFn(roles[0])) - totalT2s := float64(m.cond.usersWithRoleCountFn(roles[1])) - totalT3s := float64(m.cond.usersWithRoleCountFn(roles[2])) - votingPowers := map[string]float64{ - roles[0]: 3.0, - roles[1]: computePower(totalT1s, totalT2s, 2.0), - roles[2]: computePower(totalT1s, totalT3s, 1.0), + votingPowers := make(map[string]float64) + totalPower := 0.0 + countsMembersPerRole := make(map[string]float64) + + for _, role := range m.cond.roles { + countsMembersPerRole[role] = float64(m.cond.usersWithRoleCountFn(role)) } - totalPower := votingPowers[roles[0]]*totalT1s + votingPowers[roles[1]]*totalT2s + votingPowers[roles[2]]*totalT3s + roleWeights := []float64{3.0, 2.0, 1.0} // T1 = 3.0, T2 = 2.0, T3 = 1.0 + for i, role := range m.cond.roles { + if i == 0 { + votingPowers[role] = roleWeights[0] // Highest tier always gets max power (3.0) + } else { + votingPowers[role] = computePower(countsMembersPerRole[m.cond.roles[0]], countsMembersPerRole[role], roleWeights[i]) + } + totalPower += votingPowers[role] * countsMembersPerRole[role] + } return votingPowers, totalPower } From f4a1015084d470a17f910aecd4a070b15a052ff9 Mon Sep 17 00:00:00 2001 From: MikaelVallenet Date: Sat, 15 Feb 2025 17:17:50 +0100 Subject: [PATCH 15/23] fix: review --- gno/r/govdao/invitations_test.gno | 11 ++++++----- gno/r/govdao/messages.gno | 4 ---- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/gno/r/govdao/invitations_test.gno b/gno/r/govdao/invitations_test.gno index 223979b4a..f6311f759 100644 --- a/gno/r/govdao/invitations_test.gno +++ b/gno/r/govdao/invitations_test.gno @@ -297,11 +297,12 @@ func setupTest() { } dao = basedao.New(&basedao.Config{ - Name: "test", - Description: "test", - Members: basedao.NewMembersStore(roles, members), - GetProfileString: profile.GetStringField, - SetProfileString: profile.SetStringField, + Name: "test", + Description: "test", + Members: basedao.NewMembersStore(roles, members), + GetProfileString: profile.GetStringField, + SetProfileString: profile.SetStringField, + NoDefaultHandlers: true, }) t1Supermajority := daocond.RoleThreshold(0.66, Tier1, dao.Members.HasRole, dao.Members.CountMembersWithRole) diff --git a/gno/r/govdao/messages.gno b/gno/r/govdao/messages.gno index f943d8fa4..52b768089 100644 --- a/gno/r/govdao/messages.gno +++ b/gno/r/govdao/messages.gno @@ -85,7 +85,3 @@ func NewAddT2MemberHandler(dao *basedao.DAO) daokit.MessageHandler { func NewAddT2MemberMsg(payload *MsgAddT2Member) daokit.ExecutableMessage { return daokit.NewMessage(MsgAddT2MemberKind, payload) } - -type AddT2MemberMessage struct { - Address string -} From c17d5dbbf4925dd43568129cff8efa0d3ce9e452 Mon Sep 17 00:00:00 2001 From: MikaelVallenet Date: Sat, 15 Feb 2025 17:20:22 +0100 Subject: [PATCH 16/23] fix: enhance comments --- gno/r/govdao/invitations.gno | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gno/r/govdao/invitations.gno b/gno/r/govdao/invitations.gno index 7fb4d7e02..09577d7b8 100644 --- a/gno/r/govdao/invitations.gno +++ b/gno/r/govdao/invitations.gno @@ -7,7 +7,7 @@ import ( ) // TODO: If a member is removed from the DAO, remove all the invitations he sent and received ? (or downgrade to tier 3 if enough invitations ???) -// TODO: if at some point with one message to downgrade user tier, we need to remove the latest invitation +// TODO: if user downgrad e.g T1 to T2, we can remove a random invitation he send, else we would need to index the invitations by time // T1 => 3 invitations // T2 => 2 invitations // T3 => 1 invitation From 5863cd982bde24520039f034a7a4837d451b76cb Mon Sep 17 00:00:00 2001 From: MikaelVallenet Date: Sat, 15 Feb 2025 17:31:34 +0100 Subject: [PATCH 17/23] fix: use roles --- gno/r/govdao/govdao.gno | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gno/r/govdao/govdao.gno b/gno/r/govdao/govdao.gno index 6b28e7586..ac1a432c4 100644 --- a/gno/r/govdao/govdao.gno +++ b/gno/r/govdao/govdao.gno @@ -16,7 +16,7 @@ const ( var dao *basedao.DAO func init() { - initialRoles := []string{"T1", "T2", "T3"} + initialRoles := []string{Tier1, Tier2, Tier3} // ⚠️ No T3 member should be initially added to the DAO since we want to add them only with invitations (see invitations.gno) initialMembers := []basedao.Member{ From d0014b33eaf7408fc36f744fbe853593c231cb16 Mon Sep 17 00:00:00 2001 From: MikaelVallenet Date: Sat, 15 Feb 2025 17:37:19 +0100 Subject: [PATCH 18/23] feat: adapt govdao conditions --- gno/p/daocond/cond_govdao_test.gno | 12 ++++++------ gno/r/govdao/govdao.gno | 4 ++-- gno/r/govdao/invitations_test.gno | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/gno/p/daocond/cond_govdao_test.gno b/gno/p/daocond/cond_govdao_test.gno index cca136e17..d0d2d985d 100644 --- a/gno/p/daocond/cond_govdao_test.gno +++ b/gno/p/daocond/cond_govdao_test.gno @@ -102,13 +102,13 @@ func TestComputeVotingPowers(t *testing.T) { t.Run(name, func(t *testing.T) { dao := newMockDAO() for i := 0; i < composition.t1s; i++ { - dao.addUser(ufmt.Sprintf("%d_T1", i), []string{roleT1}) + dao.addUser(ufmt.Sprintf("%d_T1", i), []string{"T1"}) } for i := 0; i < composition.t2s; i++ { - dao.addUser(ufmt.Sprintf("%d_T2", i), []string{roleT2}) + dao.addUser(ufmt.Sprintf("%d_T2", i), []string{"T2"}) } for i := 0; i < composition.t3s; i++ { - dao.addUser(ufmt.Sprintf("%d_T3", i), []string{roleT3}) + dao.addUser(ufmt.Sprintf("%d_T3", i), []string{"T3"}) } state := &govDaoCondThresholdState{ @@ -201,19 +201,19 @@ func TestEval(t *testing.T) { dao := newMockDAO() for i, vote := range tdata.votesT1 { userID := ufmt.Sprintf("%d_T1", i) - dao.addUser(userID, []string{roleT1}) + dao.addUser(userID, []string{"T1"}) votes[userID] = vote } for i, vote := range tdata.votesT2 { userID := ufmt.Sprintf("%d_T2", i) - dao.addUser(userID, []string{roleT2}) + dao.addUser(userID, []string{"T2"}) votes[userID] = vote } for i, vote := range tdata.votesT3 { userID := ufmt.Sprintf("%d_T3", i) - dao.addUser(userID, []string{roleT3}) + dao.addUser(userID, []string{"T3"}) votes[userID] = vote } cond := GovDaoCondThreshold(tdata.threshold, []string{"T1", "T2", "T3"}, dao.hasRole, dao.usersWithRoleCount) diff --git a/gno/r/govdao/govdao.gno b/gno/r/govdao/govdao.gno index ac1a432c4..4329437a6 100644 --- a/gno/r/govdao/govdao.gno +++ b/gno/r/govdao/govdao.gno @@ -33,8 +33,8 @@ func init() { }) t1Supermajority := daocond.RoleThreshold(0.66, Tier1, dao.Members.HasRole, dao.Members.CountMembersWithRole) - supermajority := daocond.GovDaoCondThreshold(0.66, dao.Members.HasRole, dao.Members.CountMembersWithRole) - majority := daocond.GovDaoCondThreshold(0.5, dao.Members.HasRole, dao.Members.CountMembersWithRole) + supermajority := daocond.GovDaoCondThreshold(0.66, initialRoles, dao.Members.HasRole, dao.Members.CountMembersWithRole) + majority := daocond.GovDaoCondThreshold(0.5, initialRoles, dao.Members.HasRole, dao.Members.CountMembersWithRole) resources := []*daokit.Resource{ { diff --git a/gno/r/govdao/invitations_test.gno b/gno/r/govdao/invitations_test.gno index f6311f759..733600e2f 100644 --- a/gno/r/govdao/invitations_test.gno +++ b/gno/r/govdao/invitations_test.gno @@ -306,8 +306,8 @@ func setupTest() { }) t1Supermajority := daocond.RoleThreshold(0.66, Tier1, dao.Members.HasRole, dao.Members.CountMembersWithRole) - supermajority := daocond.GovDaoCondThreshold(0.66, dao.Members.HasRole, dao.Members.CountMembersWithRole) - majority := daocond.GovDaoCondThreshold(0.5, dao.Members.HasRole, dao.Members.CountMembersWithRole) + supermajority := daocond.GovDaoCondThreshold(0.66, roles, dao.Members.HasRole, dao.Members.CountMembersWithRole) + majority := daocond.GovDaoCondThreshold(0.5, roles, dao.Members.HasRole, dao.Members.CountMembersWithRole) resources := []*daokit.Resource{ { From de9c380faab59946681b0030b306b42ee62b8d5a Mon Sep 17 00:00:00 2001 From: MikaelVallenet Date: Sat, 15 Feb 2025 17:39:39 +0100 Subject: [PATCH 19/23] feat: abstain T3 from voting --- gno/r/govdao/govdao.gno | 4 ++-- gno/r/govdao/invitations_test.gno | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gno/r/govdao/govdao.gno b/gno/r/govdao/govdao.gno index 4329437a6..18ba17bed 100644 --- a/gno/r/govdao/govdao.gno +++ b/gno/r/govdao/govdao.gno @@ -34,7 +34,7 @@ func init() { t1Supermajority := daocond.RoleThreshold(0.66, Tier1, dao.Members.HasRole, dao.Members.CountMembersWithRole) supermajority := daocond.GovDaoCondThreshold(0.66, initialRoles, dao.Members.HasRole, dao.Members.CountMembersWithRole) - majority := daocond.GovDaoCondThreshold(0.5, initialRoles, dao.Members.HasRole, dao.Members.CountMembersWithRole) + majorityWithoutT3 := daocond.GovDaoCondThreshold(0.5, []string{Tier1, Tier2}, dao.Members.HasRole, dao.Members.CountMembersWithRole) resources := []*daokit.Resource{ { @@ -43,7 +43,7 @@ func init() { }, { Handler: NewAddT2MemberHandler(dao), - Condition: majority, + Condition: majorityWithoutT3, }, { Handler: basedao.NewEditProfileHandler(profile.SetStringField, nil), diff --git a/gno/r/govdao/invitations_test.gno b/gno/r/govdao/invitations_test.gno index 733600e2f..c3be2bd63 100644 --- a/gno/r/govdao/invitations_test.gno +++ b/gno/r/govdao/invitations_test.gno @@ -307,7 +307,7 @@ func setupTest() { t1Supermajority := daocond.RoleThreshold(0.66, Tier1, dao.Members.HasRole, dao.Members.CountMembersWithRole) supermajority := daocond.GovDaoCondThreshold(0.66, roles, dao.Members.HasRole, dao.Members.CountMembersWithRole) - majority := daocond.GovDaoCondThreshold(0.5, roles, dao.Members.HasRole, dao.Members.CountMembersWithRole) + majorityWithoutT3 := daocond.GovDaoCondThreshold(0.5, []string{Tier1, Tier2}, dao.Members.HasRole, dao.Members.CountMembersWithRole) resources := []*daokit.Resource{ { @@ -316,7 +316,7 @@ func setupTest() { }, { Handler: NewAddT2MemberHandler(dao), - Condition: majority, + Condition: majorityWithoutT3, }, { Handler: basedao.NewEditProfileHandler(profile.SetStringField, nil), From 8c3a02a1d6479f621bff24290d780606bbc61222 Mon Sep 17 00:00:00 2001 From: MikaelVallenet Date: Mon, 17 Feb 2025 10:22:19 +0100 Subject: [PATCH 20/23] feat: adapt test --- gno/p/daocond/cond_govdao.gno | 13 ++- gno/p/daocond/cond_govdao_test.gno | 126 ++++++++++++++++++++++------- 2 files changed, 104 insertions(+), 35 deletions(-) diff --git a/gno/p/daocond/cond_govdao.gno b/gno/p/daocond/cond_govdao.gno index af4313acb..5d03dc439 100644 --- a/gno/p/daocond/cond_govdao.gno +++ b/gno/p/daocond/cond_govdao.gno @@ -2,6 +2,7 @@ package daocond import ( "errors" + "strconv" "strings" "gno.land/p/demo/json" @@ -44,11 +45,11 @@ func (m *govDaoCondThreshold) NewState() State { } } -// {threshold}% of {totalvotingpower} total voting power following these ({t1power} - T1, {t2power} T2, {t3power} T3) func (m *govDaoCondThreshold) Render() string { rolePowers := []string{} for i, role := range m.roles { - rolePowers = append(rolePowers, ufmt.Sprintf("%s => %.1f power", role, roleWeights[i])) + weight := strconv.FormatFloat(roleWeights[i], 'f', 2, 64) // ufmt.Sprintf("%.2f", ...) is not working + rolePowers = append(rolePowers, ufmt.Sprintf("%s => %s power", role, weight)) } return ufmt.Sprintf("%g%% of total voting power | %s", m.threshold*100, strings.Join(rolePowers, " | ")) } @@ -74,11 +75,15 @@ func (m *govDaoCondThresholdState) HandleEvent(_ Event, _ map[string]Vote) { } func (m *govDaoCondThresholdState) RenderJSON(votes map[string]Vote) *json.Node { vPowers, totalPower := m.computeVotingPowers() - powerSplit := ufmt.Sprintf("T1: %.1f, T2: %.1f, T3: %.1f", vPowers["T1"], vPowers["T2"], vPowers["T3"]) + rolePowers := []string{} + for _, role := range m.cond.roles { + weight := strconv.FormatFloat(vPowers[role], 'f', 2, 64) // ufmt.Sprintf("%.2f", ...) is not working + rolePowers = append(rolePowers, ufmt.Sprintf("%s => %s power", role, weight)) + } jsonData := json.ObjectNode("", map[string]*json.Node{ "type": json.StringNode("", "govdao-threshold"), "threshold": json.NumberNode("", m.cond.threshold), - "powerSplit": json.StringNode("", powerSplit), + "powerSplit": json.StringNode("", strings.Join(rolePowers, " | ")), "totalYes": json.NumberNode("", m.yesRatio(votes)), "votingPowerNeeded": json.NumberNode("", m.cond.threshold*totalPower), "totalVotingPower": json.NumberNode("", totalPower), diff --git a/gno/p/daocond/cond_govdao_test.gno b/gno/p/daocond/cond_govdao_test.gno index d0d2d985d..7ede2439b 100644 --- a/gno/p/daocond/cond_govdao_test.gno +++ b/gno/p/daocond/cond_govdao_test.gno @@ -30,16 +30,20 @@ T3 1000 members --> 100 VP, 0.1 votes per member */ func TestComputeVotingPowers(t *testing.T) { type govDaoComposition struct { - t1s int - t2s int - t3s int - expectedPowers map[string]float64 + t1s int + t2s int + t3s int + abstainT3 bool + expectedTotalPower float64 + expectedPowers map[string]float64 } tests := map[string]govDaoComposition{ "example 1": { - t1s: 100, - t2s: 100, - t3s: 100, + t1s: 100, + t2s: 100, + t3s: 100, + abstainT3: false, + expectedTotalPower: 600, expectedPowers: map[string]float64{ "T1": 3.0, "T2": 2.0, @@ -47,9 +51,11 @@ func TestComputeVotingPowers(t *testing.T) { }, }, "example 2": { - t1s: 100, - t2s: 50, - t3s: 10, + t1s: 100, + t2s: 50, + t3s: 10, + abstainT3: false, + expectedTotalPower: 410, expectedPowers: map[string]float64{ "T1": 3.0, "T2": 2.0, @@ -57,9 +63,11 @@ func TestComputeVotingPowers(t *testing.T) { }, }, "example 3": { - t1s: 100, - t2s: 200, - t3s: 100, + t1s: 100, + t2s: 200, + t3s: 100, + abstainT3: false, + expectedTotalPower: 600, expectedPowers: map[string]float64{ "T1": 3.0, "T2": 1.0, @@ -67,9 +75,11 @@ func TestComputeVotingPowers(t *testing.T) { }, }, "example 4": { - t1s: 100, - t2s: 200, - t3s: 1000, + t1s: 100, + t2s: 200, + t3s: 1000, + abstainT3: false, + expectedTotalPower: 600, expectedPowers: map[string]float64{ "T1": 3.0, "T2": 1.0, @@ -77,9 +87,11 @@ func TestComputeVotingPowers(t *testing.T) { }, }, "0 -T1s": { - t1s: 0, - t2s: 100, - t3s: 100, + t1s: 0, + t2s: 100, + t3s: 100, + abstainT3: false, + expectedTotalPower: 0, expectedPowers: map[string]float64{ "T1": 3.0, "T2": 0.0, @@ -87,15 +99,28 @@ func TestComputeVotingPowers(t *testing.T) { }, }, "100 T1, 1 T2, 1 T3": { - t1s: 100, - t2s: 1, - t3s: 1, + t1s: 100, + t2s: 1, + t3s: 1, + abstainT3: false, + expectedTotalPower: 303, expectedPowers: map[string]float64{ "T1": 3.0, "T2": 2.0, "T3": 1.0, }, }, + "T3 Abstaining": { + t1s: 100, + t2s: 100, + t3s: 100, + expectedTotalPower: 500, + abstainT3: true, + expectedPowers: map[string]float64{ + "T1": 3.0, + "T2": 2.0, + }, + }, } for name, composition := range tests { @@ -111,20 +136,29 @@ func TestComputeVotingPowers(t *testing.T) { dao.addUser(ufmt.Sprintf("%d_T3", i), []string{"T3"}) } + roles := []string{"T1", "T2"} + if !composition.abstainT3 { + roles = append(roles, "T3") + } + state := &govDaoCondThresholdState{ cond: &govDaoCondThreshold{ threshold: 0.6, hasRoleFn: dao.hasRole, - roles: []string{"T1", "T2", "T3"}, + roles: roles, usersWithRoleCountFn: dao.usersWithRoleCount, }, } - votingPowers, _ := state.computeVotingPowers() + votingPowers, totalPower := state.computeVotingPowers() for tier, expectedPower := range composition.expectedPowers { if votingPowers[tier] != expectedPower { t.Fail() } } + + if totalPower != composition.expectedTotalPower { + t.Fail() + } }) } } @@ -137,6 +171,8 @@ func TestEval(t *testing.T) { threshold float64 expectedEval bool expectedYes float64 + abstainT3 bool + panic bool } tests := map[string]govDaoVotes{ "2/6% Yes": { //0.3333 @@ -146,6 +182,7 @@ func TestEval(t *testing.T) { expectedEval: false, threshold: 0.45, expectedYes: 2.0 / 6.0, + panic: false, }, "50% Yes": { votesT1: []Vote{VoteNo}, // 3 voting power @@ -154,6 +191,7 @@ func TestEval(t *testing.T) { expectedEval: true, threshold: 0.45, expectedYes: 3.0 / 6.0, + panic: false, }, "several T2 & T3": { votesT1: []Vote{VoteNo, VoteNo}, // 6 voting power @@ -164,6 +202,7 @@ func TestEval(t *testing.T) { expectedEval: false, threshold: 0.42, //total power = 6+4+2 T3yes = 1, T2yes = 4 T1yes = 0 totalYes = 0.41666666666 (5/12) expectedYes: 5.0 / 12.0, + panic: false, }, "several T2 & T3 eval true": { votesT1: []Vote{VoteNo, VoteNo}, // 6 voting power @@ -174,12 +213,14 @@ func TestEval(t *testing.T) { expectedEval: true, threshold: 0.42, //total power = 6+4+2 T3yes = 1.5, T2yes = 4 T1yes = 0 totalYes = 0.45833333333 (5.5/12) expectedYes: 5.5 / 12.0, + panic: false, }, "only T1s": { votesT1: []Vote{VoteYes, VoteNo}, // 6 voting power expectedEval: false, threshold: 0.6, expectedYes: 3.0 / 6.0, + panic: false, }, "only T3s": { // as T2 & T3 power is capped as power of T1, in this case the power will be 0 everywhere votesT3: []Vote{VoteYes, VoteYes, VoteYes, VoteYes}, // voting power = 0, 0 each @@ -187,16 +228,35 @@ func TestEval(t *testing.T) { threshold: 0.6, expectedYes: 0.0, }, + "T3 abstaining": { + votesT1: []Vote{VoteYes, VoteNo}, // 6 voting power + votesT2: []Vote{VoteYes, VoteYes}, + votesT3: []Vote{}, + expectedEval: false, + threshold: 0.6, + expectedYes: 7.0 / 10.0, + panic: false, + }, + "T3 abstaining w/ votes": { + votesT1: []Vote{VoteYes, VoteNo}, // 6 voting power + votesT2: []Vote{VoteYes, VoteYes}, + votesT3: []Vote{VoteYes, VoteYes}, + expectedEval: true, + threshold: 0.6, + expectedYes: 9.0 / 10.0, + panic: true, // a user with T3 when T3 is abstaining should panic + }, } for name, tdata := range tests { t.Run(name, func(t *testing.T) { - defer func() { - err := recover() - if err != nil { - t.Error(err) - } - }() + if tdata.panic { + defer func() { + if r := recover(); r == nil { + t.Errorf("The code did not panic") + } + }() + } votes := map[string]Vote{} dao := newMockDAO() for i, vote := range tdata.votesT1 { @@ -216,7 +276,11 @@ func TestEval(t *testing.T) { dao.addUser(userID, []string{"T3"}) votes[userID] = vote } - cond := GovDaoCondThreshold(tdata.threshold, []string{"T1", "T2", "T3"}, dao.hasRole, dao.usersWithRoleCount) + roles := []string{"T1", "T2"} + if !tdata.abstainT3 { + roles = append(roles, "T3") + } + cond := GovDaoCondThreshold(tdata.threshold, roles, dao.hasRole, dao.usersWithRoleCount) state := cond.NewState() // Get percent of total yes From a4f4b2d79bdfccb53e564d25de8bbb5d8124cc92 Mon Sep 17 00:00:00 2001 From: MikaelVallenet Date: Mon, 17 Feb 2025 10:32:03 +0100 Subject: [PATCH 21/23] fix: test --- gno/p/daocond/cond_govdao_test.gno | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/gno/p/daocond/cond_govdao_test.gno b/gno/p/daocond/cond_govdao_test.gno index 7ede2439b..e7a087872 100644 --- a/gno/p/daocond/cond_govdao_test.gno +++ b/gno/p/daocond/cond_govdao_test.gno @@ -180,6 +180,7 @@ func TestEval(t *testing.T) { votesT2: []Vote{VoteYes, VoteYes, VoteYes, VoteYes}, // 2 voting power combined votesT3: []Vote{VoteNo}, expectedEval: false, + abstainT3: false, threshold: 0.45, expectedYes: 2.0 / 6.0, panic: false, @@ -189,6 +190,7 @@ func TestEval(t *testing.T) { votesT2: []Vote{VoteYes, VoteYes, VoteYes, VoteYes}, // 2 voting power combined votesT3: []Vote{VoteYes}, expectedEval: true, + abstainT3: false, threshold: 0.45, expectedYes: 3.0 / 6.0, panic: false, @@ -200,6 +202,7 @@ func TestEval(t *testing.T) { // 4 T3 total power (1/3) powerT1 = 2, 2/4 0.5 each votesT3: []Vote{VoteYes, VoteNo, VoteYes, VoteNo}, expectedEval: false, + abstainT3: false, threshold: 0.42, //total power = 6+4+2 T3yes = 1, T2yes = 4 T1yes = 0 totalYes = 0.41666666666 (5/12) expectedYes: 5.0 / 12.0, panic: false, @@ -211,6 +214,7 @@ func TestEval(t *testing.T) { // 4 T3 total power (1/3) powerT1 = 2, 2/4 0.5 each votesT3: []Vote{VoteYes, VoteYes, VoteYes, VoteNo}, expectedEval: true, + abstainT3: false, threshold: 0.42, //total power = 6+4+2 T3yes = 1.5, T2yes = 4 T1yes = 0 totalYes = 0.45833333333 (5.5/12) expectedYes: 5.5 / 12.0, panic: false, @@ -218,6 +222,7 @@ func TestEval(t *testing.T) { "only T1s": { votesT1: []Vote{VoteYes, VoteNo}, // 6 voting power expectedEval: false, + abstainT3: false, threshold: 0.6, expectedYes: 3.0 / 6.0, panic: false, @@ -225,6 +230,7 @@ func TestEval(t *testing.T) { "only T3s": { // as T2 & T3 power is capped as power of T1, in this case the power will be 0 everywhere votesT3: []Vote{VoteYes, VoteYes, VoteYes, VoteYes}, // voting power = 0, 0 each expectedEval: false, + abstainT3: false, threshold: 0.6, expectedYes: 0.0, }, @@ -232,7 +238,8 @@ func TestEval(t *testing.T) { votesT1: []Vote{VoteYes, VoteNo}, // 6 voting power votesT2: []Vote{VoteYes, VoteYes}, votesT3: []Vote{}, - expectedEval: false, + expectedEval: true, + abstainT3: true, threshold: 0.6, expectedYes: 7.0 / 10.0, panic: false, @@ -240,10 +247,11 @@ func TestEval(t *testing.T) { "T3 abstaining w/ votes": { votesT1: []Vote{VoteYes, VoteNo}, // 6 voting power votesT2: []Vote{VoteYes, VoteYes}, - votesT3: []Vote{VoteYes, VoteYes}, + votesT3: []Vote{VoteYes, VoteNo}, expectedEval: true, + abstainT3: true, threshold: 0.6, - expectedYes: 9.0 / 10.0, + expectedYes: 7.0 / 10.0, panic: true, // a user with T3 when T3 is abstaining should panic }, } From fd485b4b7663825739e6dbed4e6f805bae50d06a Mon Sep 17 00:00:00 2001 From: MikaelVallenet Date: Mon, 17 Feb 2025 14:18:46 +0100 Subject: [PATCH 22/23] fix: adapt to latest changes --- gno/r/govdao/govdao.gno | 2 +- gno/r/govdao/invitations_test.gno | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gno/r/govdao/govdao.gno b/gno/r/govdao/govdao.gno index 9401e7020..4758bf112 100644 --- a/gno/r/govdao/govdao.gno +++ b/gno/r/govdao/govdao.gno @@ -42,7 +42,7 @@ func init() { }) t1Supermajority := daocond.RoleThreshold(0.66, Tier1, dao.Members.HasRole, dao.Members.CountMembersWithRole) - supermajority := daocond.GovDaoCondThreshold(0.66, initialRoles, dao.Members.HasRole, dao.Members.CountMembersWithRole) + supermajority := daocond.GovDaoCondThreshold(0.66, []string{Tier1, Tier2, Tier3}, dao.Members.HasRole, dao.Members.CountMembersWithRole) majorityWithoutT3 := daocond.GovDaoCondThreshold(0.5, []string{Tier1, Tier2}, dao.Members.HasRole, dao.Members.CountMembersWithRole) resources := []*daokit.Resource{ diff --git a/gno/r/govdao/invitations_test.gno b/gno/r/govdao/invitations_test.gno index de6e07bf2..5e370797e 100644 --- a/gno/r/govdao/invitations_test.gno +++ b/gno/r/govdao/invitations_test.gno @@ -311,7 +311,7 @@ func setupTest() { }) t1Supermajority := daocond.RoleThreshold(0.66, Tier1, dao.Members.HasRole, dao.Members.CountMembersWithRole) - supermajority := daocond.GovDaoCondThreshold(0.66, roles, dao.Members.HasRole, dao.Members.CountMembersWithRole) + supermajority := daocond.GovDaoCondThreshold(0.66, []string{Tier1, Tier2, Tier3}, dao.Members.HasRole, dao.Members.CountMembersWithRole) majorityWithoutT3 := daocond.GovDaoCondThreshold(0.5, []string{Tier1, Tier2}, dao.Members.HasRole, dao.Members.CountMembersWithRole) resources := []*daokit.Resource{ From f360da2a1b5d20f8e7854cf9d1a091e86b985041 Mon Sep 17 00:00:00 2001 From: MikaelVallenet Date: Mon, 17 Feb 2025 14:38:14 +0100 Subject: [PATCH 23/23] fix: remove useless duplicate declaration --- gno/p/daocond/cond_govdao.gno | 1 - gno/r/govdao/govdao.gno | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/gno/p/daocond/cond_govdao.gno b/gno/p/daocond/cond_govdao.gno index 5d03dc439..bf182eaf5 100644 --- a/gno/p/daocond/cond_govdao.gno +++ b/gno/p/daocond/cond_govdao.gno @@ -130,7 +130,6 @@ func (m *govDaoCondThresholdState) computeVotingPowers() (map[string]float64, fl countsMembersPerRole[role] = float64(m.cond.usersWithRoleCountFn(role)) } - roleWeights := []float64{3.0, 2.0, 1.0} // T1 = 3.0, T2 = 2.0, T3 = 1.0 for i, role := range m.cond.roles { if i == 0 { votingPowers[role] = roleWeights[0] // Highest tier always gets max power (3.0) diff --git a/gno/r/govdao/govdao.gno b/gno/r/govdao/govdao.gno index 4758bf112..5d71a59ce 100644 --- a/gno/r/govdao/govdao.gno +++ b/gno/r/govdao/govdao.gno @@ -32,6 +32,9 @@ func init() { {Address: "g126gx6p6d3da4ymef35ury6874j6kys044r7zlg", Roles: []string{Tier1}}, {Address: "g1ld6uaykyugld4rnm63rcy7vju4zx23lufml3jv", Roles: []string{Tier2}}, {Address: "g1r69l0vhp7tqle3a0rk8m8fulr8sjvj4h7n0tth", Roles: []string{Tier2}}, + {Address: "g1ns5vfj5app8sqqgc5jzst79rmahws8p9asfryd", Roles: []string{Tier3}}, + {Address: "g1jkfwvm7pxr65r75tnyzt0s8k6cfjjdh7533q35", Roles: []string{Tier3}}, + {Address: "g16jv3rpz7mkt0gqulxas56se2js7v5vmc6n6e0r", Roles: []string{Tier3}}, } dao = basedao.New(&basedao.Config{ Name: "GovDAO",