cond_gnolovedao.gno
6.05 Kb · 168 lines
1package daocond
2
3import (
4 "errors"
5 "math"
6 "strconv"
7 "strings"
8
9 "gno.land/p/nt/ufmt/v0"
10)
11
12type gnolovDaoCondThreshold struct {
13 threshold float64
14 roles []string
15 hasRoleFn func(memberId string, role string) bool
16 usersWithRoleCountFn func(role string) uint32
17}
18
19var roleWeights = []float64{3.0, 2.0, 1.0}
20
21// Creates a weighted voting condition based on role tiers with dynamic power calculation.
22// Example: GnoloveDAOCondThreshold(0.5, ["admin", "mod", "user"], hasRoleFn, countFn)
23// This is our implementation of the govdao condition following Jae Kwon's specification: https://gist.github.com/jaekwon/918ad325c4c8f7fb5d6e022e33cb7eb3
24func GnoloveDAOCondThreshold(threshold float64, roles []string, hasRoleFn func(memberId string, role string) bool, usersWithRoleCountFn func(role string) uint32) Condition {
25 if threshold <= 0 || threshold > 1 {
26 panic(errors.New("invalid threshold"))
27 }
28 if usersWithRoleCountFn == nil {
29 panic(errors.New("nil usersWithRoleCountFn"))
30 }
31 if hasRoleFn == nil {
32 panic(errors.New("nil hasRoleFn"))
33 }
34 if len(roles) > 3 {
35 panic("the gnolove dao condition handles at most 3 roles")
36 }
37 return &gnolovDaoCondThreshold{
38 threshold: threshold,
39 roles: roles,
40 hasRoleFn: hasRoleFn,
41 usersWithRoleCountFn: usersWithRoleCountFn,
42 }
43}
44
45// Checks if weighted voting power meets the threshold.
46func (c *gnolovDaoCondThreshold) Eval(ballot Ballot) bool {
47 return c.voteRatio(ballot, VoteYes) >= c.threshold
48}
49
50// Returns progress toward meeting the weighted threshold between 0.0 and 1.0.
51func (c *gnolovDaoCondThreshold) Signal(ballot Ballot) float64 {
52 return math.Min(c.voteRatio(ballot, VoteYes)/c.threshold, 1)
53}
54
55// Displays the condition with role weights as text.
56// Example output: "50% of total voting power | admin => 3.00 power | mod => 2.00 power"
57func (c *gnolovDaoCondThreshold) Render() string {
58 rolePowers := []string{}
59 for i, role := range c.roles {
60 weight := strconv.FormatFloat(roleWeights[i], 'f', 2, 64) // ufmt.Sprintf("%.2f", ...) is not working
61 rolePowers = append(rolePowers, ufmt.Sprintf("%s => %s power", role, weight))
62 }
63 return ufmt.Sprintf("%g%% of total voting power | %s", c.threshold*100, strings.Join(rolePowers, " | "))
64}
65
66// Displays the condition with current weighted vote counts and breakdown.
67// Shows detailed voting power analysis and vote distribution across roles.
68func (c *gnolovDaoCondThreshold) RenderWithVotes(ballot Ballot) string {
69 vPowers, totalPower := c.computeVotingPowers()
70 rolePowers := []string{}
71 for _, role := range c.roles {
72 weight := strconv.FormatFloat(vPowers[role], 'f', 2, 64) // ufmt.Sprintf("%.2f", ...) is not working
73 rolePowers = append(rolePowers, ufmt.Sprintf("%s => %s power", role, weight))
74 }
75 s := ""
76 s += ufmt.Sprintf("%g%% of total voting power | %s\n\n", c.threshold*100, strings.Join(rolePowers, " | "))
77 s += ufmt.Sprintf("Threshold needed: %g%% of total voting power\n\n", c.threshold*100)
78 s += ufmt.Sprintf("Yes: %d/%d\n\n", c.voteRatio(ballot, VoteYes), totalPower)
79 s += ufmt.Sprintf("No: %d/%d\n\n", c.voteRatio(ballot, VoteNo), totalPower)
80 s += ufmt.Sprintf("Abstain: %d/%d\n\n", c.voteRatio(ballot, VoteAbstain), totalPower)
81 s += ufmt.Sprintf("Voting power needed: %g%% of total voting power\n\n", c.threshold*totalPower)
82 return s
83}
84
85var _ Condition = (*gnolovDaoCondThreshold)(nil)
86
87func (c *gnolovDaoCondThreshold) voteRatio(ballot Ballot, vote Vote) float64 {
88 var total float64
89 votingPowersByTier, totalPower := c.computeVotingPowers()
90 // Case when there are zero T1s
91 if totalPower == 0.0 {
92 return totalPower
93 }
94
95 ballot.Iterate(func(voter string, v Vote) bool {
96 if v != vote {
97 return false
98 }
99 tier := c.getUserRole(voter)
100 total += votingPowersByTier[tier]
101 return false
102 })
103 return total / totalPower
104}
105
106// Returns the voter's tier role, or "" when they hold none of this condition's
107// roles.
108//
109// This used to panic. Core.Vote gates on membership only, never on holding a
110// tier role, so a member without one can reach the ballot — as can any member
111// when the condition is built over a subset of the DAO's roles. The panic was
112// raised from inside the ballot walk, so a single such vote made every later
113// Eval, Signal and Render on that proposal panic. On an immutable publish that
114// is permanent, triggered by an ordinary member casting an ordinary vote.
115//
116// A voter with no tier role simply carries no voting power: "" is absent from
117// votingPowersByTier, so the lookup yields 0.0 and they contribute nothing. That
118// is also what the existing test matrix already expects — the "T3 abstaining"
119// case asserts 7/10, which is precisely the tally with the T3 votes excluded.
120func (c *gnolovDaoCondThreshold) getUserRole(userID string) string {
121 for _, role := range c.roles {
122 if c.hasRoleFn(userID, role) {
123 return role
124 }
125 }
126 return ""
127}
128
129func (c *gnolovDaoCondThreshold) computeVotingPowers() (map[string]float64, float64) {
130 votingPowers := make(map[string]float64)
131 totalPower := 0.0
132 countsMembersPerRole := make(map[string]float64)
133
134 for _, role := range c.roles {
135 countsMembersPerRole[role] = float64(c.usersWithRoleCountFn(role))
136 }
137
138 for i, role := range c.roles {
139 if i == 0 {
140 votingPowers[role] = roleWeights[0] // Highest tier always gets max power (3.0)
141 } else {
142 votingPowers[role] = computePower(countsMembersPerRole[c.roles[0]], countsMembersPerRole[role], roleWeights[i])
143 }
144 totalPower += votingPowers[role] * countsMembersPerRole[role]
145 }
146
147 return votingPowers, totalPower
148}
149
150// max power here is the number of votes each tier gets when we have
151// the same number of member on each tier
152// T2 = 2.0 and T1 = 1.0 with the ration T1/Tn
153// we compute the actual ratio
154func computePower(T1, Tn, maxPower float64) float64 {
155 // If there are 0 Tn (T2, T3) just return the max power
156 // we could also return 0.0 as voting power
157 if Tn <= 0.0 {
158 return maxPower
159 }
160
161 computedPower := (T1 / Tn) * maxPower
162 if computedPower >= maxPower {
163 // If computed power is bigger than the max, this happens if Tn is lower than T1
164 // cap the max power to max power.
165 return maxPower
166 }
167 return computedPower
168}