cond_gnolovedao.gno
5.20 Kb ยท 154 lines
1package daocond
2
3import (
4 "errors"
5 "math"
6 "strconv"
7 "strings"
8
9 "gno.land/p/nt/ufmt"
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
106func (c *gnolovDaoCondThreshold) getUserRole(userID string) string {
107 for _, role := range c.roles {
108 if c.hasRoleFn(userID, role) {
109 return role
110 }
111 }
112 panic("No role found for user")
113}
114
115func (c *gnolovDaoCondThreshold) computeVotingPowers() (map[string]float64, float64) {
116 votingPowers := make(map[string]float64)
117 totalPower := 0.0
118 countsMembersPerRole := make(map[string]float64)
119
120 for _, role := range c.roles {
121 countsMembersPerRole[role] = float64(c.usersWithRoleCountFn(role))
122 }
123
124 for i, role := range c.roles {
125 if i == 0 {
126 votingPowers[role] = roleWeights[0] // Highest tier always gets max power (3.0)
127 } else {
128 votingPowers[role] = computePower(countsMembersPerRole[c.roles[0]], countsMembersPerRole[role], roleWeights[i])
129 }
130 totalPower += votingPowers[role] * countsMembersPerRole[role]
131 }
132
133 return votingPowers, totalPower
134}
135
136// max power here is the number of votes each tier gets when we have
137// the same number of member on each tier
138// T2 = 2.0 and T1 = 1.0 with the ration T1/Tn
139// we compute the actual ratio
140func computePower(T1, Tn, maxPower float64) float64 {
141 // If there are 0 Tn (T2, T3) just return the max power
142 // we could also return 0.0 as voting power
143 if Tn <= 0.0 {
144 return maxPower
145 }
146
147 computedPower := (T1 / Tn) * maxPower
148 if computedPower >= maxPower {
149 // If computed power is bigger than the max, this happens if Tn is lower than T1
150 // cap the max power to max power.
151 return maxPower
152 }
153 return computedPower
154}