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

980 lines
34 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/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
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.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:
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))),
})
}