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
|
federation native.IFederation
|
||||||
treasury native.ITreasury
|
treasury native.ITreasury
|
||||||
lex native.ILex
|
lex native.ILex
|
||||||
|
eligere native.IEligere
|
||||||
|
|
||||||
extensible atomic.Value
|
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 {
|
if err := validateNative(bc.lex, nativeids.Lex, nativenames.Lex, nativehashes.Lex); err != nil {
|
||||||
return nil, err
|
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.persistCond = sync.NewCond(&bc.lock)
|
||||||
bc.gcBlockTimes, _ = lru.New[uint32, uint64](defaultBlockTimesCache) // Never errors for positive size
|
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
|
// IsAdultVerified checks if the owner has a verified "age_verified" attribute
|
||||||
// indicating they are 18+ years old. Used for age-restricted purchases.
|
// indicating they are 18+ years old. Used for age-restricted purchases.
|
||||||
IsAdultVerified(d *dao.Simple, owner util.Uint160) bool
|
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
|
// IRoleRegistry is an interface required from native RoleRegistry contract
|
||||||
|
|
@ -209,6 +211,19 @@ type (
|
||||||
CheckLibertyRight(d *dao.Simple, subject util.Uint160, blockHeight uint32) bool
|
CheckLibertyRight(d *dao.Simple, subject util.Uint160, blockHeight uint32) bool
|
||||||
// Address returns the contract's script hash.
|
// Address returns the contract's script hash.
|
||||||
Address() util.Uint160
|
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)
|
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.
|
// NewDefaultContracts returns a new set of default native contracts.
|
||||||
func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract {
|
func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract {
|
||||||
mgmt := NewManagement()
|
mgmt := NewManagement()
|
||||||
|
|
@ -447,6 +468,13 @@ func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract {
|
||||||
gas.Federation = federation
|
gas.Federation = federation
|
||||||
gas.Treasury = treasury
|
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{
|
return []interop.Contract{
|
||||||
mgmt,
|
mgmt,
|
||||||
s,
|
s,
|
||||||
|
|
@ -464,5 +492,6 @@ func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract {
|
||||||
vts,
|
vts,
|
||||||
federation,
|
federation,
|
||||||
lex,
|
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 (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"math/big"
|
"math/big"
|
||||||
|
|
||||||
"github.com/tutus-one/tutus-chain/pkg/config"
|
"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 {
|
func (l *Lex) CheckLibertyRight(d *dao.Simple, subject util.Uint160, blockHeight uint32) bool {
|
||||||
return l.HasRightInternal(d, subject, state.RightLiberty, blockHeight)
|
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)
|
bc, validators, committee := chain.NewMulti(t)
|
||||||
e := neotest.NewExecutor(t, bc, validators, committee)
|
e := neotest.NewExecutor(t, bc, validators, committee)
|
||||||
|
|
||||||
|
// Native NEP17 contracts: NEO, GAS, and VTS
|
||||||
require.ElementsMatch(t, []util.Uint160{e.NativeHash(t, nativenames.Neo),
|
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) {
|
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)
|
e := neotest.NewExecutor(t, bc, validators, committee)
|
||||||
basicchain.Init(t, "../../../", e)
|
basicchain.Init(t, "../../../", e)
|
||||||
|
|
||||||
|
// Native NEP17 contracts: NEO, GAS, VTS + deployed contract
|
||||||
require.ElementsMatch(t, []util.Uint160{e.NativeHash(t, nativenames.Neo),
|
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)
|
invoker := c.WithSigners(acc)
|
||||||
committeeInvoker := c.WithSigners(c.Committee)
|
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
|
// Initially token is active
|
||||||
invoker.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
|
invoker.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
|
||||||
require.Equal(t, 1, len(stack))
|
require.Equal(t, 1, len(stack))
|
||||||
|
|
@ -255,7 +259,18 @@ func TestVita_SuspendReinstate(t *testing.T) {
|
||||||
// Non-committee cannot suspend
|
// Non-committee cannot suspend
|
||||||
invoker.InvokeFail(t, "invalid committee signature", "suspend", acc.ScriptHash().BytesBE(), "test")
|
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")
|
committeeInvoker.Invoke(t, true, "suspend", acc.ScriptHash().BytesBE(), "test suspension")
|
||||||
|
|
||||||
// Token is now suspended
|
// Token is now suspended
|
||||||
|
|
|
||||||
|
|
@ -188,6 +188,9 @@ func TestVTS_Transfer(t *testing.T) {
|
||||||
committeeInvoker := c.WithSigners(c.Committee)
|
committeeInvoker := c.WithSigners(c.Committee)
|
||||||
senderInvoker := c.WithSigners(sender)
|
senderInvoker := c.WithSigners(sender)
|
||||||
|
|
||||||
|
// Setup: Register Vita for sender (required for property rights/transfers)
|
||||||
|
registerVitaForVTS(t, e, sender)
|
||||||
|
|
||||||
// Mint to sender
|
// Mint to sender
|
||||||
committeeInvoker.Invoke(t, true, "mint", sender.ScriptHash(), 1000_00000000)
|
committeeInvoker.Invoke(t, true, "mint", sender.ScriptHash(), 1000_00000000)
|
||||||
|
|
||||||
|
|
@ -280,6 +283,9 @@ func TestVTS_Spend(t *testing.T) {
|
||||||
committeeInvoker := c.WithSigners(c.Committee)
|
committeeInvoker := c.WithSigners(c.Committee)
|
||||||
customerInvoker := c.WithSigners(customer)
|
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
|
// Setup: Register food vendor and mint to customer
|
||||||
committeeInvoker.Invoke(t, true, "registerVendor", foodVendor.ScriptHash(), "Food Store", state.CategoryFood, false)
|
committeeInvoker.Invoke(t, true, "registerVendor", foodVendor.ScriptHash(), "Food Store", state.CategoryFood, false)
|
||||||
committeeInvoker.Invoke(t, true, "mint", customer.ScriptHash(), 300_00000000)
|
committeeInvoker.Invoke(t, true, "mint", customer.ScriptHash(), 300_00000000)
|
||||||
|
|
@ -311,6 +317,9 @@ func TestVTS_CanSpendAt(t *testing.T) {
|
||||||
vendor := e.NewAccount(t)
|
vendor := e.NewAccount(t)
|
||||||
committeeInvoker := c.WithSigners(c.Committee)
|
committeeInvoker := c.WithSigners(c.Committee)
|
||||||
|
|
||||||
|
// Setup: Register Vita for customer (required for property rights)
|
||||||
|
registerVitaForVTS(t, e, customer)
|
||||||
|
|
||||||
// Setup
|
// Setup
|
||||||
committeeInvoker.Invoke(t, true, "registerVendor", vendor.ScriptHash(), "Store", state.CategoryFood, false)
|
committeeInvoker.Invoke(t, true, "registerVendor", vendor.ScriptHash(), "Store", state.CategoryFood, false)
|
||||||
committeeInvoker.Invoke(t, true, "mint", customer.ScriptHash(), 100_00000000)
|
committeeInvoker.Invoke(t, true, "mint", customer.ScriptHash(), 100_00000000)
|
||||||
|
|
@ -403,6 +412,9 @@ func TestVTS_PayWage(t *testing.T) {
|
||||||
committeeInvoker := c.WithSigners(c.Committee)
|
committeeInvoker := c.WithSigners(c.Committee)
|
||||||
employerInvoker := c.WithSigners(employer)
|
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
|
// Setup: Set tax config and fund employer
|
||||||
committeeInvoker.Invoke(t, true, "setTaxConfig", 2500, 0, treasury.ScriptHash(), 0) // 25% income tax
|
committeeInvoker.Invoke(t, true, "setTaxConfig", 2500, 0, treasury.ScriptHash(), 0) // 25% income tax
|
||||||
committeeInvoker.Invoke(t, true, "mint", employer.ScriptHash(), 10000_00000000)
|
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}
|
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 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}
|
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
|
Federation int32 = -15
|
||||||
// Lex is an ID of native Lex contract.
|
// Lex is an ID of native Lex contract.
|
||||||
Lex int32 = -16
|
Lex int32 = -16
|
||||||
|
// Eligere is an ID of native Eligere contract.
|
||||||
|
Eligere int32 = -17
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ const (
|
||||||
VTS = "VTS"
|
VTS = "VTS"
|
||||||
Federation = "Federation"
|
Federation = "Federation"
|
||||||
Lex = "Lex"
|
Lex = "Lex"
|
||||||
|
Eligere = "Eligere"
|
||||||
)
|
)
|
||||||
|
|
||||||
// All contains the list of all native contract names ordered by the contract ID.
|
// All contains the list of all native contract names ordered by the contract ID.
|
||||||
|
|
@ -38,6 +39,7 @@ var All = []string{
|
||||||
VTS,
|
VTS,
|
||||||
Federation,
|
Federation,
|
||||||
Lex,
|
Lex,
|
||||||
|
Eligere,
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsValid checks if the name is a valid native contract's name.
|
// IsValid checks if the name is a valid native contract's name.
|
||||||
|
|
@ -57,5 +59,6 @@ func IsValid(name string) bool {
|
||||||
name == RoleRegistry ||
|
name == RoleRegistry ||
|
||||||
name == VTS ||
|
name == VTS ||
|
||||||
name == Federation ||
|
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)
|
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
|
// IsAdultVerified checks if the owner has a verified "age_verified" attribute
|
||||||
// indicating they are 18+ years old. Used for age-restricted purchases.
|
// indicating they are 18+ years old. Used for age-restricted purchases.
|
||||||
// The attribute must be non-revoked and not expired.
|
// 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