tutus-chain/pkg/core/native/eligere.go

951 lines
31 KiB
Go
Executable File

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")
ErrUnderVotingAge = errors.New("voter is under voting age")
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
Tutus ITutus
Vita IVita
RoleRegistry IRoleRegistry
Lex ILex
Annos IAnnos
}
// 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.Tutus == nil {
return false
}
return e.Tutus.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)
}
// Check voter is of voting age (18+)
if e.Annos != nil && !e.Annos.IsVotingAgeInternal(ic.DAO, caller, ic.Block.Timestamp) {
panic(ErrUnderVotingAge)
}
// 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 {
// All amendments create Federal-level laws
// Constitutional rights (RightLife, RightLiberty, etc.) are immutable in code
// Constitutional proposals require 67% supermajority but still create Federal laws
lawCategory := state.LawCategoryFederal
// Call Lex to ratify the amendment
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
}