2615 lines
91 KiB
Go
Executable File
2615 lines
91 KiB
Go
Executable File
package native
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
"math/big"
|
|
|
|
"github.com/tutus-one/tutus-chain/pkg/config"
|
|
"github.com/tutus-one/tutus-chain/pkg/core/dao"
|
|
"github.com/tutus-one/tutus-chain/pkg/core/interop"
|
|
"github.com/tutus-one/tutus-chain/pkg/core/native/nativehashes"
|
|
"github.com/tutus-one/tutus-chain/pkg/core/native/nativeids"
|
|
"github.com/tutus-one/tutus-chain/pkg/core/native/nativenames"
|
|
"github.com/tutus-one/tutus-chain/pkg/core/state"
|
|
"github.com/tutus-one/tutus-chain/pkg/core/storage"
|
|
"github.com/tutus-one/tutus-chain/pkg/crypto/hash"
|
|
"github.com/tutus-one/tutus-chain/pkg/smartcontract"
|
|
"github.com/tutus-one/tutus-chain/pkg/smartcontract/callflag"
|
|
"github.com/tutus-one/tutus-chain/pkg/smartcontract/manifest"
|
|
"github.com/tutus-one/tutus-chain/pkg/util"
|
|
"github.com/tutus-one/tutus-chain/pkg/vm/stackitem"
|
|
)
|
|
|
|
// Collocatio represents the Investment native contract for democratic investment (PIO/EIO/CIO).
|
|
// Latin: "collocatio" = placement, arrangement (investment)
|
|
type Collocatio struct {
|
|
interop.ContractMD
|
|
Tutus ITutus
|
|
Vita IVita
|
|
RoleRegistry *RoleRegistry
|
|
VTS *VTS
|
|
Scire *Scire
|
|
Eligere *Eligere
|
|
Tribute *Tribute
|
|
}
|
|
|
|
// Storage prefixes for Collocatio contract.
|
|
const (
|
|
collocatioPrefixConfig byte = 0x01 // -> CollocatioConfig
|
|
collocatioPrefixOpportunity byte = 0x10 // opportunityID -> InvestmentOpportunity
|
|
collocatioPrefixOpportunityByType byte = 0x11 // type + opportunityID -> exists
|
|
collocatioPrefixOpportunityByStatus byte = 0x12 // status + opportunityID -> exists
|
|
collocatioPrefixOppCounter byte = 0x1F // -> next opportunity ID
|
|
collocatioPrefixInvestment byte = 0x20 // investmentID -> Investment
|
|
collocatioPrefixInvestmentByOpp byte = 0x21 // opportunityID + investmentID -> exists
|
|
collocatioPrefixInvestmentByInvestor byte = 0x22 // vitaID + investmentID -> exists
|
|
collocatioPrefixInvCounter byte = 0x2F // -> next investment ID
|
|
collocatioPrefixEligibility byte = 0x30 // vitaID -> InvestorEligibility
|
|
collocatioPrefixEligibilityByOwner byte = 0x31 // owner -> vitaID
|
|
collocatioPrefixViolation byte = 0x40 // violationID -> InvestmentViolation
|
|
collocatioPrefixViolationByInvestor byte = 0x41 // vitaID + violationID -> exists
|
|
collocatioPrefixViolationCounter byte = 0x4F // -> next violation ID
|
|
collocatioPrefixEmployment byte = 0x50 // employeeVitaID -> EmploymentVerification
|
|
collocatioPrefixEmploymentByEmployer byte = 0x51 // employerVitaID + employeeVitaID -> exists
|
|
collocatioPrefixContractor byte = 0x60 // contractorVitaID -> ContractorVerification
|
|
collocatioPrefixContractorByPlatform byte = 0x61 // platformHash + contractorVitaID -> exists
|
|
collocatioPrefixCommitment byte = 0x70 // commitmentID -> InvestmentCommitment
|
|
collocatioPrefixCommitmentByOpp byte = 0x71 // opportunityID + commitmentID -> exists
|
|
collocatioPrefixCommitmentByInvestor byte = 0x72 // vitaID + commitmentID -> exists
|
|
collocatioPrefixCommitmentCounter byte = 0x7F // -> next commitment ID
|
|
)
|
|
|
|
// Collocatio events.
|
|
const (
|
|
collocatioOpportunityCreatedEvent = "OpportunityCreated"
|
|
collocatioOpportunityActivatedEvent = "OpportunityActivated"
|
|
collocatioOpportunityClosedEvent = "OpportunityClosed"
|
|
collocatioOpportunityFailedEvent = "OpportunityFailed"
|
|
collocatioOpportunityCancelledEvent = "OpportunityCancelled"
|
|
collocatioInvestmentMadeEvent = "InvestmentMade"
|
|
collocatioInvestmentWithdrawnEvent = "InvestmentWithdrawn"
|
|
collocatioReturnsDistributedEvent = "ReturnsDistributed"
|
|
collocatioEligibilityUpdatedEvent = "EligibilityUpdated"
|
|
collocatioEducationCompletedEvent = "EducationCompleted"
|
|
collocatioViolationRecordedEvent = "ViolationRecorded"
|
|
collocatioViolationResolvedEvent = "ViolationResolved"
|
|
collocatioEmploymentVerifiedEvent = "EmploymentVerified"
|
|
collocatioEmploymentRevokedEvent = "EmploymentRevoked"
|
|
collocatioContractorVerifiedEvent = "ContractorVerified"
|
|
collocatioContractorRevokedEvent = "ContractorRevoked"
|
|
collocatioCommitmentCreatedEvent = "CommitmentCreated"
|
|
collocatioCommitmentRevealedEvent = "CommitmentRevealed"
|
|
collocatioCommitmentCanceledEvent = "CommitmentCanceled"
|
|
)
|
|
|
|
// RoleInvestmentManager is the role ID for investment management.
|
|
const RoleInvestmentManager uint64 = 28
|
|
|
|
// Default config values.
|
|
const (
|
|
defaultMinPIOParticipants uint64 = 100
|
|
defaultMinEIOParticipants uint64 = 10
|
|
defaultMinCIOParticipants uint64 = 25
|
|
defaultMinInvestment uint64 = 100_00000000 // 100 VTS
|
|
defaultMaxIndividualCap uint64 = 1_000_000_00000000 // 1M VTS
|
|
defaultWealthConcentration uint64 = 500 // 5%
|
|
defaultCreationFee uint64 = 1000_00000000 // 1000 VTS
|
|
defaultInvestmentFee uint64 = 50 // 0.5%
|
|
defaultWithdrawalPenalty uint64 = 200 // 2%
|
|
defaultMinVotingPeriod uint32 = 10000
|
|
defaultMinInvestmentPeriod uint32 = 20000
|
|
defaultMinMaturityPeriod uint32 = 50000
|
|
defaultMaxViolationsBeforeBan uint8 = 3
|
|
defaultCommitRevealDelay uint32 = 10 // Min blocks between commit and reveal
|
|
defaultCommitRevealWindow uint32 = 1000 // Max blocks to reveal after delay
|
|
)
|
|
|
|
var _ interop.Contract = (*Collocatio)(nil)
|
|
|
|
func newCollocatio() *Collocatio {
|
|
c := &Collocatio{
|
|
ContractMD: *interop.NewContractMD(nativenames.Collocatio, nativeids.Collocatio),
|
|
}
|
|
defer c.BuildHFSpecificMD(c.ActiveIn())
|
|
|
|
// getConfig
|
|
desc := NewDescriptor("getConfig", smartcontract.ArrayType)
|
|
md := NewMethodAndPrice(c.getConfig, 1<<15, callflag.ReadStates)
|
|
c.AddMethod(md, desc)
|
|
|
|
// getOpportunityCount
|
|
desc = NewDescriptor("getOpportunityCount", smartcontract.IntegerType)
|
|
md = NewMethodAndPrice(c.getOpportunityCount, 1<<15, callflag.ReadStates)
|
|
c.AddMethod(md, desc)
|
|
|
|
// getOpportunity
|
|
desc = NewDescriptor("getOpportunity", smartcontract.ArrayType,
|
|
manifest.NewParameter("opportunityID", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(c.getOpportunity, 1<<15, callflag.ReadStates)
|
|
c.AddMethod(md, desc)
|
|
|
|
// createOpportunity
|
|
desc = NewDescriptor("createOpportunity", smartcontract.IntegerType,
|
|
manifest.NewParameter("oppType", smartcontract.IntegerType),
|
|
manifest.NewParameter("name", smartcontract.StringType),
|
|
manifest.NewParameter("description", smartcontract.StringType),
|
|
manifest.NewParameter("termsHash", smartcontract.Hash256Type),
|
|
manifest.NewParameter("minParticipants", smartcontract.IntegerType),
|
|
manifest.NewParameter("maxParticipants", smartcontract.IntegerType),
|
|
manifest.NewParameter("minInvestment", smartcontract.IntegerType),
|
|
manifest.NewParameter("maxInvestment", smartcontract.IntegerType),
|
|
manifest.NewParameter("targetPool", smartcontract.IntegerType),
|
|
manifest.NewParameter("expectedReturns", smartcontract.IntegerType),
|
|
manifest.NewParameter("riskLevel", smartcontract.IntegerType),
|
|
manifest.NewParameter("maturityBlocks", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(c.createOpportunity, 1<<17, callflag.States|callflag.AllowNotify)
|
|
c.AddMethod(md, desc)
|
|
|
|
// activateOpportunity
|
|
desc = NewDescriptor("activateOpportunity", smartcontract.BoolType,
|
|
manifest.NewParameter("opportunityID", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(c.activateOpportunity, 1<<16, callflag.States|callflag.AllowNotify)
|
|
c.AddMethod(md, desc)
|
|
|
|
// invest
|
|
desc = NewDescriptor("invest", smartcontract.IntegerType,
|
|
manifest.NewParameter("opportunityID", smartcontract.IntegerType),
|
|
manifest.NewParameter("amount", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(c.invest, 1<<17, callflag.States|callflag.AllowNotify)
|
|
c.AddMethod(md, desc)
|
|
|
|
// withdraw
|
|
desc = NewDescriptor("withdraw", smartcontract.BoolType,
|
|
manifest.NewParameter("investmentID", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(c.withdraw, 1<<17, callflag.States|callflag.AllowNotify)
|
|
c.AddMethod(md, desc)
|
|
|
|
// getEligibility
|
|
desc = NewDescriptor("getEligibility", smartcontract.ArrayType,
|
|
manifest.NewParameter("investor", smartcontract.Hash160Type))
|
|
md = NewMethodAndPrice(c.getEligibility, 1<<15, callflag.ReadStates)
|
|
c.AddMethod(md, desc)
|
|
|
|
// setEligibility
|
|
desc = NewDescriptor("setEligibility", smartcontract.BoolType,
|
|
manifest.NewParameter("investor", smartcontract.Hash160Type),
|
|
manifest.NewParameter("eligibilityFlags", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(c.setEligibility, 1<<16, callflag.States|callflag.AllowNotify)
|
|
c.AddMethod(md, desc)
|
|
|
|
// isEligible
|
|
desc = NewDescriptor("isEligible", smartcontract.BoolType,
|
|
manifest.NewParameter("investor", smartcontract.Hash160Type),
|
|
manifest.NewParameter("oppType", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(c.isEligible, 1<<15, callflag.ReadStates)
|
|
c.AddMethod(md, desc)
|
|
|
|
// getInvestment
|
|
desc = NewDescriptor("getInvestment", smartcontract.ArrayType,
|
|
manifest.NewParameter("investmentID", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(c.getInvestment, 1<<15, callflag.ReadStates)
|
|
c.AddMethod(md, desc)
|
|
|
|
// getInvestmentCount
|
|
desc = NewDescriptor("getInvestmentCount", smartcontract.IntegerType)
|
|
md = NewMethodAndPrice(c.getInvestmentCount, 1<<15, callflag.ReadStates)
|
|
c.AddMethod(md, desc)
|
|
|
|
// recordViolation
|
|
desc = NewDescriptor("recordViolation", smartcontract.IntegerType,
|
|
manifest.NewParameter("violator", smartcontract.Hash160Type),
|
|
manifest.NewParameter("opportunityID", smartcontract.IntegerType),
|
|
manifest.NewParameter("violationType", smartcontract.StringType),
|
|
manifest.NewParameter("description", smartcontract.StringType),
|
|
manifest.NewParameter("evidenceHash", smartcontract.Hash256Type),
|
|
manifest.NewParameter("penalty", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(c.recordViolation, 1<<17, callflag.States|callflag.AllowNotify)
|
|
c.AddMethod(md, desc)
|
|
|
|
// resolveViolation
|
|
desc = NewDescriptor("resolveViolation", smartcontract.BoolType,
|
|
manifest.NewParameter("violationID", smartcontract.IntegerType),
|
|
manifest.NewParameter("resolution", smartcontract.StringType))
|
|
md = NewMethodAndPrice(c.resolveViolation, 1<<16, callflag.States|callflag.AllowNotify)
|
|
c.AddMethod(md, desc)
|
|
|
|
// getViolation
|
|
desc = NewDescriptor("getViolation", smartcontract.ArrayType,
|
|
manifest.NewParameter("violationID", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(c.getViolation, 1<<15, callflag.ReadStates)
|
|
c.AddMethod(md, desc)
|
|
|
|
// verifyEmployment
|
|
desc = NewDescriptor("verifyEmployment", smartcontract.BoolType,
|
|
manifest.NewParameter("employee", smartcontract.Hash160Type),
|
|
manifest.NewParameter("employer", smartcontract.Hash160Type),
|
|
manifest.NewParameter("position", smartcontract.StringType),
|
|
manifest.NewParameter("startDate", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(c.verifyEmployment, 1<<16, callflag.States|callflag.AllowNotify)
|
|
c.AddMethod(md, desc)
|
|
|
|
// revokeEmployment
|
|
desc = NewDescriptor("revokeEmployment", smartcontract.BoolType,
|
|
manifest.NewParameter("employee", smartcontract.Hash160Type),
|
|
manifest.NewParameter("employer", smartcontract.Hash160Type),
|
|
manifest.NewParameter("endDate", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(c.revokeEmployment, 1<<16, callflag.States|callflag.AllowNotify)
|
|
c.AddMethod(md, desc)
|
|
|
|
// getEmploymentStatus
|
|
desc = NewDescriptor("getEmploymentStatus", smartcontract.ArrayType,
|
|
manifest.NewParameter("employee", smartcontract.Hash160Type))
|
|
md = NewMethodAndPrice(c.getEmploymentStatus, 1<<15, callflag.ReadStates)
|
|
c.AddMethod(md, desc)
|
|
|
|
// verifyContractor
|
|
desc = NewDescriptor("verifyContractor", smartcontract.BoolType,
|
|
manifest.NewParameter("contractor", smartcontract.Hash160Type),
|
|
manifest.NewParameter("platform", smartcontract.Hash160Type),
|
|
manifest.NewParameter("platformID", smartcontract.StringType),
|
|
manifest.NewParameter("contractorID", smartcontract.StringType),
|
|
manifest.NewParameter("startDate", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(c.verifyContractor, 1<<16, callflag.States|callflag.AllowNotify)
|
|
c.AddMethod(md, desc)
|
|
|
|
// revokeContractor
|
|
desc = NewDescriptor("revokeContractor", smartcontract.BoolType,
|
|
manifest.NewParameter("contractor", smartcontract.Hash160Type),
|
|
manifest.NewParameter("platform", smartcontract.Hash160Type),
|
|
manifest.NewParameter("endDate", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(c.revokeContractor, 1<<16, callflag.States|callflag.AllowNotify)
|
|
c.AddMethod(md, desc)
|
|
|
|
// getContractorStatus
|
|
desc = NewDescriptor("getContractorStatus", smartcontract.ArrayType,
|
|
manifest.NewParameter("contractor", smartcontract.Hash160Type))
|
|
md = NewMethodAndPrice(c.getContractorStatus, 1<<15, callflag.ReadStates)
|
|
c.AddMethod(md, desc)
|
|
|
|
// completeInvestmentEducation
|
|
desc = NewDescriptor("completeInvestmentEducation", smartcontract.BoolType,
|
|
manifest.NewParameter("investor", smartcontract.Hash160Type),
|
|
manifest.NewParameter("certificationID", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(c.completeInvestmentEducation, 1<<16, callflag.States|callflag.AllowNotify)
|
|
c.AddMethod(md, desc)
|
|
|
|
// distributeReturns
|
|
desc = NewDescriptor("distributeReturns", smartcontract.BoolType,
|
|
manifest.NewParameter("opportunityID", smartcontract.IntegerType),
|
|
manifest.NewParameter("actualReturns", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(c.distributeReturns, 1<<18, callflag.States|callflag.AllowNotify)
|
|
c.AddMethod(md, desc)
|
|
|
|
// cancelOpportunity
|
|
desc = NewDescriptor("cancelOpportunity", smartcontract.BoolType,
|
|
manifest.NewParameter("opportunityID", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(c.cancelOpportunity, 1<<18, callflag.States|callflag.AllowNotify)
|
|
c.AddMethod(md, desc)
|
|
|
|
// getInvestmentsByOpportunity
|
|
desc = NewDescriptor("getInvestmentsByOpportunity", smartcontract.ArrayType,
|
|
manifest.NewParameter("opportunityID", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(c.getInvestmentsByOpportunity, 1<<16, callflag.ReadStates)
|
|
c.AddMethod(md, desc)
|
|
|
|
// getInvestmentsByInvestor
|
|
desc = NewDescriptor("getInvestmentsByInvestor", smartcontract.ArrayType,
|
|
manifest.NewParameter("investor", smartcontract.Hash160Type))
|
|
md = NewMethodAndPrice(c.getInvestmentsByInvestor, 1<<16, callflag.ReadStates)
|
|
c.AddMethod(md, desc)
|
|
|
|
// getOpportunitiesByType
|
|
desc = NewDescriptor("getOpportunitiesByType", smartcontract.ArrayType,
|
|
manifest.NewParameter("oppType", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(c.getOpportunitiesByType, 1<<16, callflag.ReadStates)
|
|
c.AddMethod(md, desc)
|
|
|
|
// getOpportunitiesByStatus
|
|
desc = NewDescriptor("getOpportunitiesByStatus", smartcontract.ArrayType,
|
|
manifest.NewParameter("status", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(c.getOpportunitiesByStatus, 1<<16, callflag.ReadStates)
|
|
c.AddMethod(md, desc)
|
|
|
|
// commitInvestment - Phase 1 of commit-reveal (anti-front-running)
|
|
desc = NewDescriptor("commitInvestment", smartcontract.IntegerType,
|
|
manifest.NewParameter("opportunityID", smartcontract.IntegerType),
|
|
manifest.NewParameter("commitment", smartcontract.Hash256Type))
|
|
md = NewMethodAndPrice(c.commitInvestment, 1<<16, callflag.States|callflag.AllowNotify)
|
|
c.AddMethod(md, desc)
|
|
|
|
// revealInvestment - Phase 2 of commit-reveal
|
|
desc = NewDescriptor("revealInvestment", smartcontract.IntegerType,
|
|
manifest.NewParameter("commitmentID", smartcontract.IntegerType),
|
|
manifest.NewParameter("amount", smartcontract.IntegerType),
|
|
manifest.NewParameter("nonce", smartcontract.ByteArrayType))
|
|
md = NewMethodAndPrice(c.revealInvestment, 1<<17, callflag.States|callflag.AllowNotify)
|
|
c.AddMethod(md, desc)
|
|
|
|
// cancelCommitment - Cancel a pending commitment
|
|
desc = NewDescriptor("cancelCommitment", smartcontract.BoolType,
|
|
manifest.NewParameter("commitmentID", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(c.cancelCommitment, 1<<16, callflag.States|callflag.AllowNotify)
|
|
c.AddMethod(md, desc)
|
|
|
|
// getCommitment - Query commitment details
|
|
desc = NewDescriptor("getCommitment", smartcontract.ArrayType,
|
|
manifest.NewParameter("commitmentID", smartcontract.IntegerType))
|
|
md = NewMethodAndPrice(c.getCommitment, 1<<15, callflag.ReadStates)
|
|
c.AddMethod(md, desc)
|
|
|
|
// getCommitmentCount
|
|
desc = NewDescriptor("getCommitmentCount", smartcontract.IntegerType)
|
|
md = NewMethodAndPrice(c.getCommitmentCount, 1<<15, callflag.ReadStates)
|
|
c.AddMethod(md, desc)
|
|
|
|
// ===== Events =====
|
|
eDesc := NewEventDescriptor(collocatioOpportunityCreatedEvent,
|
|
manifest.NewParameter("opportunityID", smartcontract.IntegerType),
|
|
manifest.NewParameter("oppType", smartcontract.IntegerType),
|
|
manifest.NewParameter("creator", smartcontract.Hash160Type))
|
|
c.AddEvent(NewEvent(eDesc))
|
|
|
|
eDesc = NewEventDescriptor(collocatioOpportunityActivatedEvent,
|
|
manifest.NewParameter("opportunityID", smartcontract.IntegerType))
|
|
c.AddEvent(NewEvent(eDesc))
|
|
|
|
eDesc = NewEventDescriptor(collocatioInvestmentMadeEvent,
|
|
manifest.NewParameter("investmentID", smartcontract.IntegerType),
|
|
manifest.NewParameter("opportunityID", smartcontract.IntegerType),
|
|
manifest.NewParameter("investor", smartcontract.Hash160Type),
|
|
manifest.NewParameter("amount", smartcontract.IntegerType))
|
|
c.AddEvent(NewEvent(eDesc))
|
|
|
|
eDesc = NewEventDescriptor(collocatioInvestmentWithdrawnEvent,
|
|
manifest.NewParameter("investmentID", smartcontract.IntegerType),
|
|
manifest.NewParameter("returnAmount", smartcontract.IntegerType))
|
|
c.AddEvent(NewEvent(eDesc))
|
|
|
|
eDesc = NewEventDescriptor(collocatioEligibilityUpdatedEvent,
|
|
manifest.NewParameter("investor", smartcontract.Hash160Type),
|
|
manifest.NewParameter("eligibility", smartcontract.IntegerType))
|
|
c.AddEvent(NewEvent(eDesc))
|
|
|
|
eDesc = NewEventDescriptor(collocatioOpportunityClosedEvent,
|
|
manifest.NewParameter("opportunityID", smartcontract.IntegerType))
|
|
c.AddEvent(NewEvent(eDesc))
|
|
|
|
eDesc = NewEventDescriptor(collocatioOpportunityFailedEvent,
|
|
manifest.NewParameter("opportunityID", smartcontract.IntegerType),
|
|
manifest.NewParameter("reason", smartcontract.StringType))
|
|
c.AddEvent(NewEvent(eDesc))
|
|
|
|
eDesc = NewEventDescriptor(collocatioOpportunityCancelledEvent,
|
|
manifest.NewParameter("opportunityID", smartcontract.IntegerType))
|
|
c.AddEvent(NewEvent(eDesc))
|
|
|
|
eDesc = NewEventDescriptor(collocatioReturnsDistributedEvent,
|
|
manifest.NewParameter("opportunityID", smartcontract.IntegerType),
|
|
manifest.NewParameter("totalReturns", smartcontract.IntegerType),
|
|
manifest.NewParameter("investorCount", smartcontract.IntegerType))
|
|
c.AddEvent(NewEvent(eDesc))
|
|
|
|
eDesc = NewEventDescriptor(collocatioEducationCompletedEvent,
|
|
manifest.NewParameter("investor", smartcontract.Hash160Type),
|
|
manifest.NewParameter("certificationID", smartcontract.IntegerType))
|
|
c.AddEvent(NewEvent(eDesc))
|
|
|
|
eDesc = NewEventDescriptor(collocatioViolationRecordedEvent,
|
|
manifest.NewParameter("violationID", smartcontract.IntegerType),
|
|
manifest.NewParameter("violator", smartcontract.Hash160Type),
|
|
manifest.NewParameter("penalty", smartcontract.IntegerType))
|
|
c.AddEvent(NewEvent(eDesc))
|
|
|
|
eDesc = NewEventDescriptor(collocatioViolationResolvedEvent,
|
|
manifest.NewParameter("violationID", smartcontract.IntegerType),
|
|
manifest.NewParameter("resolution", smartcontract.StringType))
|
|
c.AddEvent(NewEvent(eDesc))
|
|
|
|
eDesc = NewEventDescriptor(collocatioEmploymentVerifiedEvent,
|
|
manifest.NewParameter("employee", smartcontract.Hash160Type),
|
|
manifest.NewParameter("employer", smartcontract.Hash160Type))
|
|
c.AddEvent(NewEvent(eDesc))
|
|
|
|
eDesc = NewEventDescriptor(collocatioEmploymentRevokedEvent,
|
|
manifest.NewParameter("employee", smartcontract.Hash160Type),
|
|
manifest.NewParameter("employer", smartcontract.Hash160Type))
|
|
c.AddEvent(NewEvent(eDesc))
|
|
|
|
eDesc = NewEventDescriptor(collocatioContractorVerifiedEvent,
|
|
manifest.NewParameter("contractor", smartcontract.Hash160Type),
|
|
manifest.NewParameter("platform", smartcontract.Hash160Type))
|
|
c.AddEvent(NewEvent(eDesc))
|
|
|
|
eDesc = NewEventDescriptor(collocatioContractorRevokedEvent,
|
|
manifest.NewParameter("contractor", smartcontract.Hash160Type),
|
|
manifest.NewParameter("platform", smartcontract.Hash160Type))
|
|
c.AddEvent(NewEvent(eDesc))
|
|
|
|
eDesc = NewEventDescriptor(collocatioCommitmentCreatedEvent,
|
|
manifest.NewParameter("commitmentID", smartcontract.IntegerType),
|
|
manifest.NewParameter("opportunityID", smartcontract.IntegerType),
|
|
manifest.NewParameter("investor", smartcontract.Hash160Type))
|
|
c.AddEvent(NewEvent(eDesc))
|
|
|
|
eDesc = NewEventDescriptor(collocatioCommitmentRevealedEvent,
|
|
manifest.NewParameter("commitmentID", smartcontract.IntegerType),
|
|
manifest.NewParameter("investmentID", smartcontract.IntegerType),
|
|
manifest.NewParameter("amount", smartcontract.IntegerType))
|
|
c.AddEvent(NewEvent(eDesc))
|
|
|
|
eDesc = NewEventDescriptor(collocatioCommitmentCanceledEvent,
|
|
manifest.NewParameter("commitmentID", smartcontract.IntegerType))
|
|
c.AddEvent(NewEvent(eDesc))
|
|
|
|
return c
|
|
}
|
|
|
|
// Metadata returns contract metadata.
|
|
func (c *Collocatio) Metadata() *interop.ContractMD {
|
|
return &c.ContractMD
|
|
}
|
|
|
|
// Initialize initializes Collocatio contract.
|
|
func (c *Collocatio) Initialize(ic *interop.Context, hf *config.Hardfork, newMD *interop.HFSpecificContractMD) error {
|
|
if hf != c.ActiveIn() {
|
|
return nil
|
|
}
|
|
|
|
// Initialize default config
|
|
cfg := state.CollocatioConfig{
|
|
MinPIOParticipants: defaultMinPIOParticipants,
|
|
MinEIOParticipants: defaultMinEIOParticipants,
|
|
MinCIOParticipants: defaultMinCIOParticipants,
|
|
DefaultMinInvestment: defaultMinInvestment,
|
|
MaxIndividualCap: defaultMaxIndividualCap,
|
|
WealthConcentration: defaultWealthConcentration,
|
|
CreationFee: defaultCreationFee,
|
|
InvestmentFee: defaultInvestmentFee,
|
|
WithdrawalPenalty: defaultWithdrawalPenalty,
|
|
MinVotingPeriod: defaultMinVotingPeriod,
|
|
MinInvestmentPeriod: defaultMinInvestmentPeriod,
|
|
MinMaturityPeriod: defaultMinMaturityPeriod,
|
|
MaxViolationsBeforeBan: defaultMaxViolationsBeforeBan,
|
|
ViolationCooldown: 1000000,
|
|
CommitRevealDelay: defaultCommitRevealDelay,
|
|
CommitRevealWindow: defaultCommitRevealWindow,
|
|
}
|
|
c.setConfigInternal(ic.DAO, &cfg)
|
|
|
|
return nil
|
|
}
|
|
|
|
// InitializeCache fills native Collocatio cache from DAO.
|
|
func (c *Collocatio) InitializeCache(_ interop.IsHardforkEnabled, blockHeight uint32, d *dao.Simple) error {
|
|
return nil
|
|
}
|
|
|
|
// OnPersist implements the Contract interface.
|
|
func (c *Collocatio) OnPersist(ic *interop.Context) error {
|
|
return nil
|
|
}
|
|
|
|
// PostPersist implements the Contract interface.
|
|
// Handles lifecycle automation for opportunities.
|
|
func (c *Collocatio) PostPersist(ic *interop.Context) error {
|
|
// Run every 100 blocks for performance
|
|
if ic.Block.Index%100 != 0 {
|
|
return nil
|
|
}
|
|
|
|
// Process opportunities that need status updates
|
|
c.processActiveOpportunities(ic)
|
|
c.processClosedOpportunities(ic)
|
|
return nil
|
|
}
|
|
|
|
// processActiveOpportunities handles Active opportunities past their investment deadline.
|
|
func (c *Collocatio) processActiveOpportunities(ic *interop.Context) {
|
|
prefix := []byte{collocatioPrefixOpportunityByStatus, byte(state.OpportunityActive)}
|
|
cfg := c.getConfigInternal(ic.DAO)
|
|
|
|
ic.DAO.Seek(c.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool {
|
|
if len(k) < 9 {
|
|
return true
|
|
}
|
|
|
|
oppID := binary.BigEndian.Uint64(k[1:9])
|
|
opp := c.getOpportunityInternal(ic.DAO, oppID)
|
|
if opp == nil {
|
|
return true
|
|
}
|
|
|
|
// Check if investment deadline passed
|
|
if ic.Block.Index < opp.InvestmentDeadline {
|
|
return true
|
|
}
|
|
|
|
// Get minimum participants for this opportunity type
|
|
var minParticipants uint64
|
|
switch opp.Type {
|
|
case state.OpportunityPIO:
|
|
minParticipants = cfg.MinPIOParticipants
|
|
case state.OpportunityEIO:
|
|
minParticipants = cfg.MinEIOParticipants
|
|
case state.OpportunityCIO:
|
|
minParticipants = cfg.MinCIOParticipants
|
|
default:
|
|
minParticipants = 1
|
|
}
|
|
|
|
// Use opportunity's own min if set
|
|
if opp.MinParticipants > minParticipants {
|
|
minParticipants = opp.MinParticipants
|
|
}
|
|
|
|
// Check if opportunity met minimum participants
|
|
if opp.CurrentParticipants < minParticipants {
|
|
// Failed - didn't meet minimum participants
|
|
c.updateOpportunityStatus(ic, opp, state.OpportunityFailed)
|
|
ic.AddNotification(c.Hash, collocatioOpportunityFailedEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(oppID)),
|
|
stackitem.NewByteArray([]byte("insufficient participants")),
|
|
}))
|
|
} else {
|
|
// Success - close and move to maturity phase
|
|
c.updateOpportunityStatus(ic, opp, state.OpportunityClosed)
|
|
ic.AddNotification(c.Hash, collocatioOpportunityClosedEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(oppID)),
|
|
}))
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
|
|
// processClosedOpportunities handles Closed opportunities past their maturity date.
|
|
func (c *Collocatio) processClosedOpportunities(ic *interop.Context) {
|
|
prefix := []byte{collocatioPrefixOpportunityByStatus, byte(state.OpportunityClosed)}
|
|
|
|
ic.DAO.Seek(c.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool {
|
|
if len(k) < 9 {
|
|
return true
|
|
}
|
|
|
|
oppID := binary.BigEndian.Uint64(k[1:9])
|
|
opp := c.getOpportunityInternal(ic.DAO, oppID)
|
|
if opp == nil {
|
|
return true
|
|
}
|
|
|
|
// Check if maturity date passed
|
|
if ic.Block.Index < opp.MaturityDate {
|
|
return true
|
|
}
|
|
|
|
// Opportunity is mature and ready for returns distribution
|
|
// Note: Actual distribution is triggered by distributeReturns call
|
|
// This could emit a notification for off-chain systems
|
|
// For now, we just log that it's ready (no state change needed)
|
|
return true
|
|
})
|
|
}
|
|
|
|
// updateOpportunityStatus updates an opportunity's status and maintains status index.
|
|
func (c *Collocatio) updateOpportunityStatus(ic *interop.Context, opp *state.InvestmentOpportunity, newStatus state.OpportunityStatus) {
|
|
oldStatus := opp.Status
|
|
|
|
// Remove from old status index
|
|
oldStatusKey := makeCollocatioOppByStatusKey(oldStatus, opp.ID)
|
|
ic.DAO.DeleteStorageItem(c.ID, oldStatusKey)
|
|
|
|
// Update status
|
|
opp.Status = newStatus
|
|
opp.UpdatedAt = ic.Block.Index
|
|
|
|
// Add to new status index
|
|
newStatusKey := makeCollocatioOppByStatusKey(newStatus, opp.ID)
|
|
ic.DAO.PutStorageItem(c.ID, newStatusKey, []byte{1})
|
|
|
|
// Save opportunity
|
|
c.putOpportunity(ic.DAO, opp)
|
|
}
|
|
|
|
// ActiveIn returns nil (always active from genesis).
|
|
func (c *Collocatio) ActiveIn() *config.Hardfork {
|
|
return nil
|
|
}
|
|
|
|
// ============================================================================
|
|
// Storage Key Helpers
|
|
// ============================================================================
|
|
|
|
func makeCollocatioConfigKey() []byte {
|
|
return []byte{collocatioPrefixConfig}
|
|
}
|
|
|
|
func makeCollocatioOppKey(oppID uint64) []byte {
|
|
key := make([]byte, 9)
|
|
key[0] = collocatioPrefixOpportunity
|
|
binary.BigEndian.PutUint64(key[1:], oppID)
|
|
return key
|
|
}
|
|
|
|
func makeCollocatioOppByTypeKey(oppType state.OpportunityType, oppID uint64) []byte {
|
|
key := make([]byte, 10)
|
|
key[0] = collocatioPrefixOpportunityByType
|
|
key[1] = byte(oppType)
|
|
binary.BigEndian.PutUint64(key[2:], oppID)
|
|
return key
|
|
}
|
|
|
|
func makeCollocatioOppByStatusKey(status state.OpportunityStatus, oppID uint64) []byte {
|
|
key := make([]byte, 10)
|
|
key[0] = collocatioPrefixOpportunityByStatus
|
|
key[1] = byte(status)
|
|
binary.BigEndian.PutUint64(key[2:], oppID)
|
|
return key
|
|
}
|
|
|
|
func makeCollocatioOppCounterKey() []byte {
|
|
return []byte{collocatioPrefixOppCounter}
|
|
}
|
|
|
|
func makeCollocatioInvKey(invID uint64) []byte {
|
|
key := make([]byte, 9)
|
|
key[0] = collocatioPrefixInvestment
|
|
binary.BigEndian.PutUint64(key[1:], invID)
|
|
return key
|
|
}
|
|
|
|
func makeCollocatioInvByOppKey(oppID, invID uint64) []byte {
|
|
key := make([]byte, 17)
|
|
key[0] = collocatioPrefixInvestmentByOpp
|
|
binary.BigEndian.PutUint64(key[1:9], oppID)
|
|
binary.BigEndian.PutUint64(key[9:], invID)
|
|
return key
|
|
}
|
|
|
|
func makeCollocatioInvByInvestorKey(vitaID, invID uint64) []byte {
|
|
key := make([]byte, 17)
|
|
key[0] = collocatioPrefixInvestmentByInvestor
|
|
binary.BigEndian.PutUint64(key[1:9], vitaID)
|
|
binary.BigEndian.PutUint64(key[9:], invID)
|
|
return key
|
|
}
|
|
|
|
func makeCollocatioInvCounterKey() []byte {
|
|
return []byte{collocatioPrefixInvCounter}
|
|
}
|
|
|
|
func makeCollocatioEligKey(vitaID uint64) []byte {
|
|
key := make([]byte, 9)
|
|
key[0] = collocatioPrefixEligibility
|
|
binary.BigEndian.PutUint64(key[1:], vitaID)
|
|
return key
|
|
}
|
|
|
|
func makeCollocatioEligByOwnerKey(owner util.Uint160) []byte {
|
|
key := make([]byte, 1+util.Uint160Size)
|
|
key[0] = collocatioPrefixEligibilityByOwner
|
|
copy(key[1:], owner.BytesBE())
|
|
return key
|
|
}
|
|
|
|
func makeCollocatioViolationKey(violationID uint64) []byte {
|
|
key := make([]byte, 9)
|
|
key[0] = collocatioPrefixViolation
|
|
binary.BigEndian.PutUint64(key[1:], violationID)
|
|
return key
|
|
}
|
|
|
|
func makeCollocatioViolationByInvestorKey(vitaID, violationID uint64) []byte {
|
|
key := make([]byte, 17)
|
|
key[0] = collocatioPrefixViolationByInvestor
|
|
binary.BigEndian.PutUint64(key[1:9], vitaID)
|
|
binary.BigEndian.PutUint64(key[9:], violationID)
|
|
return key
|
|
}
|
|
|
|
func makeCollocatioViolationCounterKey() []byte {
|
|
return []byte{collocatioPrefixViolationCounter}
|
|
}
|
|
|
|
func makeCollocatioEmploymentKey(employeeVitaID uint64) []byte {
|
|
key := make([]byte, 9)
|
|
key[0] = collocatioPrefixEmployment
|
|
binary.BigEndian.PutUint64(key[1:], employeeVitaID)
|
|
return key
|
|
}
|
|
|
|
func makeCollocatioEmploymentByEmployerKey(employerVitaID, employeeVitaID uint64) []byte {
|
|
key := make([]byte, 17)
|
|
key[0] = collocatioPrefixEmploymentByEmployer
|
|
binary.BigEndian.PutUint64(key[1:9], employerVitaID)
|
|
binary.BigEndian.PutUint64(key[9:], employeeVitaID)
|
|
return key
|
|
}
|
|
|
|
func makeCollocatioContractorKey(contractorVitaID uint64) []byte {
|
|
key := make([]byte, 9)
|
|
key[0] = collocatioPrefixContractor
|
|
binary.BigEndian.PutUint64(key[1:], contractorVitaID)
|
|
return key
|
|
}
|
|
|
|
func makeCollocatioContractorByPlatformKey(platform util.Uint160, contractorVitaID uint64) []byte {
|
|
key := make([]byte, 1+util.Uint160Size+8)
|
|
key[0] = collocatioPrefixContractorByPlatform
|
|
copy(key[1:1+util.Uint160Size], platform.BytesBE())
|
|
binary.BigEndian.PutUint64(key[1+util.Uint160Size:], contractorVitaID)
|
|
return key
|
|
}
|
|
|
|
func makeCollocatioCommitmentKey(commitmentID uint64) []byte {
|
|
key := make([]byte, 9)
|
|
key[0] = collocatioPrefixCommitment
|
|
binary.BigEndian.PutUint64(key[1:], commitmentID)
|
|
return key
|
|
}
|
|
|
|
func makeCollocatioCommitmentByOppKey(oppID, commitmentID uint64) []byte {
|
|
key := make([]byte, 17)
|
|
key[0] = collocatioPrefixCommitmentByOpp
|
|
binary.BigEndian.PutUint64(key[1:9], oppID)
|
|
binary.BigEndian.PutUint64(key[9:], commitmentID)
|
|
return key
|
|
}
|
|
|
|
func makeCollocatioCommitmentByInvestorKey(vitaID, commitmentID uint64) []byte {
|
|
key := make([]byte, 17)
|
|
key[0] = collocatioPrefixCommitmentByInvestor
|
|
binary.BigEndian.PutUint64(key[1:9], vitaID)
|
|
binary.BigEndian.PutUint64(key[9:], commitmentID)
|
|
return key
|
|
}
|
|
|
|
func makeCollocatioCommitmentCounterKey() []byte {
|
|
return []byte{collocatioPrefixCommitmentCounter}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Internal Storage Methods
|
|
// ============================================================================
|
|
|
|
func (c *Collocatio) getConfigInternal(d *dao.Simple) *state.CollocatioConfig {
|
|
si := d.GetStorageItem(c.ID, makeCollocatioConfigKey())
|
|
if si == nil {
|
|
return &state.CollocatioConfig{
|
|
MinPIOParticipants: defaultMinPIOParticipants,
|
|
MinEIOParticipants: defaultMinEIOParticipants,
|
|
MinCIOParticipants: defaultMinCIOParticipants,
|
|
DefaultMinInvestment: defaultMinInvestment,
|
|
MaxIndividualCap: defaultMaxIndividualCap,
|
|
WealthConcentration: defaultWealthConcentration,
|
|
CreationFee: defaultCreationFee,
|
|
InvestmentFee: defaultInvestmentFee,
|
|
WithdrawalPenalty: defaultWithdrawalPenalty,
|
|
MinVotingPeriod: defaultMinVotingPeriod,
|
|
MinInvestmentPeriod: defaultMinInvestmentPeriod,
|
|
MinMaturityPeriod: defaultMinMaturityPeriod,
|
|
MaxViolationsBeforeBan: defaultMaxViolationsBeforeBan,
|
|
ViolationCooldown: 1000000,
|
|
}
|
|
}
|
|
cfg := new(state.CollocatioConfig)
|
|
item, _ := stackitem.Deserialize(si)
|
|
cfg.FromStackItem(item)
|
|
return cfg
|
|
}
|
|
|
|
func (c *Collocatio) setConfigInternal(d *dao.Simple, cfg *state.CollocatioConfig) {
|
|
item, _ := cfg.ToStackItem()
|
|
data, _ := stackitem.Serialize(item)
|
|
d.PutStorageItem(c.ID, makeCollocatioConfigKey(), data)
|
|
}
|
|
|
|
func (c *Collocatio) getCounter(d *dao.Simple, key []byte) uint64 {
|
|
si := d.GetStorageItem(c.ID, key)
|
|
if si == nil || len(si) < 8 {
|
|
return 0
|
|
}
|
|
return binary.BigEndian.Uint64(si)
|
|
}
|
|
|
|
func (c *Collocatio) incrementCounter(d *dao.Simple, key []byte) uint64 {
|
|
current := c.getCounter(d, key)
|
|
next := current + 1
|
|
buf := make([]byte, 8)
|
|
binary.BigEndian.PutUint64(buf, next)
|
|
d.PutStorageItem(c.ID, key, buf)
|
|
return next
|
|
}
|
|
|
|
func (c *Collocatio) getOpportunityInternal(d *dao.Simple, oppID uint64) *state.InvestmentOpportunity {
|
|
si := d.GetStorageItem(c.ID, makeCollocatioOppKey(oppID))
|
|
if si == nil {
|
|
return nil
|
|
}
|
|
opp := new(state.InvestmentOpportunity)
|
|
item, _ := stackitem.Deserialize(si)
|
|
opp.FromStackItem(item)
|
|
return opp
|
|
}
|
|
|
|
func (c *Collocatio) putOpportunity(d *dao.Simple, opp *state.InvestmentOpportunity) {
|
|
item, _ := opp.ToStackItem()
|
|
data, _ := stackitem.Serialize(item)
|
|
d.PutStorageItem(c.ID, makeCollocatioOppKey(opp.ID), data)
|
|
}
|
|
|
|
func (c *Collocatio) getInvestmentInternal(d *dao.Simple, invID uint64) *state.Investment {
|
|
si := d.GetStorageItem(c.ID, makeCollocatioInvKey(invID))
|
|
if si == nil {
|
|
return nil
|
|
}
|
|
inv := new(state.Investment)
|
|
item, _ := stackitem.Deserialize(si)
|
|
inv.FromStackItem(item)
|
|
return inv
|
|
}
|
|
|
|
func (c *Collocatio) putInvestment(d *dao.Simple, inv *state.Investment) {
|
|
item, _ := inv.ToStackItem()
|
|
data, _ := stackitem.Serialize(item)
|
|
d.PutStorageItem(c.ID, makeCollocatioInvKey(inv.ID), data)
|
|
}
|
|
|
|
func (c *Collocatio) getEligibilityInternal(d *dao.Simple, investor util.Uint160) *state.InvestorEligibility {
|
|
// First get vitaID from owner mapping
|
|
si := d.GetStorageItem(c.ID, makeCollocatioEligByOwnerKey(investor))
|
|
if si == nil || len(si) < 8 {
|
|
return nil
|
|
}
|
|
vitaID := binary.BigEndian.Uint64(si)
|
|
|
|
// Then get eligibility
|
|
eligSI := d.GetStorageItem(c.ID, makeCollocatioEligKey(vitaID))
|
|
if eligSI == nil {
|
|
return nil
|
|
}
|
|
elig := new(state.InvestorEligibility)
|
|
item, _ := stackitem.Deserialize(eligSI)
|
|
elig.FromStackItem(item)
|
|
return elig
|
|
}
|
|
|
|
func (c *Collocatio) putEligibility(d *dao.Simple, elig *state.InvestorEligibility) {
|
|
item, _ := elig.ToStackItem()
|
|
data, _ := stackitem.Serialize(item)
|
|
d.PutStorageItem(c.ID, makeCollocatioEligKey(elig.VitaID), data)
|
|
|
|
// Also store owner -> vitaID mapping
|
|
buf := make([]byte, 8)
|
|
binary.BigEndian.PutUint64(buf, elig.VitaID)
|
|
d.PutStorageItem(c.ID, makeCollocatioEligByOwnerKey(elig.Investor), buf)
|
|
}
|
|
|
|
func (c *Collocatio) getCommitmentInternal(d *dao.Simple, commitmentID uint64) *state.InvestmentCommitment {
|
|
si := d.GetStorageItem(c.ID, makeCollocatioCommitmentKey(commitmentID))
|
|
if si == nil {
|
|
return nil
|
|
}
|
|
commitment := new(state.InvestmentCommitment)
|
|
item, _ := stackitem.Deserialize(si)
|
|
commitment.FromStackItem(item)
|
|
return commitment
|
|
}
|
|
|
|
func (c *Collocatio) putCommitment(d *dao.Simple, commitment *state.InvestmentCommitment) {
|
|
item, _ := commitment.ToStackItem()
|
|
data, _ := stackitem.Serialize(item)
|
|
d.PutStorageItem(c.ID, makeCollocatioCommitmentKey(commitment.ID), data)
|
|
}
|
|
|
|
// getInvestorTotalInOpportunity returns the total amount an investor has invested in a specific opportunity.
|
|
func (c *Collocatio) getInvestorTotalInOpportunity(d *dao.Simple, vitaID, oppID uint64) uint64 {
|
|
prefix := []byte{collocatioPrefixInvestmentByInvestor}
|
|
prefix = append(prefix, make([]byte, 8)...)
|
|
binary.BigEndian.PutUint64(prefix[1:], vitaID)
|
|
|
|
var total uint64
|
|
d.Seek(c.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool {
|
|
if len(k) >= 16 {
|
|
invID := binary.BigEndian.Uint64(k[8:16])
|
|
inv := c.getInvestmentInternal(d, invID)
|
|
if inv != nil && inv.OpportunityID == oppID && inv.Status == state.InvestmentActive {
|
|
total += inv.Amount
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
return total
|
|
}
|
|
|
|
// ============================================================================
|
|
// Contract Methods
|
|
// ============================================================================
|
|
|
|
func (c *Collocatio) getConfig(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
|
|
cfg := c.getConfigInternal(ic.DAO)
|
|
return stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.MinPIOParticipants)),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.MinEIOParticipants)),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.MinCIOParticipants)),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.DefaultMinInvestment)),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.MaxIndividualCap)),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.WealthConcentration)),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.CreationFee)),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.InvestmentFee)),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.WithdrawalPenalty)),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(cfg.MinVotingPeriod))),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(cfg.MinInvestmentPeriod))),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(cfg.MinMaturityPeriod))),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(cfg.MaxViolationsBeforeBan))),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(cfg.ViolationCooldown))),
|
|
})
|
|
}
|
|
|
|
func (c *Collocatio) getOpportunityCount(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
|
|
count := c.getCounter(ic.DAO, makeCollocatioOppCounterKey())
|
|
return stackitem.NewBigInteger(new(big.Int).SetUint64(count))
|
|
}
|
|
|
|
func (c *Collocatio) getOpportunity(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
oppID := toUint64(args[0])
|
|
opp := c.getOpportunityInternal(ic.DAO, oppID)
|
|
if opp == nil {
|
|
return stackitem.Null{}
|
|
}
|
|
return opportunityToStackItem(opp)
|
|
}
|
|
|
|
func (c *Collocatio) createOpportunity(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
caller := ic.VM.GetCallingScriptHash()
|
|
|
|
oppType := state.OpportunityType(toUint64(args[0]))
|
|
name := toString(args[1])
|
|
description := toString(args[2])
|
|
termsHashBytes, err := args[3].TryBytes()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
termsHash, err := util.Uint256DecodeBytesBE(termsHashBytes)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
minParticipants := toUint64(args[4])
|
|
maxParticipants := toUint64(args[5])
|
|
minInvestment := toUint64(args[6])
|
|
maxInvestment := toUint64(args[7])
|
|
targetPool := toUint64(args[8])
|
|
expectedReturns := toUint64(args[9])
|
|
riskLevel := uint8(toUint64(args[10]))
|
|
maturityBlocks := uint32(toUint64(args[11]))
|
|
|
|
// Validate caller has Vita
|
|
vita, err := c.Vita.GetTokenByOwner(ic.DAO, caller)
|
|
if err != nil {
|
|
panic("caller must have Vita token")
|
|
}
|
|
vitaID := vita.TokenID
|
|
|
|
// Validate opportunity type
|
|
if oppType > state.OpportunityCIO {
|
|
panic("invalid opportunity type")
|
|
}
|
|
|
|
// Validate parameters
|
|
cfg := c.getConfigInternal(ic.DAO)
|
|
if maturityBlocks < cfg.MinMaturityPeriod {
|
|
panic("maturity period too short")
|
|
}
|
|
if riskLevel < 1 || riskLevel > 10 {
|
|
panic("risk level must be 1-10")
|
|
}
|
|
|
|
// Get minimum participants based on type
|
|
var minRequired uint64
|
|
switch oppType {
|
|
case state.OpportunityPIO:
|
|
minRequired = cfg.MinPIOParticipants
|
|
case state.OpportunityEIO:
|
|
minRequired = cfg.MinEIOParticipants
|
|
case state.OpportunityCIO:
|
|
minRequired = cfg.MinCIOParticipants
|
|
}
|
|
if minParticipants < minRequired {
|
|
panic(fmt.Sprintf("minimum participants must be at least %d for this type", minRequired))
|
|
}
|
|
|
|
// Charge creation fee to Treasury
|
|
if cfg.CreationFee > 0 {
|
|
if err := c.VTS.transferUnrestricted(ic, caller, nativehashes.Treasury, new(big.Int).SetUint64(cfg.CreationFee), nil); err != nil {
|
|
panic("failed to pay creation fee")
|
|
}
|
|
}
|
|
|
|
// Create opportunity
|
|
oppID := c.incrementCounter(ic.DAO, makeCollocatioOppCounterKey())
|
|
currentBlock := ic.Block.Index
|
|
|
|
opp := &state.InvestmentOpportunity{
|
|
ID: oppID,
|
|
Type: oppType,
|
|
Status: state.OpportunityDraft,
|
|
Creator: caller,
|
|
SponsorVitaID: vitaID,
|
|
Name: name,
|
|
Description: description,
|
|
TermsHash: termsHash,
|
|
MinParticipants: minParticipants,
|
|
MaxParticipants: maxParticipants,
|
|
CurrentParticipants: 0,
|
|
MinInvestment: minInvestment,
|
|
MaxInvestment: maxInvestment,
|
|
TotalPool: 0,
|
|
TargetPool: targetPool,
|
|
ExpectedReturns: expectedReturns,
|
|
RiskLevel: riskLevel,
|
|
VotingDeadline: currentBlock + cfg.MinVotingPeriod,
|
|
InvestmentDeadline: currentBlock + cfg.MinVotingPeriod + cfg.MinInvestmentPeriod,
|
|
MaturityDate: currentBlock + cfg.MinVotingPeriod + cfg.MinInvestmentPeriod + maturityBlocks,
|
|
ProposalID: 0,
|
|
CreatedAt: currentBlock,
|
|
UpdatedAt: currentBlock,
|
|
}
|
|
|
|
c.putOpportunity(ic.DAO, opp)
|
|
|
|
// Store type index
|
|
ic.DAO.PutStorageItem(c.ID, makeCollocatioOppByTypeKey(oppType, oppID), []byte{1})
|
|
|
|
// Emit event
|
|
ic.AddNotification(c.Hash, collocatioOpportunityCreatedEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(oppID)),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(oppType))),
|
|
stackitem.NewByteArray(caller.BytesBE()),
|
|
}))
|
|
|
|
return stackitem.NewBigInteger(new(big.Int).SetUint64(oppID))
|
|
}
|
|
|
|
func (c *Collocatio) activateOpportunity(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
oppID := toUint64(args[0])
|
|
|
|
opp := c.getOpportunityInternal(ic.DAO, oppID)
|
|
if opp == nil {
|
|
panic("opportunity not found")
|
|
}
|
|
|
|
caller := ic.VM.GetCallingScriptHash()
|
|
if caller != opp.Creator && !c.Tutus.CheckCommittee(ic) {
|
|
panic("only creator or committee can activate")
|
|
}
|
|
|
|
if opp.Status != state.OpportunityDraft {
|
|
panic("opportunity must be in draft status")
|
|
}
|
|
|
|
opp.Status = state.OpportunityActive
|
|
opp.UpdatedAt = ic.Block.Index
|
|
c.putOpportunity(ic.DAO, opp)
|
|
|
|
ic.AddNotification(c.Hash, collocatioOpportunityActivatedEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(oppID)),
|
|
}))
|
|
|
|
return stackitem.NewBool(true)
|
|
}
|
|
|
|
func (c *Collocatio) invest(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
oppID := toUint64(args[0])
|
|
amount := toUint64(args[1])
|
|
caller := ic.VM.GetCallingScriptHash()
|
|
|
|
// Validate caller has Vita
|
|
vita, err := c.Vita.GetTokenByOwner(ic.DAO, caller)
|
|
if err != nil {
|
|
panic("caller must have Vita token")
|
|
}
|
|
vitaID := vita.TokenID
|
|
|
|
// Get opportunity
|
|
opp := c.getOpportunityInternal(ic.DAO, oppID)
|
|
if opp == nil {
|
|
panic("opportunity not found")
|
|
}
|
|
|
|
if opp.Status != state.OpportunityActive {
|
|
panic("opportunity is not active")
|
|
}
|
|
if ic.Block.Index > opp.InvestmentDeadline {
|
|
panic("investment deadline has passed")
|
|
}
|
|
|
|
if opp.MaxParticipants > 0 && opp.CurrentParticipants >= opp.MaxParticipants {
|
|
panic("maximum participants reached")
|
|
}
|
|
|
|
if amount < opp.MinInvestment {
|
|
panic("investment below minimum")
|
|
}
|
|
if amount > opp.MaxInvestment {
|
|
panic("investment exceeds maximum")
|
|
}
|
|
|
|
// Check eligibility
|
|
if !c.isEligibleInternal(ic.DAO, caller, opp.Type) {
|
|
panic("investor not eligible for this opportunity type")
|
|
}
|
|
|
|
// Calculate fee
|
|
cfg := c.getConfigInternal(ic.DAO)
|
|
|
|
// Whale concentration check - prevent any single investor from holding too much of the pool
|
|
if cfg.WealthConcentration > 0 {
|
|
existingInvestment := c.getInvestorTotalInOpportunity(ic.DAO, vitaID, oppID)
|
|
newTotal := existingInvestment + amount
|
|
// Calculate what percentage of the pool this investor would hold
|
|
// (newTotal / (opp.TotalPool + amount)) * 10000 > WealthConcentration
|
|
futurePool := opp.TotalPool + amount
|
|
if futurePool > 0 {
|
|
concentration := (newTotal * 10000) / futurePool
|
|
if concentration > cfg.WealthConcentration {
|
|
panic("investment would exceed whale concentration limit")
|
|
}
|
|
}
|
|
}
|
|
fee := (amount * cfg.InvestmentFee) / 10000
|
|
netAmount := amount - fee
|
|
|
|
// Transfer VTS from investor
|
|
if err := c.VTS.transferUnrestricted(ic, caller, c.Hash, new(big.Int).SetUint64(amount), nil); err != nil {
|
|
panic("failed to transfer investment amount")
|
|
}
|
|
|
|
// Send fee to Treasury
|
|
if fee > 0 {
|
|
if err := c.VTS.transferUnrestricted(ic, c.Hash, nativehashes.Treasury, new(big.Int).SetUint64(fee), nil); err != nil {
|
|
panic("failed to transfer fee to treasury")
|
|
}
|
|
}
|
|
|
|
// Create investment record
|
|
invID := c.incrementCounter(ic.DAO, makeCollocatioInvCounterKey())
|
|
|
|
inv := &state.Investment{
|
|
ID: invID,
|
|
OpportunityID: oppID,
|
|
VitaID: vitaID,
|
|
Investor: caller,
|
|
Amount: netAmount,
|
|
Status: state.InvestmentActive,
|
|
ReturnAmount: 0,
|
|
CreatedAt: ic.Block.Index,
|
|
UpdatedAt: ic.Block.Index,
|
|
}
|
|
|
|
c.putInvestment(ic.DAO, inv)
|
|
|
|
// Store indexes
|
|
ic.DAO.PutStorageItem(c.ID, makeCollocatioInvByOppKey(oppID, invID), []byte{1})
|
|
ic.DAO.PutStorageItem(c.ID, makeCollocatioInvByInvestorKey(vitaID, invID), []byte{1})
|
|
|
|
// Update opportunity
|
|
opp.CurrentParticipants++
|
|
opp.TotalPool += netAmount
|
|
opp.UpdatedAt = ic.Block.Index
|
|
c.putOpportunity(ic.DAO, opp)
|
|
|
|
// Update eligibility stats
|
|
c.updateEligibilityOnInvest(ic.DAO, caller, vitaID, netAmount, ic.Block.Index)
|
|
|
|
// Emit event
|
|
ic.AddNotification(c.Hash, collocatioInvestmentMadeEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(invID)),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(oppID)),
|
|
stackitem.NewByteArray(caller.BytesBE()),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(netAmount)),
|
|
}))
|
|
|
|
return stackitem.NewBigInteger(new(big.Int).SetUint64(invID))
|
|
}
|
|
|
|
func (c *Collocatio) withdraw(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
invID := toUint64(args[0])
|
|
caller := ic.VM.GetCallingScriptHash()
|
|
|
|
inv := c.getInvestmentInternal(ic.DAO, invID)
|
|
if inv == nil {
|
|
panic("investment not found")
|
|
}
|
|
|
|
if inv.Investor != caller {
|
|
panic("only investor can withdraw")
|
|
}
|
|
|
|
if inv.Status != state.InvestmentActive {
|
|
panic("investment is not active")
|
|
}
|
|
|
|
opp := c.getOpportunityInternal(ic.DAO, inv.OpportunityID)
|
|
if opp == nil {
|
|
panic("opportunity not found")
|
|
}
|
|
|
|
// Calculate penalty for early withdrawal
|
|
cfg := c.getConfigInternal(ic.DAO)
|
|
returnAmount := inv.Amount
|
|
if ic.Block.Index < opp.MaturityDate && cfg.WithdrawalPenalty > 0 {
|
|
penalty := (inv.Amount * cfg.WithdrawalPenalty) / 10000
|
|
returnAmount = inv.Amount - penalty
|
|
if penalty > 0 {
|
|
if err := c.VTS.transferUnrestricted(ic, c.Hash, nativehashes.Treasury, new(big.Int).SetUint64(penalty), nil); err != nil {
|
|
panic("failed to transfer penalty")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Return funds
|
|
if err := c.VTS.transferUnrestricted(ic, c.Hash, caller, new(big.Int).SetUint64(returnAmount), nil); err != nil {
|
|
panic("failed to return investment")
|
|
}
|
|
|
|
// Update investment
|
|
inv.Status = state.InvestmentWithdrawn
|
|
inv.UpdatedAt = ic.Block.Index
|
|
c.putInvestment(ic.DAO, inv)
|
|
|
|
// Update opportunity
|
|
opp.CurrentParticipants--
|
|
opp.TotalPool -= inv.Amount
|
|
opp.UpdatedAt = ic.Block.Index
|
|
c.putOpportunity(ic.DAO, opp)
|
|
|
|
// Update eligibility
|
|
c.updateEligibilityOnWithdraw(ic.DAO, caller, inv.Amount, ic.Block.Index)
|
|
|
|
// Emit event
|
|
ic.AddNotification(c.Hash, collocatioInvestmentWithdrawnEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(invID)),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(returnAmount)),
|
|
}))
|
|
|
|
return stackitem.NewBool(true)
|
|
}
|
|
|
|
func (c *Collocatio) getEligibility(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
investor := toUint160(args[0])
|
|
elig := c.getEligibilityInternal(ic.DAO, investor)
|
|
if elig == nil {
|
|
return stackitem.Null{}
|
|
}
|
|
return eligibilityToStackItem(elig)
|
|
}
|
|
|
|
func (c *Collocatio) setEligibility(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
investor := toUint160(args[0])
|
|
eligFlags := state.EligibilityType(toUint64(args[1]))
|
|
|
|
// Only committee or RoleInvestmentManager can set eligibility
|
|
if !c.Tutus.CheckCommittee(ic) {
|
|
caller := ic.VM.GetCallingScriptHash()
|
|
if !c.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleInvestmentManager, ic.Block.Index) {
|
|
panic("only committee or investment manager can set eligibility")
|
|
}
|
|
}
|
|
|
|
vita, err := c.Vita.GetTokenByOwner(ic.DAO, investor)
|
|
if err != nil {
|
|
panic("investor must have Vita token")
|
|
}
|
|
vitaID := vita.TokenID
|
|
|
|
elig := c.getEligibilityInternal(ic.DAO, investor)
|
|
if elig == nil {
|
|
elig = &state.InvestorEligibility{
|
|
VitaID: vitaID,
|
|
Investor: investor,
|
|
CreatedAt: ic.Block.Index,
|
|
}
|
|
}
|
|
|
|
elig.Eligibility = eligFlags
|
|
elig.UpdatedAt = ic.Block.Index
|
|
c.putEligibility(ic.DAO, elig)
|
|
|
|
ic.AddNotification(c.Hash, collocatioEligibilityUpdatedEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewByteArray(investor.BytesBE()),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(eligFlags))),
|
|
}))
|
|
|
|
return stackitem.NewBool(true)
|
|
}
|
|
|
|
func (c *Collocatio) isEligible(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
investor := toUint160(args[0])
|
|
oppType := state.OpportunityType(toUint64(args[1]))
|
|
return stackitem.NewBool(c.isEligibleInternal(ic.DAO, investor, oppType))
|
|
}
|
|
|
|
func (c *Collocatio) isEligibleInternal(d *dao.Simple, investor util.Uint160, oppType state.OpportunityType) bool {
|
|
elig := c.getEligibilityInternal(d, investor)
|
|
if elig == nil {
|
|
return false
|
|
}
|
|
|
|
// Must have completed investment education
|
|
if !elig.ScireCompleted {
|
|
return false
|
|
}
|
|
|
|
// Check for ban
|
|
if elig.HasViolations {
|
|
cfg := c.getConfigInternal(d)
|
|
if elig.ViolationCount >= cfg.MaxViolationsBeforeBan {
|
|
return false
|
|
}
|
|
}
|
|
|
|
switch oppType {
|
|
case state.OpportunityPIO:
|
|
return elig.Eligibility&state.EligibilityPIO != 0
|
|
case state.OpportunityEIO:
|
|
if elig.Eligibility&state.EligibilityEIO == 0 {
|
|
return false
|
|
}
|
|
// Additionally verify active employment
|
|
return c.hasActiveEmployment(d, elig.VitaID)
|
|
case state.OpportunityCIO:
|
|
if elig.Eligibility&state.EligibilityCIO == 0 {
|
|
return false
|
|
}
|
|
// Additionally verify active contractor status
|
|
return c.hasActiveContractor(d, elig.VitaID)
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (c *Collocatio) getInvestment(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
invID := toUint64(args[0])
|
|
inv := c.getInvestmentInternal(ic.DAO, invID)
|
|
if inv == nil {
|
|
return stackitem.Null{}
|
|
}
|
|
return investmentToStackItem(inv)
|
|
}
|
|
|
|
func (c *Collocatio) getInvestmentCount(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
|
|
count := c.getCounter(ic.DAO, makeCollocatioInvCounterKey())
|
|
return stackitem.NewBigInteger(new(big.Int).SetUint64(count))
|
|
}
|
|
|
|
// ============================================================================
|
|
// Internal Helpers
|
|
// ============================================================================
|
|
|
|
func (c *Collocatio) updateEligibilityOnInvest(d *dao.Simple, investor util.Uint160, vitaID, amount uint64, blockHeight uint32) {
|
|
elig := c.getEligibilityInternal(d, investor)
|
|
if elig == nil {
|
|
elig = &state.InvestorEligibility{
|
|
VitaID: vitaID,
|
|
Investor: investor,
|
|
CreatedAt: blockHeight,
|
|
}
|
|
}
|
|
elig.TotalInvested += amount
|
|
elig.ActiveInvestments++
|
|
elig.LastActivity = blockHeight
|
|
elig.UpdatedAt = blockHeight
|
|
c.putEligibility(d, elig)
|
|
}
|
|
|
|
func (c *Collocatio) updateEligibilityOnWithdraw(d *dao.Simple, investor util.Uint160, amount uint64, blockHeight uint32) {
|
|
elig := c.getEligibilityInternal(d, investor)
|
|
if elig == nil {
|
|
return
|
|
}
|
|
if elig.TotalInvested >= amount {
|
|
elig.TotalInvested -= amount
|
|
}
|
|
if elig.ActiveInvestments > 0 {
|
|
elig.ActiveInvestments--
|
|
}
|
|
elig.LastActivity = blockHeight
|
|
elig.UpdatedAt = blockHeight
|
|
c.putEligibility(d, elig)
|
|
}
|
|
|
|
// ============================================================================
|
|
// Violation System
|
|
// ============================================================================
|
|
|
|
func (c *Collocatio) getViolationInternal(d *dao.Simple, violationID uint64) *state.InvestmentViolation {
|
|
si := d.GetStorageItem(c.ID, makeCollocatioViolationKey(violationID))
|
|
if si == nil {
|
|
return nil
|
|
}
|
|
v := new(state.InvestmentViolation)
|
|
item, _ := stackitem.Deserialize(si)
|
|
v.FromStackItem(item)
|
|
return v
|
|
}
|
|
|
|
func (c *Collocatio) putViolation(d *dao.Simple, v *state.InvestmentViolation) {
|
|
item, _ := v.ToStackItem()
|
|
data, _ := stackitem.Serialize(item)
|
|
d.PutStorageItem(c.ID, makeCollocatioViolationKey(v.ID), data)
|
|
}
|
|
|
|
func (c *Collocatio) recordViolation(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
violator := toUint160(args[0])
|
|
opportunityID := toUint64(args[1])
|
|
violationType := toString(args[2])
|
|
description := toString(args[3])
|
|
evidenceHashBytes, err := args[4].TryBytes()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
evidenceHash, err := util.Uint256DecodeBytesBE(evidenceHashBytes)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
penalty := toUint64(args[5])
|
|
|
|
// Authorization: Committee or RoleInvestmentManager
|
|
if !c.Tutus.CheckCommittee(ic) {
|
|
caller := ic.VM.GetCallingScriptHash()
|
|
if !c.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleInvestmentManager, ic.Block.Index) {
|
|
panic("only committee or investment manager can record violations")
|
|
}
|
|
}
|
|
|
|
// Verify violator has Vita
|
|
vita, err := c.Vita.GetTokenByOwner(ic.DAO, violator)
|
|
if err != nil || vita == nil {
|
|
panic("violator must have active Vita")
|
|
}
|
|
vitaID := vita.TokenID
|
|
|
|
// Create violation record
|
|
violationID := c.incrementCounter(ic.DAO, makeCollocatioViolationCounterKey())
|
|
caller := ic.VM.GetCallingScriptHash()
|
|
|
|
v := &state.InvestmentViolation{
|
|
ID: violationID,
|
|
VitaID: vitaID,
|
|
Violator: violator,
|
|
OpportunityID: opportunityID,
|
|
ViolationType: violationType,
|
|
Description: description,
|
|
EvidenceHash: evidenceHash,
|
|
Penalty: penalty,
|
|
ReportedBy: caller,
|
|
ReportedAt: ic.Block.Index,
|
|
ResolvedAt: 0,
|
|
Resolution: "",
|
|
}
|
|
|
|
c.putViolation(ic.DAO, v)
|
|
|
|
// Store index by investor
|
|
ic.DAO.PutStorageItem(c.ID, makeCollocatioViolationByInvestorKey(vitaID, violationID), []byte{1})
|
|
|
|
// Update eligibility
|
|
elig := c.getEligibilityInternal(ic.DAO, violator)
|
|
if elig == nil {
|
|
elig = &state.InvestorEligibility{
|
|
VitaID: vitaID,
|
|
Investor: violator,
|
|
CreatedAt: ic.Block.Index,
|
|
}
|
|
}
|
|
elig.HasViolations = true
|
|
elig.ViolationCount++
|
|
elig.UpdatedAt = ic.Block.Index
|
|
c.putEligibility(ic.DAO, elig)
|
|
|
|
// Apply penalty if specified
|
|
if penalty > 0 {
|
|
if err := c.VTS.transferUnrestricted(ic, violator, nativehashes.Treasury, new(big.Int).SetUint64(penalty), nil); err != nil {
|
|
// Don't panic if transfer fails, just record violation without penalty
|
|
}
|
|
}
|
|
|
|
// Emit event
|
|
ic.AddNotification(c.Hash, collocatioViolationRecordedEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(violationID)),
|
|
stackitem.NewByteArray(violator.BytesBE()),
|
|
stackitem.NewByteArray([]byte(violationType)),
|
|
}))
|
|
|
|
return stackitem.NewBigInteger(new(big.Int).SetUint64(violationID))
|
|
}
|
|
|
|
func (c *Collocatio) resolveViolation(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
violationID := toUint64(args[0])
|
|
resolution := toString(args[1])
|
|
|
|
// Authorization: Committee or RoleInvestmentManager
|
|
if !c.Tutus.CheckCommittee(ic) {
|
|
caller := ic.VM.GetCallingScriptHash()
|
|
if !c.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleInvestmentManager, ic.Block.Index) {
|
|
panic("only committee or investment manager can resolve violations")
|
|
}
|
|
}
|
|
|
|
v := c.getViolationInternal(ic.DAO, violationID)
|
|
if v == nil {
|
|
panic("violation not found")
|
|
}
|
|
if v.ResolvedAt != 0 {
|
|
panic("violation already resolved")
|
|
}
|
|
|
|
v.ResolvedAt = ic.Block.Index
|
|
v.Resolution = resolution
|
|
c.putViolation(ic.DAO, v)
|
|
|
|
// Emit event
|
|
ic.AddNotification(c.Hash, collocatioViolationResolvedEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(violationID)),
|
|
stackitem.NewByteArray([]byte(resolution)),
|
|
}))
|
|
|
|
return stackitem.NewBool(true)
|
|
}
|
|
|
|
func (c *Collocatio) getViolation(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
violationID := toUint64(args[0])
|
|
v := c.getViolationInternal(ic.DAO, violationID)
|
|
if v == nil {
|
|
return stackitem.Null{}
|
|
}
|
|
return violationToStackItem(v)
|
|
}
|
|
|
|
// ============================================================================
|
|
// Employment Verification (EIO)
|
|
// ============================================================================
|
|
|
|
func (c *Collocatio) getEmploymentInternal(d *dao.Simple, employeeVitaID uint64) *state.EmploymentVerification {
|
|
si := d.GetStorageItem(c.ID, makeCollocatioEmploymentKey(employeeVitaID))
|
|
if si == nil {
|
|
return nil
|
|
}
|
|
ev := new(state.EmploymentVerification)
|
|
item, _ := stackitem.Deserialize(si)
|
|
ev.FromStackItem(item)
|
|
return ev
|
|
}
|
|
|
|
func (c *Collocatio) putEmployment(d *dao.Simple, ev *state.EmploymentVerification) {
|
|
item, _ := ev.ToStackItem()
|
|
data, _ := stackitem.Serialize(item)
|
|
d.PutStorageItem(c.ID, makeCollocatioEmploymentKey(ev.VitaID), data)
|
|
}
|
|
|
|
func (c *Collocatio) verifyEmployment(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
employee := toUint160(args[0])
|
|
employer := toUint160(args[1])
|
|
position := toString(args[2])
|
|
startDate := uint32(toUint64(args[3]))
|
|
|
|
caller := ic.VM.GetCallingScriptHash()
|
|
|
|
// Authorization: Committee, RoleInvestmentManager, or employer
|
|
isAuthorized := c.Tutus.CheckCommittee(ic) ||
|
|
c.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleInvestmentManager, ic.Block.Index) ||
|
|
caller == employer
|
|
|
|
if !isAuthorized {
|
|
panic("only committee, investment manager, or employer can verify employment")
|
|
}
|
|
|
|
// Verify both employee and employer have Vita
|
|
employeeVita, err := c.Vita.GetTokenByOwner(ic.DAO, employee)
|
|
if err != nil {
|
|
panic("employee must have Vita token")
|
|
}
|
|
employerVita, err := c.Vita.GetTokenByOwner(ic.DAO, employer)
|
|
if err != nil {
|
|
panic("employer must have Vita token")
|
|
}
|
|
|
|
ev := &state.EmploymentVerification{
|
|
VitaID: employeeVita.TokenID,
|
|
Employee: employee,
|
|
EmployerVitaID: employerVita.TokenID,
|
|
Employer: employer,
|
|
Position: position,
|
|
StartDate: startDate,
|
|
EndDate: 0,
|
|
IsActive: true,
|
|
VerifiedAt: ic.Block.Index,
|
|
VerifiedBy: caller,
|
|
}
|
|
|
|
c.putEmployment(ic.DAO, ev)
|
|
|
|
// Store index by employer
|
|
ic.DAO.PutStorageItem(c.ID, makeCollocatioEmploymentByEmployerKey(employerVita.TokenID, employeeVita.TokenID), []byte{1})
|
|
|
|
// Update eligibility
|
|
elig := c.getEligibilityInternal(ic.DAO, employee)
|
|
if elig == nil {
|
|
elig = &state.InvestorEligibility{
|
|
VitaID: employeeVita.TokenID,
|
|
Investor: employee,
|
|
CreatedAt: ic.Block.Index,
|
|
}
|
|
}
|
|
elig.Eligibility |= state.EligibilityEIO
|
|
elig.UpdatedAt = ic.Block.Index
|
|
c.putEligibility(ic.DAO, elig)
|
|
|
|
// Emit event
|
|
ic.AddNotification(c.Hash, collocatioEmploymentVerifiedEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewByteArray(employee.BytesBE()),
|
|
stackitem.NewByteArray(employer.BytesBE()),
|
|
stackitem.NewByteArray([]byte(position)),
|
|
}))
|
|
|
|
return stackitem.NewBool(true)
|
|
}
|
|
|
|
func (c *Collocatio) revokeEmployment(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
employee := toUint160(args[0])
|
|
employer := toUint160(args[1])
|
|
endDate := uint32(toUint64(args[2]))
|
|
|
|
caller := ic.VM.GetCallingScriptHash()
|
|
|
|
// Authorization: Committee, RoleInvestmentManager, or employer
|
|
isAuthorized := c.Tutus.CheckCommittee(ic) ||
|
|
c.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleInvestmentManager, ic.Block.Index) ||
|
|
caller == employer
|
|
|
|
if !isAuthorized {
|
|
panic("only committee, investment manager, or employer can revoke employment")
|
|
}
|
|
|
|
// Get employee Vita
|
|
employeeVita, err := c.Vita.GetTokenByOwner(ic.DAO, employee)
|
|
if err != nil {
|
|
panic("employee must have Vita token")
|
|
}
|
|
|
|
ev := c.getEmploymentInternal(ic.DAO, employeeVita.TokenID)
|
|
if ev == nil {
|
|
panic("employment not found")
|
|
}
|
|
if !ev.IsActive {
|
|
panic("employment already revoked")
|
|
}
|
|
if ev.Employer != employer {
|
|
panic("employer mismatch")
|
|
}
|
|
|
|
ev.IsActive = false
|
|
ev.EndDate = endDate
|
|
c.putEmployment(ic.DAO, ev)
|
|
|
|
// Remove EIO eligibility
|
|
elig := c.getEligibilityInternal(ic.DAO, employee)
|
|
if elig != nil {
|
|
elig.Eligibility &^= state.EligibilityEIO
|
|
elig.UpdatedAt = ic.Block.Index
|
|
c.putEligibility(ic.DAO, elig)
|
|
}
|
|
|
|
// Emit event
|
|
ic.AddNotification(c.Hash, collocatioEmploymentRevokedEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewByteArray(employee.BytesBE()),
|
|
stackitem.NewByteArray(employer.BytesBE()),
|
|
}))
|
|
|
|
return stackitem.NewBool(true)
|
|
}
|
|
|
|
func (c *Collocatio) getEmploymentStatus(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
employee := toUint160(args[0])
|
|
|
|
vita, err := c.Vita.GetTokenByOwner(ic.DAO, employee)
|
|
if err != nil {
|
|
return stackitem.Null{}
|
|
}
|
|
|
|
ev := c.getEmploymentInternal(ic.DAO, vita.TokenID)
|
|
if ev == nil {
|
|
return stackitem.Null{}
|
|
}
|
|
return employmentToStackItem(ev)
|
|
}
|
|
|
|
func (c *Collocatio) hasActiveEmployment(d *dao.Simple, vitaID uint64) bool {
|
|
ev := c.getEmploymentInternal(d, vitaID)
|
|
return ev != nil && ev.IsActive
|
|
}
|
|
|
|
// ============================================================================
|
|
// Contractor Verification (CIO)
|
|
// ============================================================================
|
|
|
|
func (c *Collocatio) getContractorInternal(d *dao.Simple, contractorVitaID uint64) *state.ContractorVerification {
|
|
si := d.GetStorageItem(c.ID, makeCollocatioContractorKey(contractorVitaID))
|
|
if si == nil {
|
|
return nil
|
|
}
|
|
cv := new(state.ContractorVerification)
|
|
item, _ := stackitem.Deserialize(si)
|
|
cv.FromStackItem(item)
|
|
return cv
|
|
}
|
|
|
|
func (c *Collocatio) putContractor(d *dao.Simple, cv *state.ContractorVerification) {
|
|
item, _ := cv.ToStackItem()
|
|
data, _ := stackitem.Serialize(item)
|
|
d.PutStorageItem(c.ID, makeCollocatioContractorKey(cv.VitaID), data)
|
|
}
|
|
|
|
func (c *Collocatio) verifyContractor(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
contractor := toUint160(args[0])
|
|
platform := toUint160(args[1])
|
|
platformID := toString(args[2])
|
|
contractorID := toString(args[3])
|
|
startDate := uint32(toUint64(args[4]))
|
|
|
|
caller := ic.VM.GetCallingScriptHash()
|
|
|
|
// Authorization: Committee, RoleInvestmentManager, or platform
|
|
isAuthorized := c.Tutus.CheckCommittee(ic) ||
|
|
c.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleInvestmentManager, ic.Block.Index) ||
|
|
caller == platform
|
|
|
|
if !isAuthorized {
|
|
panic("only committee, investment manager, or platform can verify contractor")
|
|
}
|
|
|
|
// Verify contractor has Vita
|
|
contractorVita, err := c.Vita.GetTokenByOwner(ic.DAO, contractor)
|
|
if err != nil {
|
|
panic("contractor must have Vita token")
|
|
}
|
|
|
|
cv := &state.ContractorVerification{
|
|
VitaID: contractorVita.TokenID,
|
|
Contractor: contractor,
|
|
PlatformID: platformID,
|
|
Platform: platform,
|
|
ContractorID: contractorID,
|
|
StartDate: startDate,
|
|
EndDate: 0,
|
|
IsActive: true,
|
|
VerifiedAt: ic.Block.Index,
|
|
VerifiedBy: caller,
|
|
}
|
|
|
|
c.putContractor(ic.DAO, cv)
|
|
|
|
// Store index by platform
|
|
ic.DAO.PutStorageItem(c.ID, makeCollocatioContractorByPlatformKey(platform, contractorVita.TokenID), []byte{1})
|
|
|
|
// Update eligibility
|
|
elig := c.getEligibilityInternal(ic.DAO, contractor)
|
|
if elig == nil {
|
|
elig = &state.InvestorEligibility{
|
|
VitaID: contractorVita.TokenID,
|
|
Investor: contractor,
|
|
CreatedAt: ic.Block.Index,
|
|
}
|
|
}
|
|
elig.Eligibility |= state.EligibilityCIO
|
|
elig.UpdatedAt = ic.Block.Index
|
|
c.putEligibility(ic.DAO, elig)
|
|
|
|
// Emit event
|
|
ic.AddNotification(c.Hash, collocatioContractorVerifiedEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewByteArray(contractor.BytesBE()),
|
|
stackitem.NewByteArray(platform.BytesBE()),
|
|
stackitem.NewByteArray([]byte(platformID)),
|
|
}))
|
|
|
|
return stackitem.NewBool(true)
|
|
}
|
|
|
|
func (c *Collocatio) revokeContractor(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
contractor := toUint160(args[0])
|
|
platform := toUint160(args[1])
|
|
endDate := uint32(toUint64(args[2]))
|
|
|
|
caller := ic.VM.GetCallingScriptHash()
|
|
|
|
// Authorization: Committee, RoleInvestmentManager, or platform
|
|
isAuthorized := c.Tutus.CheckCommittee(ic) ||
|
|
c.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleInvestmentManager, ic.Block.Index) ||
|
|
caller == platform
|
|
|
|
if !isAuthorized {
|
|
panic("only committee, investment manager, or platform can revoke contractor")
|
|
}
|
|
|
|
// Get contractor Vita
|
|
contractorVita, err := c.Vita.GetTokenByOwner(ic.DAO, contractor)
|
|
if err != nil {
|
|
panic("contractor must have Vita token")
|
|
}
|
|
|
|
cv := c.getContractorInternal(ic.DAO, contractorVita.TokenID)
|
|
if cv == nil {
|
|
panic("contractor not found")
|
|
}
|
|
if !cv.IsActive {
|
|
panic("contractor already revoked")
|
|
}
|
|
if cv.Platform != platform {
|
|
panic("platform mismatch")
|
|
}
|
|
|
|
cv.IsActive = false
|
|
cv.EndDate = endDate
|
|
c.putContractor(ic.DAO, cv)
|
|
|
|
// Remove CIO eligibility
|
|
elig := c.getEligibilityInternal(ic.DAO, contractor)
|
|
if elig != nil {
|
|
elig.Eligibility &^= state.EligibilityCIO
|
|
elig.UpdatedAt = ic.Block.Index
|
|
c.putEligibility(ic.DAO, elig)
|
|
}
|
|
|
|
// Emit event
|
|
ic.AddNotification(c.Hash, collocatioContractorRevokedEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewByteArray(contractor.BytesBE()),
|
|
stackitem.NewByteArray(platform.BytesBE()),
|
|
}))
|
|
|
|
return stackitem.NewBool(true)
|
|
}
|
|
|
|
func (c *Collocatio) getContractorStatus(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
contractor := toUint160(args[0])
|
|
|
|
vita, err := c.Vita.GetTokenByOwner(ic.DAO, contractor)
|
|
if err != nil {
|
|
return stackitem.Null{}
|
|
}
|
|
|
|
cv := c.getContractorInternal(ic.DAO, vita.TokenID)
|
|
if cv == nil {
|
|
return stackitem.Null{}
|
|
}
|
|
return contractorToStackItem(cv)
|
|
}
|
|
|
|
func (c *Collocatio) hasActiveContractor(d *dao.Simple, vitaID uint64) bool {
|
|
cv := c.getContractorInternal(d, vitaID)
|
|
return cv != nil && cv.IsActive
|
|
}
|
|
|
|
// ============================================================================
|
|
// Education Completion
|
|
// ============================================================================
|
|
|
|
func (c *Collocatio) completeInvestmentEducation(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
investor := toUint160(args[0])
|
|
|
|
caller := ic.VM.GetCallingScriptHash()
|
|
|
|
// Authorization: Committee or RoleInvestmentManager
|
|
if !c.Tutus.CheckCommittee(ic) {
|
|
if !c.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleInvestmentManager, ic.Block.Index) {
|
|
panic("only committee or investment manager can complete education")
|
|
}
|
|
}
|
|
|
|
// Verify investor has Vita
|
|
vita, err := c.Vita.GetTokenByOwner(ic.DAO, investor)
|
|
if err != nil {
|
|
panic("investor must have Vita token")
|
|
}
|
|
|
|
// Update eligibility
|
|
elig := c.getEligibilityInternal(ic.DAO, investor)
|
|
if elig == nil {
|
|
elig = &state.InvestorEligibility{
|
|
VitaID: vita.TokenID,
|
|
Investor: investor,
|
|
CreatedAt: ic.Block.Index,
|
|
}
|
|
}
|
|
elig.ScireCompleted = true
|
|
elig.UpdatedAt = ic.Block.Index
|
|
c.putEligibility(ic.DAO, elig)
|
|
|
|
// Emit event
|
|
ic.AddNotification(c.Hash, collocatioEducationCompletedEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewByteArray(investor.BytesBE()),
|
|
}))
|
|
|
|
return stackitem.NewBool(true)
|
|
}
|
|
|
|
// ============================================================================
|
|
// Returns Distribution & Cancellation
|
|
// ============================================================================
|
|
|
|
func (c *Collocatio) distributeReturns(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
oppID := toUint64(args[0])
|
|
actualReturns := toUint64(args[1])
|
|
|
|
// Authorization: Committee or RoleInvestmentManager
|
|
if !c.Tutus.CheckCommittee(ic) {
|
|
caller := ic.VM.GetCallingScriptHash()
|
|
if !c.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleInvestmentManager, ic.Block.Index) {
|
|
panic("only committee or investment manager can distribute returns")
|
|
}
|
|
}
|
|
|
|
opp := c.getOpportunityInternal(ic.DAO, oppID)
|
|
if opp == nil {
|
|
panic("opportunity not found")
|
|
}
|
|
if opp.Status != state.OpportunityClosed {
|
|
panic("opportunity must be in closed status")
|
|
}
|
|
if ic.Block.Index < opp.MaturityDate {
|
|
panic("maturity date not reached")
|
|
}
|
|
if opp.TotalPool == 0 {
|
|
panic("no investments to distribute")
|
|
}
|
|
|
|
// Collect all investment IDs first (to avoid modifying during iteration)
|
|
prefix := []byte{collocatioPrefixInvestmentByOpp}
|
|
prefix = append(prefix, make([]byte, 8)...)
|
|
binary.BigEndian.PutUint64(prefix[1:], oppID)
|
|
|
|
var invIDs []uint64
|
|
ic.DAO.Seek(c.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool {
|
|
if len(k) >= 16 {
|
|
invID := binary.BigEndian.Uint64(k[8:16])
|
|
invIDs = append(invIDs, invID)
|
|
}
|
|
return true
|
|
})
|
|
|
|
// Process each investment
|
|
for _, invID := range invIDs {
|
|
inv := c.getInvestmentInternal(ic.DAO, invID)
|
|
if inv == nil || inv.Status != state.InvestmentActive {
|
|
continue
|
|
}
|
|
|
|
// Calculate proportional return
|
|
returnAmount := (inv.Amount * actualReturns) / opp.TotalPool
|
|
|
|
// Transfer return to investor
|
|
if returnAmount > 0 {
|
|
if err := c.VTS.transferUnrestricted(ic, c.Hash, inv.Investor, new(big.Int).SetUint64(returnAmount), nil); err != nil {
|
|
continue // Skip failed transfers
|
|
}
|
|
}
|
|
|
|
// Also return principal
|
|
if inv.Amount > 0 {
|
|
if err := c.VTS.transferUnrestricted(ic, c.Hash, inv.Investor, new(big.Int).SetUint64(inv.Amount), nil); err != nil {
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Update investment
|
|
inv.ReturnAmount = returnAmount
|
|
inv.Status = state.InvestmentCompleted
|
|
inv.UpdatedAt = ic.Block.Index
|
|
c.putInvestment(ic.DAO, inv)
|
|
|
|
// Update eligibility
|
|
elig := c.getEligibilityInternal(ic.DAO, inv.Investor)
|
|
if elig != nil {
|
|
elig.TotalReturns += returnAmount
|
|
elig.CompletedInvestments++
|
|
if elig.ActiveInvestments > 0 {
|
|
elig.ActiveInvestments--
|
|
}
|
|
if elig.TotalInvested >= inv.Amount {
|
|
elig.TotalInvested -= inv.Amount
|
|
}
|
|
elig.LastActivity = ic.Block.Index
|
|
elig.UpdatedAt = ic.Block.Index
|
|
c.putEligibility(ic.DAO, elig)
|
|
}
|
|
|
|
// Emit event for each distribution
|
|
ic.AddNotification(c.Hash, collocatioReturnsDistributedEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(invID)),
|
|
stackitem.NewByteArray(inv.Investor.BytesBE()),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(returnAmount)),
|
|
}))
|
|
}
|
|
|
|
// Update opportunity status
|
|
opp.Status = state.OpportunityCompleted
|
|
opp.UpdatedAt = ic.Block.Index
|
|
c.putOpportunity(ic.DAO, opp)
|
|
|
|
return stackitem.NewBool(true)
|
|
}
|
|
|
|
func (c *Collocatio) cancelOpportunity(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
oppID := toUint64(args[0])
|
|
caller := ic.VM.GetCallingScriptHash()
|
|
|
|
opp := c.getOpportunityInternal(ic.DAO, oppID)
|
|
if opp == nil {
|
|
panic("opportunity not found")
|
|
}
|
|
|
|
// Authorization: Creator (if Draft/Voting) or Committee
|
|
isCreator := caller == opp.Creator
|
|
isCommittee := c.Tutus.CheckCommittee(ic)
|
|
|
|
if opp.Status == state.OpportunityDraft || opp.Status == state.OpportunityVoting {
|
|
if !isCreator && !isCommittee {
|
|
panic("only creator or committee can cancel draft/voting opportunity")
|
|
}
|
|
} else if opp.Status == state.OpportunityActive {
|
|
if !isCommittee {
|
|
panic("only committee can cancel active opportunity")
|
|
}
|
|
} else {
|
|
panic("opportunity cannot be cancelled in current status")
|
|
}
|
|
|
|
// Collect all investment IDs first (to avoid modifying during iteration)
|
|
prefix := []byte{collocatioPrefixInvestmentByOpp}
|
|
prefix = append(prefix, make([]byte, 8)...)
|
|
binary.BigEndian.PutUint64(prefix[1:], oppID)
|
|
|
|
var invIDs []uint64
|
|
ic.DAO.Seek(c.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool {
|
|
if len(k) >= 16 {
|
|
invID := binary.BigEndian.Uint64(k[8:16])
|
|
invIDs = append(invIDs, invID)
|
|
}
|
|
return true
|
|
})
|
|
|
|
// Process each investment
|
|
for _, invID := range invIDs {
|
|
inv := c.getInvestmentInternal(ic.DAO, invID)
|
|
if inv == nil || inv.Status != state.InvestmentActive {
|
|
continue
|
|
}
|
|
|
|
// Refund full amount
|
|
if inv.Amount > 0 {
|
|
if err := c.VTS.transferUnrestricted(ic, c.Hash, inv.Investor, new(big.Int).SetUint64(inv.Amount), nil); err != nil {
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Update investment
|
|
inv.Status = state.InvestmentRefunded
|
|
inv.UpdatedAt = ic.Block.Index
|
|
c.putInvestment(ic.DAO, inv)
|
|
|
|
// Update eligibility
|
|
elig := c.getEligibilityInternal(ic.DAO, inv.Investor)
|
|
if elig != nil {
|
|
if elig.ActiveInvestments > 0 {
|
|
elig.ActiveInvestments--
|
|
}
|
|
if elig.TotalInvested >= inv.Amount {
|
|
elig.TotalInvested -= inv.Amount
|
|
}
|
|
elig.LastActivity = ic.Block.Index
|
|
elig.UpdatedAt = ic.Block.Index
|
|
c.putEligibility(ic.DAO, elig)
|
|
}
|
|
}
|
|
|
|
// Update opportunity status
|
|
opp.Status = state.OpportunityCancelled
|
|
opp.UpdatedAt = ic.Block.Index
|
|
c.putOpportunity(ic.DAO, opp)
|
|
|
|
// Emit event
|
|
ic.AddNotification(c.Hash, collocatioOpportunityCancelledEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(oppID)),
|
|
}))
|
|
|
|
return stackitem.NewBool(true)
|
|
}
|
|
|
|
// ============================================================================
|
|
// Query Methods
|
|
// ============================================================================
|
|
|
|
func (c *Collocatio) getInvestmentsByOpportunity(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
oppID := toUint64(args[0])
|
|
|
|
prefix := []byte{collocatioPrefixInvestmentByOpp}
|
|
prefix = append(prefix, make([]byte, 8)...)
|
|
binary.BigEndian.PutUint64(prefix[1:], oppID)
|
|
|
|
var ids []stackitem.Item
|
|
ic.DAO.Seek(c.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool {
|
|
if len(k) >= 16 {
|
|
invID := binary.BigEndian.Uint64(k[8:16])
|
|
ids = append(ids, stackitem.NewBigInteger(new(big.Int).SetUint64(invID)))
|
|
}
|
|
return true
|
|
})
|
|
|
|
return stackitem.NewArray(ids)
|
|
}
|
|
|
|
func (c *Collocatio) getInvestmentsByInvestor(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
investor := toUint160(args[0])
|
|
|
|
vita, err := c.Vita.GetTokenByOwner(ic.DAO, investor)
|
|
if err != nil {
|
|
return stackitem.NewArray(nil)
|
|
}
|
|
|
|
prefix := []byte{collocatioPrefixInvestmentByInvestor}
|
|
prefix = append(prefix, make([]byte, 8)...)
|
|
binary.BigEndian.PutUint64(prefix[1:], vita.TokenID)
|
|
|
|
var ids []stackitem.Item
|
|
ic.DAO.Seek(c.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool {
|
|
if len(k) >= 16 {
|
|
invID := binary.BigEndian.Uint64(k[8:16])
|
|
ids = append(ids, stackitem.NewBigInteger(new(big.Int).SetUint64(invID)))
|
|
}
|
|
return true
|
|
})
|
|
|
|
return stackitem.NewArray(ids)
|
|
}
|
|
|
|
func (c *Collocatio) getOpportunitiesByType(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
oppType := state.OpportunityType(toUint64(args[0]))
|
|
|
|
prefix := []byte{collocatioPrefixOpportunityByType, byte(oppType)}
|
|
|
|
var ids []stackitem.Item
|
|
ic.DAO.Seek(c.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool {
|
|
if len(k) >= 9 {
|
|
oppID := binary.BigEndian.Uint64(k[1:9])
|
|
ids = append(ids, stackitem.NewBigInteger(new(big.Int).SetUint64(oppID)))
|
|
}
|
|
return true
|
|
})
|
|
|
|
return stackitem.NewArray(ids)
|
|
}
|
|
|
|
func (c *Collocatio) getOpportunitiesByStatus(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
status := state.OpportunityStatus(toUint64(args[0]))
|
|
|
|
prefix := []byte{collocatioPrefixOpportunityByStatus, byte(status)}
|
|
|
|
var ids []stackitem.Item
|
|
ic.DAO.Seek(c.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool {
|
|
if len(k) >= 9 {
|
|
oppID := binary.BigEndian.Uint64(k[1:9])
|
|
ids = append(ids, stackitem.NewBigInteger(new(big.Int).SetUint64(oppID)))
|
|
}
|
|
return true
|
|
})
|
|
|
|
return stackitem.NewArray(ids)
|
|
}
|
|
|
|
// ============================================================================
|
|
// Stack Item Converters
|
|
// ============================================================================
|
|
|
|
func opportunityToStackItem(opp *state.InvestmentOpportunity) stackitem.Item {
|
|
return stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(opp.ID)),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(opp.Type))),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(opp.Status))),
|
|
stackitem.NewByteArray(opp.Creator.BytesBE()),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(opp.SponsorVitaID)),
|
|
stackitem.NewByteArray([]byte(opp.Name)),
|
|
stackitem.NewByteArray([]byte(opp.Description)),
|
|
stackitem.NewByteArray(opp.TermsHash.BytesBE()),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(opp.MinParticipants)),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(opp.MaxParticipants)),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(opp.CurrentParticipants)),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(opp.MinInvestment)),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(opp.MaxInvestment)),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(opp.TotalPool)),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(opp.TargetPool)),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(opp.ExpectedReturns)),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(opp.RiskLevel))),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(opp.VotingDeadline))),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(opp.InvestmentDeadline))),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(opp.MaturityDate))),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(opp.ProposalID)),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(opp.CreatedAt))),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(opp.UpdatedAt))),
|
|
})
|
|
}
|
|
|
|
func investmentToStackItem(inv *state.Investment) stackitem.Item {
|
|
return stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(inv.ID)),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(inv.OpportunityID)),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(inv.VitaID)),
|
|
stackitem.NewByteArray(inv.Investor.BytesBE()),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(inv.Amount)),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(inv.Status))),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(inv.ReturnAmount)),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(inv.CreatedAt))),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(inv.UpdatedAt))),
|
|
})
|
|
}
|
|
|
|
func eligibilityToStackItem(elig *state.InvestorEligibility) stackitem.Item {
|
|
return stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(elig.VitaID)),
|
|
stackitem.NewByteArray(elig.Investor.BytesBE()),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(elig.Eligibility))),
|
|
stackitem.NewBool(elig.ScireCompleted),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(elig.RiskScore))),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(elig.TotalInvested)),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(elig.TotalReturns)),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(elig.ActiveInvestments)),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(elig.CompletedInvestments)),
|
|
stackitem.NewBool(elig.HasViolations),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(elig.ViolationCount))),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(elig.LastActivity))),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(elig.CreatedAt))),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(elig.UpdatedAt))),
|
|
})
|
|
}
|
|
|
|
func violationToStackItem(v *state.InvestmentViolation) stackitem.Item {
|
|
return stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(v.ID)),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(v.VitaID)),
|
|
stackitem.NewByteArray(v.Violator.BytesBE()),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(v.OpportunityID)),
|
|
stackitem.NewByteArray([]byte(v.ViolationType)),
|
|
stackitem.NewByteArray([]byte(v.Description)),
|
|
stackitem.NewByteArray(v.EvidenceHash.BytesBE()),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(v.Penalty)),
|
|
stackitem.NewByteArray(v.ReportedBy.BytesBE()),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(v.ReportedAt))),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(v.ResolvedAt))),
|
|
stackitem.NewByteArray([]byte(v.Resolution)),
|
|
})
|
|
}
|
|
|
|
func employmentToStackItem(ev *state.EmploymentVerification) stackitem.Item {
|
|
return stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(ev.VitaID)),
|
|
stackitem.NewByteArray(ev.Employee.BytesBE()),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(ev.EmployerVitaID)),
|
|
stackitem.NewByteArray(ev.Employer.BytesBE()),
|
|
stackitem.NewByteArray([]byte(ev.Position)),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(ev.StartDate))),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(ev.EndDate))),
|
|
stackitem.NewBool(ev.IsActive),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(ev.VerifiedAt))),
|
|
stackitem.NewByteArray(ev.VerifiedBy.BytesBE()),
|
|
})
|
|
}
|
|
|
|
func contractorToStackItem(cv *state.ContractorVerification) stackitem.Item {
|
|
return stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(cv.VitaID)),
|
|
stackitem.NewByteArray(cv.Contractor.BytesBE()),
|
|
stackitem.NewByteArray([]byte(cv.PlatformID)),
|
|
stackitem.NewByteArray(cv.Platform.BytesBE()),
|
|
stackitem.NewByteArray([]byte(cv.ContractorID)),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(cv.StartDate))),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(cv.EndDate))),
|
|
stackitem.NewBool(cv.IsActive),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(cv.VerifiedAt))),
|
|
stackitem.NewByteArray(cv.VerifiedBy.BytesBE()),
|
|
})
|
|
}
|
|
|
|
func commitmentToStackItem(c *state.InvestmentCommitment) stackitem.Item {
|
|
return stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(c.ID)),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(c.OpportunityID)),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(c.VitaID)),
|
|
stackitem.NewByteArray(c.Investor.BytesBE()),
|
|
stackitem.NewByteArray(c.Commitment.BytesBE()),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(c.Status))),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(c.CommittedAt))),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(c.RevealDeadline))),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(c.RevealedAmount)),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(c.InvestmentID)),
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Commit-Reveal System (Anti-Front-Running)
|
|
// ============================================================================
|
|
|
|
// commitInvestment creates a commitment to invest without revealing the amount.
|
|
// The commitment is hash(amount || nonce || investor).
|
|
func (col *Collocatio) commitInvestment(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
oppID := toUint64(args[0])
|
|
commitmentHashBytes, err := args[1].TryBytes()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
commitmentHash, err := util.Uint256DecodeBytesBE(commitmentHashBytes)
|
|
if err != nil {
|
|
panic("invalid commitment hash")
|
|
}
|
|
|
|
caller := ic.VM.GetCallingScriptHash()
|
|
|
|
// Validate caller has Vita
|
|
vita, err := col.Vita.GetTokenByOwner(ic.DAO, caller)
|
|
if err != nil {
|
|
panic("caller must have Vita token")
|
|
}
|
|
vitaID := vita.TokenID
|
|
|
|
// Get opportunity
|
|
opp := col.getOpportunityInternal(ic.DAO, oppID)
|
|
if opp == nil {
|
|
panic("opportunity not found")
|
|
}
|
|
|
|
if opp.Status != state.OpportunityActive {
|
|
panic("opportunity is not active")
|
|
}
|
|
if ic.Block.Index > opp.InvestmentDeadline {
|
|
panic("investment deadline has passed")
|
|
}
|
|
|
|
// Check eligibility
|
|
if !col.isEligibleInternal(ic.DAO, caller, opp.Type) {
|
|
panic("investor not eligible for this opportunity type")
|
|
}
|
|
|
|
cfg := col.getConfigInternal(ic.DAO)
|
|
|
|
// Create commitment
|
|
commitmentID := col.incrementCounter(ic.DAO, makeCollocatioCommitmentCounterKey())
|
|
currentBlock := ic.Block.Index
|
|
|
|
commitment := &state.InvestmentCommitment{
|
|
ID: commitmentID,
|
|
OpportunityID: oppID,
|
|
VitaID: vitaID,
|
|
Investor: caller,
|
|
Commitment: commitmentHash,
|
|
Status: state.CommitmentPending,
|
|
CommittedAt: currentBlock,
|
|
RevealDeadline: currentBlock + cfg.CommitRevealDelay + cfg.CommitRevealWindow,
|
|
RevealedAmount: 0,
|
|
InvestmentID: 0,
|
|
}
|
|
|
|
col.putCommitment(ic.DAO, commitment)
|
|
|
|
// Store indexes
|
|
ic.DAO.PutStorageItem(col.ID, makeCollocatioCommitmentByOppKey(oppID, commitmentID), []byte{1})
|
|
ic.DAO.PutStorageItem(col.ID, makeCollocatioCommitmentByInvestorKey(vitaID, commitmentID), []byte{1})
|
|
|
|
// Emit event
|
|
ic.AddNotification(col.Hash, collocatioCommitmentCreatedEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(commitmentID)),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(oppID)),
|
|
stackitem.NewByteArray(caller.BytesBE()),
|
|
}))
|
|
|
|
return stackitem.NewBigInteger(new(big.Int).SetUint64(commitmentID))
|
|
}
|
|
|
|
// revealInvestment reveals the investment amount and executes the investment.
|
|
// The reveal must happen after CommitRevealDelay blocks but before RevealDeadline.
|
|
func (col *Collocatio) revealInvestment(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
commitmentID := toUint64(args[0])
|
|
amount := toUint64(args[1])
|
|
nonce, err := args[2].TryBytes()
|
|
if err != nil {
|
|
panic("invalid nonce")
|
|
}
|
|
|
|
caller := ic.VM.GetCallingScriptHash()
|
|
|
|
// Get commitment
|
|
commitment := col.getCommitmentInternal(ic.DAO, commitmentID)
|
|
if commitment == nil {
|
|
panic("commitment not found")
|
|
}
|
|
|
|
if commitment.Investor != caller {
|
|
panic("only commitment owner can reveal")
|
|
}
|
|
|
|
if commitment.Status != state.CommitmentPending {
|
|
panic("commitment already processed")
|
|
}
|
|
|
|
cfg := col.getConfigInternal(ic.DAO)
|
|
|
|
// Check timing
|
|
currentBlock := ic.Block.Index
|
|
revealStart := commitment.CommittedAt + cfg.CommitRevealDelay
|
|
if currentBlock < revealStart {
|
|
panic("reveal period not started yet")
|
|
}
|
|
if currentBlock > commitment.RevealDeadline {
|
|
panic("reveal deadline passed")
|
|
}
|
|
|
|
// Verify commitment: hash(amount || nonce || investor)
|
|
preimage := make([]byte, 8+len(nonce)+util.Uint160Size)
|
|
binary.BigEndian.PutUint64(preimage[:8], amount)
|
|
copy(preimage[8:8+len(nonce)], nonce)
|
|
copy(preimage[8+len(nonce):], caller.BytesBE())
|
|
|
|
// Hash the preimage using SHA256
|
|
computedHash := hash.Sha256(preimage)
|
|
if computedHash != commitment.Commitment {
|
|
panic("commitment verification failed")
|
|
}
|
|
|
|
// Get opportunity
|
|
opp := col.getOpportunityInternal(ic.DAO, commitment.OpportunityID)
|
|
if opp == nil {
|
|
panic("opportunity not found")
|
|
}
|
|
|
|
if opp.Status != state.OpportunityActive {
|
|
panic("opportunity is not active")
|
|
}
|
|
if currentBlock > opp.InvestmentDeadline {
|
|
panic("investment deadline has passed")
|
|
}
|
|
|
|
// Validate amount
|
|
if amount < opp.MinInvestment {
|
|
panic("investment below minimum")
|
|
}
|
|
if amount > opp.MaxInvestment {
|
|
panic("investment exceeds maximum")
|
|
}
|
|
|
|
if opp.MaxParticipants > 0 && opp.CurrentParticipants >= opp.MaxParticipants {
|
|
panic("maximum participants reached")
|
|
}
|
|
|
|
// Whale concentration check
|
|
if cfg.WealthConcentration > 0 {
|
|
existingInvestment := col.getInvestorTotalInOpportunity(ic.DAO, commitment.VitaID, commitment.OpportunityID)
|
|
newTotal := existingInvestment + amount
|
|
futurePool := opp.TotalPool + amount
|
|
if futurePool > 0 {
|
|
concentration := (newTotal * 10000) / futurePool
|
|
if concentration > cfg.WealthConcentration {
|
|
panic("investment would exceed whale concentration limit")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Calculate fee
|
|
fee := (amount * cfg.InvestmentFee) / 10000
|
|
netAmount := amount - fee
|
|
|
|
// Transfer VTS from investor
|
|
if err := col.VTS.transferUnrestricted(ic, caller, col.Hash, new(big.Int).SetUint64(amount), nil); err != nil {
|
|
panic("failed to transfer investment amount")
|
|
}
|
|
|
|
// Send fee to Treasury
|
|
if fee > 0 {
|
|
if err := col.VTS.transferUnrestricted(ic, col.Hash, nativehashes.Treasury, new(big.Int).SetUint64(fee), nil); err != nil {
|
|
panic("failed to transfer fee to treasury")
|
|
}
|
|
}
|
|
|
|
// Create investment record
|
|
invID := col.incrementCounter(ic.DAO, makeCollocatioInvCounterKey())
|
|
|
|
inv := &state.Investment{
|
|
ID: invID,
|
|
OpportunityID: commitment.OpportunityID,
|
|
VitaID: commitment.VitaID,
|
|
Investor: caller,
|
|
Amount: netAmount,
|
|
Status: state.InvestmentActive,
|
|
ReturnAmount: 0,
|
|
CreatedAt: currentBlock,
|
|
UpdatedAt: currentBlock,
|
|
}
|
|
|
|
col.putInvestment(ic.DAO, inv)
|
|
|
|
// Store indexes
|
|
ic.DAO.PutStorageItem(col.ID, makeCollocatioInvByOppKey(commitment.OpportunityID, invID), []byte{1})
|
|
ic.DAO.PutStorageItem(col.ID, makeCollocatioInvByInvestorKey(commitment.VitaID, invID), []byte{1})
|
|
|
|
// Update opportunity
|
|
opp.CurrentParticipants++
|
|
opp.TotalPool += netAmount
|
|
opp.UpdatedAt = currentBlock
|
|
col.putOpportunity(ic.DAO, opp)
|
|
|
|
// Update commitment
|
|
commitment.Status = state.CommitmentRevealed
|
|
commitment.RevealedAmount = amount
|
|
commitment.InvestmentID = invID
|
|
col.putCommitment(ic.DAO, commitment)
|
|
|
|
// Update eligibility
|
|
col.updateEligibilityOnInvest(ic.DAO, caller, commitment.VitaID, netAmount, currentBlock)
|
|
|
|
// Emit events
|
|
ic.AddNotification(col.Hash, collocatioCommitmentRevealedEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(commitmentID)),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(invID)),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(amount)),
|
|
}))
|
|
|
|
ic.AddNotification(col.Hash, collocatioInvestmentMadeEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(invID)),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(commitment.OpportunityID)),
|
|
stackitem.NewByteArray(caller.BytesBE()),
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(netAmount)),
|
|
}))
|
|
|
|
return stackitem.NewBigInteger(new(big.Int).SetUint64(invID))
|
|
}
|
|
|
|
// cancelCommitment cancels a pending commitment.
|
|
func (col *Collocatio) cancelCommitment(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
commitmentID := toUint64(args[0])
|
|
caller := ic.VM.GetCallingScriptHash()
|
|
|
|
commitment := col.getCommitmentInternal(ic.DAO, commitmentID)
|
|
if commitment == nil {
|
|
panic("commitment not found")
|
|
}
|
|
|
|
if commitment.Investor != caller {
|
|
panic("only commitment owner can cancel")
|
|
}
|
|
|
|
if commitment.Status != state.CommitmentPending {
|
|
panic("commitment already processed")
|
|
}
|
|
|
|
// Update commitment status
|
|
commitment.Status = state.CommitmentCanceled
|
|
col.putCommitment(ic.DAO, commitment)
|
|
|
|
// Emit event
|
|
ic.AddNotification(col.Hash, collocatioCommitmentCanceledEvent, stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewBigInteger(new(big.Int).SetUint64(commitmentID)),
|
|
}))
|
|
|
|
return stackitem.NewBool(true)
|
|
}
|
|
|
|
// getCommitment returns commitment details.
|
|
func (col *Collocatio) getCommitment(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
commitmentID := toUint64(args[0])
|
|
commitment := col.getCommitmentInternal(ic.DAO, commitmentID)
|
|
if commitment == nil {
|
|
return stackitem.Null{}
|
|
}
|
|
return commitmentToStackItem(commitment)
|
|
}
|
|
|
|
// getCommitmentCount returns the total number of commitments.
|
|
func (col *Collocatio) getCommitmentCount(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
|
|
count := col.getCounter(ic.DAO, makeCollocatioCommitmentCounterKey())
|
|
return stackitem.NewBigInteger(new(big.Int).SetUint64(count))
|
|
}
|