Add Eligere native contract for democratic voting

Implement the Eligere (Latin for "to choose/elect") contract providing
democratic governance infrastructure for citizens:

Core Features:
- Proposal lifecycle: Draft -> Active -> Passed/Rejected -> Executed
- One-person-one-vote via Vita token (soul-bound identity)
- Configurable quorum (default 10%) and thresholds (50%/67%)
- Execution delay for passed proposals before implementation

Contract Methods:
- createProposal: Create proposals with categories (Law, Investment, etc)
- vote: Cast votes (Yes/No/Abstain) with Vita verification
- tallyVotes: Finalize voting after deadline with quorum checks
- executeProposal: Execute passed proposals after delay
- Query methods: getProposal, getVote, hasVoted, getConfig

Cross-Contract Integration:
- Vita: Add GetTotalTokenCount() for quorum calculations
- Lex: Add RatifyAmendmentInternal() for law amendment execution
- Wire Eligere into blockchain.go with proper validation

Test Updates:
- Update Vita suspend test to use Lex liberty restriction (due process)
- Update management tests for Federation/Eligere hardfork timing
- Add Vita registration to VTS tests for property rights checks
- Update NEP17 contracts list to include VTS

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Tutus Development 2025-12-20 06:30:43 +00:00
parent 96f77823e5
commit 30b3be30ce
13 changed files with 1508 additions and 12 deletions

View File

@ -222,6 +222,7 @@ type Blockchain struct {
federation native.IFederation
treasury native.ITreasury
lex native.ILex
eligere native.IEligere
extensible atomic.Value
@ -485,6 +486,10 @@ func NewBlockchain(s storage.Store, cfg config.Blockchain, log *zap.Logger, newN
if err := validateNative(bc.lex, nativeids.Lex, nativenames.Lex, nativehashes.Lex); err != nil {
return nil, err
}
bc.eligere = bc.contracts.Eligere()
if err := validateNative(bc.eligere, nativeids.Eligere, nativenames.Eligere, nativehashes.Eligere); err != nil {
return nil, err
}
bc.persistCond = sync.NewCond(&bc.lock)
bc.gcBlockTimes, _ = lru.New[uint32, uint64](defaultBlockTimesCache) // Never errors for positive size

View File

@ -122,6 +122,8 @@ type (
// IsAdultVerified checks if the owner has a verified "age_verified" attribute
// indicating they are 18+ years old. Used for age-restricted purchases.
IsAdultVerified(d *dao.Simple, owner util.Uint160) bool
// GetTotalTokenCount returns total number of Vita tokens (for quorum calculation).
GetTotalTokenCount(d *dao.Simple) uint64
}
// IRoleRegistry is an interface required from native RoleRegistry contract
@ -209,6 +211,19 @@ type (
CheckLibertyRight(d *dao.Simple, subject util.Uint160, blockHeight uint32) bool
// Address returns the contract's script hash.
Address() util.Uint160
// RatifyAmendmentInternal is called by Eligere when a law amendment passes.
RatifyAmendmentInternal(ic *interop.Context, proposalID uint64, contentHash util.Uint256, category state.LawCategory, jurisdiction uint32) uint64
}
// IEligere is an interface required from native Eligere contract for
// interaction with Blockchain and other native contracts.
// Eligere provides democratic voting infrastructure.
IEligere interface {
interop.Contract
// GetProposalInternal returns a proposal by ID.
GetProposalInternal(d *dao.Simple, proposalID uint64) *state.Proposal
// Address returns the contract's script hash.
Address() util.Uint160
}
)
@ -361,6 +376,12 @@ func (cs *Contracts) Lex() ILex {
return cs.ByName(nativenames.Lex).(ILex)
}
// Eligere returns native IEligere contract implementation. It panics if
// there's no contract with proper name in cs.
func (cs *Contracts) Eligere() IEligere {
return cs.ByName(nativenames.Eligere).(IEligere)
}
// NewDefaultContracts returns a new set of default native contracts.
func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract {
mgmt := NewManagement()
@ -447,6 +468,13 @@ func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract {
gas.Federation = federation
gas.Treasury = treasury
// Create Eligere (Democratic Voting) contract
eligere := newEligere()
eligere.NEO = neo
eligere.Vita = vita
eligere.RoleRegistry = roleRegistry
eligere.Lex = lex
return []interop.Contract{
mgmt,
s,
@ -464,5 +492,6 @@ func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract {
vts,
federation,
lex,
eligere,
}
}

947
pkg/core/native/eligere.go Normal file
View File

@ -0,0 +1,947 @@
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
}

View File

@ -3,6 +3,7 @@ package native
import (
"encoding/binary"
"errors"
"fmt"
"math/big"
"github.com/tutus-one/tutus-chain/pkg/config"
@ -933,3 +934,51 @@ func (l *Lex) CheckMovementRight(d *dao.Simple, subject util.Uint160, blockHeigh
func (l *Lex) CheckLibertyRight(d *dao.Simple, subject util.Uint160, blockHeight uint32) bool {
return l.HasRightInternal(d, subject, state.RightLiberty, blockHeight)
}
// RatifyAmendmentInternal creates a new law from a passed Eligere proposal.
// This is called by the Eligere contract when a law amendment proposal passes.
// Returns the new law ID.
func (l *Lex) RatifyAmendmentInternal(ic *interop.Context, proposalID uint64, contentHash util.Uint256, category state.LawCategory, jurisdiction uint32) uint64 {
// Category validation
if category < state.LawCategoryFederal || category > state.LawCategoryAdministrative {
panic(ErrInvalidLawCategory)
}
// Cannot create constitutional laws via amendment - they are immutable
if category == state.LawCategoryConstitutional {
panic(ErrCannotModifyConst)
}
// Create law from ratified proposal
lawID := l.getLawCounter(ic.DAO)
name := fmt.Sprintf("Amendment_%d", proposalID)
law := &state.Law{
ID: lawID,
Name: name,
ContentHash: contentHash,
Category: category,
Jurisdiction: jurisdiction,
ParentID: 0,
EffectiveAt: ic.Block.Index,
ExpiresAt: 0, // Perpetual by default
EnactedAt: ic.Block.Index,
EnactedBy: ic.VM.GetCallingScriptHash(),
Status: state.LawStatusActive,
SupersededBy: 0,
RequiresVita: true,
Enforcement: state.EnforcementAutomatic,
}
l.putLaw(ic.DAO, law)
l.putLawNameIndex(ic.DAO, name, lawID)
l.putLawCounter(ic.DAO, lawID+1)
ic.AddNotification(l.Hash, LawEnactedEvent, stackitem.NewArray([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(int64(lawID))),
stackitem.NewByteArray([]byte(name)),
stackitem.NewBigInteger(big.NewInt(int64(category))),
stackitem.NewByteArray(law.EnactedBy.BytesBE()),
}))
return lawID
}

View File

@ -24,8 +24,9 @@ func TestManagement_GetNEP17Contracts(t *testing.T) {
bc, validators, committee := chain.NewMulti(t)
e := neotest.NewExecutor(t, bc, validators, committee)
// Native NEP17 contracts: NEO, GAS, and VTS
require.ElementsMatch(t, []util.Uint160{e.NativeHash(t, nativenames.Neo),
e.NativeHash(t, nativenames.Gas)}, bc.GetNEP17Contracts())
e.NativeHash(t, nativenames.Gas), e.NativeHash(t, nativenames.VTS)}, bc.GetNEP17Contracts())
})
t.Run("basic chain", func(t *testing.T) {
@ -33,8 +34,9 @@ func TestManagement_GetNEP17Contracts(t *testing.T) {
e := neotest.NewExecutor(t, bc, validators, committee)
basicchain.Init(t, "../../../", e)
// Native NEP17 contracts: NEO, GAS, VTS + deployed contract
require.ElementsMatch(t, []util.Uint160{e.NativeHash(t, nativenames.Neo),
e.NativeHash(t, nativenames.Gas), e.ContractHash(t, 1)}, bc.GetNEP17Contracts())
e.NativeHash(t, nativenames.Gas), e.NativeHash(t, nativenames.VTS), e.ContractHash(t, 1)}, bc.GetNEP17Contracts())
})
}

File diff suppressed because one or more lines are too long

View File

@ -242,6 +242,10 @@ func TestVita_SuspendReinstate(t *testing.T) {
invoker := c.WithSigners(acc)
committeeInvoker := c.WithSigners(c.Committee)
// Create Lex invoker using same executor for liberty restriction (required for due process)
lexHash := e.NativeHash(t, nativenames.Lex)
lexCommitteeInvoker := e.CommitteeInvoker(lexHash)
// Initially token is active
invoker.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
require.Equal(t, 1, len(stack))
@ -255,7 +259,18 @@ func TestVita_SuspendReinstate(t *testing.T) {
// Non-committee cannot suspend
invoker.InvokeFail(t, "invalid committee signature", "suspend", acc.ScriptHash().BytesBE(), "test")
// Committee can suspend
// First, create a liberty restriction via Lex (due process requirement)
caseID := hash.Sha256([]byte("case-123")).BytesBE() // 32-byte case reference
lexCommitteeInvoker.Invoke(t, true, "restrictRight",
acc.ScriptHash().BytesBE(), // subject
int64(state.RightLiberty), // rightID = 2 (Liberty)
int64(state.RestrictionSuspend), // restrictionType = 1 (Suspend)
int64(100), // duration in blocks
"judicial order", // reason
caseID, // caseID (Hash256)
)
// Committee can suspend (now with valid Lex restriction order)
committeeInvoker.Invoke(t, true, "suspend", acc.ScriptHash().BytesBE(), "test suspension")
// Token is now suspended

View File

@ -188,6 +188,9 @@ func TestVTS_Transfer(t *testing.T) {
committeeInvoker := c.WithSigners(c.Committee)
senderInvoker := c.WithSigners(sender)
// Setup: Register Vita for sender (required for property rights/transfers)
registerVitaForVTS(t, e, sender)
// Mint to sender
committeeInvoker.Invoke(t, true, "mint", sender.ScriptHash(), 1000_00000000)
@ -280,6 +283,9 @@ func TestVTS_Spend(t *testing.T) {
committeeInvoker := c.WithSigners(c.Committee)
customerInvoker := c.WithSigners(customer)
// Setup: Register Vita for customer (required for property rights/spending)
registerVitaForVTS(t, e, customer)
// Setup: Register food vendor and mint to customer
committeeInvoker.Invoke(t, true, "registerVendor", foodVendor.ScriptHash(), "Food Store", state.CategoryFood, false)
committeeInvoker.Invoke(t, true, "mint", customer.ScriptHash(), 300_00000000)
@ -311,6 +317,9 @@ func TestVTS_CanSpendAt(t *testing.T) {
vendor := e.NewAccount(t)
committeeInvoker := c.WithSigners(c.Committee)
// Setup: Register Vita for customer (required for property rights)
registerVitaForVTS(t, e, customer)
// Setup
committeeInvoker.Invoke(t, true, "registerVendor", vendor.ScriptHash(), "Store", state.CategoryFood, false)
committeeInvoker.Invoke(t, true, "mint", customer.ScriptHash(), 100_00000000)
@ -403,6 +412,9 @@ func TestVTS_PayWage(t *testing.T) {
committeeInvoker := c.WithSigners(c.Committee)
employerInvoker := c.WithSigners(employer)
// Setup: Register Vita for employer (required for property rights/transfers)
registerVitaForVTS(t, e, employer)
// Setup: Set tax config and fund employer
committeeInvoker.Invoke(t, true, "setTaxConfig", 2500, 0, treasury.ScriptHash(), 0) // 25% income tax
committeeInvoker.Invoke(t, true, "mint", employer.ScriptHash(), 10000_00000000)

View File

@ -41,4 +41,6 @@ var (
Federation = util.Uint160{0xfd, 0x78, 0x70, 0xb, 0xa9, 0x19, 0xed, 0xb0, 0x19, 0x40, 0xdd, 0xc7, 0x97, 0x48, 0xf4, 0x25, 0x1d, 0xb0, 0x5, 0x1f}
// Lex is a hash of native Lex contract.
Lex = util.Uint160{0x2e, 0x3f, 0xb7, 0x5, 0x8, 0x17, 0xef, 0xb1, 0xc2, 0xbe, 0x68, 0xc4, 0xd4, 0xde, 0xc6, 0xf6, 0x2d, 0x92, 0x96, 0xe6}
// Eligere is a hash of native Eligere contract.
Eligere = util.Uint160{0x1, 0x94, 0x73, 0x8e, 0xab, 0x6b, 0xc5, 0xa0, 0xff, 0xab, 0xe0, 0x2a, 0xce, 0xea, 0xd7, 0xb3, 0xa8, 0xe5, 0x7, 0x40}
)

View File

@ -39,4 +39,6 @@ const (
Federation int32 = -15
// Lex is an ID of native Lex contract.
Lex int32 = -16
// Eligere is an ID of native Eligere contract.
Eligere int32 = -17
)

View File

@ -18,6 +18,7 @@ const (
VTS = "VTS"
Federation = "Federation"
Lex = "Lex"
Eligere = "Eligere"
)
// All contains the list of all native contract names ordered by the contract ID.
@ -38,6 +39,7 @@ var All = []string{
VTS,
Federation,
Lex,
Eligere,
}
// IsValid checks if the name is a valid native contract's name.
@ -57,5 +59,6 @@ func IsValid(name string) bool {
name == RoleRegistry ||
name == VTS ||
name == Federation ||
name == Lex
name == Lex ||
name == Eligere
}

View File

@ -904,6 +904,11 @@ func (v *Vita) TokenExists(d *dao.Simple, owner util.Uint160) bool {
return v.tokenExistsForOwner(d, owner)
}
// GetTotalTokenCount returns the total number of tokens issued (for quorum calculations).
func (v *Vita) GetTotalTokenCount(d *dao.Simple) uint64 {
return v.getTokenCounter(d)
}
// IsAdultVerified checks if the owner has a verified "age_verified" attribute
// indicating they are 18+ years old. Used for age-restricted purchases.
// The attribute must be non-revoked and not expired.

419
pkg/core/state/eligere.go Normal file
View File

@ -0,0 +1,419 @@
package state
import (
"errors"
"math/big"
"github.com/tutus-one/tutus-chain/pkg/util"
"github.com/tutus-one/tutus-chain/pkg/vm/stackitem"
)
// ProposalCategory represents the type of proposal.
type ProposalCategory uint8
// Proposal categories.
const (
ProposalCategoryLawAmendment ProposalCategory = 1 // Lex integration
ProposalCategoryInvestment ProposalCategory = 2 // PIO/EIO/CIO (future)
ProposalCategoryGovernanceAction ProposalCategory = 3 // Committee/policy changes
ProposalCategoryConstitutional ProposalCategory = 4 // Supermajority required
ProposalCategoryReferendum ProposalCategory = 5 // Citizen-initiated
)
// ProposalStatus represents the lifecycle status of a proposal.
type ProposalStatus uint8
// Proposal statuses.
const (
ProposalStatusDraft ProposalStatus = 0 // Not yet open for voting
ProposalStatusActive ProposalStatus = 1 // Open for voting
ProposalStatusPassed ProposalStatus = 2 // Met threshold, awaiting execution
ProposalStatusRejected ProposalStatus = 3 // Failed to meet threshold
ProposalStatusExecuted ProposalStatus = 4 // Successfully executed
ProposalStatusCancelled ProposalStatus = 5 // Cancelled by proposer/committee
ProposalStatusExpired ProposalStatus = 6 // Voting period ended without quorum
)
// VoteChoice represents a voting decision.
type VoteChoice uint8
// Vote choices.
const (
VoteChoiceAbstain VoteChoice = 0
VoteChoiceYes VoteChoice = 1
VoteChoiceNo VoteChoice = 2
)
// Proposal represents a democratic proposal in the Eligere voting system.
type Proposal struct {
ID uint64 // Unique proposal ID
Title string // Short title (max 128 chars)
ContentHash util.Uint256 // Hash of full proposal content (stored off-chain)
Category ProposalCategory // Type of proposal
Proposer util.Uint160 // Who created the proposal
ProposerVitaID uint64 // Vita ID of proposer
// Timing (block heights)
CreatedAt uint32 // When created
VotingStartsAt uint32 // When voting opens
VotingEndsAt uint32 // elegireDeadline - when voting closes
ExecutionDelay uint32 // Blocks after passing before execution allowed
// Thresholds
QuorumPercent uint8 // Minimum participation percentage (e.g., 10%)
ThresholdPercent uint8 // Required yes votes percentage (50% or 67%)
// Results
Status ProposalStatus
TotalVotes uint64 // Total votes cast
YesVotes uint64 // Votes in favor
NoVotes uint64 // Votes against
AbstainVotes uint64 // Abstentions (count toward quorum)
// Execution target (for contract integrations like Lex)
TargetContract util.Uint160 // Contract to call on execution
TargetMethod string // Method to call
TargetParams []byte // Serialized parameters
// Execution tracking
ExecutedAt uint32 // When executed (0 if not)
ExecutedBy util.Uint160 // Who executed
}
// ToStackItem implements stackitem.Convertible interface.
func (p *Proposal) ToStackItem() (stackitem.Item, error) {
return stackitem.NewStruct([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(int64(p.ID))),
stackitem.NewByteArray([]byte(p.Title)),
stackitem.NewByteArray(p.ContentHash[:]),
stackitem.NewBigInteger(big.NewInt(int64(p.Category))),
stackitem.NewByteArray(p.Proposer.BytesBE()),
stackitem.NewBigInteger(big.NewInt(int64(p.ProposerVitaID))),
stackitem.NewBigInteger(big.NewInt(int64(p.CreatedAt))),
stackitem.NewBigInteger(big.NewInt(int64(p.VotingStartsAt))),
stackitem.NewBigInteger(big.NewInt(int64(p.VotingEndsAt))),
stackitem.NewBigInteger(big.NewInt(int64(p.ExecutionDelay))),
stackitem.NewBigInteger(big.NewInt(int64(p.QuorumPercent))),
stackitem.NewBigInteger(big.NewInt(int64(p.ThresholdPercent))),
stackitem.NewBigInteger(big.NewInt(int64(p.Status))),
stackitem.NewBigInteger(big.NewInt(int64(p.TotalVotes))),
stackitem.NewBigInteger(big.NewInt(int64(p.YesVotes))),
stackitem.NewBigInteger(big.NewInt(int64(p.NoVotes))),
stackitem.NewBigInteger(big.NewInt(int64(p.AbstainVotes))),
stackitem.NewByteArray(p.TargetContract.BytesBE()),
stackitem.NewByteArray([]byte(p.TargetMethod)),
stackitem.NewByteArray(p.TargetParams),
stackitem.NewBigInteger(big.NewInt(int64(p.ExecutedAt))),
stackitem.NewByteArray(p.ExecutedBy.BytesBE()),
}), nil
}
// FromStackItem implements stackitem.Convertible interface.
func (p *Proposal) FromStackItem(item stackitem.Item) error {
arr, ok := item.Value().([]stackitem.Item)
if !ok {
return errors.New("not a struct")
}
if len(arr) < 22 {
return errors.New("invalid proposal struct length")
}
id, err := arr[0].TryInteger()
if err != nil {
return err
}
p.ID = id.Uint64()
titleBytes, err := arr[1].TryBytes()
if err != nil {
return err
}
p.Title = string(titleBytes)
contentHashBytes, err := arr[2].TryBytes()
if err != nil {
return err
}
if len(contentHashBytes) == 32 {
copy(p.ContentHash[:], contentHashBytes)
}
category, err := arr[3].TryInteger()
if err != nil {
return err
}
p.Category = ProposalCategory(category.Uint64())
proposerBytes, err := arr[4].TryBytes()
if err != nil {
return err
}
p.Proposer, _ = util.Uint160DecodeBytesBE(proposerBytes)
proposerVitaID, err := arr[5].TryInteger()
if err != nil {
return err
}
p.ProposerVitaID = proposerVitaID.Uint64()
createdAt, err := arr[6].TryInteger()
if err != nil {
return err
}
p.CreatedAt = uint32(createdAt.Uint64())
votingStartsAt, err := arr[7].TryInteger()
if err != nil {
return err
}
p.VotingStartsAt = uint32(votingStartsAt.Uint64())
votingEndsAt, err := arr[8].TryInteger()
if err != nil {
return err
}
p.VotingEndsAt = uint32(votingEndsAt.Uint64())
executionDelay, err := arr[9].TryInteger()
if err != nil {
return err
}
p.ExecutionDelay = uint32(executionDelay.Uint64())
quorumPercent, err := arr[10].TryInteger()
if err != nil {
return err
}
p.QuorumPercent = uint8(quorumPercent.Uint64())
thresholdPercent, err := arr[11].TryInteger()
if err != nil {
return err
}
p.ThresholdPercent = uint8(thresholdPercent.Uint64())
status, err := arr[12].TryInteger()
if err != nil {
return err
}
p.Status = ProposalStatus(status.Uint64())
totalVotes, err := arr[13].TryInteger()
if err != nil {
return err
}
p.TotalVotes = totalVotes.Uint64()
yesVotes, err := arr[14].TryInteger()
if err != nil {
return err
}
p.YesVotes = yesVotes.Uint64()
noVotes, err := arr[15].TryInteger()
if err != nil {
return err
}
p.NoVotes = noVotes.Uint64()
abstainVotes, err := arr[16].TryInteger()
if err != nil {
return err
}
p.AbstainVotes = abstainVotes.Uint64()
targetContractBytes, err := arr[17].TryBytes()
if err != nil {
return err
}
p.TargetContract, _ = util.Uint160DecodeBytesBE(targetContractBytes)
targetMethodBytes, err := arr[18].TryBytes()
if err != nil {
return err
}
p.TargetMethod = string(targetMethodBytes)
targetParams, err := arr[19].TryBytes()
if err != nil {
return err
}
p.TargetParams = targetParams
executedAt, err := arr[20].TryInteger()
if err != nil {
return err
}
p.ExecutedAt = uint32(executedAt.Uint64())
executedByBytes, err := arr[21].TryBytes()
if err != nil {
return err
}
p.ExecutedBy, _ = util.Uint160DecodeBytesBE(executedByBytes)
return nil
}
// Vote represents a single vote record in the Eligere voting system.
type Vote struct {
ProposalID uint64 // Which proposal
VoterVitaID uint64 // Vita ID of voter (ensures one-person-one-vote)
Voter util.Uint160 // Voter's address
Choice VoteChoice // How they voted
VotedAt uint32 // Block height when voted
Weight uint64 // Vote weight (1 for equal voting)
}
// ToStackItem implements stackitem.Convertible interface.
func (v *Vote) ToStackItem() (stackitem.Item, error) {
return stackitem.NewStruct([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(int64(v.ProposalID))),
stackitem.NewBigInteger(big.NewInt(int64(v.VoterVitaID))),
stackitem.NewByteArray(v.Voter.BytesBE()),
stackitem.NewBigInteger(big.NewInt(int64(v.Choice))),
stackitem.NewBigInteger(big.NewInt(int64(v.VotedAt))),
stackitem.NewBigInteger(big.NewInt(int64(v.Weight))),
}), nil
}
// FromStackItem implements stackitem.Convertible interface.
func (v *Vote) FromStackItem(item stackitem.Item) error {
arr, ok := item.Value().([]stackitem.Item)
if !ok {
return errors.New("not a struct")
}
if len(arr) < 6 {
return errors.New("invalid vote struct length")
}
proposalID, err := arr[0].TryInteger()
if err != nil {
return err
}
v.ProposalID = proposalID.Uint64()
voterVitaID, err := arr[1].TryInteger()
if err != nil {
return err
}
v.VoterVitaID = voterVitaID.Uint64()
voterBytes, err := arr[2].TryBytes()
if err != nil {
return err
}
v.Voter, _ = util.Uint160DecodeBytesBE(voterBytes)
choice, err := arr[3].TryInteger()
if err != nil {
return err
}
v.Choice = VoteChoice(choice.Uint64())
votedAt, err := arr[4].TryInteger()
if err != nil {
return err
}
v.VotedAt = uint32(votedAt.Uint64())
weight, err := arr[5].TryInteger()
if err != nil {
return err
}
v.Weight = weight.Uint64()
return nil
}
// EligereConfig represents configurable parameters for the Eligere voting system.
type EligereConfig struct {
DefaultVotingPeriod uint32 // Default blocks for voting (~7 days at 1s blocks = 604800)
MinVotingPeriod uint32 // Minimum blocks (~1 day = 86400)
MaxVotingPeriod uint32 // Maximum blocks (~30 days = 2592000)
DefaultExecutionDelay uint32 // Blocks between passing and execution (~1 day = 86400)
DefaultQuorum uint8 // Default quorum percentage (e.g., 10%)
ConstitutionalThreshold uint8 // Supermajority threshold (e.g., 67%)
StandardThreshold uint8 // Simple majority (e.g., 51%)
}
// ToStackItem implements stackitem.Convertible interface.
func (c *EligereConfig) ToStackItem() (stackitem.Item, error) {
return stackitem.NewStruct([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(int64(c.DefaultVotingPeriod))),
stackitem.NewBigInteger(big.NewInt(int64(c.MinVotingPeriod))),
stackitem.NewBigInteger(big.NewInt(int64(c.MaxVotingPeriod))),
stackitem.NewBigInteger(big.NewInt(int64(c.DefaultExecutionDelay))),
stackitem.NewBigInteger(big.NewInt(int64(c.DefaultQuorum))),
stackitem.NewBigInteger(big.NewInt(int64(c.ConstitutionalThreshold))),
stackitem.NewBigInteger(big.NewInt(int64(c.StandardThreshold))),
}), nil
}
// FromStackItem implements stackitem.Convertible interface.
func (c *EligereConfig) FromStackItem(item stackitem.Item) error {
arr, ok := item.Value().([]stackitem.Item)
if !ok {
return errors.New("not a struct")
}
if len(arr) < 7 {
return errors.New("invalid config struct length")
}
defaultVotingPeriod, err := arr[0].TryInteger()
if err != nil {
return err
}
c.DefaultVotingPeriod = uint32(defaultVotingPeriod.Uint64())
minVotingPeriod, err := arr[1].TryInteger()
if err != nil {
return err
}
c.MinVotingPeriod = uint32(minVotingPeriod.Uint64())
maxVotingPeriod, err := arr[2].TryInteger()
if err != nil {
return err
}
c.MaxVotingPeriod = uint32(maxVotingPeriod.Uint64())
defaultExecutionDelay, err := arr[3].TryInteger()
if err != nil {
return err
}
c.DefaultExecutionDelay = uint32(defaultExecutionDelay.Uint64())
defaultQuorum, err := arr[4].TryInteger()
if err != nil {
return err
}
c.DefaultQuorum = uint8(defaultQuorum.Uint64())
constitutionalThreshold, err := arr[5].TryInteger()
if err != nil {
return err
}
c.ConstitutionalThreshold = uint8(constitutionalThreshold.Uint64())
standardThreshold, err := arr[6].TryInteger()
if err != nil {
return err
}
c.StandardThreshold = uint8(standardThreshold.Uint64())
return nil
}
// DefaultEligereConfig returns the default configuration for Eligere.
func DefaultEligereConfig() *EligereConfig {
return &EligereConfig{
DefaultVotingPeriod: 604800, // ~7 days at 1 block/second
MinVotingPeriod: 86400, // ~1 day minimum
MaxVotingPeriod: 2592000, // ~30 days maximum
DefaultExecutionDelay: 86400, // ~1 day delay after passing
DefaultQuorum: 10, // 10% participation required
ConstitutionalThreshold: 67, // 67% supermajority for constitutional
StandardThreshold: 51, // 51% simple majority
}
}