Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}