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

1608 lines
51 KiB
Go

package native
import (
"encoding/binary"
"errors"
"math/big"
"github.com/tutus-one/tutus-chain/pkg/config"
"github.com/tutus-one/tutus-chain/pkg/core/dao"
"github.com/tutus-one/tutus-chain/pkg/core/interop"
"github.com/tutus-one/tutus-chain/pkg/core/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
Tutus ITutus
RoleRegistry IRoleRegistry
Vita IVita
Lex ILex
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.Tutus != nil {
return v.Tutus.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")
}
}
// Check property rights via Lex (if Lex is wired)
if v.Lex != nil && !v.Lex.CheckPropertyRight(ic.DAO, from, ic.Block.Index) {
return errors.New("property rights restricted")
}
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")
}
}
// Check property rights via Lex (if Lex is wired)
if v.Lex != nil && !v.Lex.CheckPropertyRight(ic.DAO, from, ic.Block.Index) {
panic("property rights restricted")
}
// 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)
}