Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}