1608 lines
51 KiB
Go
Executable File
1608 lines
51 KiB
Go
Executable File
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)
|
|
}
|