Add VTS (Value Transfer System) native contract

VTS is programmable money with spending restrictions and automatic tax
accounting, designed to replace GAS as the primary user-facing currency.

Key features:
- NEP-17 compliant token (symbol: VTS, decimals: 8)
- Spending category restrictions (Food, Shelter, Medical, Education, Transport)
- Vendor registration and management
- Restricted VTS can only be spent at matching vendor categories
- Automatic tax withholding via payWage()
- Transaction recording for tax reporting
- Tax summary queries (getTransactions, getIncomeForPeriod, getTaxWithheld,
  getDeductibleExpenses, getTaxSummary)

Files added:
- pkg/core/native/vts.go: Main VTS contract implementation
- pkg/core/state/vts.go: VTS state structures (VTSBalance, Vendor, TaxConfig, etc.)
- pkg/core/native/native_test/vts_test.go: Comprehensive test suite

Files modified:
- pkg/core/native/contract.go: Added IVTS interface and VTS() accessor
- pkg/core/native/nativenames/names.go: Added VTS constant
- pkg/core/native/nativeids/ids.go: Added VTS ID (-14)
- pkg/core/native/nativehashes/hashes.go: Added VTS hash
- pkg/core/native/native_test/management_test.go: Updated test fixture

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Tutus Development 2025-12-20 02:57:08 +00:00
parent de34f66286
commit 86f5e127c0
8 changed files with 2588 additions and 1 deletions

View File

@ -134,6 +134,25 @@ type (
// HasPermissionInternal checks if address has permission via roles.
HasPermissionInternal(d *dao.Simple, address util.Uint160, resource, action string, scope state.Scope, blockHeight uint32) bool
}
// IVTS is an interface required from native VTS contract for
// interaction with Blockchain and other native contracts.
// VTS is programmable money with spending restrictions and automatic tax accounting.
IVTS interface {
interop.Contract
// BalanceOf returns total VTS balance (unrestricted + all restricted).
BalanceOf(d *dao.Simple, acc util.Uint160) *big.Int
// UnrestrictedBalanceOf returns only unrestricted VTS balance.
UnrestrictedBalanceOf(d *dao.Simple, acc util.Uint160) *big.Int
// Mint mints unrestricted VTS to the account.
Mint(ic *interop.Context, to util.Uint160, amount *big.Int)
// MintRestricted mints VTS with spending category restrictions.
MintRestricted(ic *interop.Context, to util.Uint160, amount *big.Int, category uint8)
// Burn burns unrestricted VTS from the account.
Burn(ic *interop.Context, from util.Uint160, amount *big.Int)
// IsVendor returns true if address is a registered active vendor.
IsVendor(d *dao.Simple, addr util.Uint160) bool
}
)
// Contracts is a convenient wrapper around an arbitrary set of native contracts
@ -261,6 +280,12 @@ func (cs *Contracts) RoleRegistry() IRoleRegistry {
return cs.ByName(nativenames.RoleRegistry).(IRoleRegistry)
}
// VTS returns native IVTS contract implementation. It panics if
// there's no contract with proper name in cs.
func (cs *Contracts) VTS() IVTS {
return cs.ByName(nativenames.VTS).(IVTS)
}
// NewDefaultContracts returns a new set of default native contracts.
func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract {
mgmt := NewManagement()
@ -319,6 +344,11 @@ func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract {
// Set RoleRegistry on PersonToken for cross-contract integration
personToken.RoleRegistry = roleRegistry
// Create VTS (Value Transfer System) contract
vts := newVTS()
vts.NEO = neo
vts.RoleRegistry = roleRegistry
return []interop.Contract{
mgmt,
s,
@ -333,5 +363,6 @@ func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract {
treasury,
personToken,
roleRegistry,
vts,
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,527 @@
package native_test
import (
"testing"
"github.com/stretchr/testify/require"
"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/neotest"
"github.com/tutus-one/tutus-chain/pkg/vm/stackitem"
)
func newVTSClient(t *testing.T) *neotest.ContractInvoker {
return newNativeClient(t, nativenames.VTS)
}
// TestVTS_NEP17Compliance tests basic NEP-17 methods.
func TestVTS_NEP17Compliance(t *testing.T) {
c := newVTSClient(t)
t.Run("symbol", func(t *testing.T) {
c.Invoke(t, "VTS", "symbol")
})
t.Run("decimals", func(t *testing.T) {
c.Invoke(t, 8, "decimals")
})
t.Run("totalSupply initial", func(t *testing.T) {
c.Invoke(t, 0, "totalSupply")
})
t.Run("balanceOf empty account", func(t *testing.T) {
acc := c.NewAccount(t)
c.Invoke(t, 0, "balanceOf", acc.ScriptHash())
})
}
// TestVTS_Mint tests minting unrestricted VTS.
func TestVTS_Mint(t *testing.T) {
c := newVTSClient(t)
e := c.Executor
acc := e.NewAccount(t)
committeeInvoker := c.WithSigners(c.Committee)
userInvoker := c.WithSigners(acc)
t.Run("non-committee cannot mint", func(t *testing.T) {
userInvoker.InvokeFail(t, "caller is not a committee member", "mint", acc.ScriptHash(), 1000_00000000)
})
t.Run("committee can mint", func(t *testing.T) {
committeeInvoker.Invoke(t, true, "mint", acc.ScriptHash(), 1000_00000000)
})
t.Run("balance updated", func(t *testing.T) {
c.Invoke(t, 1000_00000000, "balanceOf", acc.ScriptHash())
})
t.Run("unrestricted balance updated", func(t *testing.T) {
c.Invoke(t, 1000_00000000, "unrestrictedBalanceOf", acc.ScriptHash())
})
t.Run("totalSupply updated", func(t *testing.T) {
c.Invoke(t, 1000_00000000, "totalSupply")
})
}
// TestVTS_MintRestricted tests minting restricted VTS.
func TestVTS_MintRestricted(t *testing.T) {
c := newVTSClient(t)
e := c.Executor
acc := e.NewAccount(t)
committeeInvoker := c.WithSigners(c.Committee)
t.Run("mint food-restricted VTS", func(t *testing.T) {
committeeInvoker.Invoke(t, true, "mintRestricted", acc.ScriptHash(), 500_00000000, state.CategoryFood)
})
t.Run("total balance includes restricted", func(t *testing.T) {
c.Invoke(t, 500_00000000, "balanceOf", acc.ScriptHash())
})
t.Run("unrestricted is still zero", func(t *testing.T) {
c.Invoke(t, 0, "unrestrictedBalanceOf", acc.ScriptHash())
})
t.Run("restricted balance correct", func(t *testing.T) {
c.Invoke(t, 500_00000000, "restrictedBalanceOf", acc.ScriptHash(), state.CategoryFood)
})
t.Run("other restricted category is zero", func(t *testing.T) {
c.Invoke(t, 0, "restrictedBalanceOf", acc.ScriptHash(), state.CategoryShelter)
})
}
// TestVTS_Burn tests burning unrestricted VTS.
func TestVTS_Burn(t *testing.T) {
c := newVTSClient(t)
e := c.Executor
acc := e.NewAccount(t)
committeeInvoker := c.WithSigners(c.Committee)
userInvoker := c.WithSigners(acc)
// Mint first
committeeInvoker.Invoke(t, true, "mint", acc.ScriptHash(), 1000_00000000)
t.Run("non-committee cannot burn", func(t *testing.T) {
userInvoker.InvokeFail(t, "caller is not a committee member", "burn", acc.ScriptHash(), 100_00000000)
})
t.Run("committee can burn", func(t *testing.T) {
committeeInvoker.Invoke(t, true, "burn", acc.ScriptHash(), 100_00000000)
})
t.Run("balance reduced", func(t *testing.T) {
c.Invoke(t, 900_00000000, "balanceOf", acc.ScriptHash())
})
t.Run("totalSupply reduced", func(t *testing.T) {
c.Invoke(t, 900_00000000, "totalSupply")
})
}
// TestVTS_Transfer tests transferring unrestricted VTS.
func TestVTS_Transfer(t *testing.T) {
c := newVTSClient(t)
e := c.Executor
sender := e.NewAccount(t)
receiver := e.NewAccount(t)
committeeInvoker := c.WithSigners(c.Committee)
senderInvoker := c.WithSigners(sender)
// Mint to sender
committeeInvoker.Invoke(t, true, "mint", sender.ScriptHash(), 1000_00000000)
t.Run("transfer without signature fails", func(t *testing.T) {
// Transfer returns false (not panic) when signature is missing
c.Invoke(t, false, "transfer", sender.ScriptHash(), receiver.ScriptHash(), 100_00000000, nil)
})
t.Run("transfer with signature succeeds", func(t *testing.T) {
senderInvoker.Invoke(t, true, "transfer", sender.ScriptHash(), receiver.ScriptHash(), 100_00000000, nil)
})
t.Run("sender balance reduced", func(t *testing.T) {
c.Invoke(t, 900_00000000, "balanceOf", sender.ScriptHash())
})
t.Run("receiver balance increased", func(t *testing.T) {
c.Invoke(t, 100_00000000, "balanceOf", receiver.ScriptHash())
})
t.Run("cannot transfer more than balance", func(t *testing.T) {
senderInvoker.Invoke(t, false, "transfer", sender.ScriptHash(), receiver.ScriptHash(), 10000_00000000, nil)
})
t.Run("cannot transfer negative", func(t *testing.T) {
// Transfer returns false (not panic) for negative amounts
senderInvoker.Invoke(t, false, "transfer", sender.ScriptHash(), receiver.ScriptHash(), -1, nil)
})
}
// TestVTS_VendorRegistration tests vendor management.
func TestVTS_VendorRegistration(t *testing.T) {
c := newVTSClient(t)
e := c.Executor
vendorAcc := e.NewAccount(t)
userAcc := e.NewAccount(t)
committeeInvoker := c.WithSigners(c.Committee)
userInvoker := c.WithSigners(userAcc)
t.Run("non-committee cannot register vendor", func(t *testing.T) {
userInvoker.InvokeFail(t, "caller is not a committee member", "registerVendor", vendorAcc.ScriptHash(), "Test Store", state.CategoryFood)
})
t.Run("committee can register vendor", func(t *testing.T) {
committeeInvoker.Invoke(t, true, "registerVendor", vendorAcc.ScriptHash(), "Test Store", state.CategoryFood)
})
t.Run("isVendor returns true", func(t *testing.T) {
c.Invoke(t, true, "isVendor", vendorAcc.ScriptHash())
})
t.Run("non-vendor returns false", func(t *testing.T) {
c.Invoke(t, false, "isVendor", userAcc.ScriptHash())
})
t.Run("getVendorCategories", func(t *testing.T) {
c.Invoke(t, int64(state.CategoryFood), "getVendorCategories", vendorAcc.ScriptHash())
})
t.Run("getVendor returns vendor info", func(t *testing.T) {
c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
require.Equal(t, 1, len(stack))
arr, ok := stack[0].Value().([]stackitem.Item)
require.True(t, ok, "expected array")
require.Equal(t, 6, len(arr)) // Vendor struct has 6 fields
}, "getVendor", vendorAcc.ScriptHash())
})
t.Run("update vendor categories", func(t *testing.T) {
newCategories := state.CategoryFood | state.CategoryShelter
committeeInvoker.Invoke(t, true, "updateVendor", vendorAcc.ScriptHash(), "Test Store Updated", newCategories)
c.Invoke(t, int64(newCategories), "getVendorCategories", vendorAcc.ScriptHash())
})
t.Run("deactivate vendor", func(t *testing.T) {
committeeInvoker.Invoke(t, true, "deactivateVendor", vendorAcc.ScriptHash())
// isVendor should still return true (vendor exists) but Active=false
c.Invoke(t, false, "isVendor", vendorAcc.ScriptHash())
})
}
// TestVTS_Spend tests spending at vendors.
func TestVTS_Spend(t *testing.T) {
c := newVTSClient(t)
e := c.Executor
customer := e.NewAccount(t)
foodVendor := e.NewAccount(t)
committeeInvoker := c.WithSigners(c.Committee)
customerInvoker := c.WithSigners(customer)
// Setup: Register food vendor and mint to customer
committeeInvoker.Invoke(t, true, "registerVendor", foodVendor.ScriptHash(), "Food Store", state.CategoryFood)
committeeInvoker.Invoke(t, true, "mint", customer.ScriptHash(), 300_00000000)
t.Run("customer can spend at vendor", func(t *testing.T) {
customerInvoker.Invoke(t, true, "spend", customer.ScriptHash(), foodVendor.ScriptHash(), 50_00000000, nil)
})
t.Run("vendor received VTS", func(t *testing.T) {
c.Invoke(t, 50_00000000, "balanceOf", foodVendor.ScriptHash())
})
t.Run("customer balance reduced", func(t *testing.T) {
c.Invoke(t, 250_00000000, "balanceOf", customer.ScriptHash())
})
t.Run("cannot spend at non-vendor", func(t *testing.T) {
nonVendor := e.NewAccount(t)
customerInvoker.InvokeFail(t, "invalid or inactive vendor", "spend", customer.ScriptHash(), nonVendor.ScriptHash(), 10_00000000, nil)
})
}
// TestVTS_CanSpendAt tests the canSpendAt query method.
func TestVTS_CanSpendAt(t *testing.T) {
c := newVTSClient(t)
e := c.Executor
customer := e.NewAccount(t)
vendor := e.NewAccount(t)
committeeInvoker := c.WithSigners(c.Committee)
// Setup
committeeInvoker.Invoke(t, true, "registerVendor", vendor.ScriptHash(), "Store", state.CategoryFood)
committeeInvoker.Invoke(t, true, "mint", customer.ScriptHash(), 100_00000000)
t.Run("can spend within balance", func(t *testing.T) {
c.Invoke(t, true, "canSpendAt", customer.ScriptHash(), vendor.ScriptHash(), 50_00000000)
})
t.Run("cannot spend more than balance", func(t *testing.T) {
c.Invoke(t, false, "canSpendAt", customer.ScriptHash(), vendor.ScriptHash(), 200_00000000)
})
}
// TestVTS_ConvertToUnrestricted tests converting restricted VTS to unrestricted.
func TestVTS_ConvertToUnrestricted(t *testing.T) {
c := newVTSClient(t)
e := c.Executor
acc := e.NewAccount(t)
committeeInvoker := c.WithSigners(c.Committee)
userInvoker := c.WithSigners(acc)
// Mint restricted VTS
committeeInvoker.Invoke(t, true, "mintRestricted", acc.ScriptHash(), 500_00000000, state.CategoryFood)
t.Run("non-committee cannot convert", func(t *testing.T) {
userInvoker.InvokeFail(t, "caller is not a committee member", "convertToUnrestricted", acc.ScriptHash(), state.CategoryFood, 100_00000000)
})
t.Run("committee can convert", func(t *testing.T) {
committeeInvoker.Invoke(t, true, "convertToUnrestricted", acc.ScriptHash(), state.CategoryFood, 100_00000000)
})
t.Run("restricted balance reduced", func(t *testing.T) {
c.Invoke(t, 400_00000000, "restrictedBalanceOf", acc.ScriptHash(), state.CategoryFood)
})
t.Run("unrestricted balance increased", func(t *testing.T) {
c.Invoke(t, 100_00000000, "unrestrictedBalanceOf", acc.ScriptHash())
})
t.Run("total balance unchanged", func(t *testing.T) {
c.Invoke(t, 500_00000000, "balanceOf", acc.ScriptHash())
})
}
// TestVTS_TaxConfig tests tax configuration.
func TestVTS_TaxConfig(t *testing.T) {
c := newVTSClient(t)
e := c.Executor
treasuryAcc := e.NewAccount(t)
userAcc := e.NewAccount(t)
committeeInvoker := c.WithSigners(c.Committee)
userInvoker := c.WithSigners(userAcc)
t.Run("non-committee cannot set tax config", func(t *testing.T) {
userInvoker.InvokeFail(t, "caller is not a committee member", "setTaxConfig", 2500, 500, treasuryAcc.ScriptHash(), state.CategoryFood)
})
t.Run("committee can set tax config", func(t *testing.T) {
// 25% income tax, 5% sales tax
committeeInvoker.Invoke(t, true, "setTaxConfig", 2500, 500, treasuryAcc.ScriptHash(), state.CategoryFood)
})
t.Run("get tax config", func(t *testing.T) {
c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
require.Equal(t, 1, len(stack))
arr, ok := stack[0].Value().([]stackitem.Item)
require.True(t, ok, "expected array")
require.Equal(t, 4, len(arr)) // TaxConfig has 4 fields
incomeRate, _ := arr[0].TryInteger()
require.Equal(t, int64(2500), incomeRate.Int64())
}, "getTaxConfig")
})
}
// TestVTS_PayWage tests payroll with automatic tax withholding.
func TestVTS_PayWage(t *testing.T) {
c := newVTSClient(t)
e := c.Executor
employer := e.NewAccount(t)
employee := e.NewAccount(t)
treasury := e.NewAccount(t)
committeeInvoker := c.WithSigners(c.Committee)
employerInvoker := c.WithSigners(employer)
// Setup: Set tax config and fund employer
committeeInvoker.Invoke(t, true, "setTaxConfig", 2500, 0, treasury.ScriptHash(), 0) // 25% income tax
committeeInvoker.Invoke(t, true, "mint", employer.ScriptHash(), 10000_00000000)
t.Run("payWage with tax withholding", func(t *testing.T) {
// Pay $5000 gross, 25% tax rate
employerInvoker.Invoke(t, true, "payWage", employer.ScriptHash(), employee.ScriptHash(), 5000_00000000, 2500)
})
t.Run("employer balance reduced by gross", func(t *testing.T) {
c.Invoke(t, 5000_00000000, "balanceOf", employer.ScriptHash())
})
t.Run("employee receives net (75%)", func(t *testing.T) {
c.Invoke(t, 3750_00000000, "balanceOf", employee.ScriptHash())
})
t.Run("treasury receives tax (25%)", func(t *testing.T) {
c.Invoke(t, 1250_00000000, "balanceOf", treasury.ScriptHash())
})
}
// TestVTS_TaxRefund tests issuing tax refunds.
func TestVTS_TaxRefund(t *testing.T) {
c := newVTSClient(t)
e := c.Executor
acc := e.NewAccount(t)
treasury := e.NewAccount(t)
committeeInvoker := c.WithSigners(c.Committee)
userInvoker := c.WithSigners(acc)
// Setup: Set tax config
committeeInvoker.Invoke(t, true, "setTaxConfig", 2500, 0, treasury.ScriptHash(), 0)
t.Run("non-committee cannot issue refund", func(t *testing.T) {
userInvoker.InvokeFail(t, "caller is not a committee member", "issueTaxRefund", acc.ScriptHash(), 100_00000000)
})
t.Run("committee can issue refund", func(t *testing.T) {
committeeInvoker.Invoke(t, true, "issueTaxRefund", acc.ScriptHash(), 100_00000000)
})
t.Run("refund credited to account", func(t *testing.T) {
c.Invoke(t, 100_00000000, "balanceOf", acc.ScriptHash())
})
}
// TestVTS_BalanceDetails tests the balanceDetails query.
func TestVTS_BalanceDetails(t *testing.T) {
c := newVTSClient(t)
e := c.Executor
acc := e.NewAccount(t)
committeeInvoker := c.WithSigners(c.Committee)
// Mint mixed VTS
committeeInvoker.Invoke(t, true, "mint", acc.ScriptHash(), 100_00000000)
committeeInvoker.Invoke(t, true, "mintRestricted", acc.ScriptHash(), 200_00000000, state.CategoryFood)
committeeInvoker.Invoke(t, true, "mintRestricted", acc.ScriptHash(), 300_00000000, state.CategoryShelter)
t.Run("balanceDetails returns all categories", func(t *testing.T) {
c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
require.Equal(t, 1, len(stack))
arr, ok := stack[0].Value().([]stackitem.Item)
require.True(t, ok, "expected array")
// Should have unrestricted + categories with balances
require.GreaterOrEqual(t, len(arr), 1)
}, "balanceDetails", acc.ScriptHash())
})
}
// TestVTS_TaxReportingMethods tests the tax reporting query methods.
func TestVTS_TaxReportingMethods(t *testing.T) {
c := newVTSClient(t)
e := c.Executor
acc := e.NewAccount(t)
// Block range for queries
startBlock := int64(0)
endBlock := int64(1000)
t.Run("getTransactions returns array", func(t *testing.T) {
c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
require.Equal(t, 1, len(stack))
_, ok := stack[0].Value().([]stackitem.Item)
require.True(t, ok, "expected array")
}, "getTransactions", acc.ScriptHash(), startBlock, endBlock)
})
t.Run("getIncomeForPeriod returns integer", func(t *testing.T) {
c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
require.Equal(t, 1, len(stack))
_, err := stack[0].TryInteger()
require.NoError(t, err)
}, "getIncomeForPeriod", acc.ScriptHash(), startBlock, endBlock)
})
t.Run("getTaxWithheld returns integer", func(t *testing.T) {
c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
require.Equal(t, 1, len(stack))
_, err := stack[0].TryInteger()
require.NoError(t, err)
}, "getTaxWithheld", acc.ScriptHash(), startBlock, endBlock)
})
t.Run("getDeductibleExpenses returns integer", func(t *testing.T) {
c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
require.Equal(t, 1, len(stack))
_, err := stack[0].TryInteger()
require.NoError(t, err)
}, "getDeductibleExpenses", acc.ScriptHash(), startBlock, endBlock, 0)
})
t.Run("getTaxSummary returns array", func(t *testing.T) {
c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
require.Equal(t, 1, len(stack))
arr, ok := stack[0].Value().([]stackitem.Item)
require.True(t, ok, "expected array for tax summary")
// TaxSummary has 10 fields: Account, StartBlock, EndBlock, TotalIncome,
// TotalBenefits, TotalExpenses, DeductibleExpenses, TaxWithheld, EstimatedOwed, Balance
require.Equal(t, 10, len(arr))
}, "getTaxSummary", acc.ScriptHash(), startBlock, endBlock)
})
}
// TestVTS_NegativeAmount tests that negative amounts are rejected.
func TestVTS_NegativeAmount(t *testing.T) {
c := newVTSClient(t)
e := c.Executor
acc := e.NewAccount(t)
committeeInvoker := c.WithSigners(c.Committee)
t.Run("mint negative fails", func(t *testing.T) {
committeeInvoker.InvokeFail(t, "amount must be positive", "mint", acc.ScriptHash(), -1)
})
t.Run("mintRestricted negative fails", func(t *testing.T) {
committeeInvoker.InvokeFail(t, "amount must be positive", "mintRestricted", acc.ScriptHash(), -1, state.CategoryFood)
})
}
// TestVTS_MultiCategoryVendor tests vendors accepting multiple categories.
func TestVTS_MultiCategoryVendor(t *testing.T) {
c := newVTSClient(t)
e := c.Executor
customer := e.NewAccount(t)
generalStore := e.NewAccount(t)
committeeInvoker := c.WithSigners(c.Committee)
customerInvoker := c.WithSigners(customer)
// Register vendor accepting food AND shelter
categories := state.CategoryFood | state.CategoryShelter
committeeInvoker.Invoke(t, true, "registerVendor", generalStore.ScriptHash(), "General Store", categories)
// Mint restricted VTS for different categories
committeeInvoker.Invoke(t, true, "mintRestricted", customer.ScriptHash(), 100_00000000, state.CategoryFood)
committeeInvoker.Invoke(t, true, "mintRestricted", customer.ScriptHash(), 100_00000000, state.CategoryShelter)
t.Run("can spend at multi-category vendor", func(t *testing.T) {
customerInvoker.Invoke(t, true, "spend", customer.ScriptHash(), generalStore.ScriptHash(), 50_00000000, nil)
// Total balance should be reduced by 50
c.Invoke(t, 150_00000000, "balanceOf", customer.ScriptHash())
})
t.Run("can spend more at same vendor", func(t *testing.T) {
customerInvoker.Invoke(t, true, "spend", customer.ScriptHash(), generalStore.ScriptHash(), 50_00000000, nil)
customerInvoker.Invoke(t, true, "spend", customer.ScriptHash(), generalStore.ScriptHash(), 50_00000000, nil)
// Total balance should be reduced by 150
c.Invoke(t, 50_00000000, "balanceOf", customer.ScriptHash())
})
t.Run("vendor has correct total", func(t *testing.T) {
c.Invoke(t, 150_00000000, "balanceOf", generalStore.ScriptHash())
})
}

View File

@ -31,4 +31,10 @@ var (
Notary = util.Uint160{0x3b, 0xec, 0x35, 0x31, 0x11, 0x9b, 0xba, 0xd7, 0x6d, 0xd0, 0x44, 0x92, 0xb, 0xd, 0xe6, 0xc3, 0x19, 0x4f, 0xe1, 0xc1}
// Treasury is a hash of native Treasury contract.
Treasury = util.Uint160{0xc1, 0x3a, 0x56, 0xc9, 0x83, 0x53, 0xa7, 0xea, 0x6a, 0x32, 0x4d, 0x9a, 0x83, 0x5d, 0x1b, 0x5b, 0xf2, 0x26, 0x63, 0x15}
// PersonToken is a hash of native PersonToken contract.
PersonToken = util.Uint160{0x4, 0xaf, 0x34, 0xf1, 0xde, 0xdb, 0xa4, 0x7a, 0xd4, 0x30, 0xdf, 0xc7, 0x77, 0x1c, 0x26, 0x3a, 0x8a, 0x72, 0xa5, 0x21}
// RoleRegistry is a hash of native RoleRegistry contract.
RoleRegistry = util.Uint160{0xa9, 0x77, 0x74, 0xdc, 0x77, 0xc5, 0xcc, 0xf8, 0x1a, 0xd4, 0x90, 0xb5, 0x81, 0xb5, 0xf0, 0xc6, 0x61, 0x1, 0x20, 0x52}
// VTS is a hash of native VTS contract.
VTS = util.Uint160{0x68, 0x34, 0x5e, 0x82, 0x6d, 0x8a, 0xff, 0x41, 0x48, 0x23, 0x60, 0xd9, 0x83, 0xa3, 0xd0, 0xf9, 0xb7, 0x59, 0x36, 0x89}
)

View File

@ -33,4 +33,6 @@ const (
PersonToken int32 = -12
// RoleRegistry is an ID of native RoleRegistry contract.
RoleRegistry int32 = -13
// VTS is an ID of native VTS contract.
VTS int32 = -14
)

View File

@ -15,6 +15,7 @@ const (
Treasury = "Treasury"
PersonToken = "PersonToken"
RoleRegistry = "RoleRegistry"
VTS = "VTS"
)
// All contains the list of all native contract names ordered by the contract ID.
@ -32,6 +33,7 @@ var All = []string{
Treasury,
PersonToken,
RoleRegistry,
VTS,
}
// IsValid checks if the name is a valid native contract's name.
@ -48,5 +50,6 @@ func IsValid(name string) bool {
name == StdLib ||
name == Treasury ||
name == PersonToken ||
name == RoleRegistry
name == RoleRegistry ||
name == VTS
}

1565
pkg/core/native/vts.go Normal file

File diff suppressed because it is too large Load Diff

452
pkg/core/state/vts.go Normal file
View File

@ -0,0 +1,452 @@
package state
import (
"errors"
"fmt"
"math/big"
"github.com/tutus-one/tutus-chain/pkg/util"
"github.com/tutus-one/tutus-chain/pkg/vm/stackitem"
)
// Spending category constants (bitmask).
const (
CategoryUnrestricted uint8 = 0 // No restrictions (standard money)
CategoryFood uint8 = 1 << 0 // Bit 0: Food purchases
CategoryShelter uint8 = 1 << 1 // Bit 1: Rent/utilities/housing
CategoryMedical uint8 = 1 << 2 // Bit 2: Healthcare expenses
CategoryEducation uint8 = 1 << 3 // Bit 3: Tuition/books/supplies
CategoryTransport uint8 = 1 << 4 // Bit 4: Transportation
CategoryAll uint8 = 0xFF // All categories
)
// TxType represents the type of VTS transaction.
type TxType uint8
const (
TxTypeTransfer TxType = 0 // P2P transfer (neutral)
TxTypeIncome TxType = 1 // Wages, sales, dividends (taxable)
TxTypeExpense TxType = 2 // Purchases, bills (potentially deductible)
TxTypeBenefit TxType = 3 // Government benefits (tax-exempt income)
TxTypeGift TxType = 4 // Gift (may have gift tax implications)
TxTypeTax TxType = 5 // Tax payment/withholding
)
// VTSBalance represents the balance state of a VTS token holder.
// It tracks both unrestricted and category-restricted balances.
type VTSBalance struct {
Unrestricted big.Int // Unrestricted balance (can be spent anywhere)
Restricted map[uint8]*big.Int // Category -> restricted balance
}
// NewVTSBalance creates a new empty VTSBalance.
func NewVTSBalance() *VTSBalance {
return &VTSBalance{
Restricted: make(map[uint8]*big.Int),
}
}
// Total returns the total balance (unrestricted + all restricted).
func (b *VTSBalance) Total() *big.Int {
total := new(big.Int).Set(&b.Unrestricted)
for _, amt := range b.Restricted {
total.Add(total, amt)
}
return total
}
// ToStackItem converts VTSBalance to stackitem.
func (b *VTSBalance) ToStackItem() (stackitem.Item, error) {
// Format: [unrestricted, [[category, amount], [category, amount], ...]]
restrictedItems := make([]stackitem.Item, 0, len(b.Restricted))
for cat, amt := range b.Restricted {
if amt.Sign() > 0 {
restrictedItems = append(restrictedItems, stackitem.NewArray([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(int64(cat))),
stackitem.NewBigInteger(amt),
}))
}
}
return stackitem.NewStruct([]stackitem.Item{
stackitem.NewBigInteger(&b.Unrestricted),
stackitem.NewArray(restrictedItems),
}), nil
}
// FromStackItem converts stackitem to VTSBalance.
func (b *VTSBalance) FromStackItem(item stackitem.Item) error {
structItems, ok := item.Value().([]stackitem.Item)
if !ok || len(structItems) < 2 {
return errors.New("invalid VTSBalance structure")
}
unrestricted, err := structItems[0].TryInteger()
if err != nil {
return fmt.Errorf("invalid unrestricted balance: %w", err)
}
b.Unrestricted = *unrestricted
restrictedArr, ok := structItems[1].Value().([]stackitem.Item)
if !ok {
return errors.New("invalid restricted balances array")
}
b.Restricted = make(map[uint8]*big.Int)
for _, ri := range restrictedArr {
pair, ok := ri.Value().([]stackitem.Item)
if !ok || len(pair) != 2 {
return errors.New("invalid restricted balance pair")
}
catBI, err := pair[0].TryInteger()
if err != nil {
return fmt.Errorf("invalid category: %w", err)
}
amt, err := pair[1].TryInteger()
if err != nil {
return fmt.Errorf("invalid amount: %w", err)
}
b.Restricted[uint8(catBI.Int64())] = amt
}
return nil
}
// Vendor represents a registered vendor/merchant that can accept VTS payments.
type Vendor struct {
Address util.Uint160 // Vendor's script hash
Name string // Display name (max 64 chars)
Categories uint8 // Bitmask of accepted spending categories
RegisteredAt uint32 // Block height when registered
RegisteredBy util.Uint160 // Who registered this vendor
Active bool // Whether vendor is currently active
}
// ToStackItem converts Vendor to stackitem.
func (v *Vendor) ToStackItem() (stackitem.Item, error) {
return stackitem.NewStruct([]stackitem.Item{
stackitem.NewByteArray(v.Address.BytesBE()),
stackitem.NewByteArray([]byte(v.Name)),
stackitem.NewBigInteger(big.NewInt(int64(v.Categories))),
stackitem.NewBigInteger(big.NewInt(int64(v.RegisteredAt))),
stackitem.NewByteArray(v.RegisteredBy.BytesBE()),
stackitem.NewBool(v.Active),
}), nil
}
// FromStackItem converts stackitem to Vendor.
func (v *Vendor) FromStackItem(item stackitem.Item) error {
structItems, ok := item.Value().([]stackitem.Item)
if !ok || len(structItems) < 6 {
return errors.New("invalid Vendor structure")
}
addrBytes, err := structItems[0].TryBytes()
if err != nil {
return fmt.Errorf("invalid address: %w", err)
}
v.Address, err = util.Uint160DecodeBytesBE(addrBytes)
if err != nil {
return fmt.Errorf("invalid address bytes: %w", err)
}
nameBytes, err := structItems[1].TryBytes()
if err != nil {
return fmt.Errorf("invalid name: %w", err)
}
v.Name = string(nameBytes)
catBI, err := structItems[2].TryInteger()
if err != nil {
return fmt.Errorf("invalid categories: %w", err)
}
v.Categories = uint8(catBI.Int64())
regAtBI, err := structItems[3].TryInteger()
if err != nil {
return fmt.Errorf("invalid registeredAt: %w", err)
}
v.RegisteredAt = uint32(regAtBI.Int64())
regByBytes, err := structItems[4].TryBytes()
if err != nil {
return fmt.Errorf("invalid registeredBy: %w", err)
}
v.RegisteredBy, err = util.Uint160DecodeBytesBE(regByBytes)
if err != nil {
return fmt.Errorf("invalid registeredBy bytes: %w", err)
}
active, err := structItems[5].TryBool()
if err != nil {
return fmt.Errorf("invalid active: %w", err)
}
v.Active = active
return nil
}
// TransactionRecord represents a recorded VTS transaction for tax accounting.
type TransactionRecord struct {
TxHash util.Uint256 // Transaction hash
BlockHeight uint32 // When it occurred
From util.Uint160 // Sender
To util.Uint160 // Recipient
Amount big.Int // Gross amount
TxType TxType // Type of transaction
Category uint8 // Spending category (if applicable)
TaxWithheld big.Int // Tax withheld at source
TaxRate uint16 // Rate applied (basis points, e.g., 2500 = 25%)
Memo string // Optional description (max 256 chars)
}
// ToStackItem converts TransactionRecord to stackitem.
func (t *TransactionRecord) ToStackItem() (stackitem.Item, error) {
return stackitem.NewStruct([]stackitem.Item{
stackitem.NewByteArray(t.TxHash.BytesBE()),
stackitem.NewBigInteger(big.NewInt(int64(t.BlockHeight))),
stackitem.NewByteArray(t.From.BytesBE()),
stackitem.NewByteArray(t.To.BytesBE()),
stackitem.NewBigInteger(&t.Amount),
stackitem.NewBigInteger(big.NewInt(int64(t.TxType))),
stackitem.NewBigInteger(big.NewInt(int64(t.Category))),
stackitem.NewBigInteger(&t.TaxWithheld),
stackitem.NewBigInteger(big.NewInt(int64(t.TaxRate))),
stackitem.NewByteArray([]byte(t.Memo)),
}), nil
}
// FromStackItem converts stackitem to TransactionRecord.
func (t *TransactionRecord) FromStackItem(item stackitem.Item) error {
structItems, ok := item.Value().([]stackitem.Item)
if !ok || len(structItems) < 10 {
return errors.New("invalid TransactionRecord structure")
}
txHashBytes, err := structItems[0].TryBytes()
if err != nil {
return fmt.Errorf("invalid txHash: %w", err)
}
t.TxHash, err = util.Uint256DecodeBytesBE(txHashBytes)
if err != nil {
return fmt.Errorf("invalid txHash bytes: %w", err)
}
blockHeightBI, err := structItems[1].TryInteger()
if err != nil {
return fmt.Errorf("invalid blockHeight: %w", err)
}
t.BlockHeight = uint32(blockHeightBI.Int64())
fromBytes, err := structItems[2].TryBytes()
if err != nil {
return fmt.Errorf("invalid from: %w", err)
}
t.From, err = util.Uint160DecodeBytesBE(fromBytes)
if err != nil {
return fmt.Errorf("invalid from bytes: %w", err)
}
toBytes, err := structItems[3].TryBytes()
if err != nil {
return fmt.Errorf("invalid to: %w", err)
}
t.To, err = util.Uint160DecodeBytesBE(toBytes)
if err != nil {
return fmt.Errorf("invalid to bytes: %w", err)
}
amount, err := structItems[4].TryInteger()
if err != nil {
return fmt.Errorf("invalid amount: %w", err)
}
t.Amount = *amount
txTypeBI, err := structItems[5].TryInteger()
if err != nil {
return fmt.Errorf("invalid txType: %w", err)
}
t.TxType = TxType(txTypeBI.Int64())
catBI, err := structItems[6].TryInteger()
if err != nil {
return fmt.Errorf("invalid category: %w", err)
}
t.Category = uint8(catBI.Int64())
taxWithheld, err := structItems[7].TryInteger()
if err != nil {
return fmt.Errorf("invalid taxWithheld: %w", err)
}
t.TaxWithheld = *taxWithheld
taxRateBI, err := structItems[8].TryInteger()
if err != nil {
return fmt.Errorf("invalid taxRate: %w", err)
}
t.TaxRate = uint16(taxRateBI.Int64())
memoBytes, err := structItems[9].TryBytes()
if err != nil {
return fmt.Errorf("invalid memo: %w", err)
}
t.Memo = string(memoBytes)
return nil
}
// TaxConfig represents the tax configuration for VTS.
type TaxConfig struct {
DefaultIncomeRate uint16 // Default income tax rate (basis points)
DefaultSalesRate uint16 // Default sales tax rate (basis points)
TreasuryAddress util.Uint160 // Where taxes are sent
ExemptCategories uint8 // Categories exempt from sales tax (bitmask)
}
// ToStackItem converts TaxConfig to stackitem.
func (c *TaxConfig) ToStackItem() (stackitem.Item, error) {
return stackitem.NewStruct([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(int64(c.DefaultIncomeRate))),
stackitem.NewBigInteger(big.NewInt(int64(c.DefaultSalesRate))),
stackitem.NewByteArray(c.TreasuryAddress.BytesBE()),
stackitem.NewBigInteger(big.NewInt(int64(c.ExemptCategories))),
}), nil
}
// FromStackItem converts stackitem to TaxConfig.
func (c *TaxConfig) FromStackItem(item stackitem.Item) error {
structItems, ok := item.Value().([]stackitem.Item)
if !ok || len(structItems) < 4 {
return errors.New("invalid TaxConfig structure")
}
incomeRateBI, err := structItems[0].TryInteger()
if err != nil {
return fmt.Errorf("invalid defaultIncomeRate: %w", err)
}
c.DefaultIncomeRate = uint16(incomeRateBI.Int64())
salesRateBI, err := structItems[1].TryInteger()
if err != nil {
return fmt.Errorf("invalid defaultSalesRate: %w", err)
}
c.DefaultSalesRate = uint16(salesRateBI.Int64())
treasuryBytes, err := structItems[2].TryBytes()
if err != nil {
return fmt.Errorf("invalid treasuryAddress: %w", err)
}
c.TreasuryAddress, err = util.Uint160DecodeBytesBE(treasuryBytes)
if err != nil {
return fmt.Errorf("invalid treasuryAddress bytes: %w", err)
}
exemptCatBI, err := structItems[3].TryInteger()
if err != nil {
return fmt.Errorf("invalid exemptCategories: %w", err)
}
c.ExemptCategories = uint8(exemptCatBI.Int64())
return nil
}
// TaxSummary represents a tax summary for a specific period.
type TaxSummary struct {
Account util.Uint160 // Account address
StartBlock uint32 // Period start
EndBlock uint32 // Period end
TotalIncome big.Int // Total taxable income
TotalBenefits big.Int // Total tax-exempt benefits
TotalExpenses big.Int // Total expenses (for deductions)
DeductibleExpenses big.Int // Expenses that are deductible
TaxWithheld big.Int // Total tax withheld
EstimatedOwed big.Int // Estimated tax owed (income * rate)
Balance big.Int // TaxWithheld - EstimatedOwed
}
// ToStackItem converts TaxSummary to stackitem.
func (s *TaxSummary) ToStackItem() (stackitem.Item, error) {
return stackitem.NewStruct([]stackitem.Item{
stackitem.NewByteArray(s.Account.BytesBE()),
stackitem.NewBigInteger(big.NewInt(int64(s.StartBlock))),
stackitem.NewBigInteger(big.NewInt(int64(s.EndBlock))),
stackitem.NewBigInteger(&s.TotalIncome),
stackitem.NewBigInteger(&s.TotalBenefits),
stackitem.NewBigInteger(&s.TotalExpenses),
stackitem.NewBigInteger(&s.DeductibleExpenses),
stackitem.NewBigInteger(&s.TaxWithheld),
stackitem.NewBigInteger(&s.EstimatedOwed),
stackitem.NewBigInteger(&s.Balance),
}), nil
}
// FromStackItem converts stackitem to TaxSummary.
func (s *TaxSummary) FromStackItem(item stackitem.Item) error {
structItems, ok := item.Value().([]stackitem.Item)
if !ok || len(structItems) < 10 {
return errors.New("invalid TaxSummary structure")
}
accountBytes, err := structItems[0].TryBytes()
if err != nil {
return fmt.Errorf("invalid account: %w", err)
}
s.Account, err = util.Uint160DecodeBytesBE(accountBytes)
if err != nil {
return fmt.Errorf("invalid account bytes: %w", err)
}
startBI, err := structItems[1].TryInteger()
if err != nil {
return fmt.Errorf("invalid startBlock: %w", err)
}
s.StartBlock = uint32(startBI.Int64())
endBI, err := structItems[2].TryInteger()
if err != nil {
return fmt.Errorf("invalid endBlock: %w", err)
}
s.EndBlock = uint32(endBI.Int64())
totalIncome, err := structItems[3].TryInteger()
if err != nil {
return fmt.Errorf("invalid totalIncome: %w", err)
}
s.TotalIncome = *totalIncome
totalBenefits, err := structItems[4].TryInteger()
if err != nil {
return fmt.Errorf("invalid totalBenefits: %w", err)
}
s.TotalBenefits = *totalBenefits
totalExpenses, err := structItems[5].TryInteger()
if err != nil {
return fmt.Errorf("invalid totalExpenses: %w", err)
}
s.TotalExpenses = *totalExpenses
deductible, err := structItems[6].TryInteger()
if err != nil {
return fmt.Errorf("invalid deductibleExpenses: %w", err)
}
s.DeductibleExpenses = *deductible
taxWithheld, err := structItems[7].TryInteger()
if err != nil {
return fmt.Errorf("invalid taxWithheld: %w", err)
}
s.TaxWithheld = *taxWithheld
estimatedOwed, err := structItems[8].TryInteger()
if err != nil {
return fmt.Errorf("invalid estimatedOwed: %w", err)
}
s.EstimatedOwed = *estimatedOwed
balance, err := structItems[9].TryInteger()
if err != nil {
return fmt.Errorf("invalid balance: %w", err)
}
s.Balance = *balance
return nil
}