1648 lines
52 KiB
Go
1648 lines
52 KiB
Go
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
|
|
NEO INEO
|
|
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.NEO.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
|
|
}
|