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:
parent
96f77823e5
commit
30b3be30ce
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue