980 lines
34 KiB
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
|
|
Annos IAnnos
|
|
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.Annos.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.Annos.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))),
|
|
})
|
|
}
|