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/interop/runtime" "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/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" ) // VTS storage prefixes. const ( vtsPrefixAccount = 0x20 // address -> VTSBalance vtsPrefixVendor = 0x21 // address -> Vendor vtsPrefixTotalSupply = 0x11 // total supply storage vtsPrefixTaxConfig = 0x10 // tax configuration vtsPrefixVendorCount = 0x12 // vendor count vtsPrefixTxRecord = 0x30 // txHash -> TransactionRecord vtsPrefixAccountTxs = 0x31 // address + blockHeight -> []txHash index vtsPrefixTaxWithheld = 0x32 // address + year -> cumulative tax ) // VTSFactor is the divisor for VTS (10^8 = 100,000,000). const VTSFactor = 100000000 // VTS represents the VTS (Value Transfer System) native contract. // VTS is programmable money with spending restrictions and automatic tax accounting. type VTS struct { interop.ContractMD NEO INEO RoleRegistry IRoleRegistry Vita IVita symbol string decimals int64 factor int64 } // VTSCache holds cached VTS data. type VTSCache struct { vendorCount int64 } // Copy implements dao.NativeContractCache. func (c *VTSCache) Copy() dao.NativeContractCache { return &VTSCache{ vendorCount: c.vendorCount, } } // newVTS creates a new VTS native contract. func newVTS() *VTS { v := &VTS{ symbol: "VTS", decimals: 8, factor: VTSFactor, } v.ContractMD = *interop.NewContractMD(nativenames.VTS, nativeids.VTS, func(m *manifest.Manifest, hf config.Hardfork) { m.SupportedStandards = []string{manifest.NEP17StandardName} }) defer v.BuildHFSpecificMD(v.ActiveIn()) // NEP-17 Standard Methods desc := NewDescriptor("symbol", smartcontract.StringType) md := NewMethodAndPrice(v.symbolMethod, 0, callflag.NoneFlag) v.AddMethod(md, desc) desc = NewDescriptor("decimals", smartcontract.IntegerType) md = NewMethodAndPrice(v.decimalsMethod, 0, callflag.NoneFlag) v.AddMethod(md, desc) desc = NewDescriptor("totalSupply", smartcontract.IntegerType) md = NewMethodAndPrice(v.totalSupply, 1<<15, callflag.ReadStates) v.AddMethod(md, desc) desc = NewDescriptor("balanceOf", smartcontract.IntegerType, manifest.NewParameter("account", smartcontract.Hash160Type)) md = NewMethodAndPrice(v.balanceOf, 1<<15, callflag.ReadStates) v.AddMethod(md, desc) transferParams := []manifest.Parameter{ manifest.NewParameter("from", smartcontract.Hash160Type), manifest.NewParameter("to", smartcontract.Hash160Type), manifest.NewParameter("amount", smartcontract.IntegerType), } desc = NewDescriptor("transfer", smartcontract.BoolType, append(transferParams, manifest.NewParameter("data", smartcontract.AnyType))..., ) md = NewMethodAndPrice(v.transfer, 1<<17, callflag.States|callflag.AllowCall|callflag.AllowNotify) md.StorageFee = 50 v.AddMethod(md, desc) // Extended Balance Methods desc = NewDescriptor("unrestrictedBalanceOf", smartcontract.IntegerType, manifest.NewParameter("account", smartcontract.Hash160Type)) md = NewMethodAndPrice(v.unrestrictedBalanceOf, 1<<15, callflag.ReadStates) v.AddMethod(md, desc) desc = NewDescriptor("restrictedBalanceOf", smartcontract.IntegerType, manifest.NewParameter("account", smartcontract.Hash160Type), manifest.NewParameter("category", smartcontract.IntegerType)) md = NewMethodAndPrice(v.restrictedBalanceOf, 1<<15, callflag.ReadStates) v.AddMethod(md, desc) desc = NewDescriptor("balanceDetails", smartcontract.ArrayType, manifest.NewParameter("account", smartcontract.Hash160Type)) md = NewMethodAndPrice(v.balanceDetails, 1<<15, callflag.ReadStates) v.AddMethod(md, desc) // Minting Methods (Committee-Only) desc = NewDescriptor("mint", smartcontract.BoolType, manifest.NewParameter("to", smartcontract.Hash160Type), manifest.NewParameter("amount", smartcontract.IntegerType)) md = NewMethodAndPrice(v.mint, 1<<17, callflag.States|callflag.AllowNotify) v.AddMethod(md, desc) desc = NewDescriptor("mintRestricted", smartcontract.BoolType, manifest.NewParameter("to", smartcontract.Hash160Type), manifest.NewParameter("amount", smartcontract.IntegerType), manifest.NewParameter("category", smartcontract.IntegerType)) md = NewMethodAndPrice(v.mintRestricted, 1<<17, callflag.States|callflag.AllowNotify) v.AddMethod(md, desc) desc = NewDescriptor("burn", smartcontract.BoolType, manifest.NewParameter("from", smartcontract.Hash160Type), manifest.NewParameter("amount", smartcontract.IntegerType)) md = NewMethodAndPrice(v.burn, 1<<17, callflag.States|callflag.AllowNotify) v.AddMethod(md, desc) desc = NewDescriptor("convertToUnrestricted", smartcontract.BoolType, manifest.NewParameter("account", smartcontract.Hash160Type), manifest.NewParameter("category", smartcontract.IntegerType), manifest.NewParameter("amount", smartcontract.IntegerType)) md = NewMethodAndPrice(v.convertToUnrestricted, 1<<17, callflag.States|callflag.AllowNotify) v.AddMethod(md, desc) // Vendor Management Methods (Committee-Only) desc = NewDescriptor("registerVendor", smartcontract.BoolType, manifest.NewParameter("address", smartcontract.Hash160Type), manifest.NewParameter("name", smartcontract.StringType), manifest.NewParameter("categories", smartcontract.IntegerType), manifest.NewParameter("ageRestricted", smartcontract.BoolType)) md = NewMethodAndPrice(v.registerVendor, 1<<17, callflag.States|callflag.AllowNotify) v.AddMethod(md, desc) desc = NewDescriptor("updateVendor", smartcontract.BoolType, manifest.NewParameter("address", smartcontract.Hash160Type), manifest.NewParameter("name", smartcontract.StringType), manifest.NewParameter("categories", smartcontract.IntegerType), manifest.NewParameter("ageRestricted", smartcontract.BoolType)) md = NewMethodAndPrice(v.updateVendor, 1<<17, callflag.States|callflag.AllowNotify) v.AddMethod(md, desc) desc = NewDescriptor("deactivateVendor", smartcontract.BoolType, manifest.NewParameter("address", smartcontract.Hash160Type)) md = NewMethodAndPrice(v.deactivateVendor, 1<<17, callflag.States|callflag.AllowNotify) v.AddMethod(md, desc) desc = NewDescriptor("getVendor", smartcontract.ArrayType, manifest.NewParameter("address", smartcontract.Hash160Type)) md = NewMethodAndPrice(v.getVendor, 1<<15, callflag.ReadStates) v.AddMethod(md, desc) desc = NewDescriptor("isVendor", smartcontract.BoolType, manifest.NewParameter("address", smartcontract.Hash160Type)) md = NewMethodAndPrice(v.isVendor, 1<<15, callflag.ReadStates) v.AddMethod(md, desc) desc = NewDescriptor("getVendorCategories", smartcontract.IntegerType, manifest.NewParameter("address", smartcontract.Hash160Type)) md = NewMethodAndPrice(v.getVendorCategories, 1<<15, callflag.ReadStates) v.AddMethod(md, desc) // Spending Methods desc = NewDescriptor("spend", smartcontract.BoolType, manifest.NewParameter("from", smartcontract.Hash160Type), manifest.NewParameter("vendor", smartcontract.Hash160Type), manifest.NewParameter("amount", smartcontract.IntegerType), manifest.NewParameter("data", smartcontract.AnyType)) md = NewMethodAndPrice(v.spend, 1<<17, callflag.States|callflag.AllowCall|callflag.AllowNotify) md.StorageFee = 50 v.AddMethod(md, desc) desc = NewDescriptor("canSpendAt", smartcontract.BoolType, manifest.NewParameter("account", smartcontract.Hash160Type), manifest.NewParameter("vendor", smartcontract.Hash160Type), manifest.NewParameter("amount", smartcontract.IntegerType)) md = NewMethodAndPrice(v.canSpendAt, 1<<15, callflag.ReadStates) v.AddMethod(md, desc) // Tax Methods desc = NewDescriptor("payWage", smartcontract.BoolType, manifest.NewParameter("employer", smartcontract.Hash160Type), manifest.NewParameter("employee", smartcontract.Hash160Type), manifest.NewParameter("grossAmount", smartcontract.IntegerType), manifest.NewParameter("taxRate", smartcontract.IntegerType)) md = NewMethodAndPrice(v.payWage, 1<<17, callflag.States|callflag.AllowNotify) md.StorageFee = 100 v.AddMethod(md, desc) desc = NewDescriptor("setTaxConfig", smartcontract.BoolType, manifest.NewParameter("incomeRate", smartcontract.IntegerType), manifest.NewParameter("salesRate", smartcontract.IntegerType), manifest.NewParameter("treasuryAddress", smartcontract.Hash160Type), manifest.NewParameter("exemptCategories", smartcontract.IntegerType)) md = NewMethodAndPrice(v.setTaxConfig, 1<<17, callflag.States|callflag.AllowNotify) v.AddMethod(md, desc) desc = NewDescriptor("getTaxConfig", smartcontract.ArrayType) md = NewMethodAndPrice(v.getTaxConfig, 1<<15, callflag.ReadStates) v.AddMethod(md, desc) desc = NewDescriptor("issueTaxRefund", smartcontract.BoolType, manifest.NewParameter("account", smartcontract.Hash160Type), manifest.NewParameter("amount", smartcontract.IntegerType)) md = NewMethodAndPrice(v.issueTaxRefund, 1<<17, callflag.States|callflag.AllowNotify) v.AddMethod(md, desc) // Tax Reporting Methods (read-only) desc = NewDescriptor("getTransactions", smartcontract.ArrayType, manifest.NewParameter("account", smartcontract.Hash160Type), manifest.NewParameter("startBlock", smartcontract.IntegerType), manifest.NewParameter("endBlock", smartcontract.IntegerType)) md = NewMethodAndPrice(v.getTransactions, 1<<17, callflag.ReadStates) v.AddMethod(md, desc) desc = NewDescriptor("getIncomeForPeriod", smartcontract.IntegerType, manifest.NewParameter("account", smartcontract.Hash160Type), manifest.NewParameter("startBlock", smartcontract.IntegerType), manifest.NewParameter("endBlock", smartcontract.IntegerType)) md = NewMethodAndPrice(v.getIncomeForPeriod, 1<<17, callflag.ReadStates) v.AddMethod(md, desc) desc = NewDescriptor("getTaxWithheld", smartcontract.IntegerType, manifest.NewParameter("account", smartcontract.Hash160Type), manifest.NewParameter("startBlock", smartcontract.IntegerType), manifest.NewParameter("endBlock", smartcontract.IntegerType)) md = NewMethodAndPrice(v.getTaxWithheld, 1<<17, callflag.ReadStates) v.AddMethod(md, desc) desc = NewDescriptor("getDeductibleExpenses", smartcontract.IntegerType, manifest.NewParameter("account", smartcontract.Hash160Type), manifest.NewParameter("startBlock", smartcontract.IntegerType), manifest.NewParameter("endBlock", smartcontract.IntegerType), manifest.NewParameter("category", smartcontract.IntegerType)) md = NewMethodAndPrice(v.getDeductibleExpenses, 1<<17, callflag.ReadStates) v.AddMethod(md, desc) desc = NewDescriptor("getTaxSummary", smartcontract.ArrayType, manifest.NewParameter("account", smartcontract.Hash160Type), manifest.NewParameter("startBlock", smartcontract.IntegerType), manifest.NewParameter("endBlock", smartcontract.IntegerType)) md = NewMethodAndPrice(v.getTaxSummary, 1<<17, callflag.ReadStates) v.AddMethod(md, desc) // Events eDesc := NewEventDescriptor("Transfer", transferParams...) eMD := NewEvent(eDesc) v.AddEvent(eMD) eDesc = NewEventDescriptor("Mint", manifest.NewParameter("to", smartcontract.Hash160Type), manifest.NewParameter("amount", smartcontract.IntegerType), manifest.NewParameter("category", smartcontract.IntegerType)) eMD = NewEvent(eDesc) v.AddEvent(eMD) eDesc = NewEventDescriptor("Burn", manifest.NewParameter("from", smartcontract.Hash160Type), manifest.NewParameter("amount", smartcontract.IntegerType)) eMD = NewEvent(eDesc) v.AddEvent(eMD) eDesc = NewEventDescriptor("Spend", manifest.NewParameter("from", smartcontract.Hash160Type), manifest.NewParameter("vendor", smartcontract.Hash160Type), manifest.NewParameter("amount", smartcontract.IntegerType), manifest.NewParameter("categoriesUsed", smartcontract.IntegerType)) eMD = NewEvent(eDesc) v.AddEvent(eMD) eDesc = NewEventDescriptor("VendorRegistered", manifest.NewParameter("address", smartcontract.Hash160Type), manifest.NewParameter("name", smartcontract.StringType), manifest.NewParameter("categories", smartcontract.IntegerType), manifest.NewParameter("ageRestricted", smartcontract.BoolType)) eMD = NewEvent(eDesc) v.AddEvent(eMD) eDesc = NewEventDescriptor("VendorUpdated", manifest.NewParameter("address", smartcontract.Hash160Type), manifest.NewParameter("name", smartcontract.StringType), manifest.NewParameter("categories", smartcontract.IntegerType), manifest.NewParameter("ageRestricted", smartcontract.BoolType)) eMD = NewEvent(eDesc) v.AddEvent(eMD) eDesc = NewEventDescriptor("VendorDeactivated", manifest.NewParameter("address", smartcontract.Hash160Type)) eMD = NewEvent(eDesc) v.AddEvent(eMD) eDesc = NewEventDescriptor("TaxWithheld", manifest.NewParameter("from", smartcontract.Hash160Type), manifest.NewParameter("to", smartcontract.Hash160Type), manifest.NewParameter("grossAmount", smartcontract.IntegerType), manifest.NewParameter("taxAmount", smartcontract.IntegerType), manifest.NewParameter("taxRate", smartcontract.IntegerType)) eMD = NewEvent(eDesc) v.AddEvent(eMD) eDesc = NewEventDescriptor("TaxRefunded", manifest.NewParameter("account", smartcontract.Hash160Type), manifest.NewParameter("amount", smartcontract.IntegerType)) eMD = NewEvent(eDesc) v.AddEvent(eMD) eDesc = NewEventDescriptor("ConvertedToUnrestricted", manifest.NewParameter("account", smartcontract.Hash160Type), manifest.NewParameter("category", smartcontract.IntegerType), manifest.NewParameter("amount", smartcontract.IntegerType)) eMD = NewEvent(eDesc) v.AddEvent(eMD) return v } // Metadata returns VTS contract metadata. func (v *VTS) Metadata() *interop.ContractMD { return &v.ContractMD } // Initialize initializes VTS contract at genesis. func (v *VTS) Initialize(ic *interop.Context, hf *config.Hardfork, newMD *interop.HFSpecificContractMD) error { if hf != v.ActiveIn() { return nil } // Initialize with zero total supply (VTS is minted on-demand) v.putTotalSupply(ic.DAO, big.NewInt(0)) // Initialize vendor count v.putVendorCount(ic.DAO, 0) // Initialize default tax config (no taxes by default) cfg := &state.TaxConfig{ DefaultIncomeRate: 0, DefaultSalesRate: 0, TreasuryAddress: util.Uint160{}, ExemptCategories: 0, } v.putTaxConfig(ic.DAO, cfg) return nil } // InitializeCache implements the Contract interface. func (v *VTS) InitializeCache(_ interop.IsHardforkEnabled, blockHeight uint32, d *dao.Simple) error { cache := &VTSCache{ vendorCount: v.getVendorCountInternal(d), } d.SetCache(v.ID, cache) return nil } // OnPersist implements the Contract interface. func (v *VTS) OnPersist(ic *interop.Context) error { return nil } // PostPersist implements the Contract interface. func (v *VTS) PostPersist(ic *interop.Context) error { return nil } // ActiveIn implements the Contract interface. func (v *VTS) ActiveIn() *config.Hardfork { return nil } // checkCommittee verifies the caller has committee privileges. func (v *VTS) checkCommittee(ic *interop.Context) bool { if v.RoleRegistry != nil { return v.RoleRegistry.CheckCommittee(ic) } if v.NEO != nil { return v.NEO.CheckCommittee(ic) } return false } // ============ Storage Key Helpers ============ func (v *VTS) makeAccountKey(h util.Uint160) []byte { return makeUint160Key(vtsPrefixAccount, h) } func (v *VTS) makeVendorKey(h util.Uint160) []byte { return makeUint160Key(vtsPrefixVendor, h) } var vtsTotalSupplyKey = []byte{vtsPrefixTotalSupply} var vtsVendorCountKey = []byte{vtsPrefixVendorCount} var vtsTaxConfigKey = []byte{vtsPrefixTaxConfig} // ============ NEP-17 Standard Methods ============ func (v *VTS) symbolMethod(_ *interop.Context, _ []stackitem.Item) stackitem.Item { return stackitem.NewByteArray([]byte(v.symbol)) } func (v *VTS) decimalsMethod(_ *interop.Context, _ []stackitem.Item) stackitem.Item { return stackitem.NewBigInteger(big.NewInt(v.decimals)) } func (v *VTS) totalSupply(ic *interop.Context, _ []stackitem.Item) stackitem.Item { supply := v.getTotalSupplyInternal(ic.DAO) return stackitem.NewBigInteger(supply) } func (v *VTS) getTotalSupplyInternal(d *dao.Simple) *big.Int { si := d.GetStorageItem(v.ID, vtsTotalSupplyKey) if si == nil { return big.NewInt(0) } bal, err := state.NEP17BalanceFromBytes(si) if err != nil { return big.NewInt(0) } return &bal.Balance } func (v *VTS) putTotalSupply(d *dao.Simple, supply *big.Int) { bal := &state.NEP17Balance{Balance: *supply} d.PutStorageItem(v.ID, vtsTotalSupplyKey, bal.Bytes(nil)) } func (v *VTS) balanceOf(ic *interop.Context, args []stackitem.Item) stackitem.Item { h := toUint160(args[0]) balance := v.getBalanceInternal(ic.DAO, h) return stackitem.NewBigInteger(balance.Total()) } func (v *VTS) getBalanceInternal(d *dao.Simple, h util.Uint160) *state.VTSBalance { key := v.makeAccountKey(h) si := d.GetStorageItem(v.ID, key) if si == nil { return state.NewVTSBalance() } item, err := stackitem.Deserialize(si) if err != nil { return state.NewVTSBalance() } bal := state.NewVTSBalance() if err := bal.FromStackItem(item); err != nil { return state.NewVTSBalance() } return bal } func (v *VTS) putBalance(d *dao.Simple, h util.Uint160, bal *state.VTSBalance) error { key := v.makeAccountKey(h) // If balance is zero, delete the entry if bal.Total().Sign() == 0 { d.DeleteStorageItem(v.ID, key) return nil } item, err := bal.ToStackItem() if err != nil { return err } data, err := stackitem.Serialize(item) if err != nil { return err } d.PutStorageItem(v.ID, key, data) return nil } func (v *VTS) transfer(ic *interop.Context, args []stackitem.Item) stackitem.Item { from := toUint160(args[0]) to := toUint160(args[1]) amount := toBigInt(args[2]) // Transfer only unrestricted balance err := v.transferUnrestricted(ic, from, to, amount, args[3]) return stackitem.NewBool(err == nil) } func (v *VTS) transferUnrestricted(ic *interop.Context, from, to util.Uint160, amount *big.Int, data stackitem.Item) error { if amount.Sign() < 0 { return errors.New("negative amount") } // Check witness caller := ic.VM.GetCallingScriptHash() if !from.Equals(caller) { ok, err := checkWitness(ic, from) if err != nil || !ok { return errors.New("invalid signature") } } if amount.Sign() == 0 { v.emitTransfer(ic, &from, &to, amount) return nil } // Get sender balance fromBal := v.getBalanceInternal(ic.DAO, from) if fromBal.Unrestricted.Cmp(amount) < 0 { return errors.New("insufficient unrestricted funds") } // Update sender balance fromBal.Unrestricted.Sub(&fromBal.Unrestricted, amount) if err := v.putBalance(ic.DAO, from, fromBal); err != nil { return err } // Update recipient balance toBal := v.getBalanceInternal(ic.DAO, to) toBal.Unrestricted.Add(&toBal.Unrestricted, amount) if err := v.putBalance(ic.DAO, to, toBal); err != nil { return err } v.emitTransfer(ic, &from, &to, amount) return nil } func (v *VTS) emitTransfer(ic *interop.Context, from, to *util.Uint160, amount *big.Int) { ic.AddNotification(v.Hash, "Transfer", stackitem.NewArray([]stackitem.Item{ addrToStackItem(from), addrToStackItem(to), stackitem.NewBigInteger(amount), })) } // ============ Extended Balance Methods ============ func (v *VTS) unrestrictedBalanceOf(ic *interop.Context, args []stackitem.Item) stackitem.Item { h := toUint160(args[0]) bal := v.getBalanceInternal(ic.DAO, h) return stackitem.NewBigInteger(&bal.Unrestricted) } func (v *VTS) restrictedBalanceOf(ic *interop.Context, args []stackitem.Item) stackitem.Item { h := toUint160(args[0]) category := toUint8(args[1]) bal := v.getBalanceInternal(ic.DAO, h) if amt, ok := bal.Restricted[category]; ok { return stackitem.NewBigInteger(amt) } return stackitem.NewBigInteger(big.NewInt(0)) } func (v *VTS) balanceDetails(ic *interop.Context, args []stackitem.Item) stackitem.Item { h := toUint160(args[0]) bal := v.getBalanceInternal(ic.DAO, h) // Return: [unrestricted, [[category, amount], ...]] restricted := make([]stackitem.Item, 0, len(bal.Restricted)) for cat, amt := range bal.Restricted { if amt.Sign() > 0 { restricted = append(restricted, stackitem.NewArray([]stackitem.Item{ stackitem.NewBigInteger(big.NewInt(int64(cat))), stackitem.NewBigInteger(amt), })) } } return stackitem.NewArray([]stackitem.Item{ stackitem.NewBigInteger(&bal.Unrestricted), stackitem.NewArray(restricted), }) } // ============ Minting Methods ============ func (v *VTS) mint(ic *interop.Context, args []stackitem.Item) stackitem.Item { if !v.checkCommittee(ic) { panic("caller is not a committee member") } to := toUint160(args[0]) amount := toBigInt(args[1]) if amount.Sign() <= 0 { panic("amount must be positive") } v.mintUnrestricted(ic, to, amount) return stackitem.NewBool(true) } func (v *VTS) mintUnrestricted(ic *interop.Context, to util.Uint160, amount *big.Int) { bal := v.getBalanceInternal(ic.DAO, to) bal.Unrestricted.Add(&bal.Unrestricted, amount) v.putBalance(ic.DAO, to, bal) // Update total supply supply := v.getTotalSupplyInternal(ic.DAO) supply.Add(supply, amount) v.putTotalSupply(ic.DAO, supply) v.emitTransfer(ic, nil, &to, amount) ic.AddNotification(v.Hash, "Mint", stackitem.NewArray([]stackitem.Item{ stackitem.NewByteArray(to.BytesBE()), stackitem.NewBigInteger(amount), stackitem.NewBigInteger(big.NewInt(0)), // Category 0 = unrestricted })) } func (v *VTS) mintRestricted(ic *interop.Context, args []stackitem.Item) stackitem.Item { if !v.checkCommittee(ic) { panic("caller is not a committee member") } to := toUint160(args[0]) amount := toBigInt(args[1]) category := toUint8(args[2]) if amount.Sign() <= 0 { panic("amount must be positive") } if category == state.CategoryUnrestricted { panic("use mint() for unrestricted tokens") } // Restricted VTS requires recipient to have a Vita // This ensures benefits go to verified identities if !v.Vita.TokenExists(ic.DAO, to) { panic("restricted VTS requires Vita") } v.mintRestrictedInternal(ic, to, amount, category) return stackitem.NewBool(true) } func (v *VTS) mintRestrictedInternal(ic *interop.Context, to util.Uint160, amount *big.Int, category uint8) { bal := v.getBalanceInternal(ic.DAO, to) if bal.Restricted == nil { bal.Restricted = make(map[uint8]*big.Int) } if bal.Restricted[category] == nil { bal.Restricted[category] = big.NewInt(0) } bal.Restricted[category].Add(bal.Restricted[category], amount) v.putBalance(ic.DAO, to, bal) // Update total supply supply := v.getTotalSupplyInternal(ic.DAO) supply.Add(supply, amount) v.putTotalSupply(ic.DAO, supply) v.emitTransfer(ic, nil, &to, amount) ic.AddNotification(v.Hash, "Mint", stackitem.NewArray([]stackitem.Item{ stackitem.NewByteArray(to.BytesBE()), stackitem.NewBigInteger(amount), stackitem.NewBigInteger(big.NewInt(int64(category))), })) } func (v *VTS) burn(ic *interop.Context, args []stackitem.Item) stackitem.Item { if !v.checkCommittee(ic) { panic("caller is not a committee member") } from := toUint160(args[0]) amount := toBigInt(args[1]) if amount.Sign() <= 0 { panic("amount must be positive") } bal := v.getBalanceInternal(ic.DAO, from) if bal.Unrestricted.Cmp(amount) < 0 { panic("insufficient unrestricted funds") } bal.Unrestricted.Sub(&bal.Unrestricted, amount) v.putBalance(ic.DAO, from, bal) // Update total supply supply := v.getTotalSupplyInternal(ic.DAO) supply.Sub(supply, amount) v.putTotalSupply(ic.DAO, supply) v.emitTransfer(ic, &from, nil, amount) ic.AddNotification(v.Hash, "Burn", stackitem.NewArray([]stackitem.Item{ stackitem.NewByteArray(from.BytesBE()), stackitem.NewBigInteger(amount), })) return stackitem.NewBool(true) } func (v *VTS) convertToUnrestricted(ic *interop.Context, args []stackitem.Item) stackitem.Item { if !v.checkCommittee(ic) { panic("caller is not a committee member") } account := toUint160(args[0]) category := toUint8(args[1]) amount := toBigInt(args[2]) if amount.Sign() <= 0 { panic("amount must be positive") } bal := v.getBalanceInternal(ic.DAO, account) if bal.Restricted[category] == nil || bal.Restricted[category].Cmp(amount) < 0 { panic("insufficient restricted funds in category") } // Move from restricted to unrestricted bal.Restricted[category].Sub(bal.Restricted[category], amount) if bal.Restricted[category].Sign() == 0 { delete(bal.Restricted, category) } bal.Unrestricted.Add(&bal.Unrestricted, amount) v.putBalance(ic.DAO, account, bal) ic.AddNotification(v.Hash, "ConvertedToUnrestricted", stackitem.NewArray([]stackitem.Item{ stackitem.NewByteArray(account.BytesBE()), stackitem.NewBigInteger(big.NewInt(int64(category))), stackitem.NewBigInteger(amount), })) return stackitem.NewBool(true) } // ============ Vendor Management ============ func (v *VTS) registerVendor(ic *interop.Context, args []stackitem.Item) stackitem.Item { if !v.checkCommittee(ic) { panic("caller is not a committee member") } address := toUint160(args[0]) name := toString(args[1]) categories := toUint8(args[2]) ageRestricted := toBool(args[3]) if len(name) == 0 || len(name) > 64 { panic("invalid vendor name") } // Check if vendor already exists if v.getVendorInternal(ic.DAO, address) != nil { panic("vendor already registered") } vendor := &state.Vendor{ Address: address, Name: name, Categories: categories, RegisteredAt: ic.Block.Index, RegisteredBy: ic.VM.GetCallingScriptHash(), Active: true, AgeRestricted: ageRestricted, } v.putVendor(ic.DAO, vendor) // Increment vendor count count := v.getVendorCountInternal(ic.DAO) v.putVendorCount(ic.DAO, count+1) ic.AddNotification(v.Hash, "VendorRegistered", stackitem.NewArray([]stackitem.Item{ stackitem.NewByteArray(address.BytesBE()), stackitem.NewByteArray([]byte(name)), stackitem.NewBigInteger(big.NewInt(int64(categories))), stackitem.NewBool(ageRestricted), })) return stackitem.NewBool(true) } func (v *VTS) updateVendor(ic *interop.Context, args []stackitem.Item) stackitem.Item { if !v.checkCommittee(ic) { panic("caller is not a committee member") } address := toUint160(args[0]) name := toString(args[1]) categories := toUint8(args[2]) ageRestricted := toBool(args[3]) if len(name) == 0 || len(name) > 64 { panic("invalid vendor name") } vendor := v.getVendorInternal(ic.DAO, address) if vendor == nil { panic("vendor not found") } vendor.Name = name vendor.Categories = categories vendor.AgeRestricted = ageRestricted v.putVendor(ic.DAO, vendor) ic.AddNotification(v.Hash, "VendorUpdated", stackitem.NewArray([]stackitem.Item{ stackitem.NewByteArray(address.BytesBE()), stackitem.NewByteArray([]byte(name)), stackitem.NewBigInteger(big.NewInt(int64(categories))), stackitem.NewBool(ageRestricted), })) return stackitem.NewBool(true) } func (v *VTS) deactivateVendor(ic *interop.Context, args []stackitem.Item) stackitem.Item { if !v.checkCommittee(ic) { panic("caller is not a committee member") } address := toUint160(args[0]) vendor := v.getVendorInternal(ic.DAO, address) if vendor == nil { panic("vendor not found") } vendor.Active = false v.putVendor(ic.DAO, vendor) ic.AddNotification(v.Hash, "VendorDeactivated", stackitem.NewArray([]stackitem.Item{ stackitem.NewByteArray(address.BytesBE()), })) return stackitem.NewBool(true) } func (v *VTS) getVendor(ic *interop.Context, args []stackitem.Item) stackitem.Item { address := toUint160(args[0]) vendor := v.getVendorInternal(ic.DAO, address) if vendor == nil { return stackitem.Null{} } item, _ := vendor.ToStackItem() return item } func (v *VTS) getVendorInternal(d *dao.Simple, address util.Uint160) *state.Vendor { key := v.makeVendorKey(address) si := d.GetStorageItem(v.ID, key) if si == nil { return nil } item, err := stackitem.Deserialize(si) if err != nil { return nil } vendor := &state.Vendor{} if err := vendor.FromStackItem(item); err != nil { return nil } return vendor } func (v *VTS) putVendor(d *dao.Simple, vendor *state.Vendor) { key := v.makeVendorKey(vendor.Address) item, _ := vendor.ToStackItem() data, _ := stackitem.Serialize(item) d.PutStorageItem(v.ID, key, data) } func (v *VTS) isVendor(ic *interop.Context, args []stackitem.Item) stackitem.Item { address := toUint160(args[0]) vendor := v.getVendorInternal(ic.DAO, address) return stackitem.NewBool(vendor != nil && vendor.Active) } func (v *VTS) getVendorCategories(ic *interop.Context, args []stackitem.Item) stackitem.Item { address := toUint160(args[0]) vendor := v.getVendorInternal(ic.DAO, address) if vendor == nil { return stackitem.NewBigInteger(big.NewInt(0)) } return stackitem.NewBigInteger(big.NewInt(int64(vendor.Categories))) } func (v *VTS) getVendorCountInternal(d *dao.Simple) int64 { si := d.GetStorageItem(v.ID, vtsVendorCountKey) if si == nil { return 0 } return int64(binary.BigEndian.Uint64(si)) } func (v *VTS) putVendorCount(d *dao.Simple, count int64) { data := make([]byte, 8) binary.BigEndian.PutUint64(data, uint64(count)) d.PutStorageItem(v.ID, vtsVendorCountKey, data) } // ============ Spending Logic ============ func (v *VTS) spend(ic *interop.Context, args []stackitem.Item) stackitem.Item { from := toUint160(args[0]) vendor := toUint160(args[1]) amount := toBigInt(args[2]) data := args[3] if amount.Sign() <= 0 { panic("amount must be positive") } // Check witness for sender caller := ic.VM.GetCallingScriptHash() if !from.Equals(caller) { ok, err := checkWitness(ic, from) if err != nil || !ok { panic("invalid signature") } } // Verify vendor is registered and active vendorInfo := v.getVendorInternal(ic.DAO, vendor) if vendorInfo == nil || !vendorInfo.Active { panic("invalid or inactive vendor") } // Check age verification for age-restricted vendors (e.g., alcohol, tobacco) if vendorInfo.AgeRestricted { if !v.Vita.IsAdultVerified(ic.DAO, from) { panic("age verification required for this vendor") } } // Get sender's balance fromBal := v.getBalanceInternal(ic.DAO, from) // Try to spend from matching restricted categories first remaining := new(big.Int).Set(amount) var categoriesUsed uint8 = 0 for category, catBal := range fromBal.Restricted { if remaining.Sign() <= 0 { break } // Check if vendor accepts this category if vendorInfo.Categories&category != 0 { toSpend := new(big.Int) if catBal.Cmp(remaining) >= 0 { toSpend.Set(remaining) } else { toSpend.Set(catBal) } catBal.Sub(catBal, toSpend) if catBal.Sign() == 0 { delete(fromBal.Restricted, category) } remaining.Sub(remaining, toSpend) categoriesUsed |= category } } // If still remaining, use unrestricted balance if remaining.Sign() > 0 { if fromBal.Unrestricted.Cmp(remaining) < 0 { panic("insufficient funds") } fromBal.Unrestricted.Sub(&fromBal.Unrestricted, remaining) } // Save sender balance v.putBalance(ic.DAO, from, fromBal) // Credit vendor with unrestricted balance vendorBal := v.getBalanceInternal(ic.DAO, vendor) vendorBal.Unrestricted.Add(&vendorBal.Unrestricted, amount) v.putBalance(ic.DAO, vendor, vendorBal) // Emit events v.emitTransfer(ic, &from, &vendor, amount) ic.AddNotification(v.Hash, "Spend", stackitem.NewArray([]stackitem.Item{ stackitem.NewByteArray(from.BytesBE()), stackitem.NewByteArray(vendor.BytesBE()), stackitem.NewBigInteger(amount), stackitem.NewBigInteger(big.NewInt(int64(categoriesUsed))), })) _ = data // data parameter for compatibility return stackitem.NewBool(true) } func (v *VTS) canSpendAt(ic *interop.Context, args []stackitem.Item) stackitem.Item { account := toUint160(args[0]) vendor := toUint160(args[1]) amount := toBigInt(args[2]) if amount.Sign() <= 0 { return stackitem.NewBool(false) } // Check vendor vendorInfo := v.getVendorInternal(ic.DAO, vendor) if vendorInfo == nil || !vendorInfo.Active { return stackitem.NewBool(false) } // Check age verification for age-restricted vendors if vendorInfo.AgeRestricted { if !v.Vita.IsAdultVerified(ic.DAO, account) { return stackitem.NewBool(false) } } // Get balance bal := v.getBalanceInternal(ic.DAO, account) // Calculate available funds available := new(big.Int).Set(&bal.Unrestricted) for category, catBal := range bal.Restricted { if vendorInfo.Categories&category != 0 { available.Add(available, catBal) } } return stackitem.NewBool(available.Cmp(amount) >= 0) } // ============ Tax Accounting ============ func (v *VTS) payWage(ic *interop.Context, args []stackitem.Item) stackitem.Item { employer := toUint160(args[0]) employee := toUint160(args[1]) grossAmount := toBigInt(args[2]) taxRate := toUint64(args[3]) // basis points (e.g., 2500 = 25%) if grossAmount.Sign() <= 0 { panic("gross amount must be positive") } if taxRate > 10000 { panic("tax rate cannot exceed 100%") } // Check witness for employer caller := ic.VM.GetCallingScriptHash() if !employer.Equals(caller) { ok, err := checkWitness(ic, employer) if err != nil || !ok { panic("invalid signature") } } // Get employer balance employerBal := v.getBalanceInternal(ic.DAO, employer) if employerBal.Unrestricted.Cmp(grossAmount) < 0 { panic("insufficient funds") } // Calculate tax and net amounts taxAmount := new(big.Int).Mul(grossAmount, big.NewInt(int64(taxRate))) taxAmount.Div(taxAmount, big.NewInt(10000)) netAmount := new(big.Int).Sub(grossAmount, taxAmount) // Get tax config for treasury address taxConfig := v.getTaxConfigInternal(ic.DAO) // Deduct from employer employerBal.Unrestricted.Sub(&employerBal.Unrestricted, grossAmount) v.putBalance(ic.DAO, employer, employerBal) // Credit employee (net) employeeBal := v.getBalanceInternal(ic.DAO, employee) employeeBal.Unrestricted.Add(&employeeBal.Unrestricted, netAmount) v.putBalance(ic.DAO, employee, employeeBal) // Credit treasury (tax) if taxAmount.Sign() > 0 && !taxConfig.TreasuryAddress.Equals(util.Uint160{}) { treasuryBal := v.getBalanceInternal(ic.DAO, taxConfig.TreasuryAddress) treasuryBal.Unrestricted.Add(&treasuryBal.Unrestricted, taxAmount) v.putBalance(ic.DAO, taxConfig.TreasuryAddress, treasuryBal) } // Store transaction record for tax reporting record := &state.TransactionRecord{ TxHash: ic.Tx.Hash(), BlockHeight: ic.BlockHeight(), From: employer, To: employee, Amount: *grossAmount, TxType: state.TxTypeIncome, Category: state.CategoryUnrestricted, TaxWithheld: *taxAmount, TaxRate: uint16(taxRate), } v.storeTransactionRecord(ic.DAO, record) // Emit events v.emitTransfer(ic, &employer, &employee, netAmount) if taxAmount.Sign() > 0 && !taxConfig.TreasuryAddress.Equals(util.Uint160{}) { v.emitTransfer(ic, &employer, &taxConfig.TreasuryAddress, taxAmount) } ic.AddNotification(v.Hash, "TaxWithheld", stackitem.NewArray([]stackitem.Item{ stackitem.NewByteArray(employer.BytesBE()), stackitem.NewByteArray(employee.BytesBE()), stackitem.NewBigInteger(grossAmount), stackitem.NewBigInteger(taxAmount), stackitem.NewBigInteger(big.NewInt(int64(taxRate))), })) return stackitem.NewBool(true) } func (v *VTS) setTaxConfig(ic *interop.Context, args []stackitem.Item) stackitem.Item { if !v.checkCommittee(ic) { panic("caller is not a committee member") } incomeRate := uint16(toUint64(args[0])) salesRate := uint16(toUint64(args[1])) treasuryAddress := toUint160(args[2]) exemptCategories := toUint8(args[3]) if incomeRate > 10000 || salesRate > 10000 { panic("rate cannot exceed 100%") } cfg := &state.TaxConfig{ DefaultIncomeRate: incomeRate, DefaultSalesRate: salesRate, TreasuryAddress: treasuryAddress, ExemptCategories: exemptCategories, } v.putTaxConfig(ic.DAO, cfg) return stackitem.NewBool(true) } func (v *VTS) getTaxConfig(ic *interop.Context, _ []stackitem.Item) stackitem.Item { cfg := v.getTaxConfigInternal(ic.DAO) item, _ := cfg.ToStackItem() return item } func (v *VTS) getTaxConfigInternal(d *dao.Simple) *state.TaxConfig { si := d.GetStorageItem(v.ID, vtsTaxConfigKey) if si == nil { return &state.TaxConfig{} } item, err := stackitem.Deserialize(si) if err != nil { return &state.TaxConfig{} } cfg := &state.TaxConfig{} if err := cfg.FromStackItem(item); err != nil { return &state.TaxConfig{} } return cfg } func (v *VTS) putTaxConfig(d *dao.Simple, cfg *state.TaxConfig) { item, _ := cfg.ToStackItem() data, _ := stackitem.Serialize(item) d.PutStorageItem(v.ID, vtsTaxConfigKey, data) } func (v *VTS) issueTaxRefund(ic *interop.Context, args []stackitem.Item) stackitem.Item { if !v.checkCommittee(ic) { panic("caller is not a committee member") } account := toUint160(args[0]) amount := toBigInt(args[1]) if amount.Sign() <= 0 { panic("amount must be positive") } // Mint new VTS as refund (comes from treasury/system) v.mintUnrestricted(ic, account, amount) ic.AddNotification(v.Hash, "TaxRefunded", stackitem.NewArray([]stackitem.Item{ stackitem.NewByteArray(account.BytesBE()), stackitem.NewBigInteger(amount), })) return stackitem.NewBool(true) } // ============ Tax Reporting Methods ============ // makeTxRecordKey creates a storage key for a transaction record. func (v *VTS) makeTxRecordKey(blockHeight uint32, txIndex uint32) []byte { key := make([]byte, 9) key[0] = vtsPrefixTxRecord binary.BigEndian.PutUint32(key[1:5], blockHeight) binary.BigEndian.PutUint32(key[5:9], txIndex) return key } // makeAccountTxIndexKey creates a storage key for an account's transaction index. func (v *VTS) makeAccountTxIndexKey(account util.Uint160, blockHeight uint32) []byte { key := make([]byte, 25) key[0] = vtsPrefixAccountTxs copy(key[1:21], account.BytesBE()) binary.BigEndian.PutUint32(key[21:25], blockHeight) return key } // makeTaxWithheldKey creates a storage key for cumulative tax withheld. func (v *VTS) makeTaxWithheldKey(account util.Uint160, blockHeight uint32) []byte { key := make([]byte, 25) key[0] = vtsPrefixTaxWithheld copy(key[1:21], account.BytesBE()) binary.BigEndian.PutUint32(key[21:25], blockHeight) return key } // storeTransactionRecord stores a transaction record and updates indexes. func (v *VTS) storeTransactionRecord(d *dao.Simple, record *state.TransactionRecord) { // Get current tx index for this block txIndex := v.getNextTxIndex(d, record.BlockHeight) // Store the record key := v.makeTxRecordKey(record.BlockHeight, txIndex) item, _ := record.ToStackItem() data, err := stackitem.Serialize(item) if err != nil { panic("failed to serialize transaction record") } d.PutStorageItem(v.ID, key, data) // Update account indexes for both from and to v.addAccountTxIndex(d, record.From, record.BlockHeight, txIndex) if !record.To.Equals(record.From) { v.addAccountTxIndex(d, record.To, record.BlockHeight, txIndex) } // Update cumulative tax withheld for recipient (income tax) if record.TxType == state.TxTypeIncome && record.TaxWithheld.Sign() > 0 { v.addCumulativeTaxWithheld(d, record.To, record.BlockHeight, &record.TaxWithheld) } // Increment tx count for this block v.putTxCount(d, record.BlockHeight, txIndex+1) } // getNextTxIndex returns the next transaction index for a block. func (v *VTS) getNextTxIndex(d *dao.Simple, blockHeight uint32) uint32 { return v.getTxCount(d, blockHeight) } // makeTxCountKey creates a storage key for transaction count per block. func (v *VTS) makeTxCountKey(blockHeight uint32) []byte { key := make([]byte, 5) key[0] = vtsPrefixTxRecord binary.BigEndian.PutUint32(key[1:5], blockHeight) return key } // getTxCount returns the transaction count for a block. func (v *VTS) getTxCount(d *dao.Simple, blockHeight uint32) uint32 { key := v.makeTxCountKey(blockHeight) si := d.GetStorageItem(v.ID, key) if si == nil || len(si) < 4 { return 0 } return binary.BigEndian.Uint32(si) } // putTxCount stores the transaction count for a block. func (v *VTS) putTxCount(d *dao.Simple, blockHeight uint32, count uint32) { key := v.makeTxCountKey(blockHeight) data := make([]byte, 4) binary.BigEndian.PutUint32(data, count) d.PutStorageItem(v.ID, key, data) } // addAccountTxIndex adds a transaction index to an account's list. func (v *VTS) addAccountTxIndex(d *dao.Simple, account util.Uint160, blockHeight uint32, txIndex uint32) { key := v.makeAccountTxIndexKey(account, blockHeight) existing := d.GetStorageItem(v.ID, key) // Append new index newData := make([]byte, len(existing)+4) copy(newData, existing) binary.BigEndian.PutUint32(newData[len(existing):], txIndex) d.PutStorageItem(v.ID, key, newData) } // addCumulativeTaxWithheld adds to the cumulative tax withheld for an account. func (v *VTS) addCumulativeTaxWithheld(d *dao.Simple, account util.Uint160, blockHeight uint32, amount *big.Int) { key := v.makeTaxWithheldKey(account, blockHeight) existing := d.GetStorageItem(v.ID, key) cumulative := big.NewInt(0) if len(existing) > 0 { cumulative.SetBytes(existing) } cumulative.Add(cumulative, amount) d.PutStorageItem(v.ID, key, cumulative.Bytes()) } // getTransactions returns transactions for an account in a block range. func (v *VTS) getTransactions(ic *interop.Context, args []stackitem.Item) stackitem.Item { account := toUint160(args[0]) startBlock := uint32(toUint64(args[1])) endBlock := uint32(toUint64(args[2])) if endBlock < startBlock { panic("endBlock must be >= startBlock") } // Limit range to prevent DoS maxRange := uint32(10000) if endBlock-startBlock > maxRange { panic("block range too large") } var records []stackitem.Item for block := startBlock; block <= endBlock; block++ { // Get transaction indexes for this account at this block key := v.makeAccountTxIndexKey(account, block) indexData := ic.DAO.GetStorageItem(v.ID, key) // Parse indexes for i := 0; i+4 <= len(indexData); i += 4 { txIndex := binary.BigEndian.Uint32(indexData[i : i+4]) // Get the transaction record recordKey := v.makeTxRecordKey(block, txIndex) recordData := ic.DAO.GetStorageItem(v.ID, recordKey) if len(recordData) > 0 { item, err := stackitem.Deserialize(recordData) if err == nil { records = append(records, item) } } } } return stackitem.NewArray(records) } // getIncomeForPeriod returns total taxable income for an account in a block range. func (v *VTS) getIncomeForPeriod(ic *interop.Context, args []stackitem.Item) stackitem.Item { account := toUint160(args[0]) startBlock := uint32(toUint64(args[1])) endBlock := uint32(toUint64(args[2])) if endBlock < startBlock { panic("endBlock must be >= startBlock") } totalIncome := big.NewInt(0) for block := startBlock; block <= endBlock; block++ { key := v.makeAccountTxIndexKey(account, block) indexData := ic.DAO.GetStorageItem(v.ID, key) for i := 0; i+4 <= len(indexData); i += 4 { txIndex := binary.BigEndian.Uint32(indexData[i : i+4]) recordKey := v.makeTxRecordKey(block, txIndex) recordData := ic.DAO.GetStorageItem(v.ID, recordKey) if len(recordData) > 0 { item, err := stackitem.Deserialize(recordData) if err == nil { var record state.TransactionRecord if record.FromStackItem(item) == nil { // Only count income where this account is the recipient if record.To.Equals(account) && record.TxType == state.TxTypeIncome { totalIncome.Add(totalIncome, &record.Amount) } } } } } } return stackitem.NewBigInteger(totalIncome) } // getTaxWithheld returns total tax withheld for an account in a block range. func (v *VTS) getTaxWithheld(ic *interop.Context, args []stackitem.Item) stackitem.Item { account := toUint160(args[0]) startBlock := uint32(toUint64(args[1])) endBlock := uint32(toUint64(args[2])) if endBlock < startBlock { panic("endBlock must be >= startBlock") } totalTax := big.NewInt(0) for block := startBlock; block <= endBlock; block++ { key := v.makeTaxWithheldKey(account, block) data := ic.DAO.GetStorageItem(v.ID, key) if len(data) > 0 { blockTax := new(big.Int).SetBytes(data) totalTax.Add(totalTax, blockTax) } } return stackitem.NewBigInteger(totalTax) } // getDeductibleExpenses returns total deductible expenses for an account in a block range. func (v *VTS) getDeductibleExpenses(ic *interop.Context, args []stackitem.Item) stackitem.Item { account := toUint160(args[0]) startBlock := uint32(toUint64(args[1])) endBlock := uint32(toUint64(args[2])) category := toUint8(args[3]) if endBlock < startBlock { panic("endBlock must be >= startBlock") } totalExpenses := big.NewInt(0) for block := startBlock; block <= endBlock; block++ { key := v.makeAccountTxIndexKey(account, block) indexData := ic.DAO.GetStorageItem(v.ID, key) for i := 0; i+4 <= len(indexData); i += 4 { txIndex := binary.BigEndian.Uint32(indexData[i : i+4]) recordKey := v.makeTxRecordKey(block, txIndex) recordData := ic.DAO.GetStorageItem(v.ID, recordKey) if len(recordData) > 0 { item, err := stackitem.Deserialize(recordData) if err == nil { var record state.TransactionRecord if record.FromStackItem(item) == nil { // Count expenses where this account is the sender if record.From.Equals(account) && record.TxType == state.TxTypeExpense { // Filter by category if specified if category == 0 || record.Category == category { totalExpenses.Add(totalExpenses, &record.Amount) } } } } } } } return stackitem.NewBigInteger(totalExpenses) } // getTaxSummary returns a tax summary for an account in a block range. func (v *VTS) getTaxSummary(ic *interop.Context, args []stackitem.Item) stackitem.Item { account := toUint160(args[0]) startBlock := uint32(toUint64(args[1])) endBlock := uint32(toUint64(args[2])) if endBlock < startBlock { panic("endBlock must be >= startBlock") } summary := &state.TaxSummary{ Account: account, StartBlock: startBlock, EndBlock: endBlock, } // Iterate through all transactions in the period for block := startBlock; block <= endBlock; block++ { // Get cumulative tax for this block taxKey := v.makeTaxWithheldKey(account, block) taxData := ic.DAO.GetStorageItem(v.ID, taxKey) if len(taxData) > 0 { blockTax := new(big.Int).SetBytes(taxData) summary.TaxWithheld.Add(&summary.TaxWithheld, blockTax) } // Get transaction details key := v.makeAccountTxIndexKey(account, block) indexData := ic.DAO.GetStorageItem(v.ID, key) for i := 0; i+4 <= len(indexData); i += 4 { txIndex := binary.BigEndian.Uint32(indexData[i : i+4]) recordKey := v.makeTxRecordKey(block, txIndex) recordData := ic.DAO.GetStorageItem(v.ID, recordKey) if len(recordData) > 0 { item, err := stackitem.Deserialize(recordData) if err == nil { var record state.TransactionRecord if record.FromStackItem(item) == nil { if record.To.Equals(account) { switch record.TxType { case state.TxTypeIncome: summary.TotalIncome.Add(&summary.TotalIncome, &record.Amount) case state.TxTypeBenefit: summary.TotalBenefits.Add(&summary.TotalBenefits, &record.Amount) } } if record.From.Equals(account) && record.TxType == state.TxTypeExpense { summary.TotalExpenses.Add(&summary.TotalExpenses, &record.Amount) // Medical and Education expenses are typically deductible if record.Category == state.CategoryMedical || record.Category == state.CategoryEducation { summary.DeductibleExpenses.Add(&summary.DeductibleExpenses, &record.Amount) } } } } } } } // Calculate estimated tax owed (simple flat rate calculation) taxConfig := v.getTaxConfigInternal(ic.DAO) if taxConfig.DefaultIncomeRate > 0 { taxOwed := new(big.Int).Mul(&summary.TotalIncome, big.NewInt(int64(taxConfig.DefaultIncomeRate))) taxOwed.Div(taxOwed, big.NewInt(10000)) summary.EstimatedOwed = *taxOwed } // Balance = TaxWithheld - EstimatedOwed (positive = refund due) summary.Balance.Sub(&summary.TaxWithheld, &summary.EstimatedOwed) item, _ := summary.ToStackItem() return item } // ============ Public Internal Methods for Cross-Contract Use ============ // BalanceOf returns VTS balance for the account. func (v *VTS) BalanceOf(d *dao.Simple, acc util.Uint160) *big.Int { bal := v.getBalanceInternal(d, acc) return bal.Total() } // UnrestrictedBalanceOf returns unrestricted VTS balance for the account. func (v *VTS) UnrestrictedBalanceOf(d *dao.Simple, acc util.Uint160) *big.Int { bal := v.getBalanceInternal(d, acc) return &bal.Unrestricted } // Mint mints unrestricted VTS to the account. func (v *VTS) Mint(ic *interop.Context, to util.Uint160, amount *big.Int) { v.mintUnrestricted(ic, to, amount) } // MintRestricted mints restricted VTS to the account. func (v *VTS) MintRestricted(ic *interop.Context, to util.Uint160, amount *big.Int, category uint8) { v.mintRestrictedInternal(ic, to, amount, category) } // Burn burns unrestricted VTS from the account. func (v *VTS) Burn(ic *interop.Context, from util.Uint160, amount *big.Int) { bal := v.getBalanceInternal(ic.DAO, from) if bal.Unrestricted.Cmp(amount) < 0 { panic("insufficient funds") } bal.Unrestricted.Sub(&bal.Unrestricted, amount) v.putBalance(ic.DAO, from, bal) supply := v.getTotalSupplyInternal(ic.DAO) supply.Sub(supply, amount) v.putTotalSupply(ic.DAO, supply) v.emitTransfer(ic, &from, nil, amount) } // IsVendor returns true if the address is a registered active vendor. func (v *VTS) IsVendor(d *dao.Simple, addr util.Uint160) bool { vendor := v.getVendorInternal(d, addr) return vendor != nil && vendor.Active } // ============ Helper Functions ============ func checkWitness(ic *interop.Context, h util.Uint160) (bool, error) { return runtime.CheckHashedWitness(ic, h) }