tutus-chain/pkg/core/native/collocatio.go

2179 lines
74 KiB
Go

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/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
)
// 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"
)
// 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)
// 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)
// ===== 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))
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.
// 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
}
// ============================================================================
// 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.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)
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()),
})
}