package native import ( "encoding/binary" "errors" "math/big" "github.com/tutus-one/tutus-chain/pkg/config" "github.com/tutus-one/tutus-chain/pkg/core/dao" "github.com/tutus-one/tutus-chain/pkg/core/interop" "github.com/tutus-one/tutus-chain/pkg/core/native/nativeids" "github.com/tutus-one/tutus-chain/pkg/core/native/nativenames" "github.com/tutus-one/tutus-chain/pkg/core/state" "github.com/tutus-one/tutus-chain/pkg/crypto/hash" "github.com/tutus-one/tutus-chain/pkg/smartcontract" "github.com/tutus-one/tutus-chain/pkg/smartcontract/callflag" "github.com/tutus-one/tutus-chain/pkg/smartcontract/manifest" "github.com/tutus-one/tutus-chain/pkg/util" "github.com/tutus-one/tutus-chain/pkg/vm/stackitem" ) // Storage key prefixes for Eligere. const ( eligerePrefixProposal byte = 0x01 // proposalID -> Proposal eligerePrefixProposalTitle byte = 0x02 // titleHash -> proposalID eligerePrefixVote byte = 0x03 // proposalID + vitaID -> Vote eligerePrefixVoterHistory byte = 0x04 // vitaID + proposalID -> exists eligerePrefixProposalStatus byte = 0x05 // status + proposalID -> exists eligerePrefixCategory byte = 0x06 // category + proposalID -> exists eligerePrefixActiveProposals byte = 0x07 // -> serialized []proposalID eligerePrefixProposalCounter byte = 0xF0 // -> next proposalID eligerePrefixConfig byte = 0xF1 // -> EligereConfig ) // Event names for Eligere. const ( ProposalCreatedEvent = "ProposalCreated" ProposalActivatedEvent = "ProposalActivated" VoteCastEvent = "VoteCast" ProposalPassedEvent = "ProposalPassed" ProposalRejectedEvent = "ProposalRejected" ProposalExecutedEvent = "ProposalExecuted" ProposalCancelledEvent = "ProposalCancelled" ProposalExpiredEvent = "ProposalExpired" ) // Errors for Eligere. var ( ErrProposalNotFound = errors.New("proposal not found") ErrProposalNotActive = errors.New("proposal is not active for voting") ErrAlreadyVoted = errors.New("already voted on this proposal") ErrVotingNotStarted = errors.New("voting period has not started") ErrVotingEnded = errors.New("voting period has ended") ErrNotProposer = errors.New("caller is not the proposer") ErrProposalTitleExists = errors.New("proposal title already exists") ErrInvalidCategory = errors.New("invalid proposal category") ErrQuorumNotMet = errors.New("quorum not met") ErrThresholdNotMet = errors.New("threshold not met") ErrProposalNotPassed = errors.New("proposal has not passed") ErrExecutionDelayNotPassed = errors.New("execution delay has not passed") ErrProposalAlreadyExecuted = errors.New("proposal already executed") ErrNoVitaToken = errors.New("caller must have active Vita") ErrVotingRightRestricted = errors.New("voting right is restricted") ErrInvalidVotingPeriod = errors.New("invalid voting period") ErrTitleTooLong = errors.New("title too long (max 128 chars)") ErrInvalidVoteChoice = errors.New("invalid vote choice") ErrVotingPeriodNotEnded = errors.New("voting period has not ended") ErrProposalAlreadyFinalized = errors.New("proposal already finalized") ErrNotCommitteeOrProposer = errors.New("only proposer or committee can cancel") ErrCannotCancelFinalized = errors.New("cannot cancel finalized proposal") ) // Eligere represents the democratic voting native contract. type Eligere struct { interop.ContractMD NEO INEO Vita IVita RoleRegistry IRoleRegistry Lex ILex } // EligereCache contains cached data for performance. type EligereCache struct { proposalCount uint64 } // Copy implements dao.NativeContractCache. func (c *EligereCache) Copy() dao.NativeContractCache { return &EligereCache{ proposalCount: c.proposalCount, } } // newEligere creates a new Eligere native contract. func newEligere() *Eligere { e := &Eligere{} e.ContractMD = *interop.NewContractMD(nativenames.Eligere, nativeids.Eligere) defer e.BuildHFSpecificMD(e.ActiveIn()) // ============ Proposal Management Methods ============ desc := NewDescriptor("createProposal", smartcontract.IntegerType, manifest.NewParameter("title", smartcontract.StringType), manifest.NewParameter("contentHash", smartcontract.ByteArrayType), manifest.NewParameter("category", smartcontract.IntegerType), manifest.NewParameter("votingStartsAt", smartcontract.IntegerType), manifest.NewParameter("votingEndsAt", smartcontract.IntegerType), manifest.NewParameter("targetContract", smartcontract.Hash160Type), manifest.NewParameter("targetMethod", smartcontract.StringType), manifest.NewParameter("targetParams", smartcontract.ByteArrayType)) md := NewMethodAndPrice(e.createProposal, 1<<15, callflag.States|callflag.AllowNotify) e.AddMethod(md, desc) desc = NewDescriptor("cancelProposal", smartcontract.BoolType, manifest.NewParameter("proposalID", smartcontract.IntegerType)) md = NewMethodAndPrice(e.cancelProposal, 1<<15, callflag.States|callflag.AllowNotify) e.AddMethod(md, desc) // ============ Voting Methods ============ desc = NewDescriptor("vote", smartcontract.BoolType, manifest.NewParameter("proposalID", smartcontract.IntegerType), manifest.NewParameter("choice", smartcontract.IntegerType)) md = NewMethodAndPrice(e.vote, 1<<15, callflag.States|callflag.AllowNotify) e.AddMethod(md, desc) // ============ Tallying & Execution Methods ============ desc = NewDescriptor("tallyVotes", smartcontract.BoolType, manifest.NewParameter("proposalID", smartcontract.IntegerType)) md = NewMethodAndPrice(e.tallyVotes, 1<<15, callflag.States|callflag.AllowNotify) e.AddMethod(md, desc) desc = NewDescriptor("executeProposal", smartcontract.BoolType, manifest.NewParameter("proposalID", smartcontract.IntegerType)) md = NewMethodAndPrice(e.executeProposal, 1<<17, callflag.All) e.AddMethod(md, desc) // ============ Query Methods ============ desc = NewDescriptor("getProposal", smartcontract.ArrayType, manifest.NewParameter("proposalID", smartcontract.IntegerType)) md = NewMethodAndPrice(e.getProposal, 1<<15, callflag.ReadStates) e.AddMethod(md, desc) desc = NewDescriptor("hasVoted", smartcontract.BoolType, manifest.NewParameter("proposalID", smartcontract.IntegerType), manifest.NewParameter("voter", smartcontract.Hash160Type)) md = NewMethodAndPrice(e.hasVoted, 1<<15, callflag.ReadStates) e.AddMethod(md, desc) desc = NewDescriptor("getVote", smartcontract.ArrayType, manifest.NewParameter("proposalID", smartcontract.IntegerType), manifest.NewParameter("voter", smartcontract.Hash160Type)) md = NewMethodAndPrice(e.getVote, 1<<15, callflag.ReadStates) e.AddMethod(md, desc) desc = NewDescriptor("getProposalCount", smartcontract.IntegerType) md = NewMethodAndPrice(e.getProposalCount, 1<<15, callflag.ReadStates) e.AddMethod(md, desc) desc = NewDescriptor("getConfig", smartcontract.ArrayType) md = NewMethodAndPrice(e.getConfig, 1<<15, callflag.ReadStates) e.AddMethod(md, desc) // ============ Events ============ eDesc := NewEventDescriptor(ProposalCreatedEvent, manifest.NewParameter("proposalID", smartcontract.IntegerType), manifest.NewParameter("title", smartcontract.StringType), manifest.NewParameter("category", smartcontract.IntegerType), manifest.NewParameter("proposer", smartcontract.Hash160Type), manifest.NewParameter("votingEndsAt", smartcontract.IntegerType)) e.AddEvent(NewEvent(eDesc)) eDesc = NewEventDescriptor(ProposalActivatedEvent, manifest.NewParameter("proposalID", smartcontract.IntegerType)) e.AddEvent(NewEvent(eDesc)) eDesc = NewEventDescriptor(VoteCastEvent, manifest.NewParameter("proposalID", smartcontract.IntegerType), manifest.NewParameter("voterVitaID", smartcontract.IntegerType), manifest.NewParameter("choice", smartcontract.IntegerType)) e.AddEvent(NewEvent(eDesc)) eDesc = NewEventDescriptor(ProposalPassedEvent, manifest.NewParameter("proposalID", smartcontract.IntegerType), manifest.NewParameter("supportPercent", smartcontract.IntegerType), manifest.NewParameter("totalVotes", smartcontract.IntegerType)) e.AddEvent(NewEvent(eDesc)) eDesc = NewEventDescriptor(ProposalRejectedEvent, manifest.NewParameter("proposalID", smartcontract.IntegerType), manifest.NewParameter("supportPercent", smartcontract.IntegerType)) e.AddEvent(NewEvent(eDesc)) eDesc = NewEventDescriptor(ProposalExecutedEvent, manifest.NewParameter("proposalID", smartcontract.IntegerType), manifest.NewParameter("executor", smartcontract.Hash160Type), manifest.NewParameter("executedAt", smartcontract.IntegerType)) e.AddEvent(NewEvent(eDesc)) eDesc = NewEventDescriptor(ProposalCancelledEvent, manifest.NewParameter("proposalID", smartcontract.IntegerType), manifest.NewParameter("cancelledBy", smartcontract.Hash160Type)) e.AddEvent(NewEvent(eDesc)) eDesc = NewEventDescriptor(ProposalExpiredEvent, manifest.NewParameter("proposalID", smartcontract.IntegerType), manifest.NewParameter("reason", smartcontract.StringType)) e.AddEvent(NewEvent(eDesc)) return e } // Initialize implements the Contract interface. func (e *Eligere) Initialize(ic *interop.Context, hf *config.Hardfork, newMD *interop.HFSpecificContractMD) error { if hf != e.ActiveIn() { return nil } // Initialize cache cache := &EligereCache{proposalCount: 0} ic.DAO.SetCache(e.ID, cache) // Initialize config with defaults cfg := state.DefaultEligereConfig() if err := e.putConfig(ic.DAO, cfg); err != nil { return err } // Initialize proposal counter setIntWithKey(e.ID, ic.DAO, []byte{eligerePrefixProposalCounter}, 0) return nil } // InitializeCache implements the Contract interface. func (e *Eligere) InitializeCache(_ interop.IsHardforkEnabled, blockHeight uint32, d *dao.Simple) error { cache := &EligereCache{ proposalCount: e.getProposalCounter(d), } d.SetCache(e.ID, cache) return nil } // OnPersist implements the Contract interface. func (e *Eligere) OnPersist(ic *interop.Context) error { return nil } // PostPersist implements the Contract interface. func (e *Eligere) PostPersist(ic *interop.Context) error { return nil } // Metadata returns contract metadata. func (e *Eligere) Metadata() *interop.ContractMD { return &e.ContractMD } // Address returns the contract's script hash. func (e *Eligere) Address() util.Uint160 { return e.Hash } // GetProposalInternal returns a proposal by ID (for cross-native access). func (e *Eligere) GetProposalInternal(d *dao.Simple, proposalID uint64) *state.Proposal { return e.getProposalInternal(d, proposalID) } // ActiveIn implements the Contract interface. func (e *Eligere) ActiveIn() *config.Hardfork { return nil // Active from genesis } // ============ Storage Helpers ============ func (e *Eligere) makeProposalKey(proposalID uint64) []byte { key := make([]byte, 1+8) key[0] = eligerePrefixProposal binary.BigEndian.PutUint64(key[1:], proposalID) return key } func (e *Eligere) makeTitleKey(title string) []byte { h := hash.Hash160([]byte(title)) key := make([]byte, 1+20) key[0] = eligerePrefixProposalTitle copy(key[1:], h.BytesBE()) return key } func (e *Eligere) makeVoteKey(proposalID, vitaID uint64) []byte { key := make([]byte, 1+8+8) key[0] = eligerePrefixVote binary.BigEndian.PutUint64(key[1:9], proposalID) binary.BigEndian.PutUint64(key[9:], vitaID) return key } func (e *Eligere) makeVoterHistoryKey(vitaID, proposalID uint64) []byte { key := make([]byte, 1+8+8) key[0] = eligerePrefixVoterHistory binary.BigEndian.PutUint64(key[1:9], vitaID) binary.BigEndian.PutUint64(key[9:], proposalID) return key } func (e *Eligere) makeStatusIndexKey(status state.ProposalStatus, proposalID uint64) []byte { key := make([]byte, 1+1+8) key[0] = eligerePrefixProposalStatus key[1] = byte(status) binary.BigEndian.PutUint64(key[2:], proposalID) return key } func (e *Eligere) makeCategoryIndexKey(category state.ProposalCategory, proposalID uint64) []byte { key := make([]byte, 1+1+8) key[0] = eligerePrefixCategory key[1] = byte(category) binary.BigEndian.PutUint64(key[2:], proposalID) return key } // ============ Proposal Storage ============ func (e *Eligere) getProposalInternal(d *dao.Simple, proposalID uint64) *state.Proposal { key := e.makeProposalKey(proposalID) proposal := &state.Proposal{} err := getConvertibleFromDAO(e.ID, d, key, proposal) if err != nil { return nil } return proposal } func (e *Eligere) putProposal(d *dao.Simple, proposal *state.Proposal) error { key := e.makeProposalKey(proposal.ID) return putConvertibleToDAO(e.ID, d, key, proposal) } func (e *Eligere) titleExists(d *dao.Simple, title string) bool { key := e.makeTitleKey(title) si := d.GetStorageItem(e.ID, key) return si != nil } func (e *Eligere) putTitleIndex(d *dao.Simple, title string, proposalID uint64) { key := e.makeTitleKey(title) val := make([]byte, 8) binary.BigEndian.PutUint64(val, proposalID) d.PutStorageItem(e.ID, key, val) } // ============ Vote Storage ============ func (e *Eligere) getVoteInternal(d *dao.Simple, proposalID, vitaID uint64) *state.Vote { key := e.makeVoteKey(proposalID, vitaID) vote := &state.Vote{} err := getConvertibleFromDAO(e.ID, d, key, vote) if err != nil { return nil } return vote } func (e *Eligere) putVote(d *dao.Simple, vote *state.Vote) error { key := e.makeVoteKey(vote.ProposalID, vote.VoterVitaID) return putConvertibleToDAO(e.ID, d, key, vote) } func (e *Eligere) hasVotedInternal(d *dao.Simple, proposalID, vitaID uint64) bool { key := e.makeVoteKey(proposalID, vitaID) si := d.GetStorageItem(e.ID, key) return si != nil } func (e *Eligere) putVoterHistory(d *dao.Simple, vitaID, proposalID uint64) { key := e.makeVoterHistoryKey(vitaID, proposalID) d.PutStorageItem(e.ID, key, []byte{1}) } // ============ Config Storage ============ func (e *Eligere) getConfigInternal(d *dao.Simple) *state.EligereConfig { key := []byte{eligerePrefixConfig} cfg := &state.EligereConfig{} err := getConvertibleFromDAO(e.ID, d, key, cfg) if err != nil { return state.DefaultEligereConfig() } return cfg } func (e *Eligere) putConfig(d *dao.Simple, cfg *state.EligereConfig) error { key := []byte{eligerePrefixConfig} return putConvertibleToDAO(e.ID, d, key, cfg) } // ============ Counter ============ func (e *Eligere) getProposalCounter(d *dao.Simple) uint64 { key := []byte{eligerePrefixProposalCounter} return uint64(getIntWithKey(e.ID, d, key)) } func (e *Eligere) getAndIncrementProposalCounter(d *dao.Simple) uint64 { key := []byte{eligerePrefixProposalCounter} current := getIntWithKey(e.ID, d, key) setIntWithKey(e.ID, d, key, current+1) return uint64(current + 1) } // ============ Status Index ============ func (e *Eligere) updateStatusIndex(d *dao.Simple, proposalID uint64, oldStatus, newStatus state.ProposalStatus) { // Remove from old status index if oldStatus != newStatus { oldKey := e.makeStatusIndexKey(oldStatus, proposalID) d.DeleteStorageItem(e.ID, oldKey) } // Add to new status index newKey := e.makeStatusIndexKey(newStatus, proposalID) d.PutStorageItem(e.ID, newKey, []byte{1}) } // ============ Authorization Helpers ============ func (e *Eligere) checkCommittee(ic *interop.Context) bool { if e.NEO == nil { return false } return e.NEO.CheckCommittee(ic) } func (e *Eligere) hasLegislatorRole(d *dao.Simple, addr util.Uint160, blockHeight uint32) bool { if e.RoleRegistry == nil { return false } return e.RoleRegistry.HasRoleInternal(d, addr, RoleLegislator, blockHeight) } // ============ Contract Methods ============ // createProposal creates a new democratic proposal. func (e *Eligere) createProposal(ic *interop.Context, args []stackitem.Item) stackitem.Item { title := toString(args[0]) contentHashBytes := toBytes(args[1]) category := state.ProposalCategory(toBigInt(args[2]).Uint64()) votingStartsAt := uint32(toBigInt(args[3]).Uint64()) votingEndsAt := uint32(toBigInt(args[4]).Uint64()) targetContract := toUint160(args[5]) targetMethod := toString(args[6]) targetParams := toBytes(args[7]) caller := ic.VM.GetCallingScriptHash() // Validate caller has active Vita token, err := e.Vita.GetTokenByOwner(ic.DAO, caller) if err != nil || token == nil || token.Status != state.TokenStatusActive { panic(ErrNoVitaToken) } // Check voting right is not restricted (via Lex) if e.Lex != nil && !e.Lex.HasRightInternal(ic.DAO, caller, state.RightVote, ic.Block.Index) { panic(ErrVotingRightRestricted) } // For law/constitutional amendments, require legislator role or committee if category == state.ProposalCategoryLawAmendment || category == state.ProposalCategoryConstitutional { if !e.hasLegislatorRole(ic.DAO, caller, ic.Block.Index) && !e.checkCommittee(ic) { panic("legislative authority required for law amendments") } } // Validate inputs if len(title) > 128 { panic(ErrTitleTooLong) } if len(title) == 0 { panic("title cannot be empty") } if category < state.ProposalCategoryLawAmendment || category > state.ProposalCategoryReferendum { panic(ErrInvalidCategory) } // Check title uniqueness if e.titleExists(ic.DAO, title) { panic(ErrProposalTitleExists) } // Validate voting period cfg := e.getConfigInternal(ic.DAO) if votingStartsAt < ic.Block.Index { votingStartsAt = ic.Block.Index // Can't start in past } duration := votingEndsAt - votingStartsAt if duration < cfg.MinVotingPeriod || duration > cfg.MaxVotingPeriod { panic(ErrInvalidVotingPeriod) } // Determine threshold based on category threshold := cfg.StandardThreshold if category == state.ProposalCategoryConstitutional { threshold = cfg.ConstitutionalThreshold } // Get next proposal ID proposalID := e.getAndIncrementProposalCounter(ic.DAO) // Create proposal var contentHash util.Uint256 if len(contentHashBytes) == 32 { copy(contentHash[:], contentHashBytes) } proposal := &state.Proposal{ ID: proposalID, Title: title, ContentHash: contentHash, Category: category, Proposer: caller, ProposerVitaID: token.TokenID, CreatedAt: ic.Block.Index, VotingStartsAt: votingStartsAt, VotingEndsAt: votingEndsAt, ExecutionDelay: cfg.DefaultExecutionDelay, QuorumPercent: cfg.DefaultQuorum, ThresholdPercent: threshold, Status: state.ProposalStatusDraft, TargetContract: targetContract, TargetMethod: targetMethod, TargetParams: targetParams, } // If voting starts now, activate immediately if votingStartsAt <= ic.Block.Index { proposal.Status = state.ProposalStatusActive } // Store proposal if err := e.putProposal(ic.DAO, proposal); err != nil { panic(err) } // Store indexes e.putTitleIndex(ic.DAO, title, proposalID) e.updateStatusIndex(ic.DAO, proposalID, state.ProposalStatusDraft, proposal.Status) // Store category index catKey := e.makeCategoryIndexKey(category, proposalID) ic.DAO.PutStorageItem(e.ID, catKey, []byte{1}) // Emit event ic.AddNotification(e.Hash, ProposalCreatedEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewBigInteger(big.NewInt(int64(proposalID))), stackitem.NewByteArray([]byte(title)), stackitem.NewBigInteger(big.NewInt(int64(category))), stackitem.NewByteArray(caller.BytesBE()), stackitem.NewBigInteger(big.NewInt(int64(votingEndsAt))), })) return stackitem.NewBigInteger(big.NewInt(int64(proposalID))) } // cancelProposal cancels a proposal (proposer or committee only). func (e *Eligere) cancelProposal(ic *interop.Context, args []stackitem.Item) stackitem.Item { proposalID := toBigInt(args[0]).Uint64() caller := ic.VM.GetCallingScriptHash() proposal := e.getProposalInternal(ic.DAO, proposalID) if proposal == nil { panic(ErrProposalNotFound) } // Check authorization: must be proposer or committee isProposer := proposal.Proposer.Equals(caller) isCommittee := e.checkCommittee(ic) if !isProposer && !isCommittee { panic(ErrNotCommitteeOrProposer) } // Can only cancel draft or active proposals if proposal.Status != state.ProposalStatusDraft && proposal.Status != state.ProposalStatusActive { panic(ErrCannotCancelFinalized) } oldStatus := proposal.Status proposal.Status = state.ProposalStatusCancelled // Update proposal if err := e.putProposal(ic.DAO, proposal); err != nil { panic(err) } // Update status index e.updateStatusIndex(ic.DAO, proposalID, oldStatus, proposal.Status) // Emit event ic.AddNotification(e.Hash, ProposalCancelledEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewBigInteger(big.NewInt(int64(proposalID))), stackitem.NewByteArray(caller.BytesBE()), })) return stackitem.NewBool(true) } // vote casts a vote on a proposal. func (e *Eligere) vote(ic *interop.Context, args []stackitem.Item) stackitem.Item { proposalID := toBigInt(args[0]).Uint64() choice := state.VoteChoice(toBigInt(args[1]).Uint64()) caller := ic.VM.GetCallingScriptHash() // Validate vote choice if choice > state.VoteChoiceNo { panic(ErrInvalidVoteChoice) } // Validate caller has active Vita token, err := e.Vita.GetTokenByOwner(ic.DAO, caller) if err != nil || token == nil || token.Status != state.TokenStatusActive { panic(ErrNoVitaToken) } // Check voting right is not restricted if e.Lex != nil && !e.Lex.HasRightInternal(ic.DAO, caller, state.RightVote, ic.Block.Index) { panic(ErrVotingRightRestricted) } // Get proposal proposal := e.getProposalInternal(ic.DAO, proposalID) if proposal == nil { panic(ErrProposalNotFound) } // Check proposal is active if proposal.Status != state.ProposalStatusActive { // Auto-activate if draft and voting period has started if proposal.Status == state.ProposalStatusDraft && ic.Block.Index >= proposal.VotingStartsAt { proposal.Status = state.ProposalStatusActive e.updateStatusIndex(ic.DAO, proposalID, state.ProposalStatusDraft, state.ProposalStatusActive) } else { panic(ErrProposalNotActive) } } // Check voting period if ic.Block.Index < proposal.VotingStartsAt { panic(ErrVotingNotStarted) } if ic.Block.Index > proposal.VotingEndsAt { panic(ErrVotingEnded) } // Check not already voted (using Vita ID for one-person-one-vote) if e.hasVotedInternal(ic.DAO, proposalID, token.TokenID) { panic(ErrAlreadyVoted) } // Create vote record voteRecord := &state.Vote{ ProposalID: proposalID, VoterVitaID: token.TokenID, Voter: caller, Choice: choice, VotedAt: ic.Block.Index, Weight: 1, // Equal voting weight } // Store vote if err := e.putVote(ic.DAO, voteRecord); err != nil { panic(err) } // Store voter history e.putVoterHistory(ic.DAO, token.TokenID, proposalID) // Update vote counts (incremental tallying) proposal.TotalVotes++ switch choice { case state.VoteChoiceYes: proposal.YesVotes++ case state.VoteChoiceNo: proposal.NoVotes++ case state.VoteChoiceAbstain: proposal.AbstainVotes++ } // Update proposal if err := e.putProposal(ic.DAO, proposal); err != nil { panic(err) } // Emit event ic.AddNotification(e.Hash, VoteCastEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewBigInteger(big.NewInt(int64(proposalID))), stackitem.NewBigInteger(big.NewInt(int64(token.TokenID))), stackitem.NewBigInteger(big.NewInt(int64(choice))), })) return stackitem.NewBool(true) } // tallyVotes finalizes voting after the deadline. func (e *Eligere) tallyVotes(ic *interop.Context, args []stackitem.Item) stackitem.Item { proposalID := toBigInt(args[0]).Uint64() proposal := e.getProposalInternal(ic.DAO, proposalID) if proposal == nil { panic(ErrProposalNotFound) } // Must be after voting period if ic.Block.Index <= proposal.VotingEndsAt { panic(ErrVotingPeriodNotEnded) } // Must still be active if proposal.Status != state.ProposalStatusActive { panic(ErrProposalAlreadyFinalized) } // Get total eligible voters for quorum calculation // Use Vita token count as the voter base totalVoters := e.Vita.GetTotalTokenCount(ic.DAO) if totalVoters == 0 { totalVoters = 1 // Avoid division by zero } oldStatus := proposal.Status // Check quorum (total votes including abstentions) quorumRequired := (totalVoters * uint64(proposal.QuorumPercent)) / 100 if proposal.TotalVotes < quorumRequired { proposal.Status = state.ProposalStatusExpired if err := e.putProposal(ic.DAO, proposal); err != nil { panic(err) } e.updateStatusIndex(ic.DAO, proposalID, oldStatus, proposal.Status) ic.AddNotification(e.Hash, ProposalExpiredEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewBigInteger(big.NewInt(int64(proposalID))), stackitem.NewByteArray([]byte("quorum not met")), })) return stackitem.NewBool(false) } // Check threshold (yes votes vs yes+no, abstentions don't count toward threshold) totalDecisiveVotes := proposal.YesVotes + proposal.NoVotes var supportPercent uint64 = 0 if totalDecisiveVotes > 0 { supportPercent = (proposal.YesVotes * 100) / totalDecisiveVotes } if totalDecisiveVotes == 0 || supportPercent < uint64(proposal.ThresholdPercent) { proposal.Status = state.ProposalStatusRejected if err := e.putProposal(ic.DAO, proposal); err != nil { panic(err) } e.updateStatusIndex(ic.DAO, proposalID, oldStatus, proposal.Status) ic.AddNotification(e.Hash, ProposalRejectedEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewBigInteger(big.NewInt(int64(proposalID))), stackitem.NewBigInteger(big.NewInt(int64(supportPercent))), })) return stackitem.NewBool(false) } // Proposal passed proposal.Status = state.ProposalStatusPassed if err := e.putProposal(ic.DAO, proposal); err != nil { panic(err) } e.updateStatusIndex(ic.DAO, proposalID, oldStatus, proposal.Status) ic.AddNotification(e.Hash, ProposalPassedEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewBigInteger(big.NewInt(int64(proposalID))), stackitem.NewBigInteger(big.NewInt(int64(supportPercent))), stackitem.NewBigInteger(big.NewInt(int64(proposal.TotalVotes))), })) return stackitem.NewBool(true) } // executeProposal executes a passed proposal after the execution delay. func (e *Eligere) executeProposal(ic *interop.Context, args []stackitem.Item) stackitem.Item { proposalID := toBigInt(args[0]).Uint64() caller := ic.VM.GetCallingScriptHash() proposal := e.getProposalInternal(ic.DAO, proposalID) if proposal == nil { panic(ErrProposalNotFound) } // Must have passed if proposal.Status != state.ProposalStatusPassed { panic(ErrProposalNotPassed) } // Check execution delay has passed executionBlock := proposal.VotingEndsAt + proposal.ExecutionDelay if ic.Block.Index < executionBlock { panic(ErrExecutionDelayNotPassed) } // Already executed check if proposal.ExecutedAt > 0 { panic(ErrProposalAlreadyExecuted) } oldStatus := proposal.Status // Execute based on category switch proposal.Category { case state.ProposalCategoryLawAmendment: // Call Lex.ratifyAmendment if Lex is available if e.Lex != nil { e.executeLawAmendment(ic, proposal) } case state.ProposalCategoryConstitutional: // Constitutional amendments also go through Lex if e.Lex != nil { e.executeLawAmendment(ic, proposal) } case state.ProposalCategoryGovernanceAction: // Governance actions may call target contract // Implementation depends on specific action case state.ProposalCategoryInvestment: // Investment proposals - future integration case state.ProposalCategoryReferendum: // Referendums are advisory, no automatic execution } // Update proposal status proposal.Status = state.ProposalStatusExecuted proposal.ExecutedAt = ic.Block.Index proposal.ExecutedBy = caller if err := e.putProposal(ic.DAO, proposal); err != nil { panic(err) } e.updateStatusIndex(ic.DAO, proposalID, oldStatus, proposal.Status) // Emit event ic.AddNotification(e.Hash, ProposalExecutedEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewBigInteger(big.NewInt(int64(proposalID))), stackitem.NewByteArray(caller.BytesBE()), stackitem.NewBigInteger(big.NewInt(int64(ic.Block.Index))), })) return stackitem.NewBool(true) } // executeLawAmendment calls Lex to ratify a law amendment. func (e *Eligere) executeLawAmendment(ic *interop.Context, proposal *state.Proposal) { // The Lex contract's ratifyAmendment method will be called // This creates or updates a law based on the proposal if e.Lex != nil { // Determine law category based on proposal category lawCategory := state.LawCategoryFederal if proposal.Category == state.ProposalCategoryConstitutional { // Note: Constitutional rights are immutable in Lex // This would create a constitutional-level law, not modify core rights lawCategory = state.LawCategoryConstitutional } // Call Lex to ratify the amendment // The Lex contract needs to have ratifyAmendment method added e.Lex.RatifyAmendmentInternal(ic, proposal.ID, proposal.ContentHash, lawCategory, 0) } } // ============ Query Methods ============ func (e *Eligere) getProposal(ic *interop.Context, args []stackitem.Item) stackitem.Item { proposalID := toBigInt(args[0]).Uint64() proposal := e.getProposalInternal(ic.DAO, proposalID) if proposal == nil { return stackitem.Null{} } item, err := proposal.ToStackItem() if err != nil { return stackitem.Null{} } return item } func (e *Eligere) hasVoted(ic *interop.Context, args []stackitem.Item) stackitem.Item { proposalID := toBigInt(args[0]).Uint64() voter := toUint160(args[1]) // Get voter's Vita token token, err := e.Vita.GetTokenByOwner(ic.DAO, voter) if err != nil || token == nil { return stackitem.NewBool(false) } return stackitem.NewBool(e.hasVotedInternal(ic.DAO, proposalID, token.TokenID)) } func (e *Eligere) getVote(ic *interop.Context, args []stackitem.Item) stackitem.Item { proposalID := toBigInt(args[0]).Uint64() voter := toUint160(args[1]) // Get voter's Vita token token, err := e.Vita.GetTokenByOwner(ic.DAO, voter) if err != nil || token == nil { return stackitem.Null{} } vote := e.getVoteInternal(ic.DAO, proposalID, token.TokenID) if vote == nil { return stackitem.Null{} } item, err := vote.ToStackItem() if err != nil { return stackitem.Null{} } return item } func (e *Eligere) getProposalCount(ic *interop.Context, args []stackitem.Item) stackitem.Item { key := []byte{eligerePrefixProposalCounter} count := getIntWithKey(e.ID, ic.DAO, key) return stackitem.NewBigInteger(big.NewInt(count)) } func (e *Eligere) getConfig(ic *interop.Context, args []stackitem.Item) stackitem.Item { cfg := e.getConfigInternal(ic.DAO) item, err := cfg.ToStackItem() if err != nil { return stackitem.Null{} } return item }