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 }