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

1648 lines
54 KiB
Go
Executable File

package native
import (
"encoding/binary"
"errors"
"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/nativeids"
"github.com/tutus-one/tutus-chain/pkg/core/native/nativenames"
"github.com/tutus-one/tutus-chain/pkg/core/state"
"github.com/tutus-one/tutus-chain/pkg/core/storage"
"github.com/tutus-one/tutus-chain/pkg/smartcontract"
"github.com/tutus-one/tutus-chain/pkg/smartcontract/callflag"
"github.com/tutus-one/tutus-chain/pkg/smartcontract/manifest"
"github.com/tutus-one/tutus-chain/pkg/util"
"github.com/tutus-one/tutus-chain/pkg/vm/stackitem"
)
// Tribute represents the anti-hoarding economics native contract.
type Tribute struct {
interop.ContractMD
Tutus ITutus
Vita IVita
VTS IVTS
RoleRegistry IRoleRegistry
Lex ILex
}
// TributeCache represents the cached state for Tribute contract.
type TributeCache struct {
accountCount uint64
assessmentCount uint64
incentiveCount uint64
redistributionCount uint64
}
// Storage key prefixes for Tribute.
const (
tributePrefixAccount byte = 0x01 // vitaID -> VelocityAccount
tributePrefixAccountByOwner byte = 0x02 // owner -> vitaID
tributePrefixAssessment byte = 0x10 // assessmentID -> TributeAssessment
tributePrefixAssessmentByOwner byte = 0x11 // vitaID + assessmentID -> exists
tributePrefixPendingAssessment byte = 0x12 // vitaID -> latest pending assessmentID
tributePrefixIncentive byte = 0x20 // incentiveID -> CirculationIncentive
tributePrefixIncentiveByOwner byte = 0x21 // vitaID + incentiveID -> exists
tributePrefixUnclaimedIncentive byte = 0x22 // vitaID + incentiveID -> exists (unclaimed only)
tributePrefixRedistribution byte = 0x30 // redistID -> RedistributionRecord
tributePrefixAccountCounter byte = 0xF0 // -> uint64
tributePrefixAssessmentCounter byte = 0xF1 // -> next assessment ID
tributePrefixIncentiveCounter byte = 0xF2 // -> next incentive ID
tributePrefixRedistributionCtr byte = 0xF3 // -> next redistribution ID
tributePrefixTotalTributePool byte = 0xF8 // -> total tribute collected for redistribution
tributePrefixConfig byte = 0xFF // -> TributeConfig
)
// Event names for Tribute.
const (
VelocityAccountCreatedEvent = "VelocityAccountCreated"
VelocityUpdatedEvent = "VelocityUpdated"
TributeAssessedEvent = "TributeAssessed"
TributeCollectedEvent = "TributeCollected"
TributeWaivedEvent = "TributeWaived"
TributeAppealedEvent = "TributeAppealed"
IncentiveGrantedEvent = "IncentiveGranted"
IncentiveClaimedEvent = "IncentiveClaimed"
RedistributionExecutedEvent = "RedistributionExecuted"
ExemptionGrantedEvent = "ExemptionGranted"
ExemptionRevokedEvent = "ExemptionRevoked"
)
// Role constants for tribute administrators.
const (
RoleTributeAdmin uint64 = 23 // Can manage exemptions and appeals
)
// Various errors for Tribute.
var (
ErrTributeAccountNotFound = errors.New("velocity account not found")
ErrTributeAccountExists = errors.New("velocity account already exists")
ErrTributeAccountExempt = errors.New("account is exempt from tribute")
ErrTributeAccountSuspended = errors.New("velocity account is suspended")
ErrTributeNoVita = errors.New("owner must have an active Vita")
ErrTributeAssessmentNotFound = errors.New("tribute assessment not found")
ErrTributeAssessmentNotPending = errors.New("assessment is not pending")
ErrTributeAssessmentAlreadyPaid = errors.New("assessment already collected")
ErrTributeIncentiveNotFound = errors.New("incentive not found")
ErrTributeIncentiveClaimed = errors.New("incentive already claimed")
ErrTributeInsufficientBalance = errors.New("insufficient balance for tribute")
ErrTributeNotCommittee = errors.New("invalid committee signature")
ErrTributeNotOwner = errors.New("caller is not the owner")
ErrTributeNotAdmin = errors.New("caller is not an authorized tribute admin")
ErrTributePropertyRestricted = errors.New("property right is restricted")
ErrTributeInvalidAmount = errors.New("invalid amount")
ErrTributeInvalidReason = errors.New("invalid reason")
ErrTributeBelowExemption = errors.New("balance below exemption threshold")
ErrTributeNoHoarding = errors.New("no hoarding detected")
ErrTributeNothingToRedistribute = errors.New("nothing to redistribute")
)
var (
_ interop.Contract = (*Tribute)(nil)
_ dao.NativeContractCache = (*TributeCache)(nil)
)
// Copy implements NativeContractCache interface.
func (c *TributeCache) Copy() dao.NativeContractCache {
return &TributeCache{
accountCount: c.accountCount,
assessmentCount: c.assessmentCount,
incentiveCount: c.incentiveCount,
redistributionCount: c.redistributionCount,
}
}
// checkCommittee checks if the caller has committee authority.
func (t *Tribute) checkCommittee(ic *interop.Context) bool {
if t.RoleRegistry != nil {
return t.RoleRegistry.CheckCommittee(ic)
}
return t.Tutus.CheckCommittee(ic)
}
// checkTributeAdmin checks if the caller has tribute admin authority.
func (t *Tribute) checkTributeAdmin(ic *interop.Context) bool {
caller := ic.VM.GetCallingScriptHash()
if t.RoleRegistry != nil {
if t.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleTributeAdmin, ic.Block.Index) {
return true
}
}
// Committee members can also act as tribute admins
return t.checkCommittee(ic)
}
// checkPropertyRight checks if subject has property rights via Lex.
func (t *Tribute) checkPropertyRight(ic *interop.Context, subject util.Uint160) bool {
if t.Lex == nil {
return true // Allow if Lex not available
}
return t.Lex.HasRightInternal(ic.DAO, subject, state.RightProperty, ic.Block.Index)
}
// newTribute creates a new Tribute native contract.
func newTribute() *Tribute {
t := &Tribute{
ContractMD: *interop.NewContractMD(nativenames.Tribute, nativeids.Tribute),
}
defer t.BuildHFSpecificMD(t.ActiveIn())
// ===== Account Management =====
// createVelocityAccount - Create velocity tracking account for a Vita holder
desc := NewDescriptor("createVelocityAccount", smartcontract.BoolType,
manifest.NewParameter("owner", smartcontract.Hash160Type))
md := NewMethodAndPrice(t.createVelocityAccount, 1<<17, callflag.States|callflag.AllowNotify)
t.AddMethod(md, desc)
// getAccount - Get velocity account by owner
desc = NewDescriptor("getAccount", smartcontract.ArrayType,
manifest.NewParameter("owner", smartcontract.Hash160Type))
md = NewMethodAndPrice(t.getAccount, 1<<15, callflag.ReadStates)
t.AddMethod(md, desc)
// getAccountByVitaID - Get account by Vita ID
desc = NewDescriptor("getAccountByVitaID", smartcontract.ArrayType,
manifest.NewParameter("vitaID", smartcontract.IntegerType))
md = NewMethodAndPrice(t.getAccountByVitaID, 1<<15, callflag.ReadStates)
t.AddMethod(md, desc)
// recordTransaction - Record a transaction for velocity tracking
desc = NewDescriptor("recordTransaction", smartcontract.BoolType,
manifest.NewParameter("owner", smartcontract.Hash160Type),
manifest.NewParameter("amount", smartcontract.IntegerType),
manifest.NewParameter("isOutflow", smartcontract.BoolType))
md = NewMethodAndPrice(t.recordTransaction, 1<<16, callflag.States|callflag.AllowNotify)
t.AddMethod(md, desc)
// getVelocity - Get current velocity score for an owner
desc = NewDescriptor("getVelocity", smartcontract.IntegerType,
manifest.NewParameter("owner", smartcontract.Hash160Type))
md = NewMethodAndPrice(t.getVelocity, 1<<15, callflag.ReadStates)
t.AddMethod(md, desc)
// getHoardingLevel - Get current hoarding level for an owner
desc = NewDescriptor("getHoardingLevel", smartcontract.IntegerType,
manifest.NewParameter("owner", smartcontract.Hash160Type))
md = NewMethodAndPrice(t.getHoardingLevel, 1<<15, callflag.ReadStates)
t.AddMethod(md, desc)
// ===== Exemption Management =====
// grantExemption - Grant exemption from tribute (admin only)
desc = NewDescriptor("grantExemption", smartcontract.BoolType,
manifest.NewParameter("owner", smartcontract.Hash160Type),
manifest.NewParameter("reason", smartcontract.StringType))
md = NewMethodAndPrice(t.grantExemption, 1<<16, callflag.States|callflag.AllowNotify)
t.AddMethod(md, desc)
// revokeExemption - Revoke exemption from tribute (admin only)
desc = NewDescriptor("revokeExemption", smartcontract.BoolType,
manifest.NewParameter("owner", smartcontract.Hash160Type))
md = NewMethodAndPrice(t.revokeExemption, 1<<16, callflag.States|callflag.AllowNotify)
t.AddMethod(md, desc)
// isExempt - Check if account is exempt
desc = NewDescriptor("isExempt", smartcontract.BoolType,
manifest.NewParameter("owner", smartcontract.Hash160Type))
md = NewMethodAndPrice(t.isExempt, 1<<15, callflag.ReadStates)
t.AddMethod(md, desc)
// ===== Assessment Management =====
// assessTribute - Assess tribute for hoarding (called periodically or on-demand)
desc = NewDescriptor("assessTribute", smartcontract.IntegerType,
manifest.NewParameter("owner", smartcontract.Hash160Type))
md = NewMethodAndPrice(t.assessTribute, 1<<17, callflag.States|callflag.AllowNotify)
t.AddMethod(md, desc)
// getAssessment - Get assessment by ID
desc = NewDescriptor("getAssessment", smartcontract.ArrayType,
manifest.NewParameter("assessmentID", smartcontract.IntegerType))
md = NewMethodAndPrice(t.getAssessment, 1<<15, callflag.ReadStates)
t.AddMethod(md, desc)
// getPendingAssessment - Get pending assessment for an owner
desc = NewDescriptor("getPendingAssessment", smartcontract.ArrayType,
manifest.NewParameter("owner", smartcontract.Hash160Type))
md = NewMethodAndPrice(t.getPendingAssessment, 1<<15, callflag.ReadStates)
t.AddMethod(md, desc)
// collectTribute - Collect pending tribute
desc = NewDescriptor("collectTribute", smartcontract.BoolType,
manifest.NewParameter("assessmentID", smartcontract.IntegerType))
md = NewMethodAndPrice(t.collectTribute, 1<<17, callflag.States|callflag.AllowNotify)
t.AddMethod(md, desc)
// waiveTribute - Waive a tribute assessment (admin only)
desc = NewDescriptor("waiveTribute", smartcontract.BoolType,
manifest.NewParameter("assessmentID", smartcontract.IntegerType),
manifest.NewParameter("reason", smartcontract.StringType))
md = NewMethodAndPrice(t.waiveTribute, 1<<16, callflag.States|callflag.AllowNotify)
t.AddMethod(md, desc)
// appealTribute - Appeal a tribute assessment
desc = NewDescriptor("appealTribute", smartcontract.BoolType,
manifest.NewParameter("assessmentID", smartcontract.IntegerType),
manifest.NewParameter("reason", smartcontract.StringType))
md = NewMethodAndPrice(t.appealTribute, 1<<16, callflag.States|callflag.AllowNotify)
t.AddMethod(md, desc)
// ===== Incentive Management =====
// grantIncentive - Grant circulation incentive (system/admin)
desc = NewDescriptor("grantIncentive", smartcontract.IntegerType,
manifest.NewParameter("owner", smartcontract.Hash160Type),
manifest.NewParameter("incentiveType", smartcontract.IntegerType),
manifest.NewParameter("amount", smartcontract.IntegerType),
manifest.NewParameter("reason", smartcontract.StringType))
md = NewMethodAndPrice(t.grantIncentive, 1<<17, callflag.States|callflag.AllowNotify)
t.AddMethod(md, desc)
// claimIncentive - Claim a granted incentive
desc = NewDescriptor("claimIncentive", smartcontract.BoolType,
manifest.NewParameter("incentiveID", smartcontract.IntegerType))
md = NewMethodAndPrice(t.claimIncentive, 1<<17, callflag.States|callflag.AllowNotify)
t.AddMethod(md, desc)
// getIncentive - Get incentive by ID
desc = NewDescriptor("getIncentive", smartcontract.ArrayType,
manifest.NewParameter("incentiveID", smartcontract.IntegerType))
md = NewMethodAndPrice(t.getIncentive, 1<<15, callflag.ReadStates)
t.AddMethod(md, desc)
// getUnclaimedIncentives - Get count of unclaimed incentives for owner
desc = NewDescriptor("getUnclaimedIncentives", smartcontract.IntegerType,
manifest.NewParameter("owner", smartcontract.Hash160Type))
md = NewMethodAndPrice(t.getUnclaimedIncentives, 1<<15, callflag.ReadStates)
t.AddMethod(md, desc)
// ===== Redistribution =====
// redistribute - Execute wealth redistribution (committee only)
desc = NewDescriptor("redistribute", smartcontract.IntegerType,
manifest.NewParameter("targetCategory", smartcontract.StringType),
manifest.NewParameter("recipientCount", smartcontract.IntegerType))
md = NewMethodAndPrice(t.redistribute, 1<<18, callflag.States|callflag.AllowNotify)
t.AddMethod(md, desc)
// getRedistribution - Get redistribution record by ID
desc = NewDescriptor("getRedistribution", smartcontract.ArrayType,
manifest.NewParameter("redistID", smartcontract.IntegerType))
md = NewMethodAndPrice(t.getRedistribution, 1<<15, callflag.ReadStates)
t.AddMethod(md, desc)
// getTributePool - Get total tribute pool available for redistribution
desc = NewDescriptor("getTributePool", smartcontract.IntegerType)
md = NewMethodAndPrice(t.getTributePool, 1<<15, callflag.ReadStates)
t.AddMethod(md, desc)
// ===== Configuration & Stats =====
// getConfig - Get current configuration
desc = NewDescriptor("getConfig", smartcontract.ArrayType)
md = NewMethodAndPrice(t.getConfig, 1<<15, callflag.ReadStates)
t.AddMethod(md, desc)
// getTotalAccounts - Get total velocity accounts
desc = NewDescriptor("getTotalAccounts", smartcontract.IntegerType)
md = NewMethodAndPrice(t.getTotalAccounts, 1<<15, callflag.ReadStates)
t.AddMethod(md, desc)
// getTotalAssessments - Get total assessments
desc = NewDescriptor("getTotalAssessments", smartcontract.IntegerType)
md = NewMethodAndPrice(t.getTotalAssessments, 1<<15, callflag.ReadStates)
t.AddMethod(md, desc)
// getTotalIncentives - Get total incentives
desc = NewDescriptor("getTotalIncentives", smartcontract.IntegerType)
md = NewMethodAndPrice(t.getTotalIncentives, 1<<15, callflag.ReadStates)
t.AddMethod(md, desc)
// getTotalRedistributions - Get total redistributions
desc = NewDescriptor("getTotalRedistributions", smartcontract.IntegerType)
md = NewMethodAndPrice(t.getTotalRedistributions, 1<<15, callflag.ReadStates)
t.AddMethod(md, desc)
// ===== Events =====
// VelocityAccountCreated event
eDesc := NewEventDescriptor(VelocityAccountCreatedEvent,
manifest.NewParameter("vitaID", smartcontract.IntegerType),
manifest.NewParameter("owner", smartcontract.Hash160Type))
t.AddEvent(NewEvent(eDesc))
// VelocityUpdated event
eDesc = NewEventDescriptor(VelocityUpdatedEvent,
manifest.NewParameter("vitaID", smartcontract.IntegerType),
manifest.NewParameter("newVelocity", smartcontract.IntegerType),
manifest.NewParameter("hoardingLevel", smartcontract.IntegerType))
t.AddEvent(NewEvent(eDesc))
// TributeAssessed event
eDesc = NewEventDescriptor(TributeAssessedEvent,
manifest.NewParameter("assessmentID", smartcontract.IntegerType),
manifest.NewParameter("vitaID", smartcontract.IntegerType),
manifest.NewParameter("amount", smartcontract.IntegerType))
t.AddEvent(NewEvent(eDesc))
// TributeCollected event
eDesc = NewEventDescriptor(TributeCollectedEvent,
manifest.NewParameter("assessmentID", smartcontract.IntegerType),
manifest.NewParameter("amount", smartcontract.IntegerType))
t.AddEvent(NewEvent(eDesc))
// TributeWaived event
eDesc = NewEventDescriptor(TributeWaivedEvent,
manifest.NewParameter("assessmentID", smartcontract.IntegerType),
manifest.NewParameter("reason", smartcontract.StringType))
t.AddEvent(NewEvent(eDesc))
// TributeAppealed event
eDesc = NewEventDescriptor(TributeAppealedEvent,
manifest.NewParameter("assessmentID", smartcontract.IntegerType),
manifest.NewParameter("reason", smartcontract.StringType))
t.AddEvent(NewEvent(eDesc))
// IncentiveGranted event
eDesc = NewEventDescriptor(IncentiveGrantedEvent,
manifest.NewParameter("incentiveID", smartcontract.IntegerType),
manifest.NewParameter("vitaID", smartcontract.IntegerType),
manifest.NewParameter("amount", smartcontract.IntegerType))
t.AddEvent(NewEvent(eDesc))
// IncentiveClaimed event
eDesc = NewEventDescriptor(IncentiveClaimedEvent,
manifest.NewParameter("incentiveID", smartcontract.IntegerType),
manifest.NewParameter("amount", smartcontract.IntegerType))
t.AddEvent(NewEvent(eDesc))
// RedistributionExecuted event
eDesc = NewEventDescriptor(RedistributionExecutedEvent,
manifest.NewParameter("redistID", smartcontract.IntegerType),
manifest.NewParameter("totalAmount", smartcontract.IntegerType),
manifest.NewParameter("recipientCount", smartcontract.IntegerType))
t.AddEvent(NewEvent(eDesc))
// ExemptionGranted event
eDesc = NewEventDescriptor(ExemptionGrantedEvent,
manifest.NewParameter("vitaID", smartcontract.IntegerType),
manifest.NewParameter("reason", smartcontract.StringType))
t.AddEvent(NewEvent(eDesc))
// ExemptionRevoked event
eDesc = NewEventDescriptor(ExemptionRevokedEvent,
manifest.NewParameter("vitaID", smartcontract.IntegerType))
t.AddEvent(NewEvent(eDesc))
return t
}
// Metadata returns contract metadata.
func (t *Tribute) Metadata() *interop.ContractMD {
return &t.ContractMD
}
// Initialize initializes the Tribute contract.
func (t *Tribute) Initialize(ic *interop.Context, hf *config.Hardfork, newMD *interop.HFSpecificContractMD) error {
if hf != t.ActiveIn() {
return nil
}
// Initialize counters
t.setAccountCounter(ic.DAO, 0)
t.setAssessmentCounter(ic.DAO, 0)
t.setIncentiveCounter(ic.DAO, 0)
t.setRedistributionCounter(ic.DAO, 0)
t.setTributePool(ic.DAO, 0)
// Initialize config with defaults
// Velocity in basis points (0-10000 = 0%-100%)
cfg := &state.TributeConfig{
VelocityThresholdMild: 5000, // Below 50% velocity = mild hoarding
VelocityThresholdModerate: 3000, // Below 30% = moderate hoarding
VelocityThresholdSevere: 1500, // Below 15% = severe hoarding
VelocityThresholdExtreme: 500, // Below 5% = extreme hoarding
TributeRateMild: 100, // 1% tribute for mild hoarding
TributeRateModerate: 300, // 3% tribute for moderate hoarding
TributeRateSevere: 700, // 7% tribute for severe hoarding
TributeRateExtreme: 1500, // 15% tribute for extreme hoarding
IncentiveRateHigh: 50, // 0.5% incentive for high velocity
IncentiveRateVeryHigh: 150, // 1.5% incentive for very high velocity
StagnancyPeriod: 86400, // ~1 day (1-second blocks) before balance is stagnant
AssessmentPeriod: 604800, // ~7 days between assessments
GracePeriod: 259200, // ~3 days to pay tribute
MinBalanceForTribute: 1000000, // 1 VTS minimum to assess
ExemptionThreshold: 100000, // 0.1 VTS exempt
}
t.setConfig(ic.DAO, cfg)
// Initialize cache
cache := &TributeCache{
accountCount: 0,
assessmentCount: 0,
incentiveCount: 0,
redistributionCount: 0,
}
ic.DAO.SetCache(t.ID, cache)
return nil
}
// InitializeCache initializes the cache from storage.
func (t *Tribute) InitializeCache(_ interop.IsHardforkEnabled, blockHeight uint32, d *dao.Simple) error {
cache := &TributeCache{
accountCount: t.getAccountCounter(d),
assessmentCount: t.getAssessmentCounter(d),
incentiveCount: t.getIncentiveCounter(d),
redistributionCount: t.getRedistributionCounter(d),
}
d.SetCache(t.ID, cache)
return nil
}
// OnPersist is called before block is committed.
func (t *Tribute) OnPersist(ic *interop.Context) error {
return nil
}
// PostPersist is called after block is committed.
func (t *Tribute) PostPersist(ic *interop.Context) error {
return nil
}
// ActiveIn returns the hardfork at which this contract is activated.
func (t *Tribute) ActiveIn() *config.Hardfork {
return nil // Always active
}
// ===== Storage Helpers =====
func (t *Tribute) makeAccountKey(vitaID uint64) []byte {
key := make([]byte, 9)
key[0] = tributePrefixAccount
binary.BigEndian.PutUint64(key[1:], vitaID)
return key
}
func (t *Tribute) makeAccountByOwnerKey(owner util.Uint160) []byte {
key := make([]byte, 21)
key[0] = tributePrefixAccountByOwner
copy(key[1:], owner.BytesBE())
return key
}
func (t *Tribute) makeAssessmentKey(assessmentID uint64) []byte {
key := make([]byte, 9)
key[0] = tributePrefixAssessment
binary.BigEndian.PutUint64(key[1:], assessmentID)
return key
}
func (t *Tribute) makePendingAssessmentKey(vitaID uint64) []byte {
key := make([]byte, 9)
key[0] = tributePrefixPendingAssessment
binary.BigEndian.PutUint64(key[1:], vitaID)
return key
}
func (t *Tribute) makeIncentiveKey(incentiveID uint64) []byte {
key := make([]byte, 9)
key[0] = tributePrefixIncentive
binary.BigEndian.PutUint64(key[1:], incentiveID)
return key
}
func (t *Tribute) makeUnclaimedIncentiveKey(vitaID, incentiveID uint64) []byte {
key := make([]byte, 17)
key[0] = tributePrefixUnclaimedIncentive
binary.BigEndian.PutUint64(key[1:], vitaID)
binary.BigEndian.PutUint64(key[9:], incentiveID)
return key
}
func (t *Tribute) makeRedistributionKey(redistID uint64) []byte {
key := make([]byte, 9)
key[0] = tributePrefixRedistribution
binary.BigEndian.PutUint64(key[1:], redistID)
return key
}
// ===== Counter Helpers =====
func (t *Tribute) getAccountCounter(d *dao.Simple) uint64 {
si := d.GetStorageItem(t.ID, []byte{tributePrefixAccountCounter})
if si == nil {
return 0
}
return binary.BigEndian.Uint64(si)
}
func (t *Tribute) setAccountCounter(d *dao.Simple, count uint64) {
buf := make([]byte, 8)
binary.BigEndian.PutUint64(buf, count)
d.PutStorageItem(t.ID, []byte{tributePrefixAccountCounter}, buf)
}
func (t *Tribute) getAssessmentCounter(d *dao.Simple) uint64 {
si := d.GetStorageItem(t.ID, []byte{tributePrefixAssessmentCounter})
if si == nil {
return 0
}
return binary.BigEndian.Uint64(si)
}
func (t *Tribute) setAssessmentCounter(d *dao.Simple, count uint64) {
buf := make([]byte, 8)
binary.BigEndian.PutUint64(buf, count)
d.PutStorageItem(t.ID, []byte{tributePrefixAssessmentCounter}, buf)
}
func (t *Tribute) getIncentiveCounter(d *dao.Simple) uint64 {
si := d.GetStorageItem(t.ID, []byte{tributePrefixIncentiveCounter})
if si == nil {
return 0
}
return binary.BigEndian.Uint64(si)
}
func (t *Tribute) setIncentiveCounter(d *dao.Simple, count uint64) {
buf := make([]byte, 8)
binary.BigEndian.PutUint64(buf, count)
d.PutStorageItem(t.ID, []byte{tributePrefixIncentiveCounter}, buf)
}
func (t *Tribute) getRedistributionCounter(d *dao.Simple) uint64 {
si := d.GetStorageItem(t.ID, []byte{tributePrefixRedistributionCtr})
if si == nil {
return 0
}
return binary.BigEndian.Uint64(si)
}
func (t *Tribute) setRedistributionCounter(d *dao.Simple, count uint64) {
buf := make([]byte, 8)
binary.BigEndian.PutUint64(buf, count)
d.PutStorageItem(t.ID, []byte{tributePrefixRedistributionCtr}, buf)
}
func (t *Tribute) getTributePoolValue(d *dao.Simple) uint64 {
si := d.GetStorageItem(t.ID, []byte{tributePrefixTotalTributePool})
if si == nil {
return 0
}
return binary.BigEndian.Uint64(si)
}
func (t *Tribute) setTributePool(d *dao.Simple, amount uint64) {
buf := make([]byte, 8)
binary.BigEndian.PutUint64(buf, amount)
d.PutStorageItem(t.ID, []byte{tributePrefixTotalTributePool}, buf)
}
// ===== Account Storage =====
func (t *Tribute) getAccountInternal(d *dao.Simple, vitaID uint64) (*state.VelocityAccount, error) {
si := d.GetStorageItem(t.ID, t.makeAccountKey(vitaID))
if si == nil {
return nil, nil
}
acc := new(state.VelocityAccount)
item, err := stackitem.Deserialize(si)
if err != nil {
return nil, err
}
if err := acc.FromStackItem(item); err != nil {
return nil, err
}
return acc, nil
}
func (t *Tribute) putAccount(d *dao.Simple, acc *state.VelocityAccount) error {
data, err := stackitem.Serialize(acc.ToStackItem())
if err != nil {
return err
}
d.PutStorageItem(t.ID, t.makeAccountKey(acc.VitaID), data)
return nil
}
func (t *Tribute) getVitaIDByOwner(d *dao.Simple, owner util.Uint160) (uint64, bool) {
si := d.GetStorageItem(t.ID, t.makeAccountByOwnerKey(owner))
if si == nil {
return 0, false
}
return binary.BigEndian.Uint64(si), true
}
func (t *Tribute) setOwnerMapping(d *dao.Simple, owner util.Uint160, vitaID uint64) {
buf := make([]byte, 8)
binary.BigEndian.PutUint64(buf, vitaID)
d.PutStorageItem(t.ID, t.makeAccountByOwnerKey(owner), buf)
}
// ===== Assessment Storage =====
func (t *Tribute) getAssessmentInternal(d *dao.Simple, assessmentID uint64) (*state.TributeAssessment, error) {
si := d.GetStorageItem(t.ID, t.makeAssessmentKey(assessmentID))
if si == nil {
return nil, nil
}
assess := new(state.TributeAssessment)
item, err := stackitem.Deserialize(si)
if err != nil {
return nil, err
}
if err := assess.FromStackItem(item); err != nil {
return nil, err
}
return assess, nil
}
func (t *Tribute) putAssessment(d *dao.Simple, assess *state.TributeAssessment) error {
data, err := stackitem.Serialize(assess.ToStackItem())
if err != nil {
return err
}
d.PutStorageItem(t.ID, t.makeAssessmentKey(assess.ID), data)
return nil
}
func (t *Tribute) getPendingAssessmentID(d *dao.Simple, vitaID uint64) (uint64, bool) {
si := d.GetStorageItem(t.ID, t.makePendingAssessmentKey(vitaID))
if si == nil {
return 0, false
}
return binary.BigEndian.Uint64(si), true
}
func (t *Tribute) setPendingAssessmentID(d *dao.Simple, vitaID, assessmentID uint64) {
buf := make([]byte, 8)
binary.BigEndian.PutUint64(buf, assessmentID)
d.PutStorageItem(t.ID, t.makePendingAssessmentKey(vitaID), buf)
}
func (t *Tribute) deletePendingAssessment(d *dao.Simple, vitaID uint64) {
d.DeleteStorageItem(t.ID, t.makePendingAssessmentKey(vitaID))
}
// ===== Incentive Storage =====
func (t *Tribute) getIncentiveInternal(d *dao.Simple, incentiveID uint64) (*state.CirculationIncentive, error) {
si := d.GetStorageItem(t.ID, t.makeIncentiveKey(incentiveID))
if si == nil {
return nil, nil
}
inc := new(state.CirculationIncentive)
item, err := stackitem.Deserialize(si)
if err != nil {
return nil, err
}
if err := inc.FromStackItem(item); err != nil {
return nil, err
}
return inc, nil
}
func (t *Tribute) putIncentive(d *dao.Simple, inc *state.CirculationIncentive) error {
data, err := stackitem.Serialize(inc.ToStackItem())
if err != nil {
return err
}
d.PutStorageItem(t.ID, t.makeIncentiveKey(inc.ID), data)
return nil
}
func (t *Tribute) addUnclaimedIncentive(d *dao.Simple, vitaID, incentiveID uint64) {
d.PutStorageItem(t.ID, t.makeUnclaimedIncentiveKey(vitaID, incentiveID), []byte{1})
}
func (t *Tribute) removeUnclaimedIncentive(d *dao.Simple, vitaID, incentiveID uint64) {
d.DeleteStorageItem(t.ID, t.makeUnclaimedIncentiveKey(vitaID, incentiveID))
}
// ===== Redistribution Storage =====
func (t *Tribute) getRedistributionInternal(d *dao.Simple, redistID uint64) (*state.RedistributionRecord, error) {
si := d.GetStorageItem(t.ID, t.makeRedistributionKey(redistID))
if si == nil {
return nil, nil
}
rec := new(state.RedistributionRecord)
item, err := stackitem.Deserialize(si)
if err != nil {
return nil, err
}
if err := rec.FromStackItem(item); err != nil {
return nil, err
}
return rec, nil
}
func (t *Tribute) putRedistribution(d *dao.Simple, rec *state.RedistributionRecord) error {
data, err := stackitem.Serialize(rec.ToStackItem())
if err != nil {
return err
}
d.PutStorageItem(t.ID, t.makeRedistributionKey(rec.ID), data)
return nil
}
// ===== Config Storage =====
func (t *Tribute) getConfigInternal(d *dao.Simple) *state.TributeConfig {
si := d.GetStorageItem(t.ID, []byte{tributePrefixConfig})
if si == nil {
return nil
}
cfg := new(state.TributeConfig)
item, err := stackitem.Deserialize(si)
if err != nil {
return nil
}
if err := cfg.FromStackItem(item); err != nil {
return nil
}
return cfg
}
func (t *Tribute) setConfig(d *dao.Simple, cfg *state.TributeConfig) {
data, _ := stackitem.Serialize(cfg.ToStackItem())
d.PutStorageItem(t.ID, []byte{tributePrefixConfig}, data)
}
// ===== Internal Helpers =====
// calculateVelocity calculates velocity based on inflow/outflow ratio.
func (t *Tribute) calculateVelocity(totalInflow, totalOutflow uint64) uint64 {
if totalInflow == 0 {
if totalOutflow > 0 {
return 10000 // 100% velocity if only outflow
}
return 5000 // Default 50% if no activity
}
// Velocity = (outflow / inflow) * 10000 (basis points)
velocity := (totalOutflow * 10000) / totalInflow
if velocity > 10000 {
velocity = 10000
}
return velocity
}
// determineHoardingLevel determines hoarding level based on velocity.
func (t *Tribute) determineHoardingLevel(velocity uint64, cfg *state.TributeConfig) state.HoardingLevel {
if velocity >= cfg.VelocityThresholdMild {
return state.HoardingNone
}
if velocity >= cfg.VelocityThresholdModerate {
return state.HoardingMild
}
if velocity >= cfg.VelocityThresholdSevere {
return state.HoardingModerate
}
if velocity >= cfg.VelocityThresholdExtreme {
return state.HoardingSevere
}
return state.HoardingExtreme
}
// getTributeRate returns the tribute rate for a hoarding level.
func (t *Tribute) getTributeRate(level state.HoardingLevel, cfg *state.TributeConfig) uint64 {
switch level {
case state.HoardingMild:
return cfg.TributeRateMild
case state.HoardingModerate:
return cfg.TributeRateModerate
case state.HoardingSevere:
return cfg.TributeRateSevere
case state.HoardingExtreme:
return cfg.TributeRateExtreme
default:
return 0
}
}
// ===== Contract Methods =====
// createVelocityAccount creates a velocity tracking account for a Vita holder.
func (t *Tribute) createVelocityAccount(ic *interop.Context, args []stackitem.Item) stackitem.Item {
owner := toUint160(args[0])
// Verify owner has active Vita
if t.Vita == nil {
panic(ErrTributeNoVita)
}
vita, err := t.Vita.GetTokenByOwner(ic.DAO, owner)
if err != nil || vita == nil {
panic(ErrTributeNoVita)
}
if vita.Status != state.TokenStatusActive {
panic(ErrTributeNoVita)
}
vitaID := vita.TokenID
// Check if account already exists
existing, _ := t.getAccountInternal(ic.DAO, vitaID)
if existing != nil {
panic(ErrTributeAccountExists)
}
// Create account
blockHeight := ic.Block.Index
acc := &state.VelocityAccount{
VitaID: vitaID,
Owner: owner,
CurrentVelocity: 5000, // Default 50% velocity
AverageVelocity: 5000,
LastActivityBlock: blockHeight,
TotalInflow: 0,
TotalOutflow: 0,
StagnantBalance: 0,
HoardingLevel: state.HoardingNone,
ExemptionReason: "",
TotalTributePaid: 0,
TotalIncentivesRcvd: 0,
Status: state.VelocityAccountActive,
CreatedAt: blockHeight,
UpdatedAt: blockHeight,
}
if err := t.putAccount(ic.DAO, acc); err != nil {
panic(err)
}
t.setOwnerMapping(ic.DAO, owner, vitaID)
// Update counter
cache := ic.DAO.GetRWCache(t.ID).(*TributeCache)
cache.accountCount++
t.setAccountCounter(ic.DAO, cache.accountCount)
// Emit event
ic.AddNotification(t.Hash, VelocityAccountCreatedEvent, stackitem.NewArray([]stackitem.Item{
stackitem.NewBigInteger(new(big.Int).SetUint64(vitaID)),
stackitem.NewByteArray(owner.BytesBE()),
}))
return stackitem.NewBool(true)
}
// getAccount returns velocity account by owner.
func (t *Tribute) getAccount(ic *interop.Context, args []stackitem.Item) stackitem.Item {
owner := toUint160(args[0])
vitaID, found := t.getVitaIDByOwner(ic.DAO, owner)
if !found {
return stackitem.Null{}
}
acc, err := t.getAccountInternal(ic.DAO, vitaID)
if err != nil || acc == nil {
return stackitem.Null{}
}
return acc.ToStackItem()
}
// getAccountByVitaID returns velocity account by Vita ID.
func (t *Tribute) getAccountByVitaID(ic *interop.Context, args []stackitem.Item) stackitem.Item {
vitaID := toBigInt(args[0]).Uint64()
acc, err := t.getAccountInternal(ic.DAO, vitaID)
if err != nil || acc == nil {
return stackitem.Null{}
}
return acc.ToStackItem()
}
// recordTransaction records a transaction for velocity tracking.
func (t *Tribute) recordTransaction(ic *interop.Context, args []stackitem.Item) stackitem.Item {
owner := toUint160(args[0])
amount := toBigInt(args[1]).Uint64()
isOutflow := toBool(args[2])
if amount == 0 {
panic(ErrTributeInvalidAmount)
}
vitaID, found := t.getVitaIDByOwner(ic.DAO, owner)
if !found {
panic(ErrTributeAccountNotFound)
}
acc, err := t.getAccountInternal(ic.DAO, vitaID)
if err != nil || acc == nil {
panic(ErrTributeAccountNotFound)
}
if acc.Status == state.VelocityAccountSuspended {
panic(ErrTributeAccountSuspended)
}
// Update transaction totals
if isOutflow {
acc.TotalOutflow += amount
} else {
acc.TotalInflow += amount
}
// Recalculate velocity
acc.CurrentVelocity = t.calculateVelocity(acc.TotalInflow, acc.TotalOutflow)
// Update rolling average (simple average for now)
acc.AverageVelocity = (acc.AverageVelocity + acc.CurrentVelocity) / 2
// Determine hoarding level
cfg := t.getConfigInternal(ic.DAO)
acc.HoardingLevel = t.determineHoardingLevel(acc.CurrentVelocity, cfg)
// Update timestamp
acc.LastActivityBlock = ic.Block.Index
acc.UpdatedAt = ic.Block.Index
if err := t.putAccount(ic.DAO, acc); err != nil {
panic(err)
}
// Emit event
ic.AddNotification(t.Hash, VelocityUpdatedEvent, stackitem.NewArray([]stackitem.Item{
stackitem.NewBigInteger(new(big.Int).SetUint64(vitaID)),
stackitem.NewBigInteger(new(big.Int).SetUint64(acc.CurrentVelocity)),
stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(acc.HoardingLevel))),
}))
return stackitem.NewBool(true)
}
// getVelocity returns current velocity score for an owner.
func (t *Tribute) getVelocity(ic *interop.Context, args []stackitem.Item) stackitem.Item {
owner := toUint160(args[0])
vitaID, found := t.getVitaIDByOwner(ic.DAO, owner)
if !found {
return stackitem.NewBigInteger(big.NewInt(0))
}
acc, err := t.getAccountInternal(ic.DAO, vitaID)
if err != nil || acc == nil {
return stackitem.NewBigInteger(big.NewInt(0))
}
return stackitem.NewBigInteger(new(big.Int).SetUint64(acc.CurrentVelocity))
}
// getHoardingLevel returns current hoarding level for an owner.
func (t *Tribute) getHoardingLevel(ic *interop.Context, args []stackitem.Item) stackitem.Item {
owner := toUint160(args[0])
vitaID, found := t.getVitaIDByOwner(ic.DAO, owner)
if !found {
return stackitem.NewBigInteger(big.NewInt(0))
}
acc, err := t.getAccountInternal(ic.DAO, vitaID)
if err != nil || acc == nil {
return stackitem.NewBigInteger(big.NewInt(0))
}
return stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(acc.HoardingLevel)))
}
// grantExemption grants exemption from tribute (admin only).
func (t *Tribute) grantExemption(ic *interop.Context, args []stackitem.Item) stackitem.Item {
owner := toUint160(args[0])
reason := toString(args[1])
if !t.checkTributeAdmin(ic) {
panic(ErrTributeNotAdmin)
}
if len(reason) == 0 || len(reason) > 256 {
panic(ErrTributeInvalidReason)
}
vitaID, found := t.getVitaIDByOwner(ic.DAO, owner)
if !found {
panic(ErrTributeAccountNotFound)
}
acc, err := t.getAccountInternal(ic.DAO, vitaID)
if err != nil || acc == nil {
panic(ErrTributeAccountNotFound)
}
acc.Status = state.VelocityAccountExempt
acc.ExemptionReason = reason
acc.UpdatedAt = ic.Block.Index
if err := t.putAccount(ic.DAO, acc); err != nil {
panic(err)
}
// Emit event
ic.AddNotification(t.Hash, ExemptionGrantedEvent, stackitem.NewArray([]stackitem.Item{
stackitem.NewBigInteger(new(big.Int).SetUint64(vitaID)),
stackitem.NewByteArray([]byte(reason)),
}))
return stackitem.NewBool(true)
}
// revokeExemption revokes exemption from tribute (admin only).
func (t *Tribute) revokeExemption(ic *interop.Context, args []stackitem.Item) stackitem.Item {
owner := toUint160(args[0])
if !t.checkTributeAdmin(ic) {
panic(ErrTributeNotAdmin)
}
vitaID, found := t.getVitaIDByOwner(ic.DAO, owner)
if !found {
panic(ErrTributeAccountNotFound)
}
acc, err := t.getAccountInternal(ic.DAO, vitaID)
if err != nil || acc == nil {
panic(ErrTributeAccountNotFound)
}
if acc.Status != state.VelocityAccountExempt {
panic(ErrTributeAccountNotFound)
}
acc.Status = state.VelocityAccountActive
acc.ExemptionReason = ""
acc.UpdatedAt = ic.Block.Index
if err := t.putAccount(ic.DAO, acc); err != nil {
panic(err)
}
// Emit event
ic.AddNotification(t.Hash, ExemptionRevokedEvent, stackitem.NewArray([]stackitem.Item{
stackitem.NewBigInteger(new(big.Int).SetUint64(vitaID)),
}))
return stackitem.NewBool(true)
}
// isExempt checks if account is exempt from tribute.
func (t *Tribute) isExempt(ic *interop.Context, args []stackitem.Item) stackitem.Item {
owner := toUint160(args[0])
vitaID, found := t.getVitaIDByOwner(ic.DAO, owner)
if !found {
return stackitem.NewBool(false)
}
acc, err := t.getAccountInternal(ic.DAO, vitaID)
if err != nil || acc == nil {
return stackitem.NewBool(false)
}
return stackitem.NewBool(acc.Status == state.VelocityAccountExempt)
}
// assessTribute assesses tribute for hoarding.
func (t *Tribute) assessTribute(ic *interop.Context, args []stackitem.Item) stackitem.Item {
owner := toUint160(args[0])
vitaID, found := t.getVitaIDByOwner(ic.DAO, owner)
if !found {
panic(ErrTributeAccountNotFound)
}
acc, err := t.getAccountInternal(ic.DAO, vitaID)
if err != nil || acc == nil {
panic(ErrTributeAccountNotFound)
}
if acc.Status == state.VelocityAccountExempt {
panic(ErrTributeAccountExempt)
}
if acc.Status == state.VelocityAccountSuspended {
panic(ErrTributeAccountSuspended)
}
// Check if there's already a pending assessment
if existingID, exists := t.getPendingAssessmentID(ic.DAO, vitaID); exists {
existing, _ := t.getAssessmentInternal(ic.DAO, existingID)
if existing != nil && existing.Status == state.AssessmentPending {
panic("pending assessment already exists")
}
}
// Check hoarding level
if acc.HoardingLevel == state.HoardingNone {
panic(ErrTributeNoHoarding)
}
cfg := t.getConfigInternal(ic.DAO)
// Get stagnant balance (simplified: use total inflow - outflow as approximation)
stagnantBalance := uint64(0)
if acc.TotalInflow > acc.TotalOutflow {
stagnantBalance = acc.TotalInflow - acc.TotalOutflow
}
if stagnantBalance < cfg.MinBalanceForTribute {
panic(ErrTributeBelowExemption)
}
// Calculate tribute
tributeRate := t.getTributeRate(acc.HoardingLevel, cfg)
tributeAmount := (stagnantBalance * tributeRate) / 10000
// Create assessment
cache := ic.DAO.GetRWCache(t.ID).(*TributeCache)
assessmentID := cache.assessmentCount
cache.assessmentCount++
t.setAssessmentCounter(ic.DAO, cache.assessmentCount)
blockHeight := ic.Block.Index
assessment := &state.TributeAssessment{
ID: assessmentID,
VitaID: vitaID,
Owner: owner,
AssessmentBlock: blockHeight,
HoardingLevel: acc.HoardingLevel,
StagnantAmount: stagnantBalance,
TributeRate: tributeRate,
TributeAmount: tributeAmount,
DueBlock: blockHeight + cfg.GracePeriod,
CollectedBlock: 0,
Status: state.AssessmentPending,
AppealReason: "",
}
if err := t.putAssessment(ic.DAO, assessment); err != nil {
panic(err)
}
t.setPendingAssessmentID(ic.DAO, vitaID, assessmentID)
// Emit event
ic.AddNotification(t.Hash, TributeAssessedEvent, stackitem.NewArray([]stackitem.Item{
stackitem.NewBigInteger(new(big.Int).SetUint64(assessmentID)),
stackitem.NewBigInteger(new(big.Int).SetUint64(vitaID)),
stackitem.NewBigInteger(new(big.Int).SetUint64(tributeAmount)),
}))
return stackitem.NewBigInteger(new(big.Int).SetUint64(assessmentID))
}
// getAssessment returns assessment by ID.
func (t *Tribute) getAssessment(ic *interop.Context, args []stackitem.Item) stackitem.Item {
assessmentID := toBigInt(args[0]).Uint64()
assess, err := t.getAssessmentInternal(ic.DAO, assessmentID)
if err != nil || assess == nil {
return stackitem.Null{}
}
return assess.ToStackItem()
}
// getPendingAssessment returns pending assessment for an owner.
func (t *Tribute) getPendingAssessment(ic *interop.Context, args []stackitem.Item) stackitem.Item {
owner := toUint160(args[0])
vitaID, found := t.getVitaIDByOwner(ic.DAO, owner)
if !found {
return stackitem.Null{}
}
assessmentID, exists := t.getPendingAssessmentID(ic.DAO, vitaID)
if !exists {
return stackitem.Null{}
}
assess, err := t.getAssessmentInternal(ic.DAO, assessmentID)
if err != nil || assess == nil {
return stackitem.Null{}
}
if assess.Status != state.AssessmentPending {
return stackitem.Null{}
}
return assess.ToStackItem()
}
// collectTribute collects pending tribute.
func (t *Tribute) collectTribute(ic *interop.Context, args []stackitem.Item) stackitem.Item {
assessmentID := toBigInt(args[0]).Uint64()
assess, err := t.getAssessmentInternal(ic.DAO, assessmentID)
if err != nil || assess == nil {
panic(ErrTributeAssessmentNotFound)
}
if assess.Status != state.AssessmentPending {
panic(ErrTributeAssessmentNotPending)
}
// Check property right before taking tribute
if !t.checkPropertyRight(ic, assess.Owner) {
panic(ErrTributePropertyRestricted)
}
// Mark as collected
assess.Status = state.AssessmentCollected
assess.CollectedBlock = ic.Block.Index
if err := t.putAssessment(ic.DAO, assess); err != nil {
panic(err)
}
t.deletePendingAssessment(ic.DAO, assess.VitaID)
// Update account
acc, _ := t.getAccountInternal(ic.DAO, assess.VitaID)
if acc != nil {
acc.TotalTributePaid += assess.TributeAmount
acc.UpdatedAt = ic.Block.Index
t.putAccount(ic.DAO, acc)
}
// Add to tribute pool for redistribution
pool := t.getTributePoolValue(ic.DAO)
pool += assess.TributeAmount
t.setTributePool(ic.DAO, pool)
// Emit event
ic.AddNotification(t.Hash, TributeCollectedEvent, stackitem.NewArray([]stackitem.Item{
stackitem.NewBigInteger(new(big.Int).SetUint64(assessmentID)),
stackitem.NewBigInteger(new(big.Int).SetUint64(assess.TributeAmount)),
}))
return stackitem.NewBool(true)
}
// waiveTribute waives a tribute assessment (admin only).
func (t *Tribute) waiveTribute(ic *interop.Context, args []stackitem.Item) stackitem.Item {
assessmentID := toBigInt(args[0]).Uint64()
reason := toString(args[1])
if !t.checkTributeAdmin(ic) {
panic(ErrTributeNotAdmin)
}
if len(reason) == 0 || len(reason) > 256 {
panic(ErrTributeInvalidReason)
}
assess, err := t.getAssessmentInternal(ic.DAO, assessmentID)
if err != nil || assess == nil {
panic(ErrTributeAssessmentNotFound)
}
if assess.Status != state.AssessmentPending && assess.Status != state.AssessmentAppealed {
panic(ErrTributeAssessmentNotPending)
}
assess.Status = state.AssessmentWaived
assess.AppealReason = reason
if err := t.putAssessment(ic.DAO, assess); err != nil {
panic(err)
}
t.deletePendingAssessment(ic.DAO, assess.VitaID)
// Emit event
ic.AddNotification(t.Hash, TributeWaivedEvent, stackitem.NewArray([]stackitem.Item{
stackitem.NewBigInteger(new(big.Int).SetUint64(assessmentID)),
stackitem.NewByteArray([]byte(reason)),
}))
return stackitem.NewBool(true)
}
// appealTribute appeals a tribute assessment.
func (t *Tribute) appealTribute(ic *interop.Context, args []stackitem.Item) stackitem.Item {
assessmentID := toBigInt(args[0]).Uint64()
reason := toString(args[1])
if len(reason) == 0 || len(reason) > 512 {
panic(ErrTributeInvalidReason)
}
assess, err := t.getAssessmentInternal(ic.DAO, assessmentID)
if err != nil || assess == nil {
panic(ErrTributeAssessmentNotFound)
}
// Check caller is owner
ok, err := checkWitness(ic, assess.Owner)
if err != nil || !ok {
panic(ErrTributeNotOwner)
}
if assess.Status != state.AssessmentPending {
panic(ErrTributeAssessmentNotPending)
}
assess.Status = state.AssessmentAppealed
assess.AppealReason = reason
if err := t.putAssessment(ic.DAO, assess); err != nil {
panic(err)
}
// Emit event
ic.AddNotification(t.Hash, TributeAppealedEvent, stackitem.NewArray([]stackitem.Item{
stackitem.NewBigInteger(new(big.Int).SetUint64(assessmentID)),
stackitem.NewByteArray([]byte(reason)),
}))
return stackitem.NewBool(true)
}
// grantIncentive grants circulation incentive (system/admin).
func (t *Tribute) grantIncentive(ic *interop.Context, args []stackitem.Item) stackitem.Item {
owner := toUint160(args[0])
incentiveType := state.IncentiveType(toBigInt(args[1]).Uint64())
amount := toBigInt(args[2]).Uint64()
reason := toString(args[3])
if !t.checkTributeAdmin(ic) {
panic(ErrTributeNotAdmin)
}
if amount == 0 {
panic(ErrTributeInvalidAmount)
}
if len(reason) == 0 || len(reason) > 256 {
panic(ErrTributeInvalidReason)
}
vitaID, found := t.getVitaIDByOwner(ic.DAO, owner)
if !found {
panic(ErrTributeAccountNotFound)
}
acc, err := t.getAccountInternal(ic.DAO, vitaID)
if err != nil || acc == nil {
panic(ErrTributeAccountNotFound)
}
// Create incentive
cache := ic.DAO.GetRWCache(t.ID).(*TributeCache)
incentiveID := cache.incentiveCount
cache.incentiveCount++
t.setIncentiveCounter(ic.DAO, cache.incentiveCount)
blockHeight := ic.Block.Index
incentive := &state.CirculationIncentive{
ID: incentiveID,
VitaID: vitaID,
Recipient: owner,
IncentiveType: incentiveType,
Amount: amount,
Reason: reason,
VelocityScore: acc.CurrentVelocity,
GrantedBlock: blockHeight,
ClaimedBlock: 0,
Claimed: false,
}
if err := t.putIncentive(ic.DAO, incentive); err != nil {
panic(err)
}
t.addUnclaimedIncentive(ic.DAO, vitaID, incentiveID)
// Emit event
ic.AddNotification(t.Hash, IncentiveGrantedEvent, stackitem.NewArray([]stackitem.Item{
stackitem.NewBigInteger(new(big.Int).SetUint64(incentiveID)),
stackitem.NewBigInteger(new(big.Int).SetUint64(vitaID)),
stackitem.NewBigInteger(new(big.Int).SetUint64(amount)),
}))
return stackitem.NewBigInteger(new(big.Int).SetUint64(incentiveID))
}
// claimIncentive claims a granted incentive.
func (t *Tribute) claimIncentive(ic *interop.Context, args []stackitem.Item) stackitem.Item {
incentiveID := toBigInt(args[0]).Uint64()
incentive, err := t.getIncentiveInternal(ic.DAO, incentiveID)
if err != nil || incentive == nil {
panic(ErrTributeIncentiveNotFound)
}
// Check caller is recipient
ok, err := checkWitness(ic, incentive.Recipient)
if err != nil || !ok {
panic(ErrTributeNotOwner)
}
if incentive.Claimed {
panic(ErrTributeIncentiveClaimed)
}
// Mark as claimed
incentive.Claimed = true
incentive.ClaimedBlock = ic.Block.Index
if err := t.putIncentive(ic.DAO, incentive); err != nil {
panic(err)
}
t.removeUnclaimedIncentive(ic.DAO, incentive.VitaID, incentiveID)
// Update account
acc, _ := t.getAccountInternal(ic.DAO, incentive.VitaID)
if acc != nil {
acc.TotalIncentivesRcvd += incentive.Amount
acc.UpdatedAt = ic.Block.Index
t.putAccount(ic.DAO, acc)
}
// Emit event
ic.AddNotification(t.Hash, IncentiveClaimedEvent, stackitem.NewArray([]stackitem.Item{
stackitem.NewBigInteger(new(big.Int).SetUint64(incentiveID)),
stackitem.NewBigInteger(new(big.Int).SetUint64(incentive.Amount)),
}))
return stackitem.NewBool(true)
}
// getIncentive returns incentive by ID.
func (t *Tribute) getIncentive(ic *interop.Context, args []stackitem.Item) stackitem.Item {
incentiveID := toBigInt(args[0]).Uint64()
incentive, err := t.getIncentiveInternal(ic.DAO, incentiveID)
if err != nil || incentive == nil {
return stackitem.Null{}
}
return incentive.ToStackItem()
}
// getUnclaimedIncentives returns count of unclaimed incentives for owner.
func (t *Tribute) getUnclaimedIncentives(ic *interop.Context, args []stackitem.Item) stackitem.Item {
owner := toUint160(args[0])
vitaID, found := t.getVitaIDByOwner(ic.DAO, owner)
if !found {
return stackitem.NewBigInteger(big.NewInt(0))
}
// Count unclaimed incentives by iterating storage
// This is a simplified version - in production, we'd track this counter
count := uint64(0)
prefix := make([]byte, 9)
prefix[0] = tributePrefixUnclaimedIncentive
binary.BigEndian.PutUint64(prefix[1:], vitaID)
ic.DAO.Seek(t.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool {
count++
return true
})
return stackitem.NewBigInteger(new(big.Int).SetUint64(count))
}
// redistribute executes wealth redistribution (committee only).
func (t *Tribute) redistribute(ic *interop.Context, args []stackitem.Item) stackitem.Item {
targetCategory := toString(args[0])
recipientCount := toBigInt(args[1]).Uint64()
if !t.checkCommittee(ic) {
panic(ErrTributeNotCommittee)
}
pool := t.getTributePoolValue(ic.DAO)
if pool == 0 {
panic(ErrTributeNothingToRedistribute)
}
if recipientCount == 0 {
panic(ErrTributeInvalidAmount)
}
perCapita := pool / recipientCount
// Create redistribution record
cache := ic.DAO.GetRWCache(t.ID).(*TributeCache)
redistID := cache.redistributionCount
cache.redistributionCount++
t.setRedistributionCounter(ic.DAO, cache.redistributionCount)
record := &state.RedistributionRecord{
ID: redistID,
SourceAssessment: 0, // Could link to specific assessment if needed
TotalAmount: pool,
RecipientCount: recipientCount,
PerCapitaAmount: perCapita,
RedistBlock: ic.Block.Index,
TargetCategory: targetCategory,
}
if err := t.putRedistribution(ic.DAO, record); err != nil {
panic(err)
}
// Clear the pool (actual redistribution would happen via VTS transfers)
t.setTributePool(ic.DAO, 0)
// Emit event
ic.AddNotification(t.Hash, RedistributionExecutedEvent, stackitem.NewArray([]stackitem.Item{
stackitem.NewBigInteger(new(big.Int).SetUint64(redistID)),
stackitem.NewBigInteger(new(big.Int).SetUint64(pool)),
stackitem.NewBigInteger(new(big.Int).SetUint64(recipientCount)),
}))
return stackitem.NewBigInteger(new(big.Int).SetUint64(redistID))
}
// getRedistribution returns redistribution record by ID.
func (t *Tribute) getRedistribution(ic *interop.Context, args []stackitem.Item) stackitem.Item {
redistID := toBigInt(args[0]).Uint64()
rec, err := t.getRedistributionInternal(ic.DAO, redistID)
if err != nil || rec == nil {
return stackitem.Null{}
}
return rec.ToStackItem()
}
// getTributePool returns total tribute pool available for redistribution.
func (t *Tribute) getTributePool(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
return stackitem.NewBigInteger(new(big.Int).SetUint64(t.getTributePoolValue(ic.DAO)))
}
// getConfig returns current configuration.
func (t *Tribute) getConfig(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
cfg := t.getConfigInternal(ic.DAO)
if cfg == nil {
return stackitem.Null{}
}
return cfg.ToStackItem()
}
// getTotalAccounts returns total velocity accounts.
func (t *Tribute) getTotalAccounts(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
return stackitem.NewBigInteger(new(big.Int).SetUint64(t.getAccountCounter(ic.DAO)))
}
// getTotalAssessments returns total assessments.
func (t *Tribute) getTotalAssessments(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
return stackitem.NewBigInteger(new(big.Int).SetUint64(t.getAssessmentCounter(ic.DAO)))
}
// getTotalIncentives returns total incentives.
func (t *Tribute) getTotalIncentives(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
return stackitem.NewBigInteger(new(big.Int).SetUint64(t.getIncentiveCounter(ic.DAO)))
}
// getTotalRedistributions returns total redistributions.
func (t *Tribute) getTotalRedistributions(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
return stackitem.NewBigInteger(new(big.Int).SetUint64(t.getRedistributionCounter(ic.DAO)))
}
// ===== Public Internal Methods =====
// GetAccountByOwner returns velocity account by owner (internal API).
func (t *Tribute) GetAccountByOwner(d *dao.Simple, owner util.Uint160) (*state.VelocityAccount, error) {
vitaID, found := t.getVitaIDByOwner(d, owner)
if !found {
return nil, ErrTributeAccountNotFound
}
return t.getAccountInternal(d, vitaID)
}
// GetVelocity returns velocity score for an owner (internal API).
func (t *Tribute) GetVelocity(d *dao.Simple, owner util.Uint160) uint64 {
vitaID, found := t.getVitaIDByOwner(d, owner)
if !found {
return 5000 // Default 50%
}
acc, err := t.getAccountInternal(d, vitaID)
if err != nil || acc == nil {
return 5000
}
return acc.CurrentVelocity
}
// IsHoarding returns true if owner is hoarding (internal API).
func (t *Tribute) IsHoarding(d *dao.Simple, owner util.Uint160) bool {
vitaID, found := t.getVitaIDByOwner(d, owner)
if !found {
return false
}
acc, err := t.getAccountInternal(d, vitaID)
if err != nil || acc == nil {
return false
}
return acc.HoardingLevel != state.HoardingNone
}
// Address returns the contract address.
func (t *Tribute) Address() util.Uint160 {
return t.Hash
}