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))), }) }