1// Package memba_weighted_policy implements the approved founding coalition and delay2// rules. The hosting DAO must authenticate callers, classify and freeze typed3// actions, and keep every Policy and Proposal private. This package grants no4// authority over a realm and is not a replacement for those integration gates.5package memba_weighted_policy67import (8 "strings"9 "time"10)1112type Category string1314const (15 Routine Category = "routine"16 Financial Category = "financial"17 Critical Category = "critical"18)1920// VotingPeriod is fixed by the approved founding policy, not proposer input.21const VotingPeriod = 7 * 24 * time.Hour2223type Vote string2425const (26 Yes Vote = "yes"27 No Vote = "no"28 Abstain Vote = "abstain"29)3031// PersonID is the directory's unique human identity, never a wallet alias.32// The host must establish that different PersonIDs really are different people.33type Member struct {34 PersonID string35 Address address36 Founder bool37}3839// Policy fixes one founding seven-person roster. Reconfiguration deliberately40// invalidates every pending proposal, including already mature proposals.41// Only a reviewed critical-action handler in the host may call Reconfigure.42type Policy struct {43 members [7]Member44 generation uint6445}4647func New(members []Member) *Policy {48 return &Policy{members: validateRoster(members), generation: 1}49}5051func validateRoster(members []Member) [7]Member {52 if len(members) != 7 {53 panic("policy requires seven people")54 }55 var roster [7]Member56 founders := 057 for i, m := range members {58 if m.PersonID == "" || strings.TrimSpace(m.PersonID) != m.PersonID || !m.Address.IsValid() {59 panic("invalid member identity")60 }61 for j := 0; j < i; j++ {62 if roster[j].PersonID == m.PersonID || roster[j].Address == m.Address {63 panic("duplicate person or address")64 }65 }66 if m.Founder {67 founders++68 }69 roster[i] = m70 }71 if founders != 1 {72 panic("policy requires one founder and six developers")73 }74 return roster75}7677// Reconfigure is a host-only operation, not an authenticated public DAO API.78// It preserves the approved seven seats and 2/1 weights. Invalid input cannot79// mutate the roster or invalidate proposals, even if the host recovers a panic.80func (p *Policy) Reconfigure(members []Member) {81 roster := validateRoster(members)82 if p.generation == ^uint64(0) {83 panic("policy generation exhausted")84 }85 p.members = roster86 p.generation++87}8889func (p *Policy) memberIndex(who address) int {90 if p.generation == 0 {91 return -192 }93 for i, m := range p.members {94 if m.Address == who {95 return i96 }97 }98 return -199}100101// MayPause establishes member eligibility only. The host must dispatch only a102// typed, allowlisted pause operation; this is never spend or unpause authority.103func (p *Policy) MayPause(who address) bool { return p.memberIndex(who) >= 0 }104105// Proposal freezes the category, exact action bytes and roster generation.106// The host determines the category from the action handler, never user input.107// Each proposal has its own vote storage and qualification clocks.108// yesSince[i] is the start of seat i's current uninterrupted YES ballot and is109// meaningful only while votes[i] is Yes.110type Proposal struct {111 policy *Policy112 generation uint64113 category Category114 action string115 votes [7]Vote116 yesSince [7]time.Time117 createdAt time.Time118 votingDeadline time.Time119 lastChange time.Time120 consumed bool121}122123func (p *Policy) NewProposal(category Category, action string) *Proposal {124 if p.generation == 0 {125 panic("uninitialized policy")126 }127 if category != Routine && category != Financial && category != Critical {128 panic("unknown action category")129 }130 if action == "" {131 panic("empty action")132 }133 now := time.Now()134 return &Proposal{policy: p, generation: p.generation, category: category, action: action, createdAt: now, votingDeadline: now.Add(VotingPeriod), lastChange: now}135}136137func (p *Proposal) assertActive() {138 if p.consumed || p.generation != p.policy.generation {139 panic("proposal consumed or invalidated")140 }141}142143// Vote requires a caller identity already authenticated by the host realm.144// Voting closes at the deadline, including changes to existing ballots.145// A critical route starts when some coalition that is still voting YES without146// interruption first met its threshold. A withdrawal moves the clock only when147// every such coalition needed that ballot, so voters the rest of the YES set148// does not need cannot postpone execution by withdrawing and rejoining.149func (p *Proposal) Vote(who address, vote Vote) {150 p.assertActive()151 i := p.policy.memberIndex(who)152 if i < 0 {153 panic("voter is not a member")154 }155 if vote != Yes && vote != No && vote != Abstain {156 panic("invalid vote")157 }158 now := time.Now()159 if now.Before(p.lastChange) {160 panic("chain time regressed")161 }162 if !now.Before(p.votingDeadline) {163 panic("voting closed")164 }165 if p.votes[i] == vote {166 return167 }168 p.votes[i] = vote169 p.lastChange = now170 if vote == Yes {171 p.yesSince[i] = now172 }173}174175// qualifiedSince returns the earliest start of an uninterrupted YES coalition176// meeting the weighted (6 points, 4 people) or developer (5 developers)177// critical threshold, or the zero time when no current coalition qualifies.178// Candidate starts are the seats' own streak starts; the coalition for a179// candidate is every current YES ballot that began no later than it.180func (p *Proposal) qualifiedSince(developersOnly bool) time.Time {181 var earliest time.Time182 for c, candidate := range p.votes {183 if candidate != Yes {184 continue185 }186 start := p.yesSince[c]187 if !earliest.IsZero() && !start.Before(earliest) {188 continue189 }190 weight, people := 0, 0191 for j, vote := range p.votes {192 if vote != Yes || p.yesSince[j].After(start) {193 continue194 }195 founder := p.policy.members[j].Founder196 if developersOnly && founder {197 continue198 }199 people++200 weight++201 if founder {202 weight++203 }204 }205 if developersOnly && people >= 5 || !developersOnly && weight >= 6 && people >= 4 {206 earliest = start207 }208 }209 return earliest210}211212func (p *Proposal) tally() (weight, people, developers int) {213 for i, vote := range p.votes {214 if vote != Yes {215 continue216 }217 people++218 weight++219 if p.policy.members[i].Founder {220 weight++221 } else {222 developers++223 }224 }225 return226}227228// State is a copy for rendering. Reading never starts or advances a clock.229// An invalidated proposal reports no current tally or execution authorization.230type State struct {231 Category Category232 CreatedAt time.Time233 VotingDeadline time.Time234 VotingClosed bool235 Qualified bool236 Expired bool237 WeightYes int238 PeopleYes int239 DevelopersYes int240 WeightedAfter time.Time241 DeveloperAfter time.Time242 Invalidated bool243 Consumed bool244 Ready bool245}246247func (p *Proposal) State() State {248 now := time.Now()249 s := State{Category: p.category, CreatedAt: p.createdAt, VotingDeadline: p.votingDeadline, VotingClosed: !now.Before(p.votingDeadline), Invalidated: p.generation != p.policy.generation, Consumed: p.consumed}250 if s.Invalidated || s.Consumed {251 return s252 }253 s.WeightYes, s.PeopleYes, s.DevelopersYes = p.tally()254 if now.Before(p.lastChange) {255 return s256 }257 switch p.category {258 case Routine:259 s.Qualified = s.WeightYes >= 3 && s.PeopleYes >= 2260 s.Ready = s.Qualified261 case Financial:262 s.Qualified = s.WeightYes >= 5 && s.PeopleYes >= 3263 s.Ready = s.Qualified264 case Critical:265 weightedSince := p.qualifiedSince(false)266 developerSince := p.qualifiedSince(true)267 s.Qualified = !weightedSince.IsZero() || !developerSince.IsZero()268 if !weightedSince.IsZero() {269 s.WeightedAfter = weightedSince.Add(24 * time.Hour)270 s.Ready = !now.Before(s.WeightedAfter)271 }272 if !developerSince.IsZero() {273 s.DeveloperAfter = developerSince.Add(72 * time.Hour)274 s.Ready = s.Ready || !now.Before(s.DeveloperAfter)275 }276 }277 // Ballots are immutable after closing. A pre-deadline qualification can278 // finish its existing delay afterward; an unqualified ballot cannot revive.279 s.Expired = s.VotingClosed && !s.Qualified280 return s281}282283// Consume binds execution to the frozen action and marks it consumed BEFORE284// the host invokes its typed handler. The host must let handler failure abort285// the transaction: recovering a panic does not roll back Gno realm state.286// Caller authentication and current-member executor eligibility belong to the287// host. Do not expose Proposal pointers or call Consume from a read endpoint.288func (p *Proposal) Consume(action string) {289 p.assertActive()290 if action != p.action {291 panic("action changed")292 }293 if !p.State().Ready {294 panic("proposal is not ready")295 }296 p.consumed = true297}298Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.