diff --git a/pkg/core/native/collocatio.go b/pkg/core/native/collocatio.go new file mode 100644 index 0000000..7d52012 --- /dev/null +++ b/pkg/core/native/collocatio.go @@ -0,0 +1,979 @@ +package native + +import ( + "encoding/binary" + "fmt" + "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/nativehashes" + "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/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" +) + +// Collocatio represents the Investment native contract for democratic investment (PIO/EIO/CIO). +// Latin: "collocatio" = placement, arrangement (investment) +type Collocatio struct { + interop.ContractMD + NEO INEO + Vita IVita + RoleRegistry *RoleRegistry + VTS *VTS + Scire *Scire + Eligere *Eligere + Tribute *Tribute +} + +// Storage prefixes for Collocatio contract. +const ( + collocatioPrefixConfig byte = 0x01 // -> CollocatioConfig + collocatioPrefixOpportunity byte = 0x10 // opportunityID -> InvestmentOpportunity + collocatioPrefixOpportunityByType byte = 0x11 // type + opportunityID -> exists + collocatioPrefixOpportunityByStatus byte = 0x12 // status + opportunityID -> exists + collocatioPrefixOppCounter byte = 0x1F // -> next opportunity ID + collocatioPrefixInvestment byte = 0x20 // investmentID -> Investment + collocatioPrefixInvestmentByOpp byte = 0x21 // opportunityID + investmentID -> exists + collocatioPrefixInvestmentByInvestor byte = 0x22 // vitaID + investmentID -> exists + collocatioPrefixInvCounter byte = 0x2F // -> next investment ID + collocatioPrefixEligibility byte = 0x30 // vitaID -> InvestorEligibility + collocatioPrefixEligibilityByOwner byte = 0x31 // owner -> vitaID + collocatioPrefixViolation byte = 0x40 // violationID -> InvestmentViolation + collocatioPrefixViolationCounter byte = 0x4F // -> next violation ID +) + +// Collocatio events. +const ( + collocatioOpportunityCreatedEvent = "OpportunityCreated" + collocatioOpportunityActivatedEvent = "OpportunityActivated" + collocatioInvestmentMadeEvent = "InvestmentMade" + collocatioInvestmentWithdrawnEvent = "InvestmentWithdrawn" + collocatioReturnsDistributedEvent = "ReturnsDistributed" + collocatioEligibilityUpdatedEvent = "EligibilityUpdated" + collocatioViolationRecordedEvent = "ViolationRecorded" +) + +// RoleInvestmentManager is the role ID for investment management. +const RoleInvestmentManager uint64 = 28 + +// Default config values. +const ( + defaultMinPIOParticipants uint64 = 100 + defaultMinEIOParticipants uint64 = 10 + defaultMinCIOParticipants uint64 = 25 + defaultMinInvestment uint64 = 100_00000000 // 100 VTS + defaultMaxIndividualCap uint64 = 1_000_000_00000000 // 1M VTS + defaultWealthConcentration uint64 = 500 // 5% + defaultCreationFee uint64 = 1000_00000000 // 1000 VTS + defaultInvestmentFee uint64 = 50 // 0.5% + defaultWithdrawalPenalty uint64 = 200 // 2% + defaultMinVotingPeriod uint32 = 10000 + defaultMinInvestmentPeriod uint32 = 20000 + defaultMinMaturityPeriod uint32 = 50000 + defaultMaxViolationsBeforeBan uint8 = 3 +) + +var _ interop.Contract = (*Collocatio)(nil) + +func newCollocatio() *Collocatio { + c := &Collocatio{ + ContractMD: *interop.NewContractMD(nativenames.Collocatio, nativeids.Collocatio), + } + defer c.BuildHFSpecificMD(c.ActiveIn()) + + // getConfig + desc := NewDescriptor("getConfig", smartcontract.ArrayType) + md := NewMethodAndPrice(c.getConfig, 1<<15, callflag.ReadStates) + c.AddMethod(md, desc) + + // getOpportunityCount + desc = NewDescriptor("getOpportunityCount", smartcontract.IntegerType) + md = NewMethodAndPrice(c.getOpportunityCount, 1<<15, callflag.ReadStates) + c.AddMethod(md, desc) + + // getOpportunity + desc = NewDescriptor("getOpportunity", smartcontract.ArrayType, + manifest.NewParameter("opportunityID", smartcontract.IntegerType)) + md = NewMethodAndPrice(c.getOpportunity, 1<<15, callflag.ReadStates) + c.AddMethod(md, desc) + + // createOpportunity + desc = NewDescriptor("createOpportunity", smartcontract.IntegerType, + manifest.NewParameter("oppType", smartcontract.IntegerType), + manifest.NewParameter("name", smartcontract.StringType), + manifest.NewParameter("description", smartcontract.StringType), + manifest.NewParameter("termsHash", smartcontract.Hash256Type), + manifest.NewParameter("minParticipants", smartcontract.IntegerType), + manifest.NewParameter("maxParticipants", smartcontract.IntegerType), + manifest.NewParameter("minInvestment", smartcontract.IntegerType), + manifest.NewParameter("maxInvestment", smartcontract.IntegerType), + manifest.NewParameter("targetPool", smartcontract.IntegerType), + manifest.NewParameter("expectedReturns", smartcontract.IntegerType), + manifest.NewParameter("riskLevel", smartcontract.IntegerType), + manifest.NewParameter("maturityBlocks", smartcontract.IntegerType)) + md = NewMethodAndPrice(c.createOpportunity, 1<<17, callflag.States|callflag.AllowNotify) + c.AddMethod(md, desc) + + // activateOpportunity + desc = NewDescriptor("activateOpportunity", smartcontract.BoolType, + manifest.NewParameter("opportunityID", smartcontract.IntegerType)) + md = NewMethodAndPrice(c.activateOpportunity, 1<<16, callflag.States|callflag.AllowNotify) + c.AddMethod(md, desc) + + // invest + desc = NewDescriptor("invest", smartcontract.IntegerType, + manifest.NewParameter("opportunityID", smartcontract.IntegerType), + manifest.NewParameter("amount", smartcontract.IntegerType)) + md = NewMethodAndPrice(c.invest, 1<<17, callflag.States|callflag.AllowNotify) + c.AddMethod(md, desc) + + // withdraw + desc = NewDescriptor("withdraw", smartcontract.BoolType, + manifest.NewParameter("investmentID", smartcontract.IntegerType)) + md = NewMethodAndPrice(c.withdraw, 1<<17, callflag.States|callflag.AllowNotify) + c.AddMethod(md, desc) + + // getEligibility + desc = NewDescriptor("getEligibility", smartcontract.ArrayType, + manifest.NewParameter("investor", smartcontract.Hash160Type)) + md = NewMethodAndPrice(c.getEligibility, 1<<15, callflag.ReadStates) + c.AddMethod(md, desc) + + // setEligibility + desc = NewDescriptor("setEligibility", smartcontract.BoolType, + manifest.NewParameter("investor", smartcontract.Hash160Type), + manifest.NewParameter("eligibilityFlags", smartcontract.IntegerType)) + md = NewMethodAndPrice(c.setEligibility, 1<<16, callflag.States|callflag.AllowNotify) + c.AddMethod(md, desc) + + // isEligible + desc = NewDescriptor("isEligible", smartcontract.BoolType, + manifest.NewParameter("investor", smartcontract.Hash160Type), + manifest.NewParameter("oppType", smartcontract.IntegerType)) + md = NewMethodAndPrice(c.isEligible, 1<<15, callflag.ReadStates) + c.AddMethod(md, desc) + + // getInvestment + desc = NewDescriptor("getInvestment", smartcontract.ArrayType, + manifest.NewParameter("investmentID", smartcontract.IntegerType)) + md = NewMethodAndPrice(c.getInvestment, 1<<15, callflag.ReadStates) + c.AddMethod(md, desc) + + // getInvestmentCount + desc = NewDescriptor("getInvestmentCount", smartcontract.IntegerType) + md = NewMethodAndPrice(c.getInvestmentCount, 1<<15, callflag.ReadStates) + c.AddMethod(md, desc) + + // ===== Events ===== + eDesc := NewEventDescriptor(collocatioOpportunityCreatedEvent, + manifest.NewParameter("opportunityID", smartcontract.IntegerType), + manifest.NewParameter("oppType", smartcontract.IntegerType), + manifest.NewParameter("creator", smartcontract.Hash160Type)) + c.AddEvent(NewEvent(eDesc)) + + eDesc = NewEventDescriptor(collocatioOpportunityActivatedEvent, + manifest.NewParameter("opportunityID", smartcontract.IntegerType)) + c.AddEvent(NewEvent(eDesc)) + + eDesc = NewEventDescriptor(collocatioInvestmentMadeEvent, + manifest.NewParameter("investmentID", smartcontract.IntegerType), + manifest.NewParameter("opportunityID", smartcontract.IntegerType), + manifest.NewParameter("investor", smartcontract.Hash160Type), + manifest.NewParameter("amount", smartcontract.IntegerType)) + c.AddEvent(NewEvent(eDesc)) + + eDesc = NewEventDescriptor(collocatioInvestmentWithdrawnEvent, + manifest.NewParameter("investmentID", smartcontract.IntegerType), + manifest.NewParameter("returnAmount", smartcontract.IntegerType)) + c.AddEvent(NewEvent(eDesc)) + + eDesc = NewEventDescriptor(collocatioEligibilityUpdatedEvent, + manifest.NewParameter("investor", smartcontract.Hash160Type), + manifest.NewParameter("eligibility", smartcontract.IntegerType)) + c.AddEvent(NewEvent(eDesc)) + + return c +} + +// Metadata returns contract metadata. +func (c *Collocatio) Metadata() *interop.ContractMD { + return &c.ContractMD +} + +// Initialize initializes Collocatio contract. +func (c *Collocatio) Initialize(ic *interop.Context, hf *config.Hardfork, newMD *interop.HFSpecificContractMD) error { + if hf != c.ActiveIn() { + return nil + } + + // Initialize default config + cfg := state.CollocatioConfig{ + MinPIOParticipants: defaultMinPIOParticipants, + MinEIOParticipants: defaultMinEIOParticipants, + MinCIOParticipants: defaultMinCIOParticipants, + DefaultMinInvestment: defaultMinInvestment, + MaxIndividualCap: defaultMaxIndividualCap, + WealthConcentration: defaultWealthConcentration, + CreationFee: defaultCreationFee, + InvestmentFee: defaultInvestmentFee, + WithdrawalPenalty: defaultWithdrawalPenalty, + MinVotingPeriod: defaultMinVotingPeriod, + MinInvestmentPeriod: defaultMinInvestmentPeriod, + MinMaturityPeriod: defaultMinMaturityPeriod, + MaxViolationsBeforeBan: defaultMaxViolationsBeforeBan, + ViolationCooldown: 1000000, + } + c.setConfigInternal(ic.DAO, &cfg) + + return nil +} + +// InitializeCache fills native Collocatio cache from DAO. +func (c *Collocatio) InitializeCache(_ interop.IsHardforkEnabled, blockHeight uint32, d *dao.Simple) error { + return nil +} + +// OnPersist implements the Contract interface. +func (c *Collocatio) OnPersist(ic *interop.Context) error { + return nil +} + +// PostPersist implements the Contract interface. +func (c *Collocatio) PostPersist(ic *interop.Context) error { + return nil +} + +// ActiveIn returns nil (always active from genesis). +func (c *Collocatio) ActiveIn() *config.Hardfork { + return nil +} + +// ============================================================================ +// Storage Key Helpers +// ============================================================================ + +func makeCollocatioConfigKey() []byte { + return []byte{collocatioPrefixConfig} +} + +func makeCollocatioOppKey(oppID uint64) []byte { + key := make([]byte, 9) + key[0] = collocatioPrefixOpportunity + binary.BigEndian.PutUint64(key[1:], oppID) + return key +} + +func makeCollocatioOppByTypeKey(oppType state.OpportunityType, oppID uint64) []byte { + key := make([]byte, 10) + key[0] = collocatioPrefixOpportunityByType + key[1] = byte(oppType) + binary.BigEndian.PutUint64(key[2:], oppID) + return key +} + +func makeCollocatioOppCounterKey() []byte { + return []byte{collocatioPrefixOppCounter} +} + +func makeCollocatioInvKey(invID uint64) []byte { + key := make([]byte, 9) + key[0] = collocatioPrefixInvestment + binary.BigEndian.PutUint64(key[1:], invID) + return key +} + +func makeCollocatioInvByOppKey(oppID, invID uint64) []byte { + key := make([]byte, 17) + key[0] = collocatioPrefixInvestmentByOpp + binary.BigEndian.PutUint64(key[1:9], oppID) + binary.BigEndian.PutUint64(key[9:], invID) + return key +} + +func makeCollocatioInvByInvestorKey(vitaID, invID uint64) []byte { + key := make([]byte, 17) + key[0] = collocatioPrefixInvestmentByInvestor + binary.BigEndian.PutUint64(key[1:9], vitaID) + binary.BigEndian.PutUint64(key[9:], invID) + return key +} + +func makeCollocatioInvCounterKey() []byte { + return []byte{collocatioPrefixInvCounter} +} + +func makeCollocatioEligKey(vitaID uint64) []byte { + key := make([]byte, 9) + key[0] = collocatioPrefixEligibility + binary.BigEndian.PutUint64(key[1:], vitaID) + return key +} + +func makeCollocatioEligByOwnerKey(owner util.Uint160) []byte { + key := make([]byte, 1+util.Uint160Size) + key[0] = collocatioPrefixEligibilityByOwner + copy(key[1:], owner.BytesBE()) + return key +} + +// ============================================================================ +// Internal Storage Methods +// ============================================================================ + +func (c *Collocatio) getConfigInternal(d *dao.Simple) *state.CollocatioConfig { + si := d.GetStorageItem(c.ID, makeCollocatioConfigKey()) + if si == nil { + return &state.CollocatioConfig{ + MinPIOParticipants: defaultMinPIOParticipants, + MinEIOParticipants: defaultMinEIOParticipants, + MinCIOParticipants: defaultMinCIOParticipants, + DefaultMinInvestment: defaultMinInvestment, + MaxIndividualCap: defaultMaxIndividualCap, + WealthConcentration: defaultWealthConcentration, + CreationFee: defaultCreationFee, + InvestmentFee: defaultInvestmentFee, + WithdrawalPenalty: defaultWithdrawalPenalty, + MinVotingPeriod: defaultMinVotingPeriod, + MinInvestmentPeriod: defaultMinInvestmentPeriod, + MinMaturityPeriod: defaultMinMaturityPeriod, + MaxViolationsBeforeBan: defaultMaxViolationsBeforeBan, + ViolationCooldown: 1000000, + } + } + cfg := new(state.CollocatioConfig) + item, _ := stackitem.Deserialize(si) + cfg.FromStackItem(item) + return cfg +} + +func (c *Collocatio) setConfigInternal(d *dao.Simple, cfg *state.CollocatioConfig) { + item, _ := cfg.ToStackItem() + data, _ := stackitem.Serialize(item) + d.PutStorageItem(c.ID, makeCollocatioConfigKey(), data) +} + +func (c *Collocatio) getCounter(d *dao.Simple, key []byte) uint64 { + si := d.GetStorageItem(c.ID, key) + if si == nil || len(si) < 8 { + return 0 + } + return binary.BigEndian.Uint64(si) +} + +func (c *Collocatio) incrementCounter(d *dao.Simple, key []byte) uint64 { + current := c.getCounter(d, key) + next := current + 1 + buf := make([]byte, 8) + binary.BigEndian.PutUint64(buf, next) + d.PutStorageItem(c.ID, key, buf) + return next +} + +func (c *Collocatio) getOpportunityInternal(d *dao.Simple, oppID uint64) *state.InvestmentOpportunity { + si := d.GetStorageItem(c.ID, makeCollocatioOppKey(oppID)) + if si == nil { + return nil + } + opp := new(state.InvestmentOpportunity) + item, _ := stackitem.Deserialize(si) + opp.FromStackItem(item) + return opp +} + +func (c *Collocatio) putOpportunity(d *dao.Simple, opp *state.InvestmentOpportunity) { + item, _ := opp.ToStackItem() + data, _ := stackitem.Serialize(item) + d.PutStorageItem(c.ID, makeCollocatioOppKey(opp.ID), data) +} + +func (c *Collocatio) getInvestmentInternal(d *dao.Simple, invID uint64) *state.Investment { + si := d.GetStorageItem(c.ID, makeCollocatioInvKey(invID)) + if si == nil { + return nil + } + inv := new(state.Investment) + item, _ := stackitem.Deserialize(si) + inv.FromStackItem(item) + return inv +} + +func (c *Collocatio) putInvestment(d *dao.Simple, inv *state.Investment) { + item, _ := inv.ToStackItem() + data, _ := stackitem.Serialize(item) + d.PutStorageItem(c.ID, makeCollocatioInvKey(inv.ID), data) +} + +func (c *Collocatio) getEligibilityInternal(d *dao.Simple, investor util.Uint160) *state.InvestorEligibility { + // First get vitaID from owner mapping + si := d.GetStorageItem(c.ID, makeCollocatioEligByOwnerKey(investor)) + if si == nil || len(si) < 8 { + return nil + } + vitaID := binary.BigEndian.Uint64(si) + + // Then get eligibility + eligSI := d.GetStorageItem(c.ID, makeCollocatioEligKey(vitaID)) + if eligSI == nil { + return nil + } + elig := new(state.InvestorEligibility) + item, _ := stackitem.Deserialize(eligSI) + elig.FromStackItem(item) + return elig +} + +func (c *Collocatio) putEligibility(d *dao.Simple, elig *state.InvestorEligibility) { + item, _ := elig.ToStackItem() + data, _ := stackitem.Serialize(item) + d.PutStorageItem(c.ID, makeCollocatioEligKey(elig.VitaID), data) + + // Also store owner -> vitaID mapping + buf := make([]byte, 8) + binary.BigEndian.PutUint64(buf, elig.VitaID) + d.PutStorageItem(c.ID, makeCollocatioEligByOwnerKey(elig.Investor), buf) +} + +// ============================================================================ +// Contract Methods +// ============================================================================ + +func (c *Collocatio) getConfig(ic *interop.Context, _ []stackitem.Item) stackitem.Item { + cfg := c.getConfigInternal(ic.DAO) + return stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.MinPIOParticipants)), + stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.MinEIOParticipants)), + stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.MinCIOParticipants)), + stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.DefaultMinInvestment)), + stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.MaxIndividualCap)), + stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.WealthConcentration)), + stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.CreationFee)), + stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.InvestmentFee)), + stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.WithdrawalPenalty)), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(cfg.MinVotingPeriod))), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(cfg.MinInvestmentPeriod))), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(cfg.MinMaturityPeriod))), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(cfg.MaxViolationsBeforeBan))), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(cfg.ViolationCooldown))), + }) +} + +func (c *Collocatio) getOpportunityCount(ic *interop.Context, _ []stackitem.Item) stackitem.Item { + count := c.getCounter(ic.DAO, makeCollocatioOppCounterKey()) + return stackitem.NewBigInteger(new(big.Int).SetUint64(count)) +} + +func (c *Collocatio) getOpportunity(ic *interop.Context, args []stackitem.Item) stackitem.Item { + oppID := toUint64(args[0]) + opp := c.getOpportunityInternal(ic.DAO, oppID) + if opp == nil { + return stackitem.Null{} + } + return opportunityToStackItem(opp) +} + +func (c *Collocatio) createOpportunity(ic *interop.Context, args []stackitem.Item) stackitem.Item { + caller := ic.VM.GetCallingScriptHash() + + oppType := state.OpportunityType(toUint64(args[0])) + name := toString(args[1]) + description := toString(args[2]) + termsHashBytes, err := args[3].TryBytes() + if err != nil { + panic(err) + } + termsHash, err := util.Uint256DecodeBytesBE(termsHashBytes) + if err != nil { + panic(err) + } + minParticipants := toUint64(args[4]) + maxParticipants := toUint64(args[5]) + minInvestment := toUint64(args[6]) + maxInvestment := toUint64(args[7]) + targetPool := toUint64(args[8]) + expectedReturns := toUint64(args[9]) + riskLevel := uint8(toUint64(args[10])) + maturityBlocks := uint32(toUint64(args[11])) + + // Validate caller has Vita + vita, err := c.Vita.GetTokenByOwner(ic.DAO, caller) + if err != nil { + panic("caller must have Vita token") + } + vitaID := vita.TokenID + + // Validate opportunity type + if oppType > state.OpportunityCIO { + panic("invalid opportunity type") + } + + // Validate parameters + cfg := c.getConfigInternal(ic.DAO) + if maturityBlocks < cfg.MinMaturityPeriod { + panic("maturity period too short") + } + if riskLevel < 1 || riskLevel > 10 { + panic("risk level must be 1-10") + } + + // Get minimum participants based on type + var minRequired uint64 + switch oppType { + case state.OpportunityPIO: + minRequired = cfg.MinPIOParticipants + case state.OpportunityEIO: + minRequired = cfg.MinEIOParticipants + case state.OpportunityCIO: + minRequired = cfg.MinCIOParticipants + } + if minParticipants < minRequired { + panic(fmt.Sprintf("minimum participants must be at least %d for this type", minRequired)) + } + + // Charge creation fee to Treasury + if cfg.CreationFee > 0 { + if err := c.VTS.transferUnrestricted(ic, caller, nativehashes.Treasury, new(big.Int).SetUint64(cfg.CreationFee), nil); err != nil { + panic("failed to pay creation fee") + } + } + + // Create opportunity + oppID := c.incrementCounter(ic.DAO, makeCollocatioOppCounterKey()) + currentBlock := ic.Block.Index + + opp := &state.InvestmentOpportunity{ + ID: oppID, + Type: oppType, + Status: state.OpportunityDraft, + Creator: caller, + SponsorVitaID: vitaID, + Name: name, + Description: description, + TermsHash: termsHash, + MinParticipants: minParticipants, + MaxParticipants: maxParticipants, + CurrentParticipants: 0, + MinInvestment: minInvestment, + MaxInvestment: maxInvestment, + TotalPool: 0, + TargetPool: targetPool, + ExpectedReturns: expectedReturns, + RiskLevel: riskLevel, + VotingDeadline: currentBlock + cfg.MinVotingPeriod, + InvestmentDeadline: currentBlock + cfg.MinVotingPeriod + cfg.MinInvestmentPeriod, + MaturityDate: currentBlock + cfg.MinVotingPeriod + cfg.MinInvestmentPeriod + maturityBlocks, + ProposalID: 0, + CreatedAt: currentBlock, + UpdatedAt: currentBlock, + } + + c.putOpportunity(ic.DAO, opp) + + // Store type index + ic.DAO.PutStorageItem(c.ID, makeCollocatioOppByTypeKey(oppType, oppID), []byte{1}) + + // Emit event + ic.AddNotification(c.Hash, collocatioOpportunityCreatedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(oppID)), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(oppType))), + stackitem.NewByteArray(caller.BytesBE()), + })) + + return stackitem.NewBigInteger(new(big.Int).SetUint64(oppID)) +} + +func (c *Collocatio) activateOpportunity(ic *interop.Context, args []stackitem.Item) stackitem.Item { + oppID := toUint64(args[0]) + + opp := c.getOpportunityInternal(ic.DAO, oppID) + if opp == nil { + panic("opportunity not found") + } + + caller := ic.VM.GetCallingScriptHash() + if caller != opp.Creator && !c.NEO.CheckCommittee(ic) { + panic("only creator or committee can activate") + } + + if opp.Status != state.OpportunityDraft { + panic("opportunity must be in draft status") + } + + opp.Status = state.OpportunityActive + opp.UpdatedAt = ic.Block.Index + c.putOpportunity(ic.DAO, opp) + + ic.AddNotification(c.Hash, collocatioOpportunityActivatedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(oppID)), + })) + + return stackitem.NewBool(true) +} + +func (c *Collocatio) invest(ic *interop.Context, args []stackitem.Item) stackitem.Item { + oppID := toUint64(args[0]) + amount := toUint64(args[1]) + caller := ic.VM.GetCallingScriptHash() + + // Validate caller has Vita + vita, err := c.Vita.GetTokenByOwner(ic.DAO, caller) + if err != nil { + panic("caller must have Vita token") + } + vitaID := vita.TokenID + + // Get opportunity + opp := c.getOpportunityInternal(ic.DAO, oppID) + if opp == nil { + panic("opportunity not found") + } + + if opp.Status != state.OpportunityActive { + panic("opportunity is not active") + } + if ic.Block.Index > opp.InvestmentDeadline { + panic("investment deadline has passed") + } + + if opp.MaxParticipants > 0 && opp.CurrentParticipants >= opp.MaxParticipants { + panic("maximum participants reached") + } + + if amount < opp.MinInvestment { + panic("investment below minimum") + } + if amount > opp.MaxInvestment { + panic("investment exceeds maximum") + } + + // Check eligibility + if !c.isEligibleInternal(ic.DAO, caller, opp.Type) { + panic("investor not eligible for this opportunity type") + } + + // Calculate fee + cfg := c.getConfigInternal(ic.DAO) + fee := (amount * cfg.InvestmentFee) / 10000 + netAmount := amount - fee + + // Transfer VTS from investor + if err := c.VTS.transferUnrestricted(ic, caller, c.Hash, new(big.Int).SetUint64(amount), nil); err != nil { + panic("failed to transfer investment amount") + } + + // Send fee to Treasury + if fee > 0 { + if err := c.VTS.transferUnrestricted(ic, c.Hash, nativehashes.Treasury, new(big.Int).SetUint64(fee), nil); err != nil { + panic("failed to transfer fee to treasury") + } + } + + // Create investment record + invID := c.incrementCounter(ic.DAO, makeCollocatioInvCounterKey()) + + inv := &state.Investment{ + ID: invID, + OpportunityID: oppID, + VitaID: vitaID, + Investor: caller, + Amount: netAmount, + Status: state.InvestmentActive, + ReturnAmount: 0, + CreatedAt: ic.Block.Index, + UpdatedAt: ic.Block.Index, + } + + c.putInvestment(ic.DAO, inv) + + // Store indexes + ic.DAO.PutStorageItem(c.ID, makeCollocatioInvByOppKey(oppID, invID), []byte{1}) + ic.DAO.PutStorageItem(c.ID, makeCollocatioInvByInvestorKey(vitaID, invID), []byte{1}) + + // Update opportunity + opp.CurrentParticipants++ + opp.TotalPool += netAmount + opp.UpdatedAt = ic.Block.Index + c.putOpportunity(ic.DAO, opp) + + // Update eligibility stats + c.updateEligibilityOnInvest(ic.DAO, caller, vitaID, netAmount, ic.Block.Index) + + // Emit event + ic.AddNotification(c.Hash, collocatioInvestmentMadeEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(invID)), + stackitem.NewBigInteger(new(big.Int).SetUint64(oppID)), + stackitem.NewByteArray(caller.BytesBE()), + stackitem.NewBigInteger(new(big.Int).SetUint64(netAmount)), + })) + + return stackitem.NewBigInteger(new(big.Int).SetUint64(invID)) +} + +func (c *Collocatio) withdraw(ic *interop.Context, args []stackitem.Item) stackitem.Item { + invID := toUint64(args[0]) + caller := ic.VM.GetCallingScriptHash() + + inv := c.getInvestmentInternal(ic.DAO, invID) + if inv == nil { + panic("investment not found") + } + + if inv.Investor != caller { + panic("only investor can withdraw") + } + + if inv.Status != state.InvestmentActive { + panic("investment is not active") + } + + opp := c.getOpportunityInternal(ic.DAO, inv.OpportunityID) + if opp == nil { + panic("opportunity not found") + } + + // Calculate penalty for early withdrawal + cfg := c.getConfigInternal(ic.DAO) + returnAmount := inv.Amount + if ic.Block.Index < opp.MaturityDate && cfg.WithdrawalPenalty > 0 { + penalty := (inv.Amount * cfg.WithdrawalPenalty) / 10000 + returnAmount = inv.Amount - penalty + if penalty > 0 { + if err := c.VTS.transferUnrestricted(ic, c.Hash, nativehashes.Treasury, new(big.Int).SetUint64(penalty), nil); err != nil { + panic("failed to transfer penalty") + } + } + } + + // Return funds + if err := c.VTS.transferUnrestricted(ic, c.Hash, caller, new(big.Int).SetUint64(returnAmount), nil); err != nil { + panic("failed to return investment") + } + + // Update investment + inv.Status = state.InvestmentWithdrawn + inv.UpdatedAt = ic.Block.Index + c.putInvestment(ic.DAO, inv) + + // Update opportunity + opp.CurrentParticipants-- + opp.TotalPool -= inv.Amount + opp.UpdatedAt = ic.Block.Index + c.putOpportunity(ic.DAO, opp) + + // Update eligibility + c.updateEligibilityOnWithdraw(ic.DAO, caller, inv.Amount, ic.Block.Index) + + // Emit event + ic.AddNotification(c.Hash, collocatioInvestmentWithdrawnEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(invID)), + stackitem.NewBigInteger(new(big.Int).SetUint64(returnAmount)), + })) + + return stackitem.NewBool(true) +} + +func (c *Collocatio) getEligibility(ic *interop.Context, args []stackitem.Item) stackitem.Item { + investor := toUint160(args[0]) + elig := c.getEligibilityInternal(ic.DAO, investor) + if elig == nil { + return stackitem.Null{} + } + return eligibilityToStackItem(elig) +} + +func (c *Collocatio) setEligibility(ic *interop.Context, args []stackitem.Item) stackitem.Item { + investor := toUint160(args[0]) + eligFlags := state.EligibilityType(toUint64(args[1])) + + // Only committee or RoleInvestmentManager can set eligibility + if !c.NEO.CheckCommittee(ic) { + caller := ic.VM.GetCallingScriptHash() + if !c.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleInvestmentManager, ic.Block.Index) { + panic("only committee or investment manager can set eligibility") + } + } + + vita, err := c.Vita.GetTokenByOwner(ic.DAO, investor) + if err != nil { + panic("investor must have Vita token") + } + vitaID := vita.TokenID + + elig := c.getEligibilityInternal(ic.DAO, investor) + if elig == nil { + elig = &state.InvestorEligibility{ + VitaID: vitaID, + Investor: investor, + CreatedAt: ic.Block.Index, + } + } + + elig.Eligibility = eligFlags + elig.UpdatedAt = ic.Block.Index + c.putEligibility(ic.DAO, elig) + + ic.AddNotification(c.Hash, collocatioEligibilityUpdatedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewByteArray(investor.BytesBE()), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(eligFlags))), + })) + + return stackitem.NewBool(true) +} + +func (c *Collocatio) isEligible(ic *interop.Context, args []stackitem.Item) stackitem.Item { + investor := toUint160(args[0]) + oppType := state.OpportunityType(toUint64(args[1])) + return stackitem.NewBool(c.isEligibleInternal(ic.DAO, investor, oppType)) +} + +func (c *Collocatio) isEligibleInternal(d *dao.Simple, investor util.Uint160, oppType state.OpportunityType) bool { + elig := c.getEligibilityInternal(d, investor) + if elig == nil { + return false + } + + // Must have completed investment education + if !elig.ScireCompleted { + return false + } + + // Check for ban + if elig.HasViolations { + cfg := c.getConfigInternal(d) + if elig.ViolationCount >= cfg.MaxViolationsBeforeBan { + return false + } + } + + switch oppType { + case state.OpportunityPIO: + return elig.Eligibility&state.EligibilityPIO != 0 + case state.OpportunityEIO: + return elig.Eligibility&state.EligibilityEIO != 0 + case state.OpportunityCIO: + return elig.Eligibility&state.EligibilityCIO != 0 + default: + return false + } +} + +func (c *Collocatio) getInvestment(ic *interop.Context, args []stackitem.Item) stackitem.Item { + invID := toUint64(args[0]) + inv := c.getInvestmentInternal(ic.DAO, invID) + if inv == nil { + return stackitem.Null{} + } + return investmentToStackItem(inv) +} + +func (c *Collocatio) getInvestmentCount(ic *interop.Context, _ []stackitem.Item) stackitem.Item { + count := c.getCounter(ic.DAO, makeCollocatioInvCounterKey()) + return stackitem.NewBigInteger(new(big.Int).SetUint64(count)) +} + +// ============================================================================ +// Internal Helpers +// ============================================================================ + +func (c *Collocatio) updateEligibilityOnInvest(d *dao.Simple, investor util.Uint160, vitaID, amount uint64, blockHeight uint32) { + elig := c.getEligibilityInternal(d, investor) + if elig == nil { + elig = &state.InvestorEligibility{ + VitaID: vitaID, + Investor: investor, + CreatedAt: blockHeight, + } + } + elig.TotalInvested += amount + elig.ActiveInvestments++ + elig.LastActivity = blockHeight + elig.UpdatedAt = blockHeight + c.putEligibility(d, elig) +} + +func (c *Collocatio) updateEligibilityOnWithdraw(d *dao.Simple, investor util.Uint160, amount uint64, blockHeight uint32) { + elig := c.getEligibilityInternal(d, investor) + if elig == nil { + return + } + if elig.TotalInvested >= amount { + elig.TotalInvested -= amount + } + if elig.ActiveInvestments > 0 { + elig.ActiveInvestments-- + } + elig.LastActivity = blockHeight + elig.UpdatedAt = blockHeight + c.putEligibility(d, elig) +} + +// ============================================================================ +// Stack Item Converters +// ============================================================================ + +func opportunityToStackItem(opp *state.InvestmentOpportunity) stackitem.Item { + return stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(opp.ID)), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(opp.Type))), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(opp.Status))), + stackitem.NewByteArray(opp.Creator.BytesBE()), + stackitem.NewBigInteger(new(big.Int).SetUint64(opp.SponsorVitaID)), + stackitem.NewByteArray([]byte(opp.Name)), + stackitem.NewByteArray([]byte(opp.Description)), + stackitem.NewByteArray(opp.TermsHash.BytesBE()), + stackitem.NewBigInteger(new(big.Int).SetUint64(opp.MinParticipants)), + stackitem.NewBigInteger(new(big.Int).SetUint64(opp.MaxParticipants)), + stackitem.NewBigInteger(new(big.Int).SetUint64(opp.CurrentParticipants)), + stackitem.NewBigInteger(new(big.Int).SetUint64(opp.MinInvestment)), + stackitem.NewBigInteger(new(big.Int).SetUint64(opp.MaxInvestment)), + stackitem.NewBigInteger(new(big.Int).SetUint64(opp.TotalPool)), + stackitem.NewBigInteger(new(big.Int).SetUint64(opp.TargetPool)), + stackitem.NewBigInteger(new(big.Int).SetUint64(opp.ExpectedReturns)), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(opp.RiskLevel))), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(opp.VotingDeadline))), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(opp.InvestmentDeadline))), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(opp.MaturityDate))), + stackitem.NewBigInteger(new(big.Int).SetUint64(opp.ProposalID)), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(opp.CreatedAt))), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(opp.UpdatedAt))), + }) +} + +func investmentToStackItem(inv *state.Investment) stackitem.Item { + return stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(inv.ID)), + stackitem.NewBigInteger(new(big.Int).SetUint64(inv.OpportunityID)), + stackitem.NewBigInteger(new(big.Int).SetUint64(inv.VitaID)), + stackitem.NewByteArray(inv.Investor.BytesBE()), + stackitem.NewBigInteger(new(big.Int).SetUint64(inv.Amount)), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(inv.Status))), + stackitem.NewBigInteger(new(big.Int).SetUint64(inv.ReturnAmount)), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(inv.CreatedAt))), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(inv.UpdatedAt))), + }) +} + +func eligibilityToStackItem(elig *state.InvestorEligibility) stackitem.Item { + return stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(elig.VitaID)), + stackitem.NewByteArray(elig.Investor.BytesBE()), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(elig.Eligibility))), + stackitem.NewBool(elig.ScireCompleted), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(elig.RiskScore))), + stackitem.NewBigInteger(new(big.Int).SetUint64(elig.TotalInvested)), + stackitem.NewBigInteger(new(big.Int).SetUint64(elig.TotalReturns)), + stackitem.NewBigInteger(new(big.Int).SetUint64(elig.ActiveInvestments)), + stackitem.NewBigInteger(new(big.Int).SetUint64(elig.CompletedInvestments)), + stackitem.NewBool(elig.HasViolations), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(elig.ViolationCount))), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(elig.LastActivity))), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(elig.CreatedAt))), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(elig.UpdatedAt))), + }) +} diff --git a/pkg/core/native/contract.go b/pkg/core/native/contract.go index 6473995..abb6f5c 100644 --- a/pkg/core/native/contract.go +++ b/pkg/core/native/contract.go @@ -644,6 +644,16 @@ func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract { pons.Scire = scire pons.Salus = salus + // Create Collocatio (Democratic Investment) contract + collocatio := newCollocatio() + collocatio.NEO = neo + collocatio.Vita = vita + collocatio.RoleRegistry = roleRegistry + collocatio.VTS = vts + collocatio.Scire = scire + collocatio.Eligere = eligere + collocatio.Tribute = tribute + return []interop.Contract{ mgmt, s, @@ -669,5 +679,6 @@ func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract { opus, palam, pons, + collocatio, } } diff --git a/pkg/core/native/native_test/management_test.go b/pkg/core/native/native_test/management_test.go index e2795f9..17bd979 100644 --- a/pkg/core/native/native_test/management_test.go +++ b/pkg/core/native/native_test/management_test.go @@ -63,6 +63,7 @@ var ( nativenames.Opus: `{"id":-22,"hash":"0xfdb69606e4e5c74708513a7c4dfcab98177322f5","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQA==","checksum":323743677},"manifest":{"name":"Opus","abi":{"methods":[{"name":"addCapability","offset":0,"parameters":[{"name":"workerID","type":"Integer"},{"name":"name","type":"String"},{"name":"category","type":"String"},{"name":"level","type":"Integer"}],"returntype":"Integer","safe":false},{"name":"assignTask","offset":7,"parameters":[{"name":"workerID","type":"Integer"},{"name":"taskType","type":"String"},{"name":"description","type":"String"},{"name":"valueOffered","type":"Integer"}],"returntype":"Integer","safe":false},{"name":"cancelTask","offset":14,"parameters":[{"name":"taskID","type":"Integer"},{"name":"reason","type":"String"}],"returntype":"Boolean","safe":false},{"name":"completeTask","offset":21,"parameters":[{"name":"taskID","type":"Integer"},{"name":"resultHash","type":"Hash256"},{"name":"valueCompleted","type":"Integer"}],"returntype":"Boolean","safe":false},{"name":"decommissionWorker","offset":28,"parameters":[{"name":"workerID","type":"Integer"},{"name":"reason","type":"String"}],"returntype":"Boolean","safe":false},{"name":"failTask","offset":35,"parameters":[{"name":"taskID","type":"Integer"},{"name":"reason","type":"String"}],"returntype":"Boolean","safe":false},{"name":"getActiveTask","offset":42,"parameters":[{"name":"workerID","type":"Integer"}],"returntype":"Array","safe":true},{"name":"getCapability","offset":49,"parameters":[{"name":"capabilityID","type":"Integer"}],"returntype":"Array","safe":true},{"name":"getConfig","offset":56,"parameters":[],"returntype":"Array","safe":true},{"name":"getOperator","offset":63,"parameters":[{"name":"owner","type":"Hash160"}],"returntype":"Array","safe":true},{"name":"getTask","offset":70,"parameters":[{"name":"taskID","type":"Integer"}],"returntype":"Array","safe":true},{"name":"getTotalCapabilities","offset":77,"parameters":[],"returntype":"Integer","safe":true},{"name":"getTotalOperators","offset":84,"parameters":[],"returntype":"Integer","safe":true},{"name":"getTotalTasks","offset":91,"parameters":[],"returntype":"Integer","safe":true},{"name":"getTotalTribute","offset":98,"parameters":[],"returntype":"Integer","safe":true},{"name":"getTotalWorkers","offset":105,"parameters":[],"returntype":"Integer","safe":true},{"name":"getWorker","offset":112,"parameters":[{"name":"workerID","type":"Integer"}],"returntype":"Array","safe":true},{"name":"getWorkerCountByOperator","offset":119,"parameters":[{"name":"owner","type":"Hash160"}],"returntype":"Integer","safe":true},{"name":"hasCapability","offset":126,"parameters":[{"name":"workerID","type":"Integer"},{"name":"name","type":"String"}],"returntype":"Boolean","safe":true},{"name":"reactivateWorker","offset":133,"parameters":[{"name":"workerID","type":"Integer"}],"returntype":"Boolean","safe":false},{"name":"registerOperator","offset":140,"parameters":[{"name":"owner","type":"Hash160"}],"returntype":"Boolean","safe":false},{"name":"registerWorker","offset":147,"parameters":[{"name":"name","type":"String"},{"name":"description","type":"String"},{"name":"workerType","type":"Integer"},{"name":"modelHash","type":"Hash256"}],"returntype":"Integer","safe":false},{"name":"setConfig","offset":154,"parameters":[{"name":"minTributeRate","type":"Integer"},{"name":"maxTributeRate","type":"Integer"},{"name":"defaultTributeRate","type":"Integer"},{"name":"registrationFee","type":"Integer"}],"returntype":"Boolean","safe":false},{"name":"startTask","offset":161,"parameters":[{"name":"taskID","type":"Integer"}],"returntype":"Boolean","safe":false},{"name":"suspendWorker","offset":168,"parameters":[{"name":"workerID","type":"Integer"},{"name":"reason","type":"String"}],"returntype":"Boolean","safe":false},{"name":"updateWorker","offset":175,"parameters":[{"name":"workerID","type":"Integer"},{"name":"description","type":"String"}],"returntype":"Boolean","safe":false},{"name":"verifyCapability","offset":182,"parameters":[{"name":"capabilityID","type":"Integer"}],"returntype":"Boolean","safe":false},{"name":"verifyOperator","offset":189,"parameters":[{"name":"owner","type":"Hash160"}],"returntype":"Boolean","safe":false}],"events":[]},"features":{},"groups":[],"permissions":[{"contract":"*","methods":"*"}],"supportedstandards":[],"trusts":[],"extra":null},"updatecounter":0}`, nativenames.Palam: `{"id":-23,"hash":"0xae16798853df46b0e2fa8c8dab343a71fa732303","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQA==","checksum":1841570703},"manifest":{"name":"Palam","abi":{"methods":[{"name":"approveDeclassify","offset":0,"parameters":[{"name":"requestID","type":"Integer"}],"returntype":"Boolean","safe":false},{"name":"attachToFlow","offset":7,"parameters":[{"name":"flowID","type":"Hash256"},{"name":"attachmentType","type":"String"},{"name":"encryptedData","type":"ByteArray"}],"returntype":"Integer","safe":false},{"name":"denyDeclassify","offset":14,"parameters":[{"name":"requestID","type":"Integer"},{"name":"reason","type":"String"}],"returntype":"Boolean","safe":false},{"name":"getAccessLog","offset":21,"parameters":[{"name":"logID","type":"Integer"}],"returntype":"Array","safe":true},{"name":"getAttachment","offset":28,"parameters":[{"name":"attachmentID","type":"Integer"}],"returntype":"Array","safe":true},{"name":"getConfig","offset":35,"parameters":[],"returntype":"Array","safe":true},{"name":"getDeclassifyRequest","offset":42,"parameters":[{"name":"requestID","type":"Integer"}],"returntype":"Array","safe":true},{"name":"getFlow","offset":49,"parameters":[{"name":"flowID","type":"Hash256"}],"returntype":"Array","safe":false},{"name":"getRolePermissions","offset":56,"parameters":[{"name":"role","type":"Integer"}],"returntype":"Array","safe":true},{"name":"getTotalAttachments","offset":63,"parameters":[],"returntype":"Integer","safe":true},{"name":"getTotalFlows","offset":70,"parameters":[],"returntype":"Integer","safe":true},{"name":"getTotalLogs","offset":77,"parameters":[],"returntype":"Integer","safe":true},{"name":"getTotalRequests","offset":84,"parameters":[],"returntype":"Integer","safe":true},{"name":"hasDeclassifyGrant","offset":91,"parameters":[{"name":"flowID","type":"Hash256"},{"name":"requester","type":"Hash160"}],"returntype":"Boolean","safe":true},{"name":"recordFlow","offset":98,"parameters":[{"name":"tag","type":"String"},{"name":"amount","type":"Integer"},{"name":"bucket","type":"String"},{"name":"participants","type":"Array"},{"name":"consumerData","type":"ByteArray"},{"name":"merchantData","type":"ByteArray"},{"name":"distributorData","type":"ByteArray"},{"name":"producerData","type":"ByteArray"},{"name":"ngoData","type":"ByteArray"},{"name":"auditorData","type":"ByteArray"},{"name":"previousFlowID","type":"Hash256"}],"returntype":"Hash256","safe":false},{"name":"requestDeclassify","offset":105,"parameters":[{"name":"flowID","type":"Hash256"},{"name":"caseID","type":"String"},{"name":"reason","type":"String"},{"name":"expirationBlocks","type":"Integer"}],"returntype":"Integer","safe":false}],"events":[]},"features":{},"groups":[],"permissions":[{"contract":"*","methods":"*"}],"supportedstandards":[],"trusts":[],"extra":null},"updatecounter":0}`, nativenames.Pons: `{"id":-24,"hash":"0xccdd81357cc7a7532b809a3f928cb8a519d03958","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dA","checksum":1991619121},"manifest":{"name":"Pons","abi":{"methods":[{"name":"cancelSettlement","offset":0,"parameters":[{"name":"settlementID","type":"Integer"}],"returntype":"Boolean","safe":false},{"name":"completeSettlement","offset":7,"parameters":[{"name":"settlementID","type":"Integer"},{"name":"txHash","type":"Hash256"}],"returntype":"Boolean","safe":false},{"name":"createAgreement","offset":14,"parameters":[{"name":"remoteChainID","type":"Integer"},{"name":"agreementType","type":"Integer"},{"name":"termsHash","type":"Hash256"},{"name":"expirationHeight","type":"Integer"}],"returntype":"Integer","safe":false},{"name":"getAgreement","offset":21,"parameters":[{"name":"agreementID","type":"Integer"}],"returntype":"Array","safe":true},{"name":"getAgreementCount","offset":28,"parameters":[],"returntype":"Integer","safe":true},{"name":"getConfig","offset":35,"parameters":[],"returntype":"Array","safe":true},{"name":"getCredentialShare","offset":42,"parameters":[{"name":"shareID","type":"Integer"}],"returntype":"Array","safe":true},{"name":"getCredentialShareCount","offset":49,"parameters":[],"returntype":"Integer","safe":true},{"name":"getSettlementCount","offset":56,"parameters":[],"returntype":"Integer","safe":true},{"name":"getSettlementRequest","offset":63,"parameters":[{"name":"settlementID","type":"Integer"}],"returntype":"Array","safe":true},{"name":"getVerificationCount","offset":70,"parameters":[],"returntype":"Integer","safe":true},{"name":"getVerificationRequest","offset":77,"parameters":[{"name":"requestID","type":"Integer"}],"returntype":"Array","safe":true},{"name":"hasActiveAgreement","offset":84,"parameters":[{"name":"remoteChainID","type":"Integer"},{"name":"agreementType","type":"Integer"}],"returntype":"Boolean","safe":true},{"name":"requestSettlement","offset":91,"parameters":[{"name":"toChainID","type":"Integer"},{"name":"receiver","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"reference","type":"String"}],"returntype":"Integer","safe":false},{"name":"requestVerification","offset":98,"parameters":[{"name":"targetChainID","type":"Integer"},{"name":"subject","type":"Hash160"},{"name":"verificationType","type":"Integer"},{"name":"dataHash","type":"Hash256"}],"returntype":"Integer","safe":false},{"name":"respondVerification","offset":105,"parameters":[{"name":"requestID","type":"Integer"},{"name":"approved","type":"Boolean"},{"name":"responseHash","type":"Hash256"}],"returntype":"Boolean","safe":false},{"name":"revokeCredentialShare","offset":112,"parameters":[{"name":"shareID","type":"Integer"}],"returntype":"Boolean","safe":false},{"name":"setLocalChainID","offset":119,"parameters":[{"name":"chainID","type":"Integer"}],"returntype":"Boolean","safe":false},{"name":"shareCredential","offset":126,"parameters":[{"name":"targetChainID","type":"Integer"},{"name":"credentialType","type":"Integer"},{"name":"credentialID","type":"Integer"},{"name":"contentHash","type":"Hash256"},{"name":"validUntil","type":"Integer"}],"returntype":"Integer","safe":false},{"name":"updateAgreementStatus","offset":133,"parameters":[{"name":"agreementID","type":"Integer"},{"name":"newStatus","type":"Integer"}],"returntype":"Boolean","safe":false},{"name":"verifyCredentialShare","offset":140,"parameters":[{"name":"shareID","type":"Integer"}],"returntype":"Boolean","safe":true}],"events":[{"name":"AgreementCreated","parameters":[{"name":"agreementID","type":"Integer"},{"name":"remoteChainID","type":"Integer"},{"name":"agreementType","type":"Integer"}]},{"name":"AgreementUpdated","parameters":[{"name":"agreementID","type":"Integer"},{"name":"oldStatus","type":"Integer"},{"name":"newStatus","type":"Integer"}]},{"name":"AgreementTerminated","parameters":[{"name":"agreementID","type":"Integer"},{"name":"remoteChainID","type":"Integer"}]},{"name":"VerificationRequested","parameters":[{"name":"requestID","type":"Integer"},{"name":"targetChainID","type":"Integer"},{"name":"subject","type":"Hash160"},{"name":"verificationType","type":"Integer"}]},{"name":"VerificationResponded","parameters":[{"name":"requestID","type":"Integer"},{"name":"approved","type":"Boolean"}]},{"name":"SettlementRequested","parameters":[{"name":"settlementID","type":"Integer"},{"name":"toChainID","type":"Integer"},{"name":"receiver","type":"Hash160"},{"name":"amount","type":"Integer"}]},{"name":"SettlementCompleted","parameters":[{"name":"settlementID","type":"Integer"},{"name":"txHash","type":"Hash256"}]},{"name":"CredentialShared","parameters":[{"name":"shareID","type":"Integer"},{"name":"owner","type":"Hash160"},{"name":"targetChainID","type":"Integer"},{"name":"credentialType","type":"Integer"}]},{"name":"CredentialRevoked","parameters":[{"name":"shareID","type":"Integer"},{"name":"owner","type":"Hash160"}]}]},"features":{},"groups":[],"permissions":[{"contract":"*","methods":"*"}],"supportedstandards":[],"trusts":[],"extra":null},"updatecounter":0}`, + nativenames.Collocatio: `{"id":-25,"hash":"0xf528c65355bc33dd95132969d0a003eaeb859cf9","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dA","checksum":3581846399},"manifest":{"name":"Collocatio","abi":{"methods":[{"name":"activateOpportunity","offset":0,"parameters":[{"name":"opportunityID","type":"Integer"}],"returntype":"Boolean","safe":false},{"name":"createOpportunity","offset":7,"parameters":[{"name":"oppType","type":"Integer"},{"name":"name","type":"String"},{"name":"description","type":"String"},{"name":"termsHash","type":"Hash256"},{"name":"minParticipants","type":"Integer"},{"name":"maxParticipants","type":"Integer"},{"name":"minInvestment","type":"Integer"},{"name":"maxInvestment","type":"Integer"},{"name":"targetPool","type":"Integer"},{"name":"expectedReturns","type":"Integer"},{"name":"riskLevel","type":"Integer"},{"name":"maturityBlocks","type":"Integer"}],"returntype":"Integer","safe":false},{"name":"getConfig","offset":14,"parameters":[],"returntype":"Array","safe":true},{"name":"getEligibility","offset":21,"parameters":[{"name":"investor","type":"Hash160"}],"returntype":"Array","safe":true},{"name":"getInvestment","offset":28,"parameters":[{"name":"investmentID","type":"Integer"}],"returntype":"Array","safe":true},{"name":"getInvestmentCount","offset":35,"parameters":[],"returntype":"Integer","safe":true},{"name":"getOpportunity","offset":42,"parameters":[{"name":"opportunityID","type":"Integer"}],"returntype":"Array","safe":true},{"name":"getOpportunityCount","offset":49,"parameters":[],"returntype":"Integer","safe":true},{"name":"invest","offset":56,"parameters":[{"name":"opportunityID","type":"Integer"},{"name":"amount","type":"Integer"}],"returntype":"Integer","safe":false},{"name":"isEligible","offset":63,"parameters":[{"name":"investor","type":"Hash160"},{"name":"oppType","type":"Integer"}],"returntype":"Boolean","safe":true},{"name":"setEligibility","offset":70,"parameters":[{"name":"investor","type":"Hash160"},{"name":"eligibilityFlags","type":"Integer"}],"returntype":"Boolean","safe":false},{"name":"withdraw","offset":77,"parameters":[{"name":"investmentID","type":"Integer"}],"returntype":"Boolean","safe":false}],"events":[{"name":"OpportunityCreated","parameters":[{"name":"opportunityID","type":"Integer"},{"name":"oppType","type":"Integer"},{"name":"creator","type":"Hash160"}]},{"name":"OpportunityActivated","parameters":[{"name":"opportunityID","type":"Integer"}]},{"name":"InvestmentMade","parameters":[{"name":"investmentID","type":"Integer"},{"name":"opportunityID","type":"Integer"},{"name":"investor","type":"Hash160"},{"name":"amount","type":"Integer"}]},{"name":"InvestmentWithdrawn","parameters":[{"name":"investmentID","type":"Integer"},{"name":"returnAmount","type":"Integer"}]},{"name":"EligibilityUpdated","parameters":[{"name":"investor","type":"Hash160"},{"name":"eligibility","type":"Integer"}]}]},"features":{},"groups":[],"permissions":[{"contract":"*","methods":"*"}],"supportedstandards":[],"trusts":[],"extra":null},"updatecounter":0}`, } // cockatriceCSS holds serialized native contract states built for genesis block (with UpdateCounter 0) // under assumption that hardforks from Aspidochelone to Cockatrice (included) are enabled. diff --git a/pkg/core/native/nativehashes/hashes.go b/pkg/core/native/nativehashes/hashes.go index 7c610cc..7c44055 100644 --- a/pkg/core/native/nativehashes/hashes.go +++ b/pkg/core/native/nativehashes/hashes.go @@ -57,4 +57,6 @@ var ( Palam = util.Uint160{0x3, 0x23, 0x73, 0xfa, 0x71, 0x3a, 0x34, 0xab, 0x8d, 0x8c, 0xfa, 0xe2, 0xb0, 0x46, 0xdf, 0x53, 0x88, 0x79, 0x16, 0xae} // Pons is a hash of native Pons contract. Pons = util.Uint160{0x58, 0x39, 0xd0, 0x19, 0xa5, 0xb8, 0x8c, 0x92, 0x3f, 0x9a, 0x80, 0x2b, 0x53, 0xa7, 0xc7, 0x7c, 0x35, 0x81, 0xdd, 0xcc} + // Collocatio is a hash of native Collocatio contract. + Collocatio = util.Uint160{0xf9, 0x9c, 0x85, 0xeb, 0xea, 0x3, 0xa0, 0xd0, 0x69, 0x29, 0x13, 0x95, 0xdd, 0x33, 0xbc, 0x55, 0x53, 0xc6, 0x28, 0xf5} ) diff --git a/pkg/core/native/nativeids/ids.go b/pkg/core/native/nativeids/ids.go index 573d745..b8aa9bd 100644 --- a/pkg/core/native/nativeids/ids.go +++ b/pkg/core/native/nativeids/ids.go @@ -55,4 +55,6 @@ const ( Palam int32 = -23 // Pons is an ID of native Pons contract. Pons int32 = -24 + // Collocatio is an ID of native Collocatio contract. + Collocatio int32 = -25 ) diff --git a/pkg/core/native/nativenames/names.go b/pkg/core/native/nativenames/names.go index dd57807..4b2b5ad 100644 --- a/pkg/core/native/nativenames/names.go +++ b/pkg/core/native/nativenames/names.go @@ -24,8 +24,9 @@ const ( Sese = "Sese" Tribute = "Tribute" Opus = "Opus" - Palam = "Palam" - Pons = "Pons" + Palam = "Palam" + Pons = "Pons" + Collocatio = "Collocatio" ) // All contains the list of all native contract names ordered by the contract ID. @@ -54,6 +55,7 @@ var All = []string{ Opus, Palam, Pons, + Collocatio, } // IsValid checks if the name is a valid native contract's name. @@ -81,5 +83,6 @@ func IsValid(name string) bool { name == Tribute || name == Opus || name == Palam || - name == Pons + name == Pons || + name == Collocatio } diff --git a/pkg/core/state/collocatio.go b/pkg/core/state/collocatio.go new file mode 100644 index 0000000..328bbcb --- /dev/null +++ b/pkg/core/state/collocatio.go @@ -0,0 +1,955 @@ +package state + +import ( + "errors" + "fmt" + "math/big" + + "github.com/tutus-one/tutus-chain/pkg/io" + "github.com/tutus-one/tutus-chain/pkg/util" + "github.com/tutus-one/tutus-chain/pkg/vm/stackitem" +) + +// OpportunityType represents the type of investment opportunity. +type OpportunityType uint8 + +// Investment opportunity types. +const ( + OpportunityPIO OpportunityType = 0 // Public Investment Opportunity - universal access + OpportunityEIO OpportunityType = 1 // Employee Investment Opportunity - workplace + OpportunityCIO OpportunityType = 2 // Contractor Investment Opportunity - gig economy +) + +// OpportunityStatus represents the status of an investment opportunity. +type OpportunityStatus uint8 + +// Investment opportunity statuses. +const ( + OpportunityDraft OpportunityStatus = 0 // Being created + OpportunityVoting OpportunityStatus = 1 // Under democratic voting + OpportunityActive OpportunityStatus = 2 // Open for investments + OpportunityClosed OpportunityStatus = 3 // No longer accepting investments + OpportunityCompleted OpportunityStatus = 4 // Returns distributed + OpportunityCancelled OpportunityStatus = 5 // Cancelled with refunds + OpportunityFailed OpportunityStatus = 6 // Did not meet minimum requirements +) + +// InvestmentStatus represents the status of an individual investment. +type InvestmentStatus uint8 + +// Investment statuses. +const ( + InvestmentActive InvestmentStatus = 0 // Investment is active + InvestmentWithdrawn InvestmentStatus = 1 // Investor withdrew + InvestmentCompleted InvestmentStatus = 2 // Returns distributed + InvestmentRefunded InvestmentStatus = 3 // Refunded due to cancellation +) + +// EligibilityType represents eligibility flags. +type EligibilityType uint8 + +// Eligibility flags (can be combined). +const ( + EligibilityNone EligibilityType = 0 + EligibilityPIO EligibilityType = 1 << 0 // Public investment eligible + EligibilityEIO EligibilityType = 1 << 1 // Employee investment eligible + EligibilityCIO EligibilityType = 1 << 2 // Contractor investment eligible + EligibilityAll EligibilityType = EligibilityPIO | EligibilityEIO | EligibilityCIO +) + +// InvestmentOpportunity represents an investment opportunity. +type InvestmentOpportunity struct { + ID uint64 // Unique opportunity identifier + Type OpportunityType // PIO, EIO, or CIO + Status OpportunityStatus // Current status + Creator util.Uint160 // Creator's address (must have Vita) + SponsorVitaID uint64 // Sponsor's Vita ID (for EIO: employer, CIO: platform) + Name string // Opportunity name + Description string // Investment description + TermsHash util.Uint256 // Hash of full terms (off-chain) + MinParticipants uint64 // Required minimum participants + MaxParticipants uint64 // Maximum allowed participants (0 = unlimited) + CurrentParticipants uint64 // Current number of participants + MinInvestment uint64 // Minimum VTS investment per person + MaxInvestment uint64 // Maximum VTS investment per person + TotalPool uint64 // Total VTS in opportunity + TargetPool uint64 // Target total pool size + ExpectedReturns uint64 // Projected returns (basis points, 10000 = 100%) + RiskLevel uint8 // Risk level 1-10 (AI-assessed) + VotingDeadline uint32 // Block height for voting deadline + InvestmentDeadline uint32 // Block height for investment deadline + MaturityDate uint32 // Block height when returns distributed + ProposalID uint64 // Associated Eligere proposal ID (0 = none) + CreatedAt uint32 // Block height when created + UpdatedAt uint32 // Block height when last updated +} + +// DecodeBinary implements the io.Serializable interface. +func (o *InvestmentOpportunity) DecodeBinary(br *io.BinReader) { + o.ID = br.ReadU64LE() + o.Type = OpportunityType(br.ReadB()) + o.Status = OpportunityStatus(br.ReadB()) + br.ReadBytes(o.Creator[:]) + o.SponsorVitaID = br.ReadU64LE() + o.Name = br.ReadString() + o.Description = br.ReadString() + br.ReadBytes(o.TermsHash[:]) + o.MinParticipants = br.ReadU64LE() + o.MaxParticipants = br.ReadU64LE() + o.CurrentParticipants = br.ReadU64LE() + o.MinInvestment = br.ReadU64LE() + o.MaxInvestment = br.ReadU64LE() + o.TotalPool = br.ReadU64LE() + o.TargetPool = br.ReadU64LE() + o.ExpectedReturns = br.ReadU64LE() + o.RiskLevel = br.ReadB() + o.VotingDeadline = br.ReadU32LE() + o.InvestmentDeadline = br.ReadU32LE() + o.MaturityDate = br.ReadU32LE() + o.ProposalID = br.ReadU64LE() + o.CreatedAt = br.ReadU32LE() + o.UpdatedAt = br.ReadU32LE() +} + +// EncodeBinary implements the io.Serializable interface. +func (o *InvestmentOpportunity) EncodeBinary(bw *io.BinWriter) { + bw.WriteU64LE(o.ID) + bw.WriteB(byte(o.Type)) + bw.WriteB(byte(o.Status)) + bw.WriteBytes(o.Creator[:]) + bw.WriteU64LE(o.SponsorVitaID) + bw.WriteString(o.Name) + bw.WriteString(o.Description) + bw.WriteBytes(o.TermsHash[:]) + bw.WriteU64LE(o.MinParticipants) + bw.WriteU64LE(o.MaxParticipants) + bw.WriteU64LE(o.CurrentParticipants) + bw.WriteU64LE(o.MinInvestment) + bw.WriteU64LE(o.MaxInvestment) + bw.WriteU64LE(o.TotalPool) + bw.WriteU64LE(o.TargetPool) + bw.WriteU64LE(o.ExpectedReturns) + bw.WriteB(o.RiskLevel) + bw.WriteU32LE(o.VotingDeadline) + bw.WriteU32LE(o.InvestmentDeadline) + bw.WriteU32LE(o.MaturityDate) + bw.WriteU64LE(o.ProposalID) + bw.WriteU32LE(o.CreatedAt) + bw.WriteU32LE(o.UpdatedAt) +} + +// ToStackItem implements stackitem.Convertible interface. +func (o *InvestmentOpportunity) ToStackItem() (stackitem.Item, error) { + return stackitem.NewStruct([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(o.ID))), + stackitem.NewBigInteger(big.NewInt(int64(o.Type))), + stackitem.NewBigInteger(big.NewInt(int64(o.Status))), + stackitem.NewByteArray(o.Creator.BytesBE()), + stackitem.NewBigInteger(big.NewInt(int64(o.SponsorVitaID))), + stackitem.NewByteArray([]byte(o.Name)), + stackitem.NewByteArray([]byte(o.Description)), + stackitem.NewByteArray(o.TermsHash.BytesBE()), + stackitem.NewBigInteger(big.NewInt(int64(o.MinParticipants))), + stackitem.NewBigInteger(big.NewInt(int64(o.MaxParticipants))), + stackitem.NewBigInteger(big.NewInt(int64(o.CurrentParticipants))), + stackitem.NewBigInteger(big.NewInt(int64(o.MinInvestment))), + stackitem.NewBigInteger(big.NewInt(int64(o.MaxInvestment))), + stackitem.NewBigInteger(big.NewInt(int64(o.TotalPool))), + stackitem.NewBigInteger(big.NewInt(int64(o.TargetPool))), + stackitem.NewBigInteger(big.NewInt(int64(o.ExpectedReturns))), + stackitem.NewBigInteger(big.NewInt(int64(o.RiskLevel))), + stackitem.NewBigInteger(big.NewInt(int64(o.VotingDeadline))), + stackitem.NewBigInteger(big.NewInt(int64(o.InvestmentDeadline))), + stackitem.NewBigInteger(big.NewInt(int64(o.MaturityDate))), + stackitem.NewBigInteger(big.NewInt(int64(o.ProposalID))), + stackitem.NewBigInteger(big.NewInt(int64(o.CreatedAt))), + stackitem.NewBigInteger(big.NewInt(int64(o.UpdatedAt))), + }), nil +} + +// FromStackItem implements stackitem.Convertible interface. +func (o *InvestmentOpportunity) FromStackItem(item stackitem.Item) error { + items, ok := item.Value().([]stackitem.Item) + if !ok { + return errors.New("not a struct") + } + if len(items) != 23 { + return fmt.Errorf("wrong number of elements: expected 23, got %d", len(items)) + } + + id, err := items[0].TryInteger() + if err != nil { + return fmt.Errorf("invalid id: %w", err) + } + o.ID = id.Uint64() + + oppType, err := items[1].TryInteger() + if err != nil { + return fmt.Errorf("invalid type: %w", err) + } + o.Type = OpportunityType(oppType.Uint64()) + + status, err := items[2].TryInteger() + if err != nil { + return fmt.Errorf("invalid status: %w", err) + } + o.Status = OpportunityStatus(status.Uint64()) + + creator, err := items[3].TryBytes() + if err != nil { + return fmt.Errorf("invalid creator: %w", err) + } + o.Creator, err = util.Uint160DecodeBytesBE(creator) + if err != nil { + return fmt.Errorf("invalid creator address: %w", err) + } + + sponsorVitaID, err := items[4].TryInteger() + if err != nil { + return fmt.Errorf("invalid sponsorVitaID: %w", err) + } + o.SponsorVitaID = sponsorVitaID.Uint64() + + name, err := items[5].TryBytes() + if err != nil { + return fmt.Errorf("invalid name: %w", err) + } + o.Name = string(name) + + description, err := items[6].TryBytes() + if err != nil { + return fmt.Errorf("invalid description: %w", err) + } + o.Description = string(description) + + termsHash, err := items[7].TryBytes() + if err != nil { + return fmt.Errorf("invalid termsHash: %w", err) + } + o.TermsHash, err = util.Uint256DecodeBytesBE(termsHash) + if err != nil { + return fmt.Errorf("invalid termsHash: %w", err) + } + + minParticipants, err := items[8].TryInteger() + if err != nil { + return fmt.Errorf("invalid minParticipants: %w", err) + } + o.MinParticipants = minParticipants.Uint64() + + maxParticipants, err := items[9].TryInteger() + if err != nil { + return fmt.Errorf("invalid maxParticipants: %w", err) + } + o.MaxParticipants = maxParticipants.Uint64() + + currentParticipants, err := items[10].TryInteger() + if err != nil { + return fmt.Errorf("invalid currentParticipants: %w", err) + } + o.CurrentParticipants = currentParticipants.Uint64() + + minInvestment, err := items[11].TryInteger() + if err != nil { + return fmt.Errorf("invalid minInvestment: %w", err) + } + o.MinInvestment = minInvestment.Uint64() + + maxInvestment, err := items[12].TryInteger() + if err != nil { + return fmt.Errorf("invalid maxInvestment: %w", err) + } + o.MaxInvestment = maxInvestment.Uint64() + + totalPool, err := items[13].TryInteger() + if err != nil { + return fmt.Errorf("invalid totalPool: %w", err) + } + o.TotalPool = totalPool.Uint64() + + targetPool, err := items[14].TryInteger() + if err != nil { + return fmt.Errorf("invalid targetPool: %w", err) + } + o.TargetPool = targetPool.Uint64() + + expectedReturns, err := items[15].TryInteger() + if err != nil { + return fmt.Errorf("invalid expectedReturns: %w", err) + } + o.ExpectedReturns = expectedReturns.Uint64() + + riskLevel, err := items[16].TryInteger() + if err != nil { + return fmt.Errorf("invalid riskLevel: %w", err) + } + o.RiskLevel = uint8(riskLevel.Uint64()) + + votingDeadline, err := items[17].TryInteger() + if err != nil { + return fmt.Errorf("invalid votingDeadline: %w", err) + } + o.VotingDeadline = uint32(votingDeadline.Uint64()) + + investmentDeadline, err := items[18].TryInteger() + if err != nil { + return fmt.Errorf("invalid investmentDeadline: %w", err) + } + o.InvestmentDeadline = uint32(investmentDeadline.Uint64()) + + maturityDate, err := items[19].TryInteger() + if err != nil { + return fmt.Errorf("invalid maturityDate: %w", err) + } + o.MaturityDate = uint32(maturityDate.Uint64()) + + proposalID, err := items[20].TryInteger() + if err != nil { + return fmt.Errorf("invalid proposalID: %w", err) + } + o.ProposalID = proposalID.Uint64() + + createdAt, err := items[21].TryInteger() + if err != nil { + return fmt.Errorf("invalid createdAt: %w", err) + } + o.CreatedAt = uint32(createdAt.Uint64()) + + updatedAt, err := items[22].TryInteger() + if err != nil { + return fmt.Errorf("invalid updatedAt: %w", err) + } + o.UpdatedAt = uint32(updatedAt.Uint64()) + + return nil +} + +// Investment represents an individual's investment in an opportunity. +type Investment struct { + ID uint64 // Unique investment ID + OpportunityID uint64 // Opportunity invested in + VitaID uint64 // Investor's Vita ID + Investor util.Uint160 // Investor's address + Amount uint64 // VTS amount invested + Status InvestmentStatus // Current status + ReturnAmount uint64 // Returns received (when completed) + CreatedAt uint32 // Block height when invested + UpdatedAt uint32 // Block height when last updated +} + +// DecodeBinary implements the io.Serializable interface. +func (i *Investment) DecodeBinary(br *io.BinReader) { + i.ID = br.ReadU64LE() + i.OpportunityID = br.ReadU64LE() + i.VitaID = br.ReadU64LE() + br.ReadBytes(i.Investor[:]) + i.Amount = br.ReadU64LE() + i.Status = InvestmentStatus(br.ReadB()) + i.ReturnAmount = br.ReadU64LE() + i.CreatedAt = br.ReadU32LE() + i.UpdatedAt = br.ReadU32LE() +} + +// EncodeBinary implements the io.Serializable interface. +func (i *Investment) EncodeBinary(bw *io.BinWriter) { + bw.WriteU64LE(i.ID) + bw.WriteU64LE(i.OpportunityID) + bw.WriteU64LE(i.VitaID) + bw.WriteBytes(i.Investor[:]) + bw.WriteU64LE(i.Amount) + bw.WriteB(byte(i.Status)) + bw.WriteU64LE(i.ReturnAmount) + bw.WriteU32LE(i.CreatedAt) + bw.WriteU32LE(i.UpdatedAt) +} + +// ToStackItem implements stackitem.Convertible interface. +func (i *Investment) ToStackItem() (stackitem.Item, error) { + return stackitem.NewStruct([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(i.ID))), + stackitem.NewBigInteger(big.NewInt(int64(i.OpportunityID))), + stackitem.NewBigInteger(big.NewInt(int64(i.VitaID))), + stackitem.NewByteArray(i.Investor.BytesBE()), + stackitem.NewBigInteger(big.NewInt(int64(i.Amount))), + stackitem.NewBigInteger(big.NewInt(int64(i.Status))), + stackitem.NewBigInteger(big.NewInt(int64(i.ReturnAmount))), + stackitem.NewBigInteger(big.NewInt(int64(i.CreatedAt))), + stackitem.NewBigInteger(big.NewInt(int64(i.UpdatedAt))), + }), nil +} + +// FromStackItem implements stackitem.Convertible interface. +func (i *Investment) FromStackItem(item stackitem.Item) error { + items, ok := item.Value().([]stackitem.Item) + if !ok { + return errors.New("not a struct") + } + if len(items) != 9 { + return fmt.Errorf("wrong number of elements: expected 9, got %d", len(items)) + } + + id, err := items[0].TryInteger() + if err != nil { + return fmt.Errorf("invalid id: %w", err) + } + i.ID = id.Uint64() + + oppID, err := items[1].TryInteger() + if err != nil { + return fmt.Errorf("invalid opportunityID: %w", err) + } + i.OpportunityID = oppID.Uint64() + + vitaID, err := items[2].TryInteger() + if err != nil { + return fmt.Errorf("invalid vitaID: %w", err) + } + i.VitaID = vitaID.Uint64() + + investor, err := items[3].TryBytes() + if err != nil { + return fmt.Errorf("invalid investor: %w", err) + } + i.Investor, err = util.Uint160DecodeBytesBE(investor) + if err != nil { + return fmt.Errorf("invalid investor address: %w", err) + } + + amount, err := items[4].TryInteger() + if err != nil { + return fmt.Errorf("invalid amount: %w", err) + } + i.Amount = amount.Uint64() + + status, err := items[5].TryInteger() + if err != nil { + return fmt.Errorf("invalid status: %w", err) + } + i.Status = InvestmentStatus(status.Uint64()) + + returnAmount, err := items[6].TryInteger() + if err != nil { + return fmt.Errorf("invalid returnAmount: %w", err) + } + i.ReturnAmount = returnAmount.Uint64() + + createdAt, err := items[7].TryInteger() + if err != nil { + return fmt.Errorf("invalid createdAt: %w", err) + } + i.CreatedAt = uint32(createdAt.Uint64()) + + updatedAt, err := items[8].TryInteger() + if err != nil { + return fmt.Errorf("invalid updatedAt: %w", err) + } + i.UpdatedAt = uint32(updatedAt.Uint64()) + + return nil +} + +// InvestorEligibility represents an investor's eligibility status. +type InvestorEligibility struct { + VitaID uint64 // Investor's Vita ID + Investor util.Uint160 // Investor's address + Eligibility EligibilityType // Eligibility flags + ScireCompleted bool // Investment education completed + RiskScore uint8 // AI-calculated risk tolerance (1-10) + TotalInvested uint64 // Total VTS currently invested + TotalReturns uint64 // Total returns received + ActiveInvestments uint64 // Number of active investments + CompletedInvestments uint64 // Number of completed investments + HasViolations bool // Any past investment abuse + ViolationCount uint8 // Number of violations + LastActivity uint32 // Block height of last activity + CreatedAt uint32 // Block height when created + UpdatedAt uint32 // Block height when updated +} + +// DecodeBinary implements the io.Serializable interface. +func (e *InvestorEligibility) DecodeBinary(br *io.BinReader) { + e.VitaID = br.ReadU64LE() + br.ReadBytes(e.Investor[:]) + e.Eligibility = EligibilityType(br.ReadB()) + e.ScireCompleted = br.ReadBool() + e.RiskScore = br.ReadB() + e.TotalInvested = br.ReadU64LE() + e.TotalReturns = br.ReadU64LE() + e.ActiveInvestments = br.ReadU64LE() + e.CompletedInvestments = br.ReadU64LE() + e.HasViolations = br.ReadBool() + e.ViolationCount = br.ReadB() + e.LastActivity = br.ReadU32LE() + e.CreatedAt = br.ReadU32LE() + e.UpdatedAt = br.ReadU32LE() +} + +// EncodeBinary implements the io.Serializable interface. +func (e *InvestorEligibility) EncodeBinary(bw *io.BinWriter) { + bw.WriteU64LE(e.VitaID) + bw.WriteBytes(e.Investor[:]) + bw.WriteB(byte(e.Eligibility)) + bw.WriteBool(e.ScireCompleted) + bw.WriteB(e.RiskScore) + bw.WriteU64LE(e.TotalInvested) + bw.WriteU64LE(e.TotalReturns) + bw.WriteU64LE(e.ActiveInvestments) + bw.WriteU64LE(e.CompletedInvestments) + bw.WriteBool(e.HasViolations) + bw.WriteB(e.ViolationCount) + bw.WriteU32LE(e.LastActivity) + bw.WriteU32LE(e.CreatedAt) + bw.WriteU32LE(e.UpdatedAt) +} + +// ToStackItem implements stackitem.Convertible interface. +func (e *InvestorEligibility) ToStackItem() (stackitem.Item, error) { + return stackitem.NewStruct([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(e.VitaID))), + stackitem.NewByteArray(e.Investor.BytesBE()), + stackitem.NewBigInteger(big.NewInt(int64(e.Eligibility))), + stackitem.NewBool(e.ScireCompleted), + stackitem.NewBigInteger(big.NewInt(int64(e.RiskScore))), + stackitem.NewBigInteger(big.NewInt(int64(e.TotalInvested))), + stackitem.NewBigInteger(big.NewInt(int64(e.TotalReturns))), + stackitem.NewBigInteger(big.NewInt(int64(e.ActiveInvestments))), + stackitem.NewBigInteger(big.NewInt(int64(e.CompletedInvestments))), + stackitem.NewBool(e.HasViolations), + stackitem.NewBigInteger(big.NewInt(int64(e.ViolationCount))), + stackitem.NewBigInteger(big.NewInt(int64(e.LastActivity))), + stackitem.NewBigInteger(big.NewInt(int64(e.CreatedAt))), + stackitem.NewBigInteger(big.NewInt(int64(e.UpdatedAt))), + }), nil +} + +// FromStackItem implements stackitem.Convertible interface. +func (e *InvestorEligibility) FromStackItem(item stackitem.Item) error { + items, ok := item.Value().([]stackitem.Item) + if !ok { + return errors.New("not a struct") + } + if len(items) != 14 { + return fmt.Errorf("wrong number of elements: expected 14, got %d", len(items)) + } + + vitaID, err := items[0].TryInteger() + if err != nil { + return fmt.Errorf("invalid vitaID: %w", err) + } + e.VitaID = vitaID.Uint64() + + investor, err := items[1].TryBytes() + if err != nil { + return fmt.Errorf("invalid investor: %w", err) + } + e.Investor, err = util.Uint160DecodeBytesBE(investor) + if err != nil { + return fmt.Errorf("invalid investor address: %w", err) + } + + eligibility, err := items[2].TryInteger() + if err != nil { + return fmt.Errorf("invalid eligibility: %w", err) + } + e.Eligibility = EligibilityType(eligibility.Uint64()) + + scireCompleted, err := items[3].TryBool() + if err != nil { + return fmt.Errorf("invalid scireCompleted: %w", err) + } + e.ScireCompleted = scireCompleted + + riskScore, err := items[4].TryInteger() + if err != nil { + return fmt.Errorf("invalid riskScore: %w", err) + } + e.RiskScore = uint8(riskScore.Uint64()) + + totalInvested, err := items[5].TryInteger() + if err != nil { + return fmt.Errorf("invalid totalInvested: %w", err) + } + e.TotalInvested = totalInvested.Uint64() + + totalReturns, err := items[6].TryInteger() + if err != nil { + return fmt.Errorf("invalid totalReturns: %w", err) + } + e.TotalReturns = totalReturns.Uint64() + + activeInvestments, err := items[7].TryInteger() + if err != nil { + return fmt.Errorf("invalid activeInvestments: %w", err) + } + e.ActiveInvestments = activeInvestments.Uint64() + + completedInvestments, err := items[8].TryInteger() + if err != nil { + return fmt.Errorf("invalid completedInvestments: %w", err) + } + e.CompletedInvestments = completedInvestments.Uint64() + + hasViolations, err := items[9].TryBool() + if err != nil { + return fmt.Errorf("invalid hasViolations: %w", err) + } + e.HasViolations = hasViolations + + violationCount, err := items[10].TryInteger() + if err != nil { + return fmt.Errorf("invalid violationCount: %w", err) + } + e.ViolationCount = uint8(violationCount.Uint64()) + + lastActivity, err := items[11].TryInteger() + if err != nil { + return fmt.Errorf("invalid lastActivity: %w", err) + } + e.LastActivity = uint32(lastActivity.Uint64()) + + createdAt, err := items[12].TryInteger() + if err != nil { + return fmt.Errorf("invalid createdAt: %w", err) + } + e.CreatedAt = uint32(createdAt.Uint64()) + + updatedAt, err := items[13].TryInteger() + if err != nil { + return fmt.Errorf("invalid updatedAt: %w", err) + } + e.UpdatedAt = uint32(updatedAt.Uint64()) + + return nil +} + +// EmploymentVerification represents verified employment for EIO eligibility. +type EmploymentVerification struct { + VitaID uint64 // Employee's Vita ID + Employee util.Uint160 // Employee's address + EmployerVitaID uint64 // Employer's Vita ID (organization) + Employer util.Uint160 // Employer's address + Position string // Job position/role + StartDate uint32 // Block height when employment started + EndDate uint32 // Block height when employment ended (0 = current) + IsActive bool // Currently employed + VerifiedAt uint32 // Block height when verified + VerifiedBy util.Uint160 // Verifier's address (RoleInvestmentManager) +} + +// DecodeBinary implements the io.Serializable interface. +func (ev *EmploymentVerification) DecodeBinary(br *io.BinReader) { + ev.VitaID = br.ReadU64LE() + br.ReadBytes(ev.Employee[:]) + ev.EmployerVitaID = br.ReadU64LE() + br.ReadBytes(ev.Employer[:]) + ev.Position = br.ReadString() + ev.StartDate = br.ReadU32LE() + ev.EndDate = br.ReadU32LE() + ev.IsActive = br.ReadBool() + ev.VerifiedAt = br.ReadU32LE() + br.ReadBytes(ev.VerifiedBy[:]) +} + +// EncodeBinary implements the io.Serializable interface. +func (ev *EmploymentVerification) EncodeBinary(bw *io.BinWriter) { + bw.WriteU64LE(ev.VitaID) + bw.WriteBytes(ev.Employee[:]) + bw.WriteU64LE(ev.EmployerVitaID) + bw.WriteBytes(ev.Employer[:]) + bw.WriteString(ev.Position) + bw.WriteU32LE(ev.StartDate) + bw.WriteU32LE(ev.EndDate) + bw.WriteBool(ev.IsActive) + bw.WriteU32LE(ev.VerifiedAt) + bw.WriteBytes(ev.VerifiedBy[:]) +} + +// ContractorVerification represents verified contractor status for CIO eligibility. +type ContractorVerification struct { + VitaID uint64 // Contractor's Vita ID + Contractor util.Uint160 // Contractor's address + PlatformID string // Platform identifier + Platform util.Uint160 // Platform's address + ContractorID string // Platform-specific contractor ID + StartDate uint32 // Block height when started + EndDate uint32 // Block height when ended (0 = current) + IsActive bool // Currently active + VerifiedAt uint32 // Block height when verified + VerifiedBy util.Uint160 // Verifier's address +} + +// DecodeBinary implements the io.Serializable interface. +func (cv *ContractorVerification) DecodeBinary(br *io.BinReader) { + cv.VitaID = br.ReadU64LE() + br.ReadBytes(cv.Contractor[:]) + cv.PlatformID = br.ReadString() + br.ReadBytes(cv.Platform[:]) + cv.ContractorID = br.ReadString() + cv.StartDate = br.ReadU32LE() + cv.EndDate = br.ReadU32LE() + cv.IsActive = br.ReadBool() + cv.VerifiedAt = br.ReadU32LE() + br.ReadBytes(cv.VerifiedBy[:]) +} + +// EncodeBinary implements the io.Serializable interface. +func (cv *ContractorVerification) EncodeBinary(bw *io.BinWriter) { + bw.WriteU64LE(cv.VitaID) + bw.WriteBytes(cv.Contractor[:]) + bw.WriteString(cv.PlatformID) + bw.WriteBytes(cv.Platform[:]) + bw.WriteString(cv.ContractorID) + bw.WriteU32LE(cv.StartDate) + bw.WriteU32LE(cv.EndDate) + bw.WriteBool(cv.IsActive) + bw.WriteU32LE(cv.VerifiedAt) + bw.WriteBytes(cv.VerifiedBy[:]) +} + +// InvestmentViolation represents a recorded investment abuse violation. +type InvestmentViolation struct { + ID uint64 // Unique violation ID + VitaID uint64 // Violator's Vita ID + Violator util.Uint160 // Violator's address + OpportunityID uint64 // Related opportunity (0 = general) + ViolationType string // Type of violation + Description string // Violation description + EvidenceHash util.Uint256 // Hash of evidence (off-chain) + Penalty uint64 // VTS penalty applied + ReportedBy util.Uint160 // Reporter's address + ReportedAt uint32 // Block height when reported + ResolvedAt uint32 // Block height when resolved (0 = pending) + Resolution string // Resolution description +} + +// DecodeBinary implements the io.Serializable interface. +func (v *InvestmentViolation) DecodeBinary(br *io.BinReader) { + v.ID = br.ReadU64LE() + v.VitaID = br.ReadU64LE() + br.ReadBytes(v.Violator[:]) + v.OpportunityID = br.ReadU64LE() + v.ViolationType = br.ReadString() + v.Description = br.ReadString() + br.ReadBytes(v.EvidenceHash[:]) + v.Penalty = br.ReadU64LE() + br.ReadBytes(v.ReportedBy[:]) + v.ReportedAt = br.ReadU32LE() + v.ResolvedAt = br.ReadU32LE() + v.Resolution = br.ReadString() +} + +// EncodeBinary implements the io.Serializable interface. +func (v *InvestmentViolation) EncodeBinary(bw *io.BinWriter) { + bw.WriteU64LE(v.ID) + bw.WriteU64LE(v.VitaID) + bw.WriteBytes(v.Violator[:]) + bw.WriteU64LE(v.OpportunityID) + bw.WriteString(v.ViolationType) + bw.WriteString(v.Description) + bw.WriteBytes(v.EvidenceHash[:]) + bw.WriteU64LE(v.Penalty) + bw.WriteBytes(v.ReportedBy[:]) + bw.WriteU32LE(v.ReportedAt) + bw.WriteU32LE(v.ResolvedAt) + bw.WriteString(v.Resolution) +} + +// CollocatioConfig represents configurable parameters for the investment contract. +type CollocatioConfig struct { + // Minimum requirements + MinPIOParticipants uint64 // Default minimum for PIO (default: 100) + MinEIOParticipants uint64 // Default minimum for EIO (default: 10) + MinCIOParticipants uint64 // Default minimum for CIO (default: 25) + + // Investment limits + DefaultMinInvestment uint64 // Default minimum investment in VTS (default: 100) + MaxIndividualCap uint64 // Maximum any individual can invest (default: 1000000) + WealthConcentration uint64 // Max % of pool by single investor (basis points, default: 500 = 5%) + + // Fees + CreationFee uint64 // VTS fee to create opportunity + InvestmentFee uint64 // Fee per investment (basis points, default: 50 = 0.5%) + WithdrawalPenalty uint64 // Early withdrawal penalty (basis points, default: 200 = 2%) + + // Timing + MinVotingPeriod uint32 // Minimum blocks for voting (default: 10000) + MinInvestmentPeriod uint32 // Minimum blocks for investment window (default: 20000) + MinMaturityPeriod uint32 // Minimum blocks until maturity (default: 50000) + + // Violation thresholds + MaxViolationsBeforeBan uint8 // Violations before permanent ban (default: 3) + ViolationCooldown uint32 // Blocks before violation expires (default: 1000000) +} + +// DecodeBinary implements the io.Serializable interface. +func (c *CollocatioConfig) DecodeBinary(br *io.BinReader) { + c.MinPIOParticipants = br.ReadU64LE() + c.MinEIOParticipants = br.ReadU64LE() + c.MinCIOParticipants = br.ReadU64LE() + c.DefaultMinInvestment = br.ReadU64LE() + c.MaxIndividualCap = br.ReadU64LE() + c.WealthConcentration = br.ReadU64LE() + c.CreationFee = br.ReadU64LE() + c.InvestmentFee = br.ReadU64LE() + c.WithdrawalPenalty = br.ReadU64LE() + c.MinVotingPeriod = br.ReadU32LE() + c.MinInvestmentPeriod = br.ReadU32LE() + c.MinMaturityPeriod = br.ReadU32LE() + c.MaxViolationsBeforeBan = br.ReadB() + c.ViolationCooldown = br.ReadU32LE() +} + +// EncodeBinary implements the io.Serializable interface. +func (c *CollocatioConfig) EncodeBinary(bw *io.BinWriter) { + bw.WriteU64LE(c.MinPIOParticipants) + bw.WriteU64LE(c.MinEIOParticipants) + bw.WriteU64LE(c.MinCIOParticipants) + bw.WriteU64LE(c.DefaultMinInvestment) + bw.WriteU64LE(c.MaxIndividualCap) + bw.WriteU64LE(c.WealthConcentration) + bw.WriteU64LE(c.CreationFee) + bw.WriteU64LE(c.InvestmentFee) + bw.WriteU64LE(c.WithdrawalPenalty) + bw.WriteU32LE(c.MinVotingPeriod) + bw.WriteU32LE(c.MinInvestmentPeriod) + bw.WriteU32LE(c.MinMaturityPeriod) + bw.WriteB(c.MaxViolationsBeforeBan) + bw.WriteU32LE(c.ViolationCooldown) +} + +// ToStackItem implements stackitem.Convertible interface. +func (c *CollocatioConfig) ToStackItem() (stackitem.Item, error) { + return stackitem.NewStruct([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(c.MinPIOParticipants))), + stackitem.NewBigInteger(big.NewInt(int64(c.MinEIOParticipants))), + stackitem.NewBigInteger(big.NewInt(int64(c.MinCIOParticipants))), + stackitem.NewBigInteger(big.NewInt(int64(c.DefaultMinInvestment))), + stackitem.NewBigInteger(big.NewInt(int64(c.MaxIndividualCap))), + stackitem.NewBigInteger(big.NewInt(int64(c.WealthConcentration))), + stackitem.NewBigInteger(big.NewInt(int64(c.CreationFee))), + stackitem.NewBigInteger(big.NewInt(int64(c.InvestmentFee))), + stackitem.NewBigInteger(big.NewInt(int64(c.WithdrawalPenalty))), + stackitem.NewBigInteger(big.NewInt(int64(c.MinVotingPeriod))), + stackitem.NewBigInteger(big.NewInt(int64(c.MinInvestmentPeriod))), + stackitem.NewBigInteger(big.NewInt(int64(c.MinMaturityPeriod))), + stackitem.NewBigInteger(big.NewInt(int64(c.MaxViolationsBeforeBan))), + stackitem.NewBigInteger(big.NewInt(int64(c.ViolationCooldown))), + }), nil +} + +// FromStackItem implements stackitem.Convertible interface. +func (c *CollocatioConfig) FromStackItem(item stackitem.Item) error { + items, ok := item.Value().([]stackitem.Item) + if !ok { + return errors.New("not a struct") + } + if len(items) != 14 { + return fmt.Errorf("wrong number of elements: expected 14, got %d", len(items)) + } + + minPIO, err := items[0].TryInteger() + if err != nil { + return fmt.Errorf("invalid minPIOParticipants: %w", err) + } + c.MinPIOParticipants = minPIO.Uint64() + + minEIO, err := items[1].TryInteger() + if err != nil { + return fmt.Errorf("invalid minEIOParticipants: %w", err) + } + c.MinEIOParticipants = minEIO.Uint64() + + minCIO, err := items[2].TryInteger() + if err != nil { + return fmt.Errorf("invalid minCIOParticipants: %w", err) + } + c.MinCIOParticipants = minCIO.Uint64() + + defaultMin, err := items[3].TryInteger() + if err != nil { + return fmt.Errorf("invalid defaultMinInvestment: %w", err) + } + c.DefaultMinInvestment = defaultMin.Uint64() + + maxCap, err := items[4].TryInteger() + if err != nil { + return fmt.Errorf("invalid maxIndividualCap: %w", err) + } + c.MaxIndividualCap = maxCap.Uint64() + + wealthConc, err := items[5].TryInteger() + if err != nil { + return fmt.Errorf("invalid wealthConcentration: %w", err) + } + c.WealthConcentration = wealthConc.Uint64() + + creationFee, err := items[6].TryInteger() + if err != nil { + return fmt.Errorf("invalid creationFee: %w", err) + } + c.CreationFee = creationFee.Uint64() + + investmentFee, err := items[7].TryInteger() + if err != nil { + return fmt.Errorf("invalid investmentFee: %w", err) + } + c.InvestmentFee = investmentFee.Uint64() + + withdrawalPenalty, err := items[8].TryInteger() + if err != nil { + return fmt.Errorf("invalid withdrawalPenalty: %w", err) + } + c.WithdrawalPenalty = withdrawalPenalty.Uint64() + + minVoting, err := items[9].TryInteger() + if err != nil { + return fmt.Errorf("invalid minVotingPeriod: %w", err) + } + c.MinVotingPeriod = uint32(minVoting.Uint64()) + + minInvestment, err := items[10].TryInteger() + if err != nil { + return fmt.Errorf("invalid minInvestmentPeriod: %w", err) + } + c.MinInvestmentPeriod = uint32(minInvestment.Uint64()) + + minMaturity, err := items[11].TryInteger() + if err != nil { + return fmt.Errorf("invalid minMaturityPeriod: %w", err) + } + c.MinMaturityPeriod = uint32(minMaturity.Uint64()) + + maxViolations, err := items[12].TryInteger() + if err != nil { + return fmt.Errorf("invalid maxViolationsBeforeBan: %w", err) + } + c.MaxViolationsBeforeBan = uint8(maxViolations.Uint64()) + + cooldown, err := items[13].TryInteger() + if err != nil { + return fmt.Errorf("invalid violationCooldown: %w", err) + } + c.ViolationCooldown = uint32(cooldown.Uint64()) + + return nil +} + +// DefaultCollocatioConfig returns the default configuration. +func DefaultCollocatioConfig() CollocatioConfig { + return CollocatioConfig{ + MinPIOParticipants: 100, + MinEIOParticipants: 10, + MinCIOParticipants: 25, + DefaultMinInvestment: 100_00000000, // 100 VTS + MaxIndividualCap: 1_000_000_00000000, // 1M VTS + WealthConcentration: 500, // 5% + CreationFee: 1000_00000000, // 1000 VTS + InvestmentFee: 50, // 0.5% + WithdrawalPenalty: 200, // 2% + MinVotingPeriod: 10000, // ~10000 blocks + MinInvestmentPeriod: 20000, // ~20000 blocks + MinMaturityPeriod: 50000, // ~50000 blocks + MaxViolationsBeforeBan: 3, + ViolationCooldown: 1000000, // ~1M blocks + } +}