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/core/storage" "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" ) // Collocatio represents the Investment native contract for democratic investment (PIO/EIO/CIO). // Latin: "collocatio" = placement, arrangement (investment) type Collocatio struct { interop.ContractMD Tutus ITutus 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 collocatioPrefixViolationByInvestor byte = 0x41 // vitaID + violationID -> exists collocatioPrefixViolationCounter byte = 0x4F // -> next violation ID collocatioPrefixEmployment byte = 0x50 // employeeVitaID -> EmploymentVerification collocatioPrefixEmploymentByEmployer byte = 0x51 // employerVitaID + employeeVitaID -> exists collocatioPrefixContractor byte = 0x60 // contractorVitaID -> ContractorVerification collocatioPrefixContractorByPlatform byte = 0x61 // platformHash + contractorVitaID -> exists collocatioPrefixCommitment byte = 0x70 // commitmentID -> InvestmentCommitment collocatioPrefixCommitmentByOpp byte = 0x71 // opportunityID + commitmentID -> exists collocatioPrefixCommitmentByInvestor byte = 0x72 // vitaID + commitmentID -> exists collocatioPrefixCommitmentCounter byte = 0x7F // -> next commitment ID ) // Collocatio events. const ( collocatioOpportunityCreatedEvent = "OpportunityCreated" collocatioOpportunityActivatedEvent = "OpportunityActivated" collocatioOpportunityClosedEvent = "OpportunityClosed" collocatioOpportunityFailedEvent = "OpportunityFailed" collocatioOpportunityCancelledEvent = "OpportunityCancelled" collocatioInvestmentMadeEvent = "InvestmentMade" collocatioInvestmentWithdrawnEvent = "InvestmentWithdrawn" collocatioReturnsDistributedEvent = "ReturnsDistributed" collocatioEligibilityUpdatedEvent = "EligibilityUpdated" collocatioEducationCompletedEvent = "EducationCompleted" collocatioViolationRecordedEvent = "ViolationRecorded" collocatioViolationResolvedEvent = "ViolationResolved" collocatioEmploymentVerifiedEvent = "EmploymentVerified" collocatioEmploymentRevokedEvent = "EmploymentRevoked" collocatioContractorVerifiedEvent = "ContractorVerified" collocatioContractorRevokedEvent = "ContractorRevoked" collocatioCommitmentCreatedEvent = "CommitmentCreated" collocatioCommitmentRevealedEvent = "CommitmentRevealed" collocatioCommitmentCanceledEvent = "CommitmentCanceled" ) // 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 defaultCommitRevealDelay uint32 = 10 // Min blocks between commit and reveal defaultCommitRevealWindow uint32 = 1000 // Max blocks to reveal after delay ) 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) // recordViolation desc = NewDescriptor("recordViolation", smartcontract.IntegerType, manifest.NewParameter("violator", smartcontract.Hash160Type), manifest.NewParameter("opportunityID", smartcontract.IntegerType), manifest.NewParameter("violationType", smartcontract.StringType), manifest.NewParameter("description", smartcontract.StringType), manifest.NewParameter("evidenceHash", smartcontract.Hash256Type), manifest.NewParameter("penalty", smartcontract.IntegerType)) md = NewMethodAndPrice(c.recordViolation, 1<<17, callflag.States|callflag.AllowNotify) c.AddMethod(md, desc) // resolveViolation desc = NewDescriptor("resolveViolation", smartcontract.BoolType, manifest.NewParameter("violationID", smartcontract.IntegerType), manifest.NewParameter("resolution", smartcontract.StringType)) md = NewMethodAndPrice(c.resolveViolation, 1<<16, callflag.States|callflag.AllowNotify) c.AddMethod(md, desc) // getViolation desc = NewDescriptor("getViolation", smartcontract.ArrayType, manifest.NewParameter("violationID", smartcontract.IntegerType)) md = NewMethodAndPrice(c.getViolation, 1<<15, callflag.ReadStates) c.AddMethod(md, desc) // verifyEmployment desc = NewDescriptor("verifyEmployment", smartcontract.BoolType, manifest.NewParameter("employee", smartcontract.Hash160Type), manifest.NewParameter("employer", smartcontract.Hash160Type), manifest.NewParameter("position", smartcontract.StringType), manifest.NewParameter("startDate", smartcontract.IntegerType)) md = NewMethodAndPrice(c.verifyEmployment, 1<<16, callflag.States|callflag.AllowNotify) c.AddMethod(md, desc) // revokeEmployment desc = NewDescriptor("revokeEmployment", smartcontract.BoolType, manifest.NewParameter("employee", smartcontract.Hash160Type), manifest.NewParameter("employer", smartcontract.Hash160Type), manifest.NewParameter("endDate", smartcontract.IntegerType)) md = NewMethodAndPrice(c.revokeEmployment, 1<<16, callflag.States|callflag.AllowNotify) c.AddMethod(md, desc) // getEmploymentStatus desc = NewDescriptor("getEmploymentStatus", smartcontract.ArrayType, manifest.NewParameter("employee", smartcontract.Hash160Type)) md = NewMethodAndPrice(c.getEmploymentStatus, 1<<15, callflag.ReadStates) c.AddMethod(md, desc) // verifyContractor desc = NewDescriptor("verifyContractor", smartcontract.BoolType, manifest.NewParameter("contractor", smartcontract.Hash160Type), manifest.NewParameter("platform", smartcontract.Hash160Type), manifest.NewParameter("platformID", smartcontract.StringType), manifest.NewParameter("contractorID", smartcontract.StringType), manifest.NewParameter("startDate", smartcontract.IntegerType)) md = NewMethodAndPrice(c.verifyContractor, 1<<16, callflag.States|callflag.AllowNotify) c.AddMethod(md, desc) // revokeContractor desc = NewDescriptor("revokeContractor", smartcontract.BoolType, manifest.NewParameter("contractor", smartcontract.Hash160Type), manifest.NewParameter("platform", smartcontract.Hash160Type), manifest.NewParameter("endDate", smartcontract.IntegerType)) md = NewMethodAndPrice(c.revokeContractor, 1<<16, callflag.States|callflag.AllowNotify) c.AddMethod(md, desc) // getContractorStatus desc = NewDescriptor("getContractorStatus", smartcontract.ArrayType, manifest.NewParameter("contractor", smartcontract.Hash160Type)) md = NewMethodAndPrice(c.getContractorStatus, 1<<15, callflag.ReadStates) c.AddMethod(md, desc) // completeInvestmentEducation desc = NewDescriptor("completeInvestmentEducation", smartcontract.BoolType, manifest.NewParameter("investor", smartcontract.Hash160Type), manifest.NewParameter("certificationID", smartcontract.IntegerType)) md = NewMethodAndPrice(c.completeInvestmentEducation, 1<<16, callflag.States|callflag.AllowNotify) c.AddMethod(md, desc) // distributeReturns desc = NewDescriptor("distributeReturns", smartcontract.BoolType, manifest.NewParameter("opportunityID", smartcontract.IntegerType), manifest.NewParameter("actualReturns", smartcontract.IntegerType)) md = NewMethodAndPrice(c.distributeReturns, 1<<18, callflag.States|callflag.AllowNotify) c.AddMethod(md, desc) // cancelOpportunity desc = NewDescriptor("cancelOpportunity", smartcontract.BoolType, manifest.NewParameter("opportunityID", smartcontract.IntegerType)) md = NewMethodAndPrice(c.cancelOpportunity, 1<<18, callflag.States|callflag.AllowNotify) c.AddMethod(md, desc) // getInvestmentsByOpportunity desc = NewDescriptor("getInvestmentsByOpportunity", smartcontract.ArrayType, manifest.NewParameter("opportunityID", smartcontract.IntegerType)) md = NewMethodAndPrice(c.getInvestmentsByOpportunity, 1<<16, callflag.ReadStates) c.AddMethod(md, desc) // getInvestmentsByInvestor desc = NewDescriptor("getInvestmentsByInvestor", smartcontract.ArrayType, manifest.NewParameter("investor", smartcontract.Hash160Type)) md = NewMethodAndPrice(c.getInvestmentsByInvestor, 1<<16, callflag.ReadStates) c.AddMethod(md, desc) // getOpportunitiesByType desc = NewDescriptor("getOpportunitiesByType", smartcontract.ArrayType, manifest.NewParameter("oppType", smartcontract.IntegerType)) md = NewMethodAndPrice(c.getOpportunitiesByType, 1<<16, callflag.ReadStates) c.AddMethod(md, desc) // getOpportunitiesByStatus desc = NewDescriptor("getOpportunitiesByStatus", smartcontract.ArrayType, manifest.NewParameter("status", smartcontract.IntegerType)) md = NewMethodAndPrice(c.getOpportunitiesByStatus, 1<<16, callflag.ReadStates) c.AddMethod(md, desc) // commitInvestment - Phase 1 of commit-reveal (anti-front-running) desc = NewDescriptor("commitInvestment", smartcontract.IntegerType, manifest.NewParameter("opportunityID", smartcontract.IntegerType), manifest.NewParameter("commitment", smartcontract.Hash256Type)) md = NewMethodAndPrice(c.commitInvestment, 1<<16, callflag.States|callflag.AllowNotify) c.AddMethod(md, desc) // revealInvestment - Phase 2 of commit-reveal desc = NewDescriptor("revealInvestment", smartcontract.IntegerType, manifest.NewParameter("commitmentID", smartcontract.IntegerType), manifest.NewParameter("amount", smartcontract.IntegerType), manifest.NewParameter("nonce", smartcontract.ByteArrayType)) md = NewMethodAndPrice(c.revealInvestment, 1<<17, callflag.States|callflag.AllowNotify) c.AddMethod(md, desc) // cancelCommitment - Cancel a pending commitment desc = NewDescriptor("cancelCommitment", smartcontract.BoolType, manifest.NewParameter("commitmentID", smartcontract.IntegerType)) md = NewMethodAndPrice(c.cancelCommitment, 1<<16, callflag.States|callflag.AllowNotify) c.AddMethod(md, desc) // getCommitment - Query commitment details desc = NewDescriptor("getCommitment", smartcontract.ArrayType, manifest.NewParameter("commitmentID", smartcontract.IntegerType)) md = NewMethodAndPrice(c.getCommitment, 1<<15, callflag.ReadStates) c.AddMethod(md, desc) // getCommitmentCount desc = NewDescriptor("getCommitmentCount", smartcontract.IntegerType) md = NewMethodAndPrice(c.getCommitmentCount, 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)) eDesc = NewEventDescriptor(collocatioOpportunityClosedEvent, manifest.NewParameter("opportunityID", smartcontract.IntegerType)) c.AddEvent(NewEvent(eDesc)) eDesc = NewEventDescriptor(collocatioOpportunityFailedEvent, manifest.NewParameter("opportunityID", smartcontract.IntegerType), manifest.NewParameter("reason", smartcontract.StringType)) c.AddEvent(NewEvent(eDesc)) eDesc = NewEventDescriptor(collocatioOpportunityCancelledEvent, manifest.NewParameter("opportunityID", smartcontract.IntegerType)) c.AddEvent(NewEvent(eDesc)) eDesc = NewEventDescriptor(collocatioReturnsDistributedEvent, manifest.NewParameter("opportunityID", smartcontract.IntegerType), manifest.NewParameter("totalReturns", smartcontract.IntegerType), manifest.NewParameter("investorCount", smartcontract.IntegerType)) c.AddEvent(NewEvent(eDesc)) eDesc = NewEventDescriptor(collocatioEducationCompletedEvent, manifest.NewParameter("investor", smartcontract.Hash160Type), manifest.NewParameter("certificationID", smartcontract.IntegerType)) c.AddEvent(NewEvent(eDesc)) eDesc = NewEventDescriptor(collocatioViolationRecordedEvent, manifest.NewParameter("violationID", smartcontract.IntegerType), manifest.NewParameter("violator", smartcontract.Hash160Type), manifest.NewParameter("penalty", smartcontract.IntegerType)) c.AddEvent(NewEvent(eDesc)) eDesc = NewEventDescriptor(collocatioViolationResolvedEvent, manifest.NewParameter("violationID", smartcontract.IntegerType), manifest.NewParameter("resolution", smartcontract.StringType)) c.AddEvent(NewEvent(eDesc)) eDesc = NewEventDescriptor(collocatioEmploymentVerifiedEvent, manifest.NewParameter("employee", smartcontract.Hash160Type), manifest.NewParameter("employer", smartcontract.Hash160Type)) c.AddEvent(NewEvent(eDesc)) eDesc = NewEventDescriptor(collocatioEmploymentRevokedEvent, manifest.NewParameter("employee", smartcontract.Hash160Type), manifest.NewParameter("employer", smartcontract.Hash160Type)) c.AddEvent(NewEvent(eDesc)) eDesc = NewEventDescriptor(collocatioContractorVerifiedEvent, manifest.NewParameter("contractor", smartcontract.Hash160Type), manifest.NewParameter("platform", smartcontract.Hash160Type)) c.AddEvent(NewEvent(eDesc)) eDesc = NewEventDescriptor(collocatioContractorRevokedEvent, manifest.NewParameter("contractor", smartcontract.Hash160Type), manifest.NewParameter("platform", smartcontract.Hash160Type)) c.AddEvent(NewEvent(eDesc)) eDesc = NewEventDescriptor(collocatioCommitmentCreatedEvent, manifest.NewParameter("commitmentID", smartcontract.IntegerType), manifest.NewParameter("opportunityID", smartcontract.IntegerType), manifest.NewParameter("investor", smartcontract.Hash160Type)) c.AddEvent(NewEvent(eDesc)) eDesc = NewEventDescriptor(collocatioCommitmentRevealedEvent, manifest.NewParameter("commitmentID", smartcontract.IntegerType), manifest.NewParameter("investmentID", smartcontract.IntegerType), manifest.NewParameter("amount", smartcontract.IntegerType)) c.AddEvent(NewEvent(eDesc)) eDesc = NewEventDescriptor(collocatioCommitmentCanceledEvent, manifest.NewParameter("commitmentID", 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, CommitRevealDelay: defaultCommitRevealDelay, CommitRevealWindow: defaultCommitRevealWindow, } 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. // Handles lifecycle automation for opportunities. func (c *Collocatio) PostPersist(ic *interop.Context) error { // Run every 100 blocks for performance if ic.Block.Index%100 != 0 { return nil } // Process opportunities that need status updates c.processActiveOpportunities(ic) c.processClosedOpportunities(ic) return nil } // processActiveOpportunities handles Active opportunities past their investment deadline. func (c *Collocatio) processActiveOpportunities(ic *interop.Context) { prefix := []byte{collocatioPrefixOpportunityByStatus, byte(state.OpportunityActive)} cfg := c.getConfigInternal(ic.DAO) ic.DAO.Seek(c.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { if len(k) < 9 { return true } oppID := binary.BigEndian.Uint64(k[1:9]) opp := c.getOpportunityInternal(ic.DAO, oppID) if opp == nil { return true } // Check if investment deadline passed if ic.Block.Index < opp.InvestmentDeadline { return true } // Get minimum participants for this opportunity type var minParticipants uint64 switch opp.Type { case state.OpportunityPIO: minParticipants = cfg.MinPIOParticipants case state.OpportunityEIO: minParticipants = cfg.MinEIOParticipants case state.OpportunityCIO: minParticipants = cfg.MinCIOParticipants default: minParticipants = 1 } // Use opportunity's own min if set if opp.MinParticipants > minParticipants { minParticipants = opp.MinParticipants } // Check if opportunity met minimum participants if opp.CurrentParticipants < minParticipants { // Failed - didn't meet minimum participants c.updateOpportunityStatus(ic, opp, state.OpportunityFailed) ic.AddNotification(c.Hash, collocatioOpportunityFailedEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewBigInteger(new(big.Int).SetUint64(oppID)), stackitem.NewByteArray([]byte("insufficient participants")), })) } else { // Success - close and move to maturity phase c.updateOpportunityStatus(ic, opp, state.OpportunityClosed) ic.AddNotification(c.Hash, collocatioOpportunityClosedEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewBigInteger(new(big.Int).SetUint64(oppID)), })) } return true }) } // processClosedOpportunities handles Closed opportunities past their maturity date. func (c *Collocatio) processClosedOpportunities(ic *interop.Context) { prefix := []byte{collocatioPrefixOpportunityByStatus, byte(state.OpportunityClosed)} ic.DAO.Seek(c.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { if len(k) < 9 { return true } oppID := binary.BigEndian.Uint64(k[1:9]) opp := c.getOpportunityInternal(ic.DAO, oppID) if opp == nil { return true } // Check if maturity date passed if ic.Block.Index < opp.MaturityDate { return true } // Opportunity is mature and ready for returns distribution // Note: Actual distribution is triggered by distributeReturns call // This could emit a notification for off-chain systems // For now, we just log that it's ready (no state change needed) return true }) } // updateOpportunityStatus updates an opportunity's status and maintains status index. func (c *Collocatio) updateOpportunityStatus(ic *interop.Context, opp *state.InvestmentOpportunity, newStatus state.OpportunityStatus) { oldStatus := opp.Status // Remove from old status index oldStatusKey := makeCollocatioOppByStatusKey(oldStatus, opp.ID) ic.DAO.DeleteStorageItem(c.ID, oldStatusKey) // Update status opp.Status = newStatus opp.UpdatedAt = ic.Block.Index // Add to new status index newStatusKey := makeCollocatioOppByStatusKey(newStatus, opp.ID) ic.DAO.PutStorageItem(c.ID, newStatusKey, []byte{1}) // Save opportunity c.putOpportunity(ic.DAO, opp) } // 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 makeCollocatioOppByStatusKey(status state.OpportunityStatus, oppID uint64) []byte { key := make([]byte, 10) key[0] = collocatioPrefixOpportunityByStatus key[1] = byte(status) 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 } func makeCollocatioViolationKey(violationID uint64) []byte { key := make([]byte, 9) key[0] = collocatioPrefixViolation binary.BigEndian.PutUint64(key[1:], violationID) return key } func makeCollocatioViolationByInvestorKey(vitaID, violationID uint64) []byte { key := make([]byte, 17) key[0] = collocatioPrefixViolationByInvestor binary.BigEndian.PutUint64(key[1:9], vitaID) binary.BigEndian.PutUint64(key[9:], violationID) return key } func makeCollocatioViolationCounterKey() []byte { return []byte{collocatioPrefixViolationCounter} } func makeCollocatioEmploymentKey(employeeVitaID uint64) []byte { key := make([]byte, 9) key[0] = collocatioPrefixEmployment binary.BigEndian.PutUint64(key[1:], employeeVitaID) return key } func makeCollocatioEmploymentByEmployerKey(employerVitaID, employeeVitaID uint64) []byte { key := make([]byte, 17) key[0] = collocatioPrefixEmploymentByEmployer binary.BigEndian.PutUint64(key[1:9], employerVitaID) binary.BigEndian.PutUint64(key[9:], employeeVitaID) return key } func makeCollocatioContractorKey(contractorVitaID uint64) []byte { key := make([]byte, 9) key[0] = collocatioPrefixContractor binary.BigEndian.PutUint64(key[1:], contractorVitaID) return key } func makeCollocatioContractorByPlatformKey(platform util.Uint160, contractorVitaID uint64) []byte { key := make([]byte, 1+util.Uint160Size+8) key[0] = collocatioPrefixContractorByPlatform copy(key[1:1+util.Uint160Size], platform.BytesBE()) binary.BigEndian.PutUint64(key[1+util.Uint160Size:], contractorVitaID) return key } func makeCollocatioCommitmentKey(commitmentID uint64) []byte { key := make([]byte, 9) key[0] = collocatioPrefixCommitment binary.BigEndian.PutUint64(key[1:], commitmentID) return key } func makeCollocatioCommitmentByOppKey(oppID, commitmentID uint64) []byte { key := make([]byte, 17) key[0] = collocatioPrefixCommitmentByOpp binary.BigEndian.PutUint64(key[1:9], oppID) binary.BigEndian.PutUint64(key[9:], commitmentID) return key } func makeCollocatioCommitmentByInvestorKey(vitaID, commitmentID uint64) []byte { key := make([]byte, 17) key[0] = collocatioPrefixCommitmentByInvestor binary.BigEndian.PutUint64(key[1:9], vitaID) binary.BigEndian.PutUint64(key[9:], commitmentID) return key } func makeCollocatioCommitmentCounterKey() []byte { return []byte{collocatioPrefixCommitmentCounter} } // ============================================================================ // 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) } func (c *Collocatio) getCommitmentInternal(d *dao.Simple, commitmentID uint64) *state.InvestmentCommitment { si := d.GetStorageItem(c.ID, makeCollocatioCommitmentKey(commitmentID)) if si == nil { return nil } commitment := new(state.InvestmentCommitment) item, _ := stackitem.Deserialize(si) commitment.FromStackItem(item) return commitment } func (c *Collocatio) putCommitment(d *dao.Simple, commitment *state.InvestmentCommitment) { item, _ := commitment.ToStackItem() data, _ := stackitem.Serialize(item) d.PutStorageItem(c.ID, makeCollocatioCommitmentKey(commitment.ID), data) } // getInvestorTotalInOpportunity returns the total amount an investor has invested in a specific opportunity. func (c *Collocatio) getInvestorTotalInOpportunity(d *dao.Simple, vitaID, oppID uint64) uint64 { prefix := []byte{collocatioPrefixInvestmentByInvestor} prefix = append(prefix, make([]byte, 8)...) binary.BigEndian.PutUint64(prefix[1:], vitaID) var total uint64 d.Seek(c.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { if len(k) >= 16 { invID := binary.BigEndian.Uint64(k[8:16]) inv := c.getInvestmentInternal(d, invID) if inv != nil && inv.OpportunityID == oppID && inv.Status == state.InvestmentActive { total += inv.Amount } } return true }) return total } // ============================================================================ // 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.Tutus.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) // Whale concentration check - prevent any single investor from holding too much of the pool if cfg.WealthConcentration > 0 { existingInvestment := c.getInvestorTotalInOpportunity(ic.DAO, vitaID, oppID) newTotal := existingInvestment + amount // Calculate what percentage of the pool this investor would hold // (newTotal / (opp.TotalPool + amount)) * 10000 > WealthConcentration futurePool := opp.TotalPool + amount if futurePool > 0 { concentration := (newTotal * 10000) / futurePool if concentration > cfg.WealthConcentration { panic("investment would exceed whale concentration limit") } } } 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.Tutus.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: if elig.Eligibility&state.EligibilityEIO == 0 { return false } // Additionally verify active employment return c.hasActiveEmployment(d, elig.VitaID) case state.OpportunityCIO: if elig.Eligibility&state.EligibilityCIO == 0 { return false } // Additionally verify active contractor status return c.hasActiveContractor(d, elig.VitaID) 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) } // ============================================================================ // Violation System // ============================================================================ func (c *Collocatio) getViolationInternal(d *dao.Simple, violationID uint64) *state.InvestmentViolation { si := d.GetStorageItem(c.ID, makeCollocatioViolationKey(violationID)) if si == nil { return nil } v := new(state.InvestmentViolation) item, _ := stackitem.Deserialize(si) v.FromStackItem(item) return v } func (c *Collocatio) putViolation(d *dao.Simple, v *state.InvestmentViolation) { item, _ := v.ToStackItem() data, _ := stackitem.Serialize(item) d.PutStorageItem(c.ID, makeCollocatioViolationKey(v.ID), data) } func (c *Collocatio) recordViolation(ic *interop.Context, args []stackitem.Item) stackitem.Item { violator := toUint160(args[0]) opportunityID := toUint64(args[1]) violationType := toString(args[2]) description := toString(args[3]) evidenceHashBytes, err := args[4].TryBytes() if err != nil { panic(err) } evidenceHash, err := util.Uint256DecodeBytesBE(evidenceHashBytes) if err != nil { panic(err) } penalty := toUint64(args[5]) // Authorization: Committee or RoleInvestmentManager if !c.Tutus.CheckCommittee(ic) { caller := ic.VM.GetCallingScriptHash() if !c.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleInvestmentManager, ic.Block.Index) { panic("only committee or investment manager can record violations") } } // Verify violator has Vita vita, err := c.Vita.GetTokenByOwner(ic.DAO, violator) if err != nil || vita == nil { panic("violator must have active Vita") } vitaID := vita.TokenID // Create violation record violationID := c.incrementCounter(ic.DAO, makeCollocatioViolationCounterKey()) caller := ic.VM.GetCallingScriptHash() v := &state.InvestmentViolation{ ID: violationID, VitaID: vitaID, Violator: violator, OpportunityID: opportunityID, ViolationType: violationType, Description: description, EvidenceHash: evidenceHash, Penalty: penalty, ReportedBy: caller, ReportedAt: ic.Block.Index, ResolvedAt: 0, Resolution: "", } c.putViolation(ic.DAO, v) // Store index by investor ic.DAO.PutStorageItem(c.ID, makeCollocatioViolationByInvestorKey(vitaID, violationID), []byte{1}) // Update eligibility elig := c.getEligibilityInternal(ic.DAO, violator) if elig == nil { elig = &state.InvestorEligibility{ VitaID: vitaID, Investor: violator, CreatedAt: ic.Block.Index, } } elig.HasViolations = true elig.ViolationCount++ elig.UpdatedAt = ic.Block.Index c.putEligibility(ic.DAO, elig) // Apply penalty if specified if penalty > 0 { if err := c.VTS.transferUnrestricted(ic, violator, nativehashes.Treasury, new(big.Int).SetUint64(penalty), nil); err != nil { // Don't panic if transfer fails, just record violation without penalty } } // Emit event ic.AddNotification(c.Hash, collocatioViolationRecordedEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewBigInteger(new(big.Int).SetUint64(violationID)), stackitem.NewByteArray(violator.BytesBE()), stackitem.NewByteArray([]byte(violationType)), })) return stackitem.NewBigInteger(new(big.Int).SetUint64(violationID)) } func (c *Collocatio) resolveViolation(ic *interop.Context, args []stackitem.Item) stackitem.Item { violationID := toUint64(args[0]) resolution := toString(args[1]) // Authorization: Committee or RoleInvestmentManager if !c.Tutus.CheckCommittee(ic) { caller := ic.VM.GetCallingScriptHash() if !c.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleInvestmentManager, ic.Block.Index) { panic("only committee or investment manager can resolve violations") } } v := c.getViolationInternal(ic.DAO, violationID) if v == nil { panic("violation not found") } if v.ResolvedAt != 0 { panic("violation already resolved") } v.ResolvedAt = ic.Block.Index v.Resolution = resolution c.putViolation(ic.DAO, v) // Emit event ic.AddNotification(c.Hash, collocatioViolationResolvedEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewBigInteger(new(big.Int).SetUint64(violationID)), stackitem.NewByteArray([]byte(resolution)), })) return stackitem.NewBool(true) } func (c *Collocatio) getViolation(ic *interop.Context, args []stackitem.Item) stackitem.Item { violationID := toUint64(args[0]) v := c.getViolationInternal(ic.DAO, violationID) if v == nil { return stackitem.Null{} } return violationToStackItem(v) } // ============================================================================ // Employment Verification (EIO) // ============================================================================ func (c *Collocatio) getEmploymentInternal(d *dao.Simple, employeeVitaID uint64) *state.EmploymentVerification { si := d.GetStorageItem(c.ID, makeCollocatioEmploymentKey(employeeVitaID)) if si == nil { return nil } ev := new(state.EmploymentVerification) item, _ := stackitem.Deserialize(si) ev.FromStackItem(item) return ev } func (c *Collocatio) putEmployment(d *dao.Simple, ev *state.EmploymentVerification) { item, _ := ev.ToStackItem() data, _ := stackitem.Serialize(item) d.PutStorageItem(c.ID, makeCollocatioEmploymentKey(ev.VitaID), data) } func (c *Collocatio) verifyEmployment(ic *interop.Context, args []stackitem.Item) stackitem.Item { employee := toUint160(args[0]) employer := toUint160(args[1]) position := toString(args[2]) startDate := uint32(toUint64(args[3])) caller := ic.VM.GetCallingScriptHash() // Authorization: Committee, RoleInvestmentManager, or employer isAuthorized := c.Tutus.CheckCommittee(ic) || c.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleInvestmentManager, ic.Block.Index) || caller == employer if !isAuthorized { panic("only committee, investment manager, or employer can verify employment") } // Verify both employee and employer have Vita employeeVita, err := c.Vita.GetTokenByOwner(ic.DAO, employee) if err != nil { panic("employee must have Vita token") } employerVita, err := c.Vita.GetTokenByOwner(ic.DAO, employer) if err != nil { panic("employer must have Vita token") } ev := &state.EmploymentVerification{ VitaID: employeeVita.TokenID, Employee: employee, EmployerVitaID: employerVita.TokenID, Employer: employer, Position: position, StartDate: startDate, EndDate: 0, IsActive: true, VerifiedAt: ic.Block.Index, VerifiedBy: caller, } c.putEmployment(ic.DAO, ev) // Store index by employer ic.DAO.PutStorageItem(c.ID, makeCollocatioEmploymentByEmployerKey(employerVita.TokenID, employeeVita.TokenID), []byte{1}) // Update eligibility elig := c.getEligibilityInternal(ic.DAO, employee) if elig == nil { elig = &state.InvestorEligibility{ VitaID: employeeVita.TokenID, Investor: employee, CreatedAt: ic.Block.Index, } } elig.Eligibility |= state.EligibilityEIO elig.UpdatedAt = ic.Block.Index c.putEligibility(ic.DAO, elig) // Emit event ic.AddNotification(c.Hash, collocatioEmploymentVerifiedEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewByteArray(employee.BytesBE()), stackitem.NewByteArray(employer.BytesBE()), stackitem.NewByteArray([]byte(position)), })) return stackitem.NewBool(true) } func (c *Collocatio) revokeEmployment(ic *interop.Context, args []stackitem.Item) stackitem.Item { employee := toUint160(args[0]) employer := toUint160(args[1]) endDate := uint32(toUint64(args[2])) caller := ic.VM.GetCallingScriptHash() // Authorization: Committee, RoleInvestmentManager, or employer isAuthorized := c.Tutus.CheckCommittee(ic) || c.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleInvestmentManager, ic.Block.Index) || caller == employer if !isAuthorized { panic("only committee, investment manager, or employer can revoke employment") } // Get employee Vita employeeVita, err := c.Vita.GetTokenByOwner(ic.DAO, employee) if err != nil { panic("employee must have Vita token") } ev := c.getEmploymentInternal(ic.DAO, employeeVita.TokenID) if ev == nil { panic("employment not found") } if !ev.IsActive { panic("employment already revoked") } if ev.Employer != employer { panic("employer mismatch") } ev.IsActive = false ev.EndDate = endDate c.putEmployment(ic.DAO, ev) // Remove EIO eligibility elig := c.getEligibilityInternal(ic.DAO, employee) if elig != nil { elig.Eligibility &^= state.EligibilityEIO elig.UpdatedAt = ic.Block.Index c.putEligibility(ic.DAO, elig) } // Emit event ic.AddNotification(c.Hash, collocatioEmploymentRevokedEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewByteArray(employee.BytesBE()), stackitem.NewByteArray(employer.BytesBE()), })) return stackitem.NewBool(true) } func (c *Collocatio) getEmploymentStatus(ic *interop.Context, args []stackitem.Item) stackitem.Item { employee := toUint160(args[0]) vita, err := c.Vita.GetTokenByOwner(ic.DAO, employee) if err != nil { return stackitem.Null{} } ev := c.getEmploymentInternal(ic.DAO, vita.TokenID) if ev == nil { return stackitem.Null{} } return employmentToStackItem(ev) } func (c *Collocatio) hasActiveEmployment(d *dao.Simple, vitaID uint64) bool { ev := c.getEmploymentInternal(d, vitaID) return ev != nil && ev.IsActive } // ============================================================================ // Contractor Verification (CIO) // ============================================================================ func (c *Collocatio) getContractorInternal(d *dao.Simple, contractorVitaID uint64) *state.ContractorVerification { si := d.GetStorageItem(c.ID, makeCollocatioContractorKey(contractorVitaID)) if si == nil { return nil } cv := new(state.ContractorVerification) item, _ := stackitem.Deserialize(si) cv.FromStackItem(item) return cv } func (c *Collocatio) putContractor(d *dao.Simple, cv *state.ContractorVerification) { item, _ := cv.ToStackItem() data, _ := stackitem.Serialize(item) d.PutStorageItem(c.ID, makeCollocatioContractorKey(cv.VitaID), data) } func (c *Collocatio) verifyContractor(ic *interop.Context, args []stackitem.Item) stackitem.Item { contractor := toUint160(args[0]) platform := toUint160(args[1]) platformID := toString(args[2]) contractorID := toString(args[3]) startDate := uint32(toUint64(args[4])) caller := ic.VM.GetCallingScriptHash() // Authorization: Committee, RoleInvestmentManager, or platform isAuthorized := c.Tutus.CheckCommittee(ic) || c.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleInvestmentManager, ic.Block.Index) || caller == platform if !isAuthorized { panic("only committee, investment manager, or platform can verify contractor") } // Verify contractor has Vita contractorVita, err := c.Vita.GetTokenByOwner(ic.DAO, contractor) if err != nil { panic("contractor must have Vita token") } cv := &state.ContractorVerification{ VitaID: contractorVita.TokenID, Contractor: contractor, PlatformID: platformID, Platform: platform, ContractorID: contractorID, StartDate: startDate, EndDate: 0, IsActive: true, VerifiedAt: ic.Block.Index, VerifiedBy: caller, } c.putContractor(ic.DAO, cv) // Store index by platform ic.DAO.PutStorageItem(c.ID, makeCollocatioContractorByPlatformKey(platform, contractorVita.TokenID), []byte{1}) // Update eligibility elig := c.getEligibilityInternal(ic.DAO, contractor) if elig == nil { elig = &state.InvestorEligibility{ VitaID: contractorVita.TokenID, Investor: contractor, CreatedAt: ic.Block.Index, } } elig.Eligibility |= state.EligibilityCIO elig.UpdatedAt = ic.Block.Index c.putEligibility(ic.DAO, elig) // Emit event ic.AddNotification(c.Hash, collocatioContractorVerifiedEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewByteArray(contractor.BytesBE()), stackitem.NewByteArray(platform.BytesBE()), stackitem.NewByteArray([]byte(platformID)), })) return stackitem.NewBool(true) } func (c *Collocatio) revokeContractor(ic *interop.Context, args []stackitem.Item) stackitem.Item { contractor := toUint160(args[0]) platform := toUint160(args[1]) endDate := uint32(toUint64(args[2])) caller := ic.VM.GetCallingScriptHash() // Authorization: Committee, RoleInvestmentManager, or platform isAuthorized := c.Tutus.CheckCommittee(ic) || c.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleInvestmentManager, ic.Block.Index) || caller == platform if !isAuthorized { panic("only committee, investment manager, or platform can revoke contractor") } // Get contractor Vita contractorVita, err := c.Vita.GetTokenByOwner(ic.DAO, contractor) if err != nil { panic("contractor must have Vita token") } cv := c.getContractorInternal(ic.DAO, contractorVita.TokenID) if cv == nil { panic("contractor not found") } if !cv.IsActive { panic("contractor already revoked") } if cv.Platform != platform { panic("platform mismatch") } cv.IsActive = false cv.EndDate = endDate c.putContractor(ic.DAO, cv) // Remove CIO eligibility elig := c.getEligibilityInternal(ic.DAO, contractor) if elig != nil { elig.Eligibility &^= state.EligibilityCIO elig.UpdatedAt = ic.Block.Index c.putEligibility(ic.DAO, elig) } // Emit event ic.AddNotification(c.Hash, collocatioContractorRevokedEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewByteArray(contractor.BytesBE()), stackitem.NewByteArray(platform.BytesBE()), })) return stackitem.NewBool(true) } func (c *Collocatio) getContractorStatus(ic *interop.Context, args []stackitem.Item) stackitem.Item { contractor := toUint160(args[0]) vita, err := c.Vita.GetTokenByOwner(ic.DAO, contractor) if err != nil { return stackitem.Null{} } cv := c.getContractorInternal(ic.DAO, vita.TokenID) if cv == nil { return stackitem.Null{} } return contractorToStackItem(cv) } func (c *Collocatio) hasActiveContractor(d *dao.Simple, vitaID uint64) bool { cv := c.getContractorInternal(d, vitaID) return cv != nil && cv.IsActive } // ============================================================================ // Education Completion // ============================================================================ func (c *Collocatio) completeInvestmentEducation(ic *interop.Context, args []stackitem.Item) stackitem.Item { investor := toUint160(args[0]) caller := ic.VM.GetCallingScriptHash() // Authorization: Committee or RoleInvestmentManager if !c.Tutus.CheckCommittee(ic) { if !c.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleInvestmentManager, ic.Block.Index) { panic("only committee or investment manager can complete education") } } // Verify investor has Vita vita, err := c.Vita.GetTokenByOwner(ic.DAO, investor) if err != nil { panic("investor must have Vita token") } // Update eligibility elig := c.getEligibilityInternal(ic.DAO, investor) if elig == nil { elig = &state.InvestorEligibility{ VitaID: vita.TokenID, Investor: investor, CreatedAt: ic.Block.Index, } } elig.ScireCompleted = true elig.UpdatedAt = ic.Block.Index c.putEligibility(ic.DAO, elig) // Emit event ic.AddNotification(c.Hash, collocatioEducationCompletedEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewByteArray(investor.BytesBE()), })) return stackitem.NewBool(true) } // ============================================================================ // Returns Distribution & Cancellation // ============================================================================ func (c *Collocatio) distributeReturns(ic *interop.Context, args []stackitem.Item) stackitem.Item { oppID := toUint64(args[0]) actualReturns := toUint64(args[1]) // Authorization: Committee or RoleInvestmentManager if !c.Tutus.CheckCommittee(ic) { caller := ic.VM.GetCallingScriptHash() if !c.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleInvestmentManager, ic.Block.Index) { panic("only committee or investment manager can distribute returns") } } opp := c.getOpportunityInternal(ic.DAO, oppID) if opp == nil { panic("opportunity not found") } if opp.Status != state.OpportunityClosed { panic("opportunity must be in closed status") } if ic.Block.Index < opp.MaturityDate { panic("maturity date not reached") } if opp.TotalPool == 0 { panic("no investments to distribute") } // Collect all investment IDs first (to avoid modifying during iteration) prefix := []byte{collocatioPrefixInvestmentByOpp} prefix = append(prefix, make([]byte, 8)...) binary.BigEndian.PutUint64(prefix[1:], oppID) var invIDs []uint64 ic.DAO.Seek(c.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { if len(k) >= 16 { invID := binary.BigEndian.Uint64(k[8:16]) invIDs = append(invIDs, invID) } return true }) // Process each investment for _, invID := range invIDs { inv := c.getInvestmentInternal(ic.DAO, invID) if inv == nil || inv.Status != state.InvestmentActive { continue } // Calculate proportional return returnAmount := (inv.Amount * actualReturns) / opp.TotalPool // Transfer return to investor if returnAmount > 0 { if err := c.VTS.transferUnrestricted(ic, c.Hash, inv.Investor, new(big.Int).SetUint64(returnAmount), nil); err != nil { continue // Skip failed transfers } } // Also return principal if inv.Amount > 0 { if err := c.VTS.transferUnrestricted(ic, c.Hash, inv.Investor, new(big.Int).SetUint64(inv.Amount), nil); err != nil { continue } } // Update investment inv.ReturnAmount = returnAmount inv.Status = state.InvestmentCompleted inv.UpdatedAt = ic.Block.Index c.putInvestment(ic.DAO, inv) // Update eligibility elig := c.getEligibilityInternal(ic.DAO, inv.Investor) if elig != nil { elig.TotalReturns += returnAmount elig.CompletedInvestments++ if elig.ActiveInvestments > 0 { elig.ActiveInvestments-- } if elig.TotalInvested >= inv.Amount { elig.TotalInvested -= inv.Amount } elig.LastActivity = ic.Block.Index elig.UpdatedAt = ic.Block.Index c.putEligibility(ic.DAO, elig) } // Emit event for each distribution ic.AddNotification(c.Hash, collocatioReturnsDistributedEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewBigInteger(new(big.Int).SetUint64(invID)), stackitem.NewByteArray(inv.Investor.BytesBE()), stackitem.NewBigInteger(new(big.Int).SetUint64(returnAmount)), })) } // Update opportunity status opp.Status = state.OpportunityCompleted opp.UpdatedAt = ic.Block.Index c.putOpportunity(ic.DAO, opp) return stackitem.NewBool(true) } func (c *Collocatio) cancelOpportunity(ic *interop.Context, args []stackitem.Item) stackitem.Item { oppID := toUint64(args[0]) caller := ic.VM.GetCallingScriptHash() opp := c.getOpportunityInternal(ic.DAO, oppID) if opp == nil { panic("opportunity not found") } // Authorization: Creator (if Draft/Voting) or Committee isCreator := caller == opp.Creator isCommittee := c.Tutus.CheckCommittee(ic) if opp.Status == state.OpportunityDraft || opp.Status == state.OpportunityVoting { if !isCreator && !isCommittee { panic("only creator or committee can cancel draft/voting opportunity") } } else if opp.Status == state.OpportunityActive { if !isCommittee { panic("only committee can cancel active opportunity") } } else { panic("opportunity cannot be cancelled in current status") } // Collect all investment IDs first (to avoid modifying during iteration) prefix := []byte{collocatioPrefixInvestmentByOpp} prefix = append(prefix, make([]byte, 8)...) binary.BigEndian.PutUint64(prefix[1:], oppID) var invIDs []uint64 ic.DAO.Seek(c.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { if len(k) >= 16 { invID := binary.BigEndian.Uint64(k[8:16]) invIDs = append(invIDs, invID) } return true }) // Process each investment for _, invID := range invIDs { inv := c.getInvestmentInternal(ic.DAO, invID) if inv == nil || inv.Status != state.InvestmentActive { continue } // Refund full amount if inv.Amount > 0 { if err := c.VTS.transferUnrestricted(ic, c.Hash, inv.Investor, new(big.Int).SetUint64(inv.Amount), nil); err != nil { continue } } // Update investment inv.Status = state.InvestmentRefunded inv.UpdatedAt = ic.Block.Index c.putInvestment(ic.DAO, inv) // Update eligibility elig := c.getEligibilityInternal(ic.DAO, inv.Investor) if elig != nil { if elig.ActiveInvestments > 0 { elig.ActiveInvestments-- } if elig.TotalInvested >= inv.Amount { elig.TotalInvested -= inv.Amount } elig.LastActivity = ic.Block.Index elig.UpdatedAt = ic.Block.Index c.putEligibility(ic.DAO, elig) } } // Update opportunity status opp.Status = state.OpportunityCancelled opp.UpdatedAt = ic.Block.Index c.putOpportunity(ic.DAO, opp) // Emit event ic.AddNotification(c.Hash, collocatioOpportunityCancelledEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewBigInteger(new(big.Int).SetUint64(oppID)), })) return stackitem.NewBool(true) } // ============================================================================ // Query Methods // ============================================================================ func (c *Collocatio) getInvestmentsByOpportunity(ic *interop.Context, args []stackitem.Item) stackitem.Item { oppID := toUint64(args[0]) prefix := []byte{collocatioPrefixInvestmentByOpp} prefix = append(prefix, make([]byte, 8)...) binary.BigEndian.PutUint64(prefix[1:], oppID) var ids []stackitem.Item ic.DAO.Seek(c.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { if len(k) >= 16 { invID := binary.BigEndian.Uint64(k[8:16]) ids = append(ids, stackitem.NewBigInteger(new(big.Int).SetUint64(invID))) } return true }) return stackitem.NewArray(ids) } func (c *Collocatio) getInvestmentsByInvestor(ic *interop.Context, args []stackitem.Item) stackitem.Item { investor := toUint160(args[0]) vita, err := c.Vita.GetTokenByOwner(ic.DAO, investor) if err != nil { return stackitem.NewArray(nil) } prefix := []byte{collocatioPrefixInvestmentByInvestor} prefix = append(prefix, make([]byte, 8)...) binary.BigEndian.PutUint64(prefix[1:], vita.TokenID) var ids []stackitem.Item ic.DAO.Seek(c.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { if len(k) >= 16 { invID := binary.BigEndian.Uint64(k[8:16]) ids = append(ids, stackitem.NewBigInteger(new(big.Int).SetUint64(invID))) } return true }) return stackitem.NewArray(ids) } func (c *Collocatio) getOpportunitiesByType(ic *interop.Context, args []stackitem.Item) stackitem.Item { oppType := state.OpportunityType(toUint64(args[0])) prefix := []byte{collocatioPrefixOpportunityByType, byte(oppType)} var ids []stackitem.Item ic.DAO.Seek(c.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { if len(k) >= 9 { oppID := binary.BigEndian.Uint64(k[1:9]) ids = append(ids, stackitem.NewBigInteger(new(big.Int).SetUint64(oppID))) } return true }) return stackitem.NewArray(ids) } func (c *Collocatio) getOpportunitiesByStatus(ic *interop.Context, args []stackitem.Item) stackitem.Item { status := state.OpportunityStatus(toUint64(args[0])) prefix := []byte{collocatioPrefixOpportunityByStatus, byte(status)} var ids []stackitem.Item ic.DAO.Seek(c.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { if len(k) >= 9 { oppID := binary.BigEndian.Uint64(k[1:9]) ids = append(ids, stackitem.NewBigInteger(new(big.Int).SetUint64(oppID))) } return true }) return stackitem.NewArray(ids) } // ============================================================================ // 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))), }) } func violationToStackItem(v *state.InvestmentViolation) stackitem.Item { return stackitem.NewArray([]stackitem.Item{ stackitem.NewBigInteger(new(big.Int).SetUint64(v.ID)), stackitem.NewBigInteger(new(big.Int).SetUint64(v.VitaID)), stackitem.NewByteArray(v.Violator.BytesBE()), stackitem.NewBigInteger(new(big.Int).SetUint64(v.OpportunityID)), stackitem.NewByteArray([]byte(v.ViolationType)), stackitem.NewByteArray([]byte(v.Description)), stackitem.NewByteArray(v.EvidenceHash.BytesBE()), stackitem.NewBigInteger(new(big.Int).SetUint64(v.Penalty)), stackitem.NewByteArray(v.ReportedBy.BytesBE()), stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(v.ReportedAt))), stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(v.ResolvedAt))), stackitem.NewByteArray([]byte(v.Resolution)), }) } func employmentToStackItem(ev *state.EmploymentVerification) stackitem.Item { return stackitem.NewArray([]stackitem.Item{ stackitem.NewBigInteger(new(big.Int).SetUint64(ev.VitaID)), stackitem.NewByteArray(ev.Employee.BytesBE()), stackitem.NewBigInteger(new(big.Int).SetUint64(ev.EmployerVitaID)), stackitem.NewByteArray(ev.Employer.BytesBE()), stackitem.NewByteArray([]byte(ev.Position)), stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(ev.StartDate))), stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(ev.EndDate))), stackitem.NewBool(ev.IsActive), stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(ev.VerifiedAt))), stackitem.NewByteArray(ev.VerifiedBy.BytesBE()), }) } func contractorToStackItem(cv *state.ContractorVerification) stackitem.Item { return stackitem.NewArray([]stackitem.Item{ stackitem.NewBigInteger(new(big.Int).SetUint64(cv.VitaID)), stackitem.NewByteArray(cv.Contractor.BytesBE()), stackitem.NewByteArray([]byte(cv.PlatformID)), stackitem.NewByteArray(cv.Platform.BytesBE()), stackitem.NewByteArray([]byte(cv.ContractorID)), stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(cv.StartDate))), stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(cv.EndDate))), stackitem.NewBool(cv.IsActive), stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(cv.VerifiedAt))), stackitem.NewByteArray(cv.VerifiedBy.BytesBE()), }) } func commitmentToStackItem(c *state.InvestmentCommitment) stackitem.Item { return stackitem.NewArray([]stackitem.Item{ stackitem.NewBigInteger(new(big.Int).SetUint64(c.ID)), stackitem.NewBigInteger(new(big.Int).SetUint64(c.OpportunityID)), stackitem.NewBigInteger(new(big.Int).SetUint64(c.VitaID)), stackitem.NewByteArray(c.Investor.BytesBE()), stackitem.NewByteArray(c.Commitment.BytesBE()), stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(c.Status))), stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(c.CommittedAt))), stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(c.RevealDeadline))), stackitem.NewBigInteger(new(big.Int).SetUint64(c.RevealedAmount)), stackitem.NewBigInteger(new(big.Int).SetUint64(c.InvestmentID)), }) } // ============================================================================ // Commit-Reveal System (Anti-Front-Running) // ============================================================================ // commitInvestment creates a commitment to invest without revealing the amount. // The commitment is hash(amount || nonce || investor). func (col *Collocatio) commitInvestment(ic *interop.Context, args []stackitem.Item) stackitem.Item { oppID := toUint64(args[0]) commitmentHashBytes, err := args[1].TryBytes() if err != nil { panic(err) } commitmentHash, err := util.Uint256DecodeBytesBE(commitmentHashBytes) if err != nil { panic("invalid commitment hash") } caller := ic.VM.GetCallingScriptHash() // Validate caller has Vita vita, err := col.Vita.GetTokenByOwner(ic.DAO, caller) if err != nil { panic("caller must have Vita token") } vitaID := vita.TokenID // Get opportunity opp := col.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") } // Check eligibility if !col.isEligibleInternal(ic.DAO, caller, opp.Type) { panic("investor not eligible for this opportunity type") } cfg := col.getConfigInternal(ic.DAO) // Create commitment commitmentID := col.incrementCounter(ic.DAO, makeCollocatioCommitmentCounterKey()) currentBlock := ic.Block.Index commitment := &state.InvestmentCommitment{ ID: commitmentID, OpportunityID: oppID, VitaID: vitaID, Investor: caller, Commitment: commitmentHash, Status: state.CommitmentPending, CommittedAt: currentBlock, RevealDeadline: currentBlock + cfg.CommitRevealDelay + cfg.CommitRevealWindow, RevealedAmount: 0, InvestmentID: 0, } col.putCommitment(ic.DAO, commitment) // Store indexes ic.DAO.PutStorageItem(col.ID, makeCollocatioCommitmentByOppKey(oppID, commitmentID), []byte{1}) ic.DAO.PutStorageItem(col.ID, makeCollocatioCommitmentByInvestorKey(vitaID, commitmentID), []byte{1}) // Emit event ic.AddNotification(col.Hash, collocatioCommitmentCreatedEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewBigInteger(new(big.Int).SetUint64(commitmentID)), stackitem.NewBigInteger(new(big.Int).SetUint64(oppID)), stackitem.NewByteArray(caller.BytesBE()), })) return stackitem.NewBigInteger(new(big.Int).SetUint64(commitmentID)) } // revealInvestment reveals the investment amount and executes the investment. // The reveal must happen after CommitRevealDelay blocks but before RevealDeadline. func (col *Collocatio) revealInvestment(ic *interop.Context, args []stackitem.Item) stackitem.Item { commitmentID := toUint64(args[0]) amount := toUint64(args[1]) nonce, err := args[2].TryBytes() if err != nil { panic("invalid nonce") } caller := ic.VM.GetCallingScriptHash() // Get commitment commitment := col.getCommitmentInternal(ic.DAO, commitmentID) if commitment == nil { panic("commitment not found") } if commitment.Investor != caller { panic("only commitment owner can reveal") } if commitment.Status != state.CommitmentPending { panic("commitment already processed") } cfg := col.getConfigInternal(ic.DAO) // Check timing currentBlock := ic.Block.Index revealStart := commitment.CommittedAt + cfg.CommitRevealDelay if currentBlock < revealStart { panic("reveal period not started yet") } if currentBlock > commitment.RevealDeadline { panic("reveal deadline passed") } // Verify commitment: hash(amount || nonce || investor) preimage := make([]byte, 8+len(nonce)+util.Uint160Size) binary.BigEndian.PutUint64(preimage[:8], amount) copy(preimage[8:8+len(nonce)], nonce) copy(preimage[8+len(nonce):], caller.BytesBE()) // Hash the preimage using SHA256 computedHash := hash.Sha256(preimage) if computedHash != commitment.Commitment { panic("commitment verification failed") } // Get opportunity opp := col.getOpportunityInternal(ic.DAO, commitment.OpportunityID) if opp == nil { panic("opportunity not found") } if opp.Status != state.OpportunityActive { panic("opportunity is not active") } if currentBlock > opp.InvestmentDeadline { panic("investment deadline has passed") } // Validate amount if amount < opp.MinInvestment { panic("investment below minimum") } if amount > opp.MaxInvestment { panic("investment exceeds maximum") } if opp.MaxParticipants > 0 && opp.CurrentParticipants >= opp.MaxParticipants { panic("maximum participants reached") } // Whale concentration check if cfg.WealthConcentration > 0 { existingInvestment := col.getInvestorTotalInOpportunity(ic.DAO, commitment.VitaID, commitment.OpportunityID) newTotal := existingInvestment + amount futurePool := opp.TotalPool + amount if futurePool > 0 { concentration := (newTotal * 10000) / futurePool if concentration > cfg.WealthConcentration { panic("investment would exceed whale concentration limit") } } } // Calculate fee fee := (amount * cfg.InvestmentFee) / 10000 netAmount := amount - fee // Transfer VTS from investor if err := col.VTS.transferUnrestricted(ic, caller, col.Hash, new(big.Int).SetUint64(amount), nil); err != nil { panic("failed to transfer investment amount") } // Send fee to Treasury if fee > 0 { if err := col.VTS.transferUnrestricted(ic, col.Hash, nativehashes.Treasury, new(big.Int).SetUint64(fee), nil); err != nil { panic("failed to transfer fee to treasury") } } // Create investment record invID := col.incrementCounter(ic.DAO, makeCollocatioInvCounterKey()) inv := &state.Investment{ ID: invID, OpportunityID: commitment.OpportunityID, VitaID: commitment.VitaID, Investor: caller, Amount: netAmount, Status: state.InvestmentActive, ReturnAmount: 0, CreatedAt: currentBlock, UpdatedAt: currentBlock, } col.putInvestment(ic.DAO, inv) // Store indexes ic.DAO.PutStorageItem(col.ID, makeCollocatioInvByOppKey(commitment.OpportunityID, invID), []byte{1}) ic.DAO.PutStorageItem(col.ID, makeCollocatioInvByInvestorKey(commitment.VitaID, invID), []byte{1}) // Update opportunity opp.CurrentParticipants++ opp.TotalPool += netAmount opp.UpdatedAt = currentBlock col.putOpportunity(ic.DAO, opp) // Update commitment commitment.Status = state.CommitmentRevealed commitment.RevealedAmount = amount commitment.InvestmentID = invID col.putCommitment(ic.DAO, commitment) // Update eligibility col.updateEligibilityOnInvest(ic.DAO, caller, commitment.VitaID, netAmount, currentBlock) // Emit events ic.AddNotification(col.Hash, collocatioCommitmentRevealedEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewBigInteger(new(big.Int).SetUint64(commitmentID)), stackitem.NewBigInteger(new(big.Int).SetUint64(invID)), stackitem.NewBigInteger(new(big.Int).SetUint64(amount)), })) ic.AddNotification(col.Hash, collocatioInvestmentMadeEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewBigInteger(new(big.Int).SetUint64(invID)), stackitem.NewBigInteger(new(big.Int).SetUint64(commitment.OpportunityID)), stackitem.NewByteArray(caller.BytesBE()), stackitem.NewBigInteger(new(big.Int).SetUint64(netAmount)), })) return stackitem.NewBigInteger(new(big.Int).SetUint64(invID)) } // cancelCommitment cancels a pending commitment. func (col *Collocatio) cancelCommitment(ic *interop.Context, args []stackitem.Item) stackitem.Item { commitmentID := toUint64(args[0]) caller := ic.VM.GetCallingScriptHash() commitment := col.getCommitmentInternal(ic.DAO, commitmentID) if commitment == nil { panic("commitment not found") } if commitment.Investor != caller { panic("only commitment owner can cancel") } if commitment.Status != state.CommitmentPending { panic("commitment already processed") } // Update commitment status commitment.Status = state.CommitmentCanceled col.putCommitment(ic.DAO, commitment) // Emit event ic.AddNotification(col.Hash, collocatioCommitmentCanceledEvent, stackitem.NewArray([]stackitem.Item{ stackitem.NewBigInteger(new(big.Int).SetUint64(commitmentID)), })) return stackitem.NewBool(true) } // getCommitment returns commitment details. func (col *Collocatio) getCommitment(ic *interop.Context, args []stackitem.Item) stackitem.Item { commitmentID := toUint64(args[0]) commitment := col.getCommitmentInternal(ic.DAO, commitmentID) if commitment == nil { return stackitem.Null{} } return commitmentToStackItem(commitment) } // getCommitmentCount returns the total number of commitments. func (col *Collocatio) getCommitmentCount(ic *interop.Context, _ []stackitem.Item) stackitem.Item { count := col.getCounter(ic.DAO, makeCollocatioCommitmentCounterKey()) return stackitem.NewBigInteger(new(big.Int).SetUint64(count)) }