diff --git a/pkg/core/native/native_test/tribute_test.go b/pkg/core/native/native_test/tribute_test.go new file mode 100644 index 0000000..cc842f1 --- /dev/null +++ b/pkg/core/native/native_test/tribute_test.go @@ -0,0 +1,293 @@ +package native_test + +import ( + "testing" + + "github.com/nspcc-dev/neo-go/pkg/crypto/hash" + "github.com/stretchr/testify/require" + "github.com/tutus-one/tutus-chain/pkg/core/native/nativenames" + "github.com/tutus-one/tutus-chain/pkg/neotest" + "github.com/tutus-one/tutus-chain/pkg/vm/stackitem" +) + +func newTributeClient(t *testing.T) *neotest.ContractInvoker { + return newNativeClient(t, nativenames.Tribute) +} + +// TestTribute_GetConfig tests the getConfig method. +func TestTribute_GetConfig(t *testing.T) { + c := newTributeClient(t) + + // Get default config + c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) { + require.Equal(t, 1, len(stack)) + arr, ok := stack[0].Value().([]stackitem.Item) + require.True(t, ok, "expected array result") + require.GreaterOrEqual(t, len(arr), 15) // TributeConfig has 15 fields + }, "getConfig") +} + +// TestTribute_GetTotalAccounts tests the getTotalAccounts method. +func TestTribute_GetTotalAccounts(t *testing.T) { + c := newTributeClient(t) + + // Initially should be 0 + c.Invoke(t, 0, "getTotalAccounts") +} + +// TestTribute_GetTotalAssessments tests the getTotalAssessments method. +func TestTribute_GetTotalAssessments(t *testing.T) { + c := newTributeClient(t) + + // Initially should be 0 + c.Invoke(t, 0, "getTotalAssessments") +} + +// TestTribute_GetTotalIncentives tests the getTotalIncentives method. +func TestTribute_GetTotalIncentives(t *testing.T) { + c := newTributeClient(t) + + // Initially should be 0 + c.Invoke(t, 0, "getTotalIncentives") +} + +// TestTribute_GetTotalRedistributions tests the getTotalRedistributions method. +func TestTribute_GetTotalRedistributions(t *testing.T) { + c := newTributeClient(t) + + // Initially should be 0 + c.Invoke(t, 0, "getTotalRedistributions") +} + +// TestTribute_GetTributePool tests the getTributePool method. +func TestTribute_GetTributePool(t *testing.T) { + c := newTributeClient(t) + + // Initially should be 0 + c.Invoke(t, 0, "getTributePool") +} + +// TestTribute_GetAccount_NonExistent tests getting a non-existent account. +func TestTribute_GetAccount_NonExistent(t *testing.T) { + c := newTributeClient(t) + e := c.Executor + + acc := e.NewAccount(t) + + // Non-existent account should return null + c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) { + require.Equal(t, 1, len(stack)) + require.Nil(t, stack[0].Value(), "expected null for non-existent account") + }, "getAccount", acc.ScriptHash()) +} + +// TestTribute_GetVelocity_NonExistent tests getting velocity for non-existent account. +func TestTribute_GetVelocity_NonExistent(t *testing.T) { + c := newTributeClient(t) + e := c.Executor + + acc := e.NewAccount(t) + + // Non-existent account should return 0 velocity + c.Invoke(t, 0, "getVelocity", acc.ScriptHash()) +} + +// TestTribute_GetHoardingLevel_NonExistent tests getting hoarding level for non-existent account. +func TestTribute_GetHoardingLevel_NonExistent(t *testing.T) { + c := newTributeClient(t) + e := c.Executor + + acc := e.NewAccount(t) + + // Non-existent account should return 0 (HoardingNone) + c.Invoke(t, 0, "getHoardingLevel", acc.ScriptHash()) +} + +// TestTribute_IsExempt_NonExistent tests checking exemption for non-existent account. +func TestTribute_IsExempt_NonExistent(t *testing.T) { + c := newTributeClient(t) + e := c.Executor + + acc := e.NewAccount(t) + + // Non-existent account should return false + c.Invoke(t, false, "isExempt", acc.ScriptHash()) +} + +// TestTribute_GetAssessment_NonExistent tests getting a non-existent assessment. +func TestTribute_GetAssessment_NonExistent(t *testing.T) { + c := newTributeClient(t) + + // Non-existent assessment should return null + c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) { + require.Equal(t, 1, len(stack)) + require.Nil(t, stack[0].Value(), "expected null for non-existent assessment") + }, "getAssessment", int64(999)) +} + +// TestTribute_GetIncentive_NonExistent tests getting a non-existent incentive. +func TestTribute_GetIncentive_NonExistent(t *testing.T) { + c := newTributeClient(t) + + // Non-existent incentive should return null + c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) { + require.Equal(t, 1, len(stack)) + require.Nil(t, stack[0].Value(), "expected null for non-existent incentive") + }, "getIncentive", int64(999)) +} + +// TestTribute_GetRedistribution_NonExistent tests getting a non-existent redistribution. +func TestTribute_GetRedistribution_NonExistent(t *testing.T) { + c := newTributeClient(t) + + // Non-existent redistribution should return null + c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) { + require.Equal(t, 1, len(stack)) + require.Nil(t, stack[0].Value(), "expected null for non-existent redistribution") + }, "getRedistribution", int64(999)) +} + +// TestTribute_CreateVelocityAccount_NoVita tests that creating account without Vita fails. +func TestTribute_CreateVelocityAccount_NoVita(t *testing.T) { + c := newTributeClient(t) + e := c.Executor + + acc := e.NewAccount(t) + invoker := c.WithSigners(acc) + + // Should fail - no Vita registered + invoker.InvokeFail(t, "owner must have an active Vita", "createVelocityAccount", acc.ScriptHash()) +} + +// TestTribute_GrantExemption_NotAdmin tests that non-admin cannot grant exemption. +func TestTribute_GrantExemption_NotAdmin(t *testing.T) { + c := newTributeClient(t) + e := c.Executor + + acc := e.NewAccount(t) + invoker := c.WithSigners(acc) + + // Should fail - not admin + invoker.InvokeFail(t, "caller is not an authorized tribute admin", "grantExemption", + acc.ScriptHash(), "test exemption") +} + +// TestTribute_RevokeExemption_NotAdmin tests that non-admin cannot revoke exemption. +func TestTribute_RevokeExemption_NotAdmin(t *testing.T) { + c := newTributeClient(t) + e := c.Executor + + acc := e.NewAccount(t) + invoker := c.WithSigners(acc) + + // Should fail - not admin + invoker.InvokeFail(t, "caller is not an authorized tribute admin", "revokeExemption", acc.ScriptHash()) +} + +// TestTribute_GrantIncentive_NotAdmin tests that non-admin cannot grant incentive. +func TestTribute_GrantIncentive_NotAdmin(t *testing.T) { + c := newTributeClient(t) + e := c.Executor + + acc := e.NewAccount(t) + invoker := c.WithSigners(acc) + + // Should fail - not admin + invoker.InvokeFail(t, "caller is not an authorized tribute admin", "grantIncentive", + acc.ScriptHash(), int64(0), int64(1000), "test incentive") +} + +// TestTribute_WaiveTribute_NotAdmin tests that non-admin cannot waive tribute. +func TestTribute_WaiveTribute_NotAdmin(t *testing.T) { + c := newTributeClient(t) + e := c.Executor + + acc := e.NewAccount(t) + invoker := c.WithSigners(acc) + + // Should fail - not admin + invoker.InvokeFail(t, "caller is not an authorized tribute admin", "waiveTribute", + int64(0), "test waive") +} + +// TestTribute_Redistribute_NotCommittee tests that non-committee cannot redistribute. +func TestTribute_Redistribute_NotCommittee(t *testing.T) { + c := newTributeClient(t) + e := c.Executor + + acc := e.NewAccount(t) + invoker := c.WithSigners(acc) + + // Should fail - not committee + invoker.InvokeFail(t, "invalid committee signature", "redistribute", + "all_citizens", int64(100)) +} + +// TestTribute_GetPendingAssessment_NoAccount tests getting pending assessment for non-existent account. +func TestTribute_GetPendingAssessment_NoAccount(t *testing.T) { + c := newTributeClient(t) + e := c.Executor + + acc := e.NewAccount(t) + + // No account = null + c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) { + require.Equal(t, 1, len(stack)) + require.Nil(t, stack[0].Value(), "expected null for non-existent pending assessment") + }, "getPendingAssessment", acc.ScriptHash()) +} + +// TestTribute_GetUnclaimedIncentives_NonExistent tests getting unclaimed incentives for non-existent account. +func TestTribute_GetUnclaimedIncentives_NonExistent(t *testing.T) { + c := newTributeClient(t) + e := c.Executor + + acc := e.NewAccount(t) + + // Non-existent account should return 0 + c.Invoke(t, 0, "getUnclaimedIncentives", acc.ScriptHash()) +} + +// TestTribute_CreateVelocityAccountWithVita tests account creation with a valid Vita. +func TestTribute_CreateVelocityAccountWithVita(t *testing.T) { + c := newTributeClient(t) + e := c.Executor + + // Register Vita first + vitaHash := e.NativeHash(t, nativenames.Vita) + acc := e.NewAccount(t) + vitaInvoker := e.NewInvoker(vitaHash, acc) + + owner := acc.ScriptHash() + personHash := hash.Sha256(owner.BytesBE()).BytesBE() + isEntity := false + recoveryHash := hash.Sha256([]byte("recovery")).BytesBE() + + // Register Vita token + vitaInvoker.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) { + require.Equal(t, 1, len(stack)) + _, ok := stack[0].Value().([]byte) + require.True(t, ok, "expected ByteArray result") + }, "register", owner.BytesBE(), personHash, isEntity, recoveryHash) + + // Now create Tribute account + tributeInvoker := c.WithSigners(acc) + tributeInvoker.Invoke(t, true, "createVelocityAccount", owner.BytesBE()) + + // Verify account exists + tributeInvoker.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) { + require.Equal(t, 1, len(stack)) + arr, ok := stack[0].Value().([]stackitem.Item) + require.True(t, ok, "expected array result for existing account") + require.GreaterOrEqual(t, len(arr), 15) // VelocityAccount has 15 fields + }, "getAccount", owner.BytesBE()) + + // Verify total accounts increased + c.Invoke(t, 1, "getTotalAccounts") + + // Verify default velocity (5000 = 50%) + c.Invoke(t, 5000, "getVelocity", owner.BytesBE()) + + // Verify no hoarding (0 = HoardingNone) + c.Invoke(t, 0, "getHoardingLevel", owner.BytesBE()) +} diff --git a/pkg/core/native/tribute.go b/pkg/core/native/tribute.go new file mode 100644 index 0000000..5536e62 --- /dev/null +++ b/pkg/core/native/tribute.go @@ -0,0 +1,1647 @@ +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 +} diff --git a/pkg/core/state/tribute.go b/pkg/core/state/tribute.go new file mode 100644 index 0000000..09f93b1 --- /dev/null +++ b/pkg/core/state/tribute.go @@ -0,0 +1,647 @@ +package state + +import ( + "errors" + "math/big" + + "github.com/tutus-one/tutus-chain/pkg/util" + "github.com/tutus-one/tutus-chain/pkg/vm/stackitem" +) + +// VelocityAccountStatus represents the velocity tracking account status. +type VelocityAccountStatus uint8 + +const ( + // VelocityAccountActive indicates the account is actively tracked. + VelocityAccountActive VelocityAccountStatus = 0 + // VelocityAccountExempt indicates the account is exempt from tribute. + VelocityAccountExempt VelocityAccountStatus = 1 + // VelocityAccountSuspended indicates velocity tracking is suspended. + VelocityAccountSuspended VelocityAccountStatus = 2 +) + +// AssessmentStatus represents the status of a tribute assessment. +type AssessmentStatus uint8 + +const ( + // AssessmentPending indicates the assessment is pending collection. + AssessmentPending AssessmentStatus = 0 + // AssessmentCollected indicates the tribute has been collected. + AssessmentCollected AssessmentStatus = 1 + // AssessmentWaived indicates the assessment was waived. + AssessmentWaived AssessmentStatus = 2 + // AssessmentAppealed indicates the assessment is under appeal. + AssessmentAppealed AssessmentStatus = 3 +) + +// HoardingLevel represents the severity of resource hoarding. +type HoardingLevel uint8 + +const ( + // HoardingNone indicates no hoarding detected. + HoardingNone HoardingLevel = 0 + // HoardingMild indicates mild hoarding (below normal velocity). + HoardingMild HoardingLevel = 1 + // HoardingModerate indicates moderate hoarding. + HoardingModerate HoardingLevel = 2 + // HoardingSevere indicates severe hoarding. + HoardingSevere HoardingLevel = 3 + // HoardingExtreme indicates extreme hoarding (stagnant resources). + HoardingExtreme HoardingLevel = 4 +) + +// IncentiveType represents types of circulation incentives. +type IncentiveType uint8 + +const ( + // IncentiveVelocityBonus rewards high resource velocity. + IncentiveVelocityBonus IncentiveType = 0 + // IncentiveProductiveUse rewards productive resource use. + IncentiveProductiveUse IncentiveType = 1 + // IncentiveCommunitySupport rewards community contributions. + IncentiveCommunitySupport IncentiveType = 2 + // IncentiveEducationSpending rewards education investment. + IncentiveEducationSpending IncentiveType = 3 + // IncentiveHealthcareSpending rewards healthcare investment. + IncentiveHealthcareSpending IncentiveType = 4 +) + +// VelocityAccount tracks resource velocity for a Vita holder. +type VelocityAccount struct { + VitaID uint64 // Owner's Vita token ID + Owner util.Uint160 // Owner's address + CurrentVelocity uint64 // Current velocity score (0-10000 basis points) + AverageVelocity uint64 // Rolling average velocity + LastActivityBlock uint32 // Last transaction block + TotalInflow uint64 // Total resources received + TotalOutflow uint64 // Total resources spent/transferred + StagnantBalance uint64 // Balance considered stagnant + HoardingLevel HoardingLevel // Current hoarding assessment + ExemptionReason string // Reason for exemption (if any) + TotalTributePaid uint64 // Lifetime tribute paid + TotalIncentivesRcvd uint64 // Lifetime incentives received + Status VelocityAccountStatus // Account status + CreatedAt uint32 // Block height when created + UpdatedAt uint32 // Last update block +} + +// ToStackItem implements stackitem.Convertible. +func (a *VelocityAccount) ToStackItem() stackitem.Item { + return stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(a.VitaID)), + stackitem.NewByteArray(a.Owner.BytesBE()), + stackitem.NewBigInteger(new(big.Int).SetUint64(a.CurrentVelocity)), + stackitem.NewBigInteger(new(big.Int).SetUint64(a.AverageVelocity)), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(a.LastActivityBlock))), + stackitem.NewBigInteger(new(big.Int).SetUint64(a.TotalInflow)), + stackitem.NewBigInteger(new(big.Int).SetUint64(a.TotalOutflow)), + stackitem.NewBigInteger(new(big.Int).SetUint64(a.StagnantBalance)), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(a.HoardingLevel))), + stackitem.NewByteArray([]byte(a.ExemptionReason)), + stackitem.NewBigInteger(new(big.Int).SetUint64(a.TotalTributePaid)), + stackitem.NewBigInteger(new(big.Int).SetUint64(a.TotalIncentivesRcvd)), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(a.Status))), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(a.CreatedAt))), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(a.UpdatedAt))), + }) +} + +// FromStackItem implements stackitem.Convertible. +func (a *VelocityAccount) FromStackItem(item stackitem.Item) error { + arr, ok := item.Value().([]stackitem.Item) + if !ok || len(arr) < 15 { + return errors.New("not a struct") + } + vitaID, err := arr[0].TryInteger() + if err != nil { + return err + } + a.VitaID = vitaID.Uint64() + + ownerBytes, err := arr[1].TryBytes() + if err != nil { + return err + } + a.Owner, err = util.Uint160DecodeBytesBE(ownerBytes) + if err != nil { + return err + } + + currentVel, err := arr[2].TryInteger() + if err != nil { + return err + } + a.CurrentVelocity = currentVel.Uint64() + + avgVel, err := arr[3].TryInteger() + if err != nil { + return err + } + a.AverageVelocity = avgVel.Uint64() + + lastActivity, err := arr[4].TryInteger() + if err != nil { + return err + } + a.LastActivityBlock = uint32(lastActivity.Uint64()) + + totalIn, err := arr[5].TryInteger() + if err != nil { + return err + } + a.TotalInflow = totalIn.Uint64() + + totalOut, err := arr[6].TryInteger() + if err != nil { + return err + } + a.TotalOutflow = totalOut.Uint64() + + stagnant, err := arr[7].TryInteger() + if err != nil { + return err + } + a.StagnantBalance = stagnant.Uint64() + + hoardLevel, err := arr[8].TryInteger() + if err != nil { + return err + } + a.HoardingLevel = HoardingLevel(hoardLevel.Uint64()) + + exemptReason, err := arr[9].TryBytes() + if err != nil { + return err + } + a.ExemptionReason = string(exemptReason) + + totalTrib, err := arr[10].TryInteger() + if err != nil { + return err + } + a.TotalTributePaid = totalTrib.Uint64() + + totalInc, err := arr[11].TryInteger() + if err != nil { + return err + } + a.TotalIncentivesRcvd = totalInc.Uint64() + + status, err := arr[12].TryInteger() + if err != nil { + return err + } + a.Status = VelocityAccountStatus(status.Uint64()) + + created, err := arr[13].TryInteger() + if err != nil { + return err + } + a.CreatedAt = uint32(created.Uint64()) + + updated, err := arr[14].TryInteger() + if err != nil { + return err + } + a.UpdatedAt = uint32(updated.Uint64()) + + return nil +} + +// TributeAssessment represents a hoarding tribute assessment. +type TributeAssessment struct { + ID uint64 // Unique assessment ID + VitaID uint64 // Owner's Vita ID + Owner util.Uint160 // Owner's address + AssessmentBlock uint32 // Block when assessment made + HoardingLevel HoardingLevel // Hoarding level at assessment + StagnantAmount uint64 // Amount considered stagnant + TributeRate uint64 // Rate applied (basis points) + TributeAmount uint64 // Tribute amount due + DueBlock uint32 // Block by which tribute is due + CollectedBlock uint32 // Block when collected (0 if not) + Status AssessmentStatus // Assessment status + AppealReason string // Reason for appeal (if any) +} + +// ToStackItem implements stackitem.Convertible. +func (a *TributeAssessment) ToStackItem() stackitem.Item { + return stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(a.ID)), + stackitem.NewBigInteger(new(big.Int).SetUint64(a.VitaID)), + stackitem.NewByteArray(a.Owner.BytesBE()), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(a.AssessmentBlock))), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(a.HoardingLevel))), + stackitem.NewBigInteger(new(big.Int).SetUint64(a.StagnantAmount)), + stackitem.NewBigInteger(new(big.Int).SetUint64(a.TributeRate)), + stackitem.NewBigInteger(new(big.Int).SetUint64(a.TributeAmount)), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(a.DueBlock))), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(a.CollectedBlock))), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(a.Status))), + stackitem.NewByteArray([]byte(a.AppealReason)), + }) +} + +// FromStackItem implements stackitem.Convertible. +func (a *TributeAssessment) FromStackItem(item stackitem.Item) error { + arr, ok := item.Value().([]stackitem.Item) + if !ok || len(arr) < 12 { + return errors.New("not a struct") + } + + id, err := arr[0].TryInteger() + if err != nil { + return err + } + a.ID = id.Uint64() + + vitaID, err := arr[1].TryInteger() + if err != nil { + return err + } + a.VitaID = vitaID.Uint64() + + ownerBytes, err := arr[2].TryBytes() + if err != nil { + return err + } + a.Owner, err = util.Uint160DecodeBytesBE(ownerBytes) + if err != nil { + return err + } + + assessBlock, err := arr[3].TryInteger() + if err != nil { + return err + } + a.AssessmentBlock = uint32(assessBlock.Uint64()) + + hoardLevel, err := arr[4].TryInteger() + if err != nil { + return err + } + a.HoardingLevel = HoardingLevel(hoardLevel.Uint64()) + + stagnant, err := arr[5].TryInteger() + if err != nil { + return err + } + a.StagnantAmount = stagnant.Uint64() + + rate, err := arr[6].TryInteger() + if err != nil { + return err + } + a.TributeRate = rate.Uint64() + + amount, err := arr[7].TryInteger() + if err != nil { + return err + } + a.TributeAmount = amount.Uint64() + + due, err := arr[8].TryInteger() + if err != nil { + return err + } + a.DueBlock = uint32(due.Uint64()) + + collected, err := arr[9].TryInteger() + if err != nil { + return err + } + a.CollectedBlock = uint32(collected.Uint64()) + + status, err := arr[10].TryInteger() + if err != nil { + return err + } + a.Status = AssessmentStatus(status.Uint64()) + + appealReason, err := arr[11].TryBytes() + if err != nil { + return err + } + a.AppealReason = string(appealReason) + + return nil +} + +// CirculationIncentive represents a reward for resource circulation. +type CirculationIncentive struct { + ID uint64 // Unique incentive ID + VitaID uint64 // Recipient's Vita ID + Recipient util.Uint160 // Recipient's address + IncentiveType IncentiveType // Type of incentive + Amount uint64 // Incentive amount + Reason string // Reason for incentive + VelocityScore uint64 // Velocity score that triggered incentive + GrantedBlock uint32 // Block when granted + ClaimedBlock uint32 // Block when claimed (0 if not) + Claimed bool // Whether incentive was claimed +} + +// ToStackItem implements stackitem.Convertible. +func (i *CirculationIncentive) ToStackItem() stackitem.Item { + return stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(i.ID)), + stackitem.NewBigInteger(new(big.Int).SetUint64(i.VitaID)), + stackitem.NewByteArray(i.Recipient.BytesBE()), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(i.IncentiveType))), + stackitem.NewBigInteger(new(big.Int).SetUint64(i.Amount)), + stackitem.NewByteArray([]byte(i.Reason)), + stackitem.NewBigInteger(new(big.Int).SetUint64(i.VelocityScore)), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(i.GrantedBlock))), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(i.ClaimedBlock))), + stackitem.NewBool(i.Claimed), + }) +} + +// FromStackItem implements stackitem.Convertible. +func (i *CirculationIncentive) FromStackItem(item stackitem.Item) error { + arr, ok := item.Value().([]stackitem.Item) + if !ok || len(arr) < 10 { + return errors.New("not a struct") + } + + id, err := arr[0].TryInteger() + if err != nil { + return err + } + i.ID = id.Uint64() + + vitaID, err := arr[1].TryInteger() + if err != nil { + return err + } + i.VitaID = vitaID.Uint64() + + recipBytes, err := arr[2].TryBytes() + if err != nil { + return err + } + i.Recipient, err = util.Uint160DecodeBytesBE(recipBytes) + if err != nil { + return err + } + + incType, err := arr[3].TryInteger() + if err != nil { + return err + } + i.IncentiveType = IncentiveType(incType.Uint64()) + + amount, err := arr[4].TryInteger() + if err != nil { + return err + } + i.Amount = amount.Uint64() + + reason, err := arr[5].TryBytes() + if err != nil { + return err + } + i.Reason = string(reason) + + velScore, err := arr[6].TryInteger() + if err != nil { + return err + } + i.VelocityScore = velScore.Uint64() + + granted, err := arr[7].TryInteger() + if err != nil { + return err + } + i.GrantedBlock = uint32(granted.Uint64()) + + claimed, err := arr[8].TryInteger() + if err != nil { + return err + } + i.ClaimedBlock = uint32(claimed.Uint64()) + + claimedBool, err := arr[9].TryBool() + if err != nil { + return err + } + i.Claimed = claimedBool + + return nil +} + +// RedistributionRecord tracks wealth redistribution events. +type RedistributionRecord struct { + ID uint64 // Unique record ID + SourceAssessment uint64 // Source assessment ID + TotalAmount uint64 // Total amount redistributed + RecipientCount uint64 // Number of recipients + PerCapitaAmount uint64 // Amount per recipient + RedistBlock uint32 // Block when redistribution occurred + TargetCategory string // Category of recipients (e.g., "low_velocity", "all_citizens") +} + +// ToStackItem implements stackitem.Convertible. +func (r *RedistributionRecord) ToStackItem() stackitem.Item { + return stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(r.ID)), + stackitem.NewBigInteger(new(big.Int).SetUint64(r.SourceAssessment)), + stackitem.NewBigInteger(new(big.Int).SetUint64(r.TotalAmount)), + stackitem.NewBigInteger(new(big.Int).SetUint64(r.RecipientCount)), + stackitem.NewBigInteger(new(big.Int).SetUint64(r.PerCapitaAmount)), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(r.RedistBlock))), + stackitem.NewByteArray([]byte(r.TargetCategory)), + }) +} + +// FromStackItem implements stackitem.Convertible. +func (r *RedistributionRecord) FromStackItem(item stackitem.Item) error { + arr, ok := item.Value().([]stackitem.Item) + if !ok || len(arr) < 7 { + return errors.New("not a struct") + } + + id, err := arr[0].TryInteger() + if err != nil { + return err + } + r.ID = id.Uint64() + + sourceAssess, err := arr[1].TryInteger() + if err != nil { + return err + } + r.SourceAssessment = sourceAssess.Uint64() + + total, err := arr[2].TryInteger() + if err != nil { + return err + } + r.TotalAmount = total.Uint64() + + recipCount, err := arr[3].TryInteger() + if err != nil { + return err + } + r.RecipientCount = recipCount.Uint64() + + perCapita, err := arr[4].TryInteger() + if err != nil { + return err + } + r.PerCapitaAmount = perCapita.Uint64() + + redistBlock, err := arr[5].TryInteger() + if err != nil { + return err + } + r.RedistBlock = uint32(redistBlock.Uint64()) + + targetCat, err := arr[6].TryBytes() + if err != nil { + return err + } + r.TargetCategory = string(targetCat) + + return nil +} + +// TributeConfig holds configurable parameters for the Tribute system. +type TributeConfig struct { + VelocityThresholdMild uint64 // Velocity below this = mild hoarding (basis points) + VelocityThresholdModerate uint64 // Velocity below this = moderate hoarding + VelocityThresholdSevere uint64 // Velocity below this = severe hoarding + VelocityThresholdExtreme uint64 // Velocity below this = extreme hoarding + TributeRateMild uint64 // Tribute rate for mild hoarding (basis points) + TributeRateModerate uint64 // Tribute rate for moderate hoarding + TributeRateSevere uint64 // Tribute rate for severe hoarding + TributeRateExtreme uint64 // Tribute rate for extreme hoarding + IncentiveRateHigh uint64 // Incentive rate for high velocity + IncentiveRateVeryHigh uint64 // Incentive rate for very high velocity + StagnancyPeriod uint32 // Blocks before balance considered stagnant + AssessmentPeriod uint32 // Blocks between assessments + GracePeriod uint32 // Blocks before tribute due after assessment + MinBalanceForTribute uint64 // Minimum balance to assess tribute + ExemptionThreshold uint64 // Balance below which exempt from tribute +} + +// ToStackItem implements stackitem.Convertible. +func (c *TributeConfig) ToStackItem() stackitem.Item { + return stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(c.VelocityThresholdMild)), + stackitem.NewBigInteger(new(big.Int).SetUint64(c.VelocityThresholdModerate)), + stackitem.NewBigInteger(new(big.Int).SetUint64(c.VelocityThresholdSevere)), + stackitem.NewBigInteger(new(big.Int).SetUint64(c.VelocityThresholdExtreme)), + stackitem.NewBigInteger(new(big.Int).SetUint64(c.TributeRateMild)), + stackitem.NewBigInteger(new(big.Int).SetUint64(c.TributeRateModerate)), + stackitem.NewBigInteger(new(big.Int).SetUint64(c.TributeRateSevere)), + stackitem.NewBigInteger(new(big.Int).SetUint64(c.TributeRateExtreme)), + stackitem.NewBigInteger(new(big.Int).SetUint64(c.IncentiveRateHigh)), + stackitem.NewBigInteger(new(big.Int).SetUint64(c.IncentiveRateVeryHigh)), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(c.StagnancyPeriod))), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(c.AssessmentPeriod))), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(c.GracePeriod))), + stackitem.NewBigInteger(new(big.Int).SetUint64(c.MinBalanceForTribute)), + stackitem.NewBigInteger(new(big.Int).SetUint64(c.ExemptionThreshold)), + }) +} + +// FromStackItem implements stackitem.Convertible. +func (c *TributeConfig) FromStackItem(item stackitem.Item) error { + arr, ok := item.Value().([]stackitem.Item) + if !ok || len(arr) < 15 { + return errors.New("not a struct") + } + + threshMild, err := arr[0].TryInteger() + if err != nil { + return err + } + c.VelocityThresholdMild = threshMild.Uint64() + + threshMod, err := arr[1].TryInteger() + if err != nil { + return err + } + c.VelocityThresholdModerate = threshMod.Uint64() + + threshSev, err := arr[2].TryInteger() + if err != nil { + return err + } + c.VelocityThresholdSevere = threshSev.Uint64() + + threshExt, err := arr[3].TryInteger() + if err != nil { + return err + } + c.VelocityThresholdExtreme = threshExt.Uint64() + + rateMild, err := arr[4].TryInteger() + if err != nil { + return err + } + c.TributeRateMild = rateMild.Uint64() + + rateMod, err := arr[5].TryInteger() + if err != nil { + return err + } + c.TributeRateModerate = rateMod.Uint64() + + rateSev, err := arr[6].TryInteger() + if err != nil { + return err + } + c.TributeRateSevere = rateSev.Uint64() + + rateExt, err := arr[7].TryInteger() + if err != nil { + return err + } + c.TributeRateExtreme = rateExt.Uint64() + + incHigh, err := arr[8].TryInteger() + if err != nil { + return err + } + c.IncentiveRateHigh = incHigh.Uint64() + + incVeryHigh, err := arr[9].TryInteger() + if err != nil { + return err + } + c.IncentiveRateVeryHigh = incVeryHigh.Uint64() + + stagnancy, err := arr[10].TryInteger() + if err != nil { + return err + } + c.StagnancyPeriod = uint32(stagnancy.Uint64()) + + assessPeriod, err := arr[11].TryInteger() + if err != nil { + return err + } + c.AssessmentPeriod = uint32(assessPeriod.Uint64()) + + grace, err := arr[12].TryInteger() + if err != nil { + return err + } + c.GracePeriod = uint32(grace.Uint64()) + + minBal, err := arr[13].TryInteger() + if err != nil { + return err + } + c.MinBalanceForTribute = minBal.Uint64() + + exemptThresh, err := arr[14].TryInteger() + if err != nil { + return err + } + c.ExemptionThreshold = exemptThresh.Uint64() + + return nil +}